View Javadoc

1   /*
2    *  XNap - A P2P framework and client.
3    *
4    *  See the file AUTHORS for copyright information.
5    *
6    *  This program is free software; you can redistribute it and/or modify
7    *  it under the terms of the GNU General Public License as published by
8    *  the Free Software Foundation.
9    *
10   *  This program is distributed in the hope that it will be useful,
11   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   *  GNU General Public License for more details.
14   *
15   *  You should have received a copy of the GNU General Public License
16   *  along with this program; if not, write to the Free Software
17   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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  	//--- Constant(s) ---
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; // 30 min
73  
74  	/***
75  	 * The upper bound for the search interval.  */
76  	public static final int MAX_SEARCH_INTERVAL = 3 * 60 * 60 * 1000; // 3 h
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 	//--- Data Field(s) ---
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     //--- Constructor(s) ---
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     //--- Methods ---
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 			// check if there is already a child download that downloads
200 			// the result
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 				// another thread has already done the moving
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 			// ignore exception, usually when this occurs we are
616 			// usually already stopped and stop() is only invoked as a
617 			// safe-guard
618 
619 			//logger.warn("unexpected state", e);
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     //--- Inner Class(es) ---
706 
707     private class StateMachine extends FiniteStateMachine
708     {
709 
710 		private AutoSearchTask searchTask;
711 
712 		//--- Constructor(s) ---
713 
714 		public StateMachine()
715 		{
716 			super(State.NOT_STARTED, TRANSITION_TABLE);
717 		}
718 
719 		//--- Method(s) ---
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 				// notify user?
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 			// FIX: show confirmation dialog
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 }