package uc.files.downloadqueue; import helpers.GH; import helpers.IObservable; import helpers.StatusObject; import helpers.Observable.IObserver; import helpers.StatusObject.ChangeType; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import logger.LoggerFactory; import org.apache.log4j.Logger; import uc.DCClient; import uc.IUser; import uc.PI; import uc.crypto.HashValue; import uc.files.IDownloadable; import uc.files.IHasDownloadable; import uc.files.IDownloadable.IDownloadableFile; import uc.files.transfer.AbstractFileTransfer; import uc.files.transfer.AbstractWritableFileInterval; import uc.files.transfer.FileTransferInformation; import uc.files.transfer.IFileTransfer; import uc.files.transfer.TransferChange; import uc.protocols.TransferType; /** * base class for all kinds of DownloadQueueEntrys * * implements comparable for ordering DownloadQueueEntrys * by priority.. * * @author quicksilver * */ public abstract class AbstractDownloadQueueEntry implements Comparable<AbstractDownloadQueueEntry>, IObserver<TransferChange>, IHasDownloadable { private static final Logger logger = LoggerFactory.make(); private final Object sync = new Object(); //object to sync on for adding and removing user.. /** * all running transfer will be listed here */ protected final List<IFileTransfer> runningFileTransfers = new CopyOnWriteArrayList<IFileTransfer>(); /** * users that are associated with this DownloadQueuEntry * because we determined that they have the file/TTHL and * want it from them */ protected final Set<IUser> users = new CopyOnWriteArraySet<IUser>();// Collections.synchronizedSet(new HashSet<User>()); private final Set<IUser> removedUsers = new CopyOnWriteArraySet<IUser>() ; // Collections.synchronizedSet(new HashSet<User>()); private volatile int priority = 255/2; //the higher the priority.. the sooner it will be uploaded.. /** * what kind of DQE this is.. */ protected final TransferType type; /** * Actions that have to be taken when the file has finished its download.. */ protected final Set<AbstractDownloadFinished> downloadFinished = new CopyOnWriteArraySet<AbstractDownloadFinished>(); private final Date added; //the time this was added /** * when the file finished downloading.. */ protected volatile Date finished = null; protected final DownloadQueue dq; /** * base constructor for the DownloadQueueEntry * @param type */ protected AbstractDownloadQueueEntry(DownloadQueue dq,TransferType type, int priority,Date added) { this.dq = dq; this.type = type; this.added = added; this.priority = priority; } /** * * @return if this DownloadQueueEntry can * currently be downloaded (if a transfer can be initiated) * and getDownload will probably return true */ public abstract boolean isDownloadable(); /** * requests a download for this file * so DQE recognizes that some download will soon start for this file * * * @param fti - where information will be filled * information that will be filled is * start position and length of the download * additional a HashValue will be filled in if applicable * * * @return true if a download can be started and information was filled * into fti false if no Download can be started.. */ public abstract boolean getDownload(FileTransferInformation fti); /** * when the download is confirmed a FileInterval for writing * will be created with this method * @param fti - the FTI containing information about * the interval range * @return an interval that the soon to be started transfer can write too */ public abstract AbstractWritableFileInterval getInterval(FileTransferInformation fti); /** * registers a transfer with this DownloadQueueEntry * @param ft - the fileTransfer that uses this DownloadQueueEntry * (currently this are only downloads later hopefully uploads too) */ public void startedDownload(AbstractFileTransfer ft) { logger.debug("startedDownload()"); runningFileTransfers.add(ft); ft.addObserver(this); if (runningFileTransfers.size() == 1) { dq.notifyObservers(new StatusObject(this,ChangeType.CHANGED, DownloadQueue.DQE_FIRST_TRANSFER_STARTED,ft)); } dq.notifyObservers(new StatusObject(this,ChangeType.CHANGED, DownloadQueue.DQE_TRANSFER_STARTED,ft)); } public final void update(IObservable<TransferChange> ft, TransferChange arg) { if (arg == TransferChange.FINISHED) { runningFileTransfers.remove(ft); ft.deleteObserver(this); dq.notifyObservers(new StatusObject(this,ChangeType.CHANGED, DownloadQueue.DQE_TRANSFER_FINISHED,ft)); if (runningFileTransfers.isEmpty()) { dq.notifyObservers(new StatusObject(this,ChangeType.CHANGED, DownloadQueue.DQE_LAST_TRANSFER_FINISHED,ft)); } } } /** * adds a user to this dqe * * @param usr the user to be added */ public void addUser(IUser usr){ logger.debug("1addding User to DQE:"+usr.getNick()+" "+getClass().getSimpleName()); if (!users.contains(usr)) { users.add(usr); removedUsers.remove(usr); synchronized(sync) { usr.addDQE(this); logger.debug("2addding User to DQE:"+usr.getNick()+" "+getClass().getSimpleName()); addUserSuper(usr); } dq.notifyObservers(new StatusObject(this,ChangeType.CHANGED)); } } protected void addUserSuper(IUser usr) {} /** * removes a user from this dqe * @param usr the user to be removed */ public void removeUser(IUser usr) { if (users.contains(usr)) { users.remove(usr); removedUsers.add(usr); synchronized(sync) { usr.removeDQE(this); if (type == TransferType.FILELIST) { remove(); } DQEDAO dqedoa = DQEDAO.get(this); if (dqedoa != null) { dq.getDatabase().changeUserOfDQE(usr,dqedoa.getTTHRoot(),false); } } dq.notifyObservers(new StatusObject(this,ChangeType.CHANGED)); } } public int getNrOfRunningDownloads() { int count = 0; for (IFileTransfer aft : runningFileTransfers) { if (!aft.isUpload()) { count++; } } return count; } /** * @return the number of bytes that were already downloaded for this * DownloadQueueEntry */ public abstract long getDownloadedBytes(); /** * * @return how many bytes the download is in total * getDownloaded() / getSize() should be a rough percentage on how * much has been downloaded so far. */ public abstract long getSize(); /** * the path where this DownloadQueueEntry will be transfered to * when the download has finished * @return */ public abstract File getTargetPath(); public abstract void setTargetPath(File target); /** * @return the temporary path where the file should * be downloaded to and where the file is stored while in * progress * */ public File getTempPath() { String id = getID().toString(); File[] files = PI.getTempDownloadDirectory().listFiles(); if (files != null) { for (File f : files) { if (f.isFile() && f.getName().contains(id)) { return f; } } } return new File(PI.getTempDownloadDirectory(), GH.replaceInvalidFilename(getFileName())+"."+id +".dctmp"); } /** * convenience method for the name of the file * @return the last part of the path .. */ public String getFileName() { return getTargetPath().getName(); } /** * convenience method for returning the parent folder * of the file * @return the parent folder of the path where the file will be * stored when the download has completed.. */ public File getFolder() { return getTargetPath().getParentFile(); } /** * * @return an id for this DQE that will be used * for unique identification * in case of a filelist this will be the * id of the user * for file or TTHL the TTH root of the file */ public abstract HashValue getID(); /** * * @return if the download has finished on the DQE */ public boolean isFinished() { return finished != null; } /** * * @return an IDownlodable if this is a file or TTHl * or null if it is a filelist */ public abstract IDownloadableFile downloadableData(); public IDownloadable getDownloadable() { return downloadableData(); } /** * stores the file to destination after it is finished * if needed the file is copied and deleted as this may * help sometimes */ public boolean storeToDestination() { synchronized(this) { logger.debug("storeToDestination called"); } File source = getTempPath(); logger.debug("sourcefile"+source); File dest = getTargetPath(); dest = new File(dest.getParentFile(),GH.replaceInvalidFilename(dest.getName())); // if in last name there is a invalid filename.. logger.debug("destfile"+dest); try { moveFile(source,dest,dq.getDcc()); } catch(IOException ioe) { logger.warn(ioe,ioe); return false; } synchronized(this) { logger.debug("storeToDestination finished"); } return true; } /** * removes the file from the download queue and deletes the tempfile */ public final synchronized void remove() { for (IUser usr: users) { removeUser(usr); } //remove from the persistence.. DQEDAO del = DQEDAO.get(this); if (del != null) { dq.getDatabase().modifyDQEDAO(del, ChangeType.REMOVED); } dq.removeFile(this); for (IFileTransfer ft : new ArrayList<IFileTransfer>(runningFileTransfers)) { ft.cancel(); } if (getTempPath().isFile()) { if (!getTempPath().delete()) { getTempPath().deleteOnExit(); scheduledDeletion(getTempPath(),dq.getDcc()); } } } /** * @return the time this DQE has completed downloading */ public Date getFinished() { return new Date(finished.getTime()); } /** * @return the time this DQE was created */ public Date getAdded() { return new Date(added.getTime()); } public void addDoAfterDownload(AbstractDownloadFinished action) { downloadFinished.add(action); } public Set<AbstractDownloadFinished> getDoAfterDownload() { return Collections.unmodifiableSet(downloadFinished); } protected void executeDoAfterDownload() { //execute tasks.. for (final AbstractDownloadFinished runnable : downloadFinished) { getDCC().executeDir(new Runnable() { public void run() { runnable.finishedDownload(getTargetPath()); } }); } } /** * tries to move a file.. * @param source - from * @param dest - to * @return if moving was successful. */ public static boolean moveFile(final File source, File dest,DCClient dcc) throws IOException { if (dest.isFile()) { if (!dest.delete()) { throw new IOException("Could not move to destination undeletable file present"); } } if (!dest.getParentFile().isDirectory()) { if (!dest.getParentFile().mkdirs()) {; //create parent of target ..if not exists.. throw new IOException("Could not move to destination. Could not create needed Folders. "+dest); } } logger.debug("renameing "+source+" to "+dest); boolean renameWorked = source.renameTo(dest); if (!renameWorked) { //rename didn't work .. so copy try { GH.copy(source, dest); //delete the temp file finally and if we can't .. mark for deletion on exit ... if (!source.delete()) { source.deleteOnExit(); scheduledDeletion(source,dcc); } } catch (IOException ioe) { throw ioe; } } return true; } /** * creates a scheduler for an undeleted file that tries to delete it * @param toDelete */ private static void scheduledDeletion(final File toDelete,DCClient dcc) { final ScheduledExecutorService ses = dcc.getSchedulerDir(); if (!ses.isShutdown()) { ses.schedule(new Runnable() { public void run() { if (toDelete.isFile() && !toDelete.delete() && !ses.isShutdown()) { ses.schedule(this,30,TimeUnit.SECONDS); } } }, 30, TimeUnit.SECONDS); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((getID() == null) ? 0 : getID().hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final AbstractDownloadQueueEntry other = (AbstractDownloadQueueEntry) obj; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; if (getID() == null) { if (other.getID() != null) return false; } else if (!getID().equals(other.getID())) return false; return true; } /** * compares DQEs for what should be downloaded first * TODO may be also discriminate DQEs that can not be written to currently */ public int compareTo(AbstractDownloadQueueEntry arg0) { int i = -type.compareTo(arg0.type); if (i != 0) { return i; } i = -GH.compareTo(priority, arg0.priority); if (i != 0) { return i; } i = GH.compareTo(getNrOfOnlineUsers(), arg0.getNrOfOnlineUsers()); if (i != 0) { return i; } i = getFileName().compareTo(arg0.getFileName()); if (i != 0) { return i; } return GH.compareTo(getDownloadedBytes(), arg0.getDownloadedBytes()); } public TransferType getType() { return type; } public int getPriority() { return priority; } public void setPriority(int priority) { this.priority = priority; dq.notifyObservers(new StatusObject(this,ChangeType.CHANGED)); } DCClient getDCC() { return dq.getDcc(); } public Set<IUser> getUsers() { return Collections.unmodifiableSet(users); } public int getNrOfUsers() { return users.size(); } public int getNrOfOnlineUsers() { int count = 0; for (IUser u:users) { count += u.isOnline() ? 1 : 0 ; } return count; } public Set<IUser> getRemovedUsers() { return Collections.unmodifiableSet(removedUsers); } public List<IFileTransfer> getRunningFileTransfers() { return Collections.unmodifiableList(runningFileTransfers); } public String toString() { return getFileName()+" Users: "+getNrOfUsers(); } /** * * @return miilliseconds expected until finishing.. * */ public long getTimeRemaining() { long leftBytes = getSize()-getDownloadedBytes(); long totalSpeed = 0; for (IFileTransfer ft:runningFileTransfers) { totalSpeed += ft.getSpeed(); } if (totalSpeed == 0) { return Long.MAX_VALUE; } else { return leftBytes / totalSpeed; } } /** * determines the highest common folder of a list of Files.. * * @param dqes * @return */ public static File getCommonParent(List<AbstractDownloadQueueEntry> dqes) { HashSet<File> folders = new HashSet<File>(); for (AbstractDownloadQueueEntry dqe: dqes) { folders.add(dqe.getFolder()); } return getCommonParent(folders); } private static File getCommonParent(Set<File> files) { File currentCommon = null; for (File f: files) { if (currentCommon == null) { currentCommon = f; } else { currentCommon = commonParent(currentCommon,f); if (currentCommon == null) { return null; } } } return currentCommon; } private static boolean isParent(File folder, File possibleChild) { File parentFolder = possibleChild.getParentFile(); if (folder.equals(parentFolder)) { return true; } else if (parentFolder == null) { return false; } else { return isParent(folder, parentFolder); } } private static File commonParent(File f1, File f2) { if (f1.equals(f2)) { return f1; } else if (isParent(f1,f2)) { return f1; } else if (isParent(f2,f1)) { return f2; } else { File parf1 = f1.getParentFile(); File parf2 = f2.getParentFile(); if (parf1 != null && parf2 != null) { return commonParent(parf1, parf2); } else { return null; } } } DownloadQueue getDq() { return dq; } }