1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.xnap.plugin.opennap.net;
21
22 import java.awt.event.ActionEvent;
23 import java.io.File;
24 import java.io.IOException;
25 import java.util.Hashtable;
26 import java.util.Iterator;
27
28 import javax.swing.Action;
29 import javax.swing.Icon;
30
31 import org.xnap.gui.XNapFrame;
32 import org.xnap.XNap;
33 import org.xnap.action.*;
34 import org.xnap.gui.action.*;
35 import org.xnap.peer.Peer;
36 import org.xnap.plugin.Plugin;
37 import org.xnap.plugin.opennap.OpenNapPlugin;
38 import org.xnap.plugin.opennap.gui.OpenNapDownloadContainerEditorDialog;
39 import org.xnap.search.DefaultSearchFilter;
40 import org.xnap.search.Search;
41 import org.xnap.search.SearchFilter;
42 import org.xnap.search.SearchHandler;
43 import org.xnap.search.SearchResult;
44 import org.xnap.transfer.AbstractDownload;
45 import org.xnap.transfer.DownloadManager;
46 import org.xnap.transfer.Queueable;
47 import org.xnap.transfer.Segment;
48 import org.xnap.transfer.action.AbstractDeleteAction;
49 import org.xnap.transfer.action.AbstractEditAction;
50 import org.xnap.transfer.action.AbstractFindMoreSourcesAction;
51 import org.xnap.transfer.action.AbstractStartAction;
52 import org.xnap.transfer.action.AbstractStopAction;
53 import org.xnap.transfer.action.AbstractTransferAction;
54 import org.xnap.util.FileHelper;
55 import org.xnap.util.FiniteStateMachine;
56 import org.xnap.util.IllegalOperationException;
57 import org.xnap.util.Preferences;
58 import org.xnap.util.*;
59 import org.xnap.util.State;
60
61 /***
62 *
63 */
64 public class OpenNapDownloadContainer extends AbstractDownload
65 implements Queueable, SearchHandler {
66
67
68
69 /***
70 * The initial auto search interval. The interval is doubled with
71 * each search. */
72 public static final int INITIAL_SEARCH_INTERVAL = 30 * 60 * 1000;
73
74 /***
75 * The upper bound for the search interval. */
76 public static final int MAX_SEARCH_INTERVAL = 3 * 60 * 60 * 1000;
77
78 /***
79 * Maximum number of auto searches. */
80 public static final int MAX_SEARCH_COUNT = 10;
81
82 /***
83 * The state transition table. */
84 private static final Hashtable TRANSITION_TABLE;
85 static {
86 State[][] table = new State[][] {
87 { State.NOT_STARTED,
88 State.RUNNING, State.DELETED, },
89 { State.RUNNING,
90 State.STOPPING, State.SUCCEEDED, },
91 { State.STOPPING,
92 State.NOT_STARTED, },
93 { State.SUCCEEDED,
94 State.DELETED, },
95 };
96
97 TRANSITION_TABLE = FiniteStateMachine.createStateTable(table);
98 }
99
100
101
102 private OpenNapDownloadContainerData data;
103 private OpenNapSegmentManager segmentManager;
104
105 private long enqueueTime;
106 private int queuePosition;
107 private StateMachine sm = new StateMachine();
108 private long bytesTransferred;
109 private long totalBytesTransferred;
110 private DefaultSearchFilter filter;
111 private int runningCount = 0;
112 private int minQueuePos = -1;
113
114 private ToggleAction autoSearchAction = new AutoSearchAction();
115 private AbstractTransferAction deleteAction = new DeleteAction();
116 private AbstractTransferAction startAction = new StartAction();
117 private AbstractTransferAction stopAction = new StopAction();
118
119 /***
120 * The number of children that are in downloading state.
121 */
122 private int downloadingCount = 0;
123
124
125
126 public OpenNapDownloadContainer(OpenNapSearchResult results[])
127 {
128 data = new OpenNapDownloadContainerData();
129 data.filename = results[0].getShortFilename();
130 data.filesize = results[0].getFilesize();
131 data.autoSearchingEnabled
132 = Preferences.getInstance().getAlwaysAutoDownload();
133
134 if (results[0].getFilter() != null) {
135
136 setSearchFilter(results[0].getFilter());
137 }
138 else {
139 DefaultSearchFilter filter = new DefaultSearchFilter();
140 filter.put
141 (SearchFilter.TEXT,
142 StringHelper.stripExtra(FileHelper.name(data.filename)));
143 setSearchFilter(filter);
144 }
145
146 segmentManager = new OpenNapSegmentManager
147 (this, OpenNapPlugin.getPreferences().getMultiSourceDownloading());
148
149 for (int i = 0; i < results.length; i++) {
150 add(results[i], false);
151 }
152
153 initialize();
154
155 File path = new File(Preferences.getInstance().getIncompleteDir());
156 OpenNapPlugin.getTransferManager().getResumeRepository().add
157 (path, this.data);
158 enqueueTime = System.currentTimeMillis();
159 DownloadManager.getInstance().getQueue().add(this);
160 }
161
162 /***
163 * Invoked by {@link OpenNapResumeRepository} when restoring download from
164 * resume data.
165 */
166 public OpenNapDownloadContainer(OpenNapDownloadContainerData data)
167 {
168 this.data = data;
169 this.filter = createFilterFromData();
170
171 segmentManager = new OpenNapSegmentManager
172 (this, OpenNapPlugin.getPreferences().getMultiSourceDownloading(),
173 data.segments);
174 totalBytesTransferred = segmentManager.getBytesTransferred();
175
176 initialize();
177 }
178
179
180
181 private void initialize()
182 {
183 deleteAction.setEnabledLater(true);
184 startAction.setEnabledLater(true);
185 stopAction.setEnabledLater(false);
186 autoSearchAction.setSelected(data.autoSearchingEnabled);
187 }
188
189 /***
190 * Creates a new download for result and adds it as a child. If a
191 * download for result is already existing, starts it if not
192 * running.
193 *
194 * @return true, if download for result was added or already
195 * existing; false, otherwise */
196 public boolean add(OpenNapSearchResult result, boolean matchFilter)
197 {
198 synchronized (sm) {
199
200
201 for (Iterator i = OpenNapDownloadContainer.this.iterator();
202 i.hasNext();) {
203
204 OpenNapDownload d = (OpenNapDownload)i.next();
205 if (result.equals(d.getResult())) {
206 if (d.isRestartable()) {
207 if (d.getResult().getOpenNapUser().isDownloadDenied()) {
208 d.setStateDescription(XNap.tr("Downloads from this peer are disabled"));
209 }
210 else {
211 d.start();
212 }
213 return true;
214 }
215 }
216 }
217
218 if (matchFilter) {
219 if (filter == null || !filter.matches(result)) {
220 return false;
221 }
222 }
223
224 OpenNapDownload d = new OpenNapDownload(this, result);
225 add(d);
226
227 if (d.getResult().getOpenNapUser().isDownloadDenied()) {
228 d.setStateDescription(XNap.tr("Downloads from this peer are disabled"));
229 }
230 else if (sm.getState() == State.RUNNING) {
231 d.start();
232 }
233
234 return true;
235 }
236 }
237
238 /***
239 * Notifies the {@link OpenNapTransferManager} that the download
240 * was removed. Invoked by {@link DownloadManager} when the
241 * download is done and clear finished is executed by user. */
242 public void cleared()
243 {
244 OpenNapPlugin.getTransferManager().remove(this);
245 }
246
247 public long getEnqueueTime()
248 {
249 return enqueueTime;
250 }
251
252 /***
253 *
254 */
255 public String getFilename()
256 {
257 return data.filename;
258 }
259
260 /***
261 *
262 */
263 public long getFilesize()
264 {
265 return data.filesize;
266 }
267
268 public Plugin getPlugin()
269 {
270 return OpenNapPlugin.getInstance();
271 }
272
273 /***
274 * @see xnap.transfer.Transfer#getActions()
275 */
276 public Action[] getActions()
277 {
278 return new Action[] {
279 startAction,
280 stopAction,
281 new FindMoreSourcesAction(),
282 new EditAction(),
283 deleteAction,
284 null,
285 new OptionSubmenuAction(new Action[] { autoSearchAction }),
286 };
287 }
288
289 /***
290 *
291 */
292 public long getBytesTransferred()
293 {
294 return bytesTransferred;
295 }
296
297 /***
298 * @see xnap.transfer.Transfer#getFile()
299 */
300 public File getFile()
301 {
302 OpenNapSegment segment = segmentManager.getRoot();
303 return (segment != null) ? segment.getFile() : null;
304 }
305
306 public Icon getIcon()
307 {
308 return OpenNapPlugin.ICON_16;
309 }
310
311 /***
312 * Returns 1.
313 */
314 public int getPriority()
315 {
316 return 1;
317 }
318
319 /***
320 * Returns the position in the {@link DownloadManager} queue.
321 */
322 public int getQueuePosition()
323 {
324 return queuePosition;
325 }
326
327 /***
328 * @see xnap.transfer.Transfer#getPeer()
329 */
330 public Peer getPeer()
331 {
332 return null;
333 }
334
335 /***
336 * @return a copy of the filter used for searching; null, if no
337 * search filter has been set, yet (i.e. download was started from
338 * browse) */
339 public SearchFilter getSearchFilter()
340 {
341 return (filter != null) ? (SearchFilter)filter.clone() : null;
342 }
343
344 public Segment[] getSegments()
345 {
346 return segmentManager.getSegments();
347 }
348
349 /***
350 * @see xnap.transfer.Transfer#getStatus()
351 */
352 public String getStatus()
353 {
354 return sm.getDescription();
355 }
356
357 /***
358 * @see xnap.transfer.Transfer#getTotalBytesTransferred()
359 */
360 public long getTotalBytesTransferred()
361 {
362 return totalBytesTransferred;
363 }
364
365 public boolean isAutoSearchingEnabled()
366 {
367 return data.autoSearchingEnabled;
368 }
369
370 public boolean isDone()
371 {
372 State s = sm.getState();
373 return s == State.SUCCEEDED;
374 }
375
376 /***
377 * Returns true, if the root segment is complete.
378 */
379 private boolean isDownloadComplete()
380 {
381 OpenNapSegment segment = segmentManager.getRoot();
382 return segment != null && segment.isFinished()
383 && segment.getEnd() == getFilesize();
384 }
385
386 public boolean isRunning()
387 {
388 return sm.getState() == State.RUNNING;
389 }
390
391 public void resultReceived(SearchResult result)
392 {
393 add((OpenNapSearchResult)result, true);
394 }
395
396 public void setAutoSearchingEnabled(boolean autoSearchingEnabled)
397 {
398 data.autoSearchingEnabled = autoSearchingEnabled;
399 }
400
401 public void setFilename(String filename)
402 {
403 data.filename = filename;
404 }
405
406 /***
407 * @param filter the search filter
408 */
409 public void setSearchFilter(SearchFilter filter)
410 {
411 data.searchText = filter.getText();
412 data.realm = OpenNapPlugin.getSearchManager().getRealm
413 (filter.getMediaType());
414
415 this.filter = createFilterFromData();
416 }
417
418 void setSegments(OpenNapSegment[] segments)
419 {
420 data.segments = new OpenNapSegmentData[segments.length];
421 for (int i = 0; i < segments.length; i++) {
422 data.segments[i] = segments[i].getData();
423 }
424 }
425
426 private DefaultSearchFilter createFilterFromData()
427 {
428 DefaultSearchFilter filter = new DefaultSearchFilter();
429 filter.put(SearchFilter.TEXT, data.searchText);
430 if (data.realm != null) {
431 filter.put(SearchFilter.MEDIA_TYPE,
432 OpenNapPlugin.getSearchManager().getMediaType(data.realm));
433 }
434 filter.put(SearchFilter.MAX_FILESIZE, new Long(data.filesize));
435 filter.put(SearchFilter.MIN_FILESIZE, new Long(data.filesize));
436 return filter;
437 }
438
439 /***
440 * Invoked when the state of <code>search</code> changes.
441 */
442 public void stateChanged(Search search)
443 {
444 if (search.isDone()) {
445 setStateDescription(XNap.tr("Finished search for more sources"));
446 }
447 }
448
449 /***
450 *
451 */
452 public void setQueuePosition(int position)
453 {
454 queuePosition = position;
455 }
456
457 synchronized void commit(int transferred)
458 {
459 bytesTransferred += transferred;
460 totalBytesTransferred += transferred;
461 }
462
463 /***
464 * Invoked by {@link OpenNapDownloadRunner} objects.
465 */
466 synchronized void done(OpenNapDownload d)
467 {
468 synchronized (sm) {
469 if (sm.getState() == State.SUCCEEDED) {
470
471 return;
472 }
473 else if (sm.getState() == State.NOT_STARTED) {
474 logger.error("Illegal download state detected");
475 }
476
477 }
478
479 if (isDownloadComplete()) {
480 setStateDescription
481 (XNap.tr("Moving file to destination directory"));
482 try {
483 OpenNapSegment segment = getSegmentManager().getRoot();
484 String dir = FileHelper.getDownloadDir(getFilename());
485 File newFile = FileHelper.moveUnique
486 (segment.getFile(), dir, getFilename());
487 segment.setFile(newFile);
488
489 setState(State.SUCCEEDED);
490 }
491 catch (IOException e) {
492 logger.debug("could not rename finished file", e);
493 setState
494 (State.SUCCEEDED,
495 XNap.tr("Could not create file (check download dir)"));
496 }
497 }
498 else if (!d.isRestartable()) {
499 add(d.getResult(), false);
500 }
501 }
502
503 OpenNapSegmentManager getSegmentManager()
504 {
505 return segmentManager;
506 }
507
508 /***
509 * Invoked by {@link OpenNapDownload} children when child is queued. */
510 synchronized void remotelyQueued(int position)
511 {
512 if ((minQueuePos == -1 || position < minQueuePos) && position != 0) {
513 minQueuePos = -1;
514 for (Iterator i = OpenNapDownloadContainer.this.iterator();
515 i.hasNext();) {
516 OpenNapDownload d = (OpenNapDownload)i.next();
517 if (d.isQueued()) {
518 int pos = d.getQueuePosition();
519 if (pos < minQueuePos || minQueuePos == -1) {
520 minQueuePos = pos;
521 }
522 }
523 }
524 }
525 updateDescription();
526 }
527
528 private void searchForSources()
529 {
530 if (filter != null && filter.getText().length() > 0) {
531 Search s = OpenNapPlugin.getSearchManager().search(filter);
532
533 setStateDescription(XNap.tr("Searching for more sources") + "...");
534 s.start(this);
535 }
536 else {
537 setStateDescription(XNap.tr("Invalid search settings"));
538 }
539 }
540
541 private void setState(State newState, String description)
542 {
543 sm.setState(newState, description);
544 stateChanged();
545 }
546
547 private void setState(State newState)
548 {
549 sm.setState(newState);
550 stateChanged();
551 }
552
553 void setStateDescription(String description)
554 {
555 sm.setDescription(description);
556 stateChanged();
557 }
558
559 /***
560 * Invoked when the user manually starts the download or if it
561 * reaches to the top of the queue. */
562 public boolean startTransfer()
563 {
564 try {
565 setState(State.RUNNING);
566 synchronized (sm) {
567 for (Iterator i = OpenNapDownloadContainer.this.iterator();
568 i.hasNext();) {
569 ((OpenNapDownload)i.next()).start();
570 }
571 }
572 }
573 catch (IllegalOperationException e) {
574 logger.error("unexpected exception", e);
575 }
576 return true;
577 }
578
579 /***
580 * Invoked by {@link OpenNapDownload} children to notify the
581 * container that a download has started. For each call to start()
582 * a call to {@link #stopped(OpenNapDownload)} needs to be made.
583 *
584 * @return true, if the start was accepted */
585 boolean started(OpenNapDownload d)
586 {
587 boolean notify = false;
588 synchronized (sm) {
589 if (sm.getState() == State.STOPPING
590 || sm.getState() == State.SUCCEEDED) {
591 return false;
592 }
593
594 if (sm.getState() == State.NOT_STARTED) {
595 sm.setState(State.RUNNING);
596 notify = true;
597 }
598 runningCount++;
599 }
600
601 if (notify) {
602 stateChanged();
603 }
604 return true;
605 }
606
607 public void stop()
608 {
609 try {
610 setState(State.STOPPING);
611 minQueuePos = -1;
612 setQueuePosition(-1);
613 }
614 catch (IllegalOperationException e) {
615
616
617
618
619
620 }
621 }
622
623 private void stopAllChildren()
624 {
625 synchronized (sm) {
626 for (Iterator i = OpenNapDownloadContainer.this.iterator();
627 i.hasNext();) {
628 ((OpenNapDownload)i.next()).stop(XNap.tr("Stopped"));
629 }
630 }
631 }
632
633 /***
634 * Invoked by {@link OpenNapDownload} children.
635 *
636 * @see #started(OpenNapDownload)
637 */
638 void stopped(OpenNapDownload d)
639 {
640 boolean notify = false;
641 synchronized (sm) {
642 runningCount--;
643 if (runningCount == 0 && sm.getState() == State.STOPPING) {
644 sm.setState(State.NOT_STARTED);
645 notify = true;
646 minQueuePos = -1;
647 updateDescription();
648 }
649 }
650
651 if (notify) {
652 stateChanged();
653 }
654 }
655
656 /***
657 * Invoked by {@link OpenNapDownload} children when DOWNLOADING
658 * state is entered. */
659 protected synchronized void transferStarted()
660 {
661 downloadingCount++;
662 if (downloadingCount == 1) {
663 bytesTransferred = 0;
664 super.transferStarted();
665
666 updateDescription();
667 }
668 }
669
670 /***
671 * Invoked by {@link OpenNapDownload} children when DOWNLOADING
672 * state is left. */
673 protected synchronized void transferStopped()
674 {
675 downloadingCount--;
676 if (downloadingCount == 0) {
677 super.transferStopped();
678
679 updateDescription();
680 }
681 }
682
683 private synchronized void updateDescription()
684 {
685 synchronized(sm) {
686 if (sm.getState() == State.RUNNING) {
687 if (downloadingCount > 0) {
688 sm.setDescription(XNap.tr("Downloading"));
689 setQueuePosition(0);
690 }
691 else if (minQueuePos > 0) {
692 sm.setDescription
693 (XNap.tr("Remotely queued ({0})",
694 new Integer(minQueuePos)));
695 setQueuePosition(minQueuePos);
696 }
697 else {
698 sm.setDescription(XNap.tr("Running"));
699 }
700 }
701 }
702 stateChanged();
703 }
704
705
706
707 private class StateMachine extends FiniteStateMachine
708 {
709
710 private AutoSearchTask searchTask;
711
712
713
714 public StateMachine()
715 {
716 super(State.NOT_STARTED, TRANSITION_TABLE);
717 }
718
719
720
721 protected synchronized void stateChanged(State oldState,
722 State newState)
723 {
724 if (newState == State.RUNNING) {
725 startAction.setEnabledLater(false);
726 stopAction.setEnabledLater(true);
727 deleteAction.setEnabledLater(false);
728
729 searchTask = new AutoSearchTask();
730 Scheduler.run
731 ((getChildCount() > 0) ? INITIAL_SEARCH_INTERVAL : 0,
732 INITIAL_SEARCH_INTERVAL,
733 searchTask);
734 }
735 else if (newState == State.STOPPING) {
736 if (runningCount > 0) {
737 stopAllChildren();
738 stopAction.setEnabledLater(false);
739 }
740 else {
741 this.setState(State.NOT_STARTED);
742 }
743 }
744 else if (newState == State.SUCCEEDED
745 || newState == State.NOT_STARTED) {
746 DownloadManager.getInstance().getQueue().remove
747 (OpenNapDownloadContainer.this);
748 stopAction.setEnabledLater(false);
749 deleteAction.setEnabledLater(true);
750 }
751 else if (newState == State.DELETED) {
752 OpenNapPlugin.getTransferManager().remove
753 (OpenNapDownloadContainer.this);
754 DownloadManager.getInstance().remove
755 (OpenNapDownloadContainer.this);
756 if (getFile() != null) {
757 getFile().delete();
758 }
759 }
760
761 if (newState == State.SUCCEEDED
762 || newState == State.DELETED) {
763 OpenNapPlugin.getTransferManager().getResumeRepository().remove
764 (OpenNapDownloadContainer.this.data);
765
766 stopAllChildren();
767 }
768 else if (newState == State.NOT_STARTED) {
769 startAction.setEnabledLater(true);
770 }
771
772 if (oldState == State.RUNNING) {
773 searchTask.cancel();
774 searchTask = null;
775 }
776 }
777 }
778
779 /***
780 * Finds more sources regular intervals. */
781 private class AutoSearchTask extends XNapTask {
782
783 private long nextSearch = 0;
784 private int searchCount = 0;
785 private long searchInterval = INITIAL_SEARCH_INTERVAL;
786
787 public AutoSearchTask()
788 {
789 }
790
791 public void run()
792 {
793 if (!isAutoSearchingEnabled()) {
794 return;
795 }
796
797 if (nextSearch < System.currentTimeMillis()) {
798 searchForSources();
799 }
800
801 searchInterval *= 2;
802 if (searchInterval > MAX_SEARCH_INTERVAL) {
803 searchInterval = MAX_SEARCH_INTERVAL;
804 }
805 nextSearch = System.currentTimeMillis() + searchInterval;
806
807 if (++searchCount == MAX_SEARCH_COUNT) {
808
809 this.cancel();
810 }
811 }
812
813 }
814
815 private class AutoSearchAction extends AbstractToggleAction {
816
817 public AutoSearchAction()
818 {
819 putValue(Action.NAME, XNap.tr("Automatically search for more sources"));
820 }
821
822 public void toggled(boolean selected)
823 {
824 setAutoSearchingEnabled(selected);
825 }
826
827 }
828
829 private class DeleteAction extends AbstractDeleteAction {
830
831 public void actionPerformed(ActionEvent event)
832 {
833
834
835 try {
836 setState(State.DELETED);
837 }
838 catch (IllegalOperationException e) {
839 logger.warn("unexpected state");
840 }
841 }
842
843 }
844
845 private class EditAction extends AbstractEditAction {
846
847 public void actionPerformed(ActionEvent e)
848 {
849 OpenNapDownloadContainerEditorDialog.showDialog
850 (XNapFrame.getInstance(), OpenNapDownloadContainer.this);
851 }
852
853 }
854
855 private class FindMoreSourcesAction extends AbstractFindMoreSourcesAction {
856
857 public void actionPerformed(ActionEvent e)
858 {
859 searchForSources();
860 }
861
862 }
863
864 private class StartAction extends AbstractStartAction {
865
866 public void actionPerformed(ActionEvent e)
867 {
868 startTransfer();
869 }
870
871 }
872
873 private class StopAction extends AbstractStopAction {
874
875 public void actionPerformed(ActionEvent event)
876 {
877 stop();
878 }
879
880 }
881
882 private class StartRunner implements Runnable {
883
884 private OpenNapDownload d;
885
886 public StartRunner(OpenNapDownload d)
887 {
888 this.d = d;
889 }
890
891 public void run()
892 {
893 synchronized (sm) {
894 if (sm.getState() == State.RUNNING) {
895 d.start();
896 }
897 }
898 }
899
900 }
901
902 }