/***************************************************
*
* cismet GmbH, Saarbruecken, Germany
*
* ... and it just works.
*
****************************************************/
package de.cismet.tools.gui.downloadmanager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import java.io.File;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import javax.swing.event.EventListenerList;
import de.cismet.tools.configuration.Configurable;
import de.cismet.tools.configuration.NoWriteError;
/**
* The download manager manages all current downloads. New downloads are added to a collection, completed downloads are
* removed. Erroneous downloads remain in the collection. The download manager observes all download objects for state
* changed and informs the download manager panel via the DownloadListChangedListener interface.
*
* @author jweintraut
* @version $Revision$, $Date$
*/
public class DownloadManager implements Observer, Configurable {
//~ Static fields/initializers ---------------------------------------------
private static final Logger LOG = Logger.getLogger(DownloadManager.class);
private static final String XML_CONF_ROOT = "downloads";
private static final String XML_CONF_DIRECTORY = "directory";
private static final String XML_CONF_PARALLEL_DOWNLOADS = "parallelDownloads";
private static final String XML_CONF_NOTIFICATION_DISPLAY_TIME = "notificationDisplayTime";
private static final String XML_CONF_DIALOG = "dialog";
private static final String XML_CONF_DIALOG_AKSFORTITLE = "askForTitle";
private static final String XML_CONF_DIALOG_OPENAUTOMATICALLY = "openAutomatically";
private static final String XML_CONF_DIALOG_CLOSEAUTOMATICALLY = "closeAutomatically";
private static final String XML_CONF_DIALOG_USERTITLE = "userTitle";
private static DownloadManager instance = null;
//~ Instance fields --------------------------------------------------------
private File destinationDirectory = new File(System.getProperty("user.home") + System.getProperty("file.separator")
+ "cidsDownload");
private int parallelDownloads = 2;
private int notificationDisplayTime = 3;
private LinkedList<Download> downloads = new LinkedList<Download>();
private List<Download> downloadsToStart = new LinkedList<Download>();
private EventListenerList listeners = new EventListenerList();
private int countDownloadsTotal = 0;
private volatile int countDownloadsRunning = 0;
private int countDownloadsErroneous = 0;
private int countDownloadsCompleted = 0;
private int countDownloadsCancelled = 0;
//~ Constructors -----------------------------------------------------------
/**
* Creates a new DownloadManager object.
*/
private DownloadManager() {
}
//~ Methods ----------------------------------------------------------------
/**
* It's a Singleton. There can only be one download manager.
*
* @return The download manager.
*/
public static DownloadManager instance() {
if (instance == null) {
instance = new DownloadManager();
}
return instance;
}
/**
* This method is used to add a download to the download list.
*
* @param download A new download to add.
*/
public synchronized void add(final Download download) {
if ((download == null) || downloads.contains(download)) {
return;
}
downloads.add(download);
countDownloadsTotal++;
download.addObserver(this);
if (download instanceof MultipleDownload) {
final MultipleDownload multipleDownload = (MultipleDownload)download;
downloadsToStart.add(multipleDownload);
for (final Download singleDownload : multipleDownload.getDownloads()) {
singleDownload.addObserver(this);
singleDownload.addObserver(multipleDownload);
downloadsToStart.add(singleDownload);
}
} else {
downloadsToStart.add(download);
}
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
download,
DownloadListChangedEvent.Action.ADDED));
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
download,
DownloadListChangedEvent.Action.CHANGED_COUNTERS));
startDownloads();
}
/**
* Checks if the encapsulated downloads of a BackgroundTaskMultipleDownload have already been added to the
* DownloadManager. If a single download has not yet been added it is added and the observers will be notified.
*
* @param backgroundTaskMultipleDownload DOCUMENT ME!
*/
public synchronized void addDownloadsSubsequently(
final BackgroundTaskMultipleDownload backgroundTaskMultipleDownload) {
if ((backgroundTaskMultipleDownload == null) || !downloads.contains(backgroundTaskMultipleDownload)) {
return;
}
boolean downloadsWereAdded = false;
for (final Download encapsulatedDownload : backgroundTaskMultipleDownload.getDownloads()) {
if (!downloads.contains(encapsulatedDownload)) {
encapsulatedDownload.addObserver(this);
encapsulatedDownload.addObserver(backgroundTaskMultipleDownload);
downloadsToStart.add(encapsulatedDownload);
downloadsWereAdded = true;
}
}
if (downloadsWereAdded) {
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
backgroundTaskMultipleDownload,
DownloadListChangedEvent.Action.ADDED_DOWNLOADS_SUBSEQUENTLY));
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
backgroundTaskMultipleDownload,
DownloadListChangedEvent.Action.CHANGED_COUNTERS));
startDownloads();
}
}
/**
* Removes obsolete downloads. Only completed downloads are obsolete.
*/
public synchronized void removeObsoleteDownloads() {
final Collection<Download> downloadsRemoved = new LinkedList<Download>();
for (final Download download : downloads) {
if ((download.getStatus() == Download.State.COMPLETED)
|| (download.getStatus() == Download.State.COMPLETED_WITH_ERROR)
|| (download.getStatus() == Download.State.ABORTED)) {
downloadsRemoved.add(download);
}
}
if (downloadsRemoved.size() <= 0) {
return;
}
for (final Download download : downloadsRemoved) {
downloads.remove(download);
countDownloadsTotal--;
switch (download.getStatus()) {
case COMPLETED_WITH_ERROR: {
countDownloadsErroneous--;
break;
}
case COMPLETED: {
countDownloadsCompleted--;
break;
}
case ABORTED: {
countDownloadsCancelled--;
break;
}
}
download.deleteObserver(this);
if (download instanceof MultipleDownload) {
final MultipleDownload multipleDownload = (MultipleDownload)download;
for (final Download singleDownload : multipleDownload.getDownloads()) {
singleDownload.deleteObserver(this);
singleDownload.deleteObserver(multipleDownload);
}
}
}
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
downloadsRemoved,
DownloadListChangedEvent.Action.REMOVED));
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
downloadsRemoved,
DownloadListChangedEvent.Action.CHANGED_COUNTERS));
}
/**
* Remove a specified download from the download list.
*
* @param download The download to remove.
*/
public synchronized void removeDownload(final Download download) {
downloads.remove(download);
download.deleteObserver(this);
if (download instanceof MultipleDownload) {
final MultipleDownload multipleDownload = (MultipleDownload)download;
for (final Download singleDownload : multipleDownload.getDownloads()) {
singleDownload.deleteObserver(this);
singleDownload.deleteObserver(multipleDownload);
downloadsToStart.remove(singleDownload);
}
} else {
downloadsToStart.remove(download);
}
countDownloadsTotal--;
switch (download.getStatus()) {
case COMPLETED_WITH_ERROR: {
countDownloadsErroneous--;
break;
}
case COMPLETED: {
countDownloadsCompleted--;
break;
}
case ABORTED: {
countDownloadsCancelled--;
break;
}
}
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
download,
DownloadListChangedEvent.Action.REMOVED));
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
download,
DownloadListChangedEvent.Action.CHANGED_COUNTERS));
}
/**
* Starts pending downloads.
*/
private synchronized void startDownloads() {
int downloadsRunning = countDownloadsRunning;
final Iterator<Download> downloadToStartIter = downloadsToStart.iterator();
final List<Download> startableDownloads = new LinkedList<Download>();
while (downloadToStartIter.hasNext() && (downloadsRunning < parallelDownloads)) {
final Download downloadToStart = downloadToStartIter.next();
if (downloadToStart.getStatus().equals(Download.State.WAITING)) {
startableDownloads.add(downloadToStart);
downloadsRunning++;
downloadToStartIter.remove();
}
}
for (final Download downloadToStart : startableDownloads) {
downloadToStart.startDownload();
}
}
/**
* Returns the current download list.
*
* @return The current download list.
*/
public Collection<Download> getDownloads() {
return downloads;
}
/**
* Returns the count of erroneous downloads.
*
* @return The count of erroneous downloads.
*/
public int getCountDownloadsErroneous() {
return countDownloadsErroneous;
}
/**
* Returns the count of completed downloads.
*
* @return The count of completed downloads.
*/
public int getCountDownloadsCompleted() {
return countDownloadsCompleted;
}
/**
* Returns the total count of downloads.
*
* @return The total count of downloads.
*/
public int getCountDownloadsTotal() {
return countDownloadsTotal;
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public int getCountDownloadsCancelled() {
return countDownloadsCancelled;
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public int getParallelDownloads() {
return parallelDownloads;
}
/**
* DOCUMENT ME!
*
* @param parallelDownloads DOCUMENT ME!
*/
public void setParallelDownloads(final int parallelDownloads) {
this.parallelDownloads = parallelDownloads;
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public int getNotificationDisplayTime() {
return notificationDisplayTime;
}
/**
* DOCUMENT ME!
*
* @param notificationDisplayTime DOCUMENT ME!
*/
public void setNotificationDisplayTime(final int notificationDisplayTime) {
this.notificationDisplayTime = notificationDisplayTime;
}
/**
* Returns the destination directory.
*
* @return The destination directory for downloads.
*/
public File getDestinationDirectory() {
return destinationDirectory;
}
/**
* Sets the destination directory for downloads. Setting this does only affect new downloads. If the given download
* location is invalid, the DownloadManager will be disabled.
*
* @param destinationDirectory The new destination directory for downloads.
*/
public void setDestinationDirectory(final File destinationDirectory) {
this.destinationDirectory = destinationDirectory;
DownloadManagerDialog.getInstance().destinationDirectoryChanged();
if (!destinationDirectory.isDirectory() || !destinationDirectory.canWrite()) {
LOG.error("The download manager can't use the directory '" + destinationDirectory.getAbsolutePath() + "'.");
}
}
/**
* Returns a flag which tells whether download manager is enabled or not.
*
* @return The flag whether the download manager is enabled or not.
*
* @deprecated DOCUMENT ME!
*/
public boolean isEnabled() {
return true;
}
@Override
public synchronized void update(final Observable o, final Object arg) {
if (arg != null) {
/*
* in this case we assume that there was an update that doesnt concern us here. E.G this could happen if the
* title of a download has changed and it wants to notify the its observers about it. This feature was
* introduced in issue cismet/cismet-gui-commons#29 and is used for example in the NasDownload
*/
return;
}
if (!(o instanceof Download)) {
return;
}
final Download download = (Download)o;
switch (download.getStatus()) {
case COMPLETED: {
if (!(download instanceof MultipleDownload)) {
countDownloadsRunning--;
}
if (downloads.contains(download)) {
countDownloadsCompleted++;
}
startDownloads();
break;
}
case COMPLETED_WITH_ERROR: {
if (!(download instanceof MultipleDownload)) {
countDownloadsRunning--;
}
if (downloads.contains(download)) {
countDownloadsErroneous++;
}
startDownloads();
break;
}
case RUNNING: {
if (!(download instanceof MultipleDownload)) {
countDownloadsRunning++;
}
break;
}
case ABORTED: {
if (!(download instanceof MultipleDownload)) {
countDownloadsRunning--;
}
if (downloads.contains(download)) {
// countDownloadsCompleted++;
countDownloadsCancelled++;
}
startDownloads();
break;
}
}
notifyDownloadListChanged(new DownloadListChangedEvent(
this,
download,
DownloadListChangedEvent.Action.CHANGED_COUNTERS));
}
/**
* Adds a new DownloadListChangedListener.
*
* @param listener The listener to add.
*/
public void addDownloadListChangedListener(final DownloadListChangedListener listener) {
listeners.add(DownloadListChangedListener.class, listener);
}
/**
* Removes a DownloadListChangedListener.
*
* @param listener The listener to remove.
*/
public void removeDownloadListChangedListener(final DownloadListChangedListener listener) {
listeners.remove(DownloadListChangedListener.class, listener);
}
/**
* Notifies all current DownloadListChangedListeners.
*
* @param event The event to notify about.
*/
protected synchronized void notifyDownloadListChanged(final DownloadListChangedEvent event) {
for (final DownloadListChangedListener listener : listeners.getListeners(DownloadListChangedListener.class)) {
listener.downloadListChanged(event);
}
}
@Override
public void configure(final Element parent) {
DownloadManagerDialog.getInstance().setAskForJobNameEnabled(true);
DownloadManagerDialog.getInstance().setJobName("");
DownloadManagerDialog.getInstance().setOpenAutomaticallyEnabled(true);
destinationDirectory = new File(System.getProperty("user.home") + System.getProperty("file.separator")
+ "cidsDownload");
Element downloads = null;
if (parent == null) {
LOG.warn("The download manager isn't configured. Using default values.");
} else {
downloads = parent.getChild(XML_CONF_ROOT);
}
if (downloads == null) {
LOG.warn("The download manager isn't configured. Using default values.");
if (!destinationDirectory.isDirectory() || !destinationDirectory.canWrite()) {
LOG.error("The download manager can't use the directory '" + destinationDirectory.getAbsolutePath()
+ "'.");
}
return;
}
final Element directory = downloads.getChild(XML_CONF_DIRECTORY);
if ((directory == null) || (directory.getTextTrim() == null)) {
LOG.warn("There is no destination directory configured for downloads. Using default destination directory '"
+ System.getProperty("user.home") + System.getProperty("file.separator") + "cidsDownload'.");
} else {
destinationDirectory = new File(directory.getTextTrim());
}
if (!destinationDirectory.isDirectory() || !destinationDirectory.canWrite()) {
LOG.error("The download manager can't use the directory '" + destinationDirectory.getAbsolutePath() + "'.");
}
final Element parallelDownloads = downloads.getChild(XML_CONF_PARALLEL_DOWNLOADS);
if ((parallelDownloads == null) || (parallelDownloads.getTextTrim() == null)) {
LOG.warn("There is no limit for parallel downloads configured. Using default limit '2'.");
} else {
try {
this.parallelDownloads = Integer.parseInt(parallelDownloads.getText());
} catch (NumberFormatException e) {
LOG.warn("Configuration for limit of parallel downloads is invalid. Using default value of '2'", e);
this.parallelDownloads = 2;
}
}
final Element notificationDisplayTime = downloads.getChild(XML_CONF_NOTIFICATION_DISPLAY_TIME);
if ((notificationDisplayTime == null) || (notificationDisplayTime.getTextTrim() == null)) {
LOG.warn("There is no display time for download notifications configured. Using default time '3' sec.");
} else {
try {
this.notificationDisplayTime = Integer.parseInt(notificationDisplayTime.getText());
} catch (NumberFormatException e) {
LOG.warn(
"Configuration for display time of download notification is invalid. Using default value of '3' sec",
e);
this.notificationDisplayTime = 3;
}
}
final Element dialog = downloads.getChild(XML_CONF_DIALOG);
if (dialog == null) {
LOG.warn("The download dialog isn't configured. Using default values.");
return;
}
final Element askForTitle = dialog.getChild(XML_CONF_DIALOG_AKSFORTITLE);
if ((askForTitle == null) || (askForTitle.getTextTrim() == null)) {
LOG.warn(
"There is no configuration whether to ask for download titles or not. Using default value 'true'.");
} else {
final String value = askForTitle.getTextTrim();
DownloadManagerDialog.getInstance()
.setAskForJobNameEnabled("1".equals(value) || "true".equalsIgnoreCase(value));
}
final Element openAutomatically = dialog.getChild(XML_CONF_DIALOG_OPENAUTOMATICALLY);
if ((openAutomatically == null) || (openAutomatically.getTextTrim() == null)) {
LOG.warn(
"There is no configuration whether to open downloads automatically or not. Using default value 'true'.");
} else {
final String value = openAutomatically.getTextTrim();
DownloadManagerDialog.getInstance()
.setOpenAutomaticallyEnabled("1".equals(value) || "true".equalsIgnoreCase(value));
}
final Element closeAutomatically = dialog.getChild(XML_CONF_DIALOG_CLOSEAUTOMATICALLY);
if ((closeAutomatically == null) || (closeAutomatically.getTextTrim() == null)) {
LOG.warn(
"There is no configuration whether to close the download manager dialog automatically or not. Using default value 'true'.");
} else {
final String value = closeAutomatically.getTextTrim();
DownloadManagerDialog.getInstance()
.setCloseAutomaticallyEnabled("1".equals(value) || "true".equalsIgnoreCase(value));
}
final Element userTitle = dialog.getChild(XML_CONF_DIALOG_USERTITLE);
if ((userTitle == null) || (userTitle.getTextTrim() == null)) {
LOG.warn("There is no user title for downloads configured. Using default value 'cidsDownload'.");
} else {
DownloadManagerDialog.getInstance().setJobName(userTitle.getTextTrim());
}
// refresh the destination path in the download manager dialog
DownloadManagerDialog.getInstance().destinationDirectoryChanged();
}
@Override
public void masterConfigure(final Element parent) {
// NOP
}
@Override
public Element getConfiguration() throws NoWriteError {
final Element root = new Element(XML_CONF_ROOT);
final Element directory = new Element(XML_CONF_DIRECTORY);
directory.addContent(destinationDirectory.getAbsolutePath());
final Element dialog = new Element(XML_CONF_DIALOG);
final Element parallelDownloads = new Element(XML_CONF_PARALLEL_DOWNLOADS);
parallelDownloads.addContent(String.valueOf(this.parallelDownloads));
final Element askForTitle = new Element(XML_CONF_DIALOG_AKSFORTITLE);
askForTitle.addContent(DownloadManagerDialog.getInstance().isAskForJobNameEnabled() ? "true" : "false");
final Element openAutomatically = new Element(XML_CONF_DIALOG_OPENAUTOMATICALLY);
openAutomatically.addContent(DownloadManagerDialog.getInstance().isOpenAutomaticallyEnabled() ? "true"
: "false");
final Element closeAutomatically = new Element(XML_CONF_DIALOG_CLOSEAUTOMATICALLY);
closeAutomatically.addContent(DownloadManagerDialog.getInstance().isCloseAutomaticallyEnabled() ? "true"
: "false");
final Element userTitle = new Element(XML_CONF_DIALOG_USERTITLE);
userTitle.addContent(DownloadManagerDialog.getInstance().getJobName());
dialog.addContent(askForTitle);
dialog.addContent(openAutomatically);
dialog.addContent(closeAutomatically);
dialog.addContent(userTitle);
root.addContent(directory);
root.addContent(parallelDownloads);
root.addContent(dialog);
return root;
}
}