ch.cyberduck.core.Transfer.java Source code

Java tutorial

Introduction

Here is the source code for ch.cyberduck.core.Transfer.java

Source

package ch.cyberduck.core;

/*
 *  Copyright (c) 2005 David Kocher. All rights reserved.
 *  http://cyberduck.ch/
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  Bug fixes, suggestions and comments should be sent to:
 *  dkocher@cyberduck.ch
 */

import ch.cyberduck.core.i18n.Locale;
import ch.cyberduck.core.io.BandwidthThrottle;
import ch.cyberduck.core.serializer.Deserializer;
import ch.cyberduck.core.serializer.DeserializerFactory;
import ch.cyberduck.core.serializer.Serializer;
import ch.cyberduck.core.serializer.SerializerFactory;
import ch.cyberduck.ui.growl.Growl;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;

/**
 * @version $Id$
 */
public abstract class Transfer implements Serializable {
    private static Logger log = Logger.getLogger(Transfer.class);

    /**
     * Files and folders initially selected to be part of this transfer
     */
    protected List<Path> roots;

    /**
     * The sum of the file length of all files in the <code>queue</code>
     */
    protected double size = 0;

    /**
     * The number bytes already transferred of the ifles in the <code>queue</code>
     */
    protected double transferred = 0;

    /**
     * The transfer has been canceled and should
     * not continue any forther processing
     */
    private boolean canceled;

    // Backward compatibilty for serializaton
    public static final int KIND_DOWNLOAD = 0;
    public static final int KIND_UPLOAD = 1;
    public static final int KIND_SYNC = 2;

    protected Transfer() {
        ;
    }

    /**
     * @return True if in <code>canceled</code> state
     */
    public boolean isCanceled() {
        return canceled;
    }

    private boolean running;

    /**
     * @return True if in <code>running</code> state
     */
    public boolean isRunning() {
        return running;
    }

    private boolean queued;

    /**
     * @return True if in <code>queued</code> state
     */
    public boolean isQueued() {
        return queued;
    }

    private Session session;

    /**
     * The transfer has been reset
     */
    private boolean reset;

    /**
     * Last transfered in milliseconds
     */
    private Date timestamp;

    /**
     * @return
     */
    public abstract boolean isResumable();

    /**
     * Create a transfer with a single root which can
     * be a plain file or a directory
     *
     * @param root File or directory
     */
    public Transfer(Path root) {
        this(new Collection<Path>(Collections.<Path>singletonList(root)));
    }

    /**
     * @param items
     */
    public Transfer(List<Path> items) {
        this.roots = items;
        this.session = this.getRoot().getSession();
        // Intialize bandwidth setting
        this.init();
    }

    /**
     * Called from the constructor for initialization
     */
    protected abstract void init();

    public <T> Transfer(T dict, Session s) {
        this.session = s;
        this.init(dict);
    }

    public <T> void init(T serialized) {
        final Deserializer dict = DeserializerFactory.createDeserializer(serialized);
        final List rootsObj = dict.listForKey("Roots");
        if (rootsObj != null) {
            roots = new Collection<Path>();
            for (Object rootDict : rootsObj) {
                roots.add(PathFactory.createPath(this.session, rootDict));
            }
        }
        Object sizeObj = dict.stringForKey("Size");
        if (sizeObj != null) {
            this.size = Double.parseDouble(sizeObj.toString());
        }
        Object timestampObj = dict.stringForKey("Timestamp");
        if (timestampObj != null) {
            this.timestamp = new Date(Long.parseLong(timestampObj.toString()));
        }
        Object currentObj = dict.stringForKey("Current");
        if (currentObj != null) {
            this.transferred = Double.parseDouble(currentObj.toString());
        }
        this.init();
        Object bandwidthObj = dict.stringForKey("Bandwidth");
        if (bandwidthObj != null) {
            this.bandwidth.setRate(Float.parseFloat(bandwidthObj.toString()));
        }
    }

    public abstract <T> T getAsDictionary();

    public Serializer getSerializer() {
        final Serializer dict = SerializerFactory.createSerializer();
        dict.setObjectForKey(this.getSession().getHost(), "Host");
        dict.setListForKey(this.roots, "Roots");
        dict.setStringForKey(String.valueOf(this.getSize()), "Size");
        dict.setStringForKey(String.valueOf(this.getTransferred()), "Current");
        if (timestamp != null) {
            dict.setStringForKey(String.valueOf(timestamp.getTime()), "Timestamp");
        }
        if (bandwidth != null) {
            dict.setStringForKey(String.valueOf(bandwidth.getRate()), "Bandwidth");
        }
        return dict;
    }

    private Set<TransferListener> listeners = Collections.synchronizedSet(new HashSet<TransferListener>());

    /**
     * @param listener
     */
    public void addListener(TransferListener listener) {
        listeners.add(listener);
    }

    /**
     * @param listener
     */
    public void removeListener(TransferListener listener) {
        listeners.remove(listener);
    }

    protected void fireTransferWillStart() {
        if (log.isDebugEnabled()) {
            log.debug("fireTransferWillStart:" + this);
        }
        canceled = false;
        running = true;
        queued = false;
        for (TransferListener listener : listeners.toArray(new TransferListener[listeners.size()])) {
            listener.transferWillStart();
        }
    }

    public void fireTransferQueued() {
        if (log.isDebugEnabled()) {
            log.debug("fireTransferQueued:" + this);
        }
        final Session session = this.getSession();
        Growl.instance().notify("Transfer queued", session.getHost().getHostname());
        session.message(Locale.localizedString("Maximum allowed connections exceeded. Waiting", "Status"));
        queued = true;
        for (TransferListener listener : listeners.toArray(new TransferListener[listeners.size()])) {
            listener.transferQueued();
        }
    }

    public void fireTransferResumed() {
        if (log.isDebugEnabled()) {
            log.debug("fireTransferResumed:" + this);
        }
        queued = false;
        for (TransferListener listener : listeners.toArray(new TransferListener[listeners.size()])) {
            listener.transferResumed();
        }
    }

    protected void fireTransferDidEnd() {
        if (log.isDebugEnabled()) {
            log.debug("fireTransferDidEnd:" + this);
        }
        running = false;
        queued = false;
        timestamp = new Date();
        for (TransferListener listener : listeners.toArray(new TransferListener[listeners.size()])) {
            listener.transferDidEnd();
        }
        Queue.instance().remove(this);
    }

    protected void fireWillTransferPath(Path path) {
        for (TransferListener listener : listeners.toArray(new TransferListener[listeners.size()])) {
            listener.willTransferPath(path);
        }
    }

    protected void fireDidTransferPath(Path path) {
        for (TransferListener listener : listeners.toArray(new TransferListener[listeners.size()])) {
            listener.didTransferPath(path);
        }
    }

    /**
     * In Bytes per second
     */
    protected BandwidthThrottle bandwidth;

    /**
     * @param bytesPerSecond
     */
    public void setBandwidth(float bytesPerSecond) {
        log.debug("setBandwidth:" + bytesPerSecond);
        bandwidth.setRate(bytesPerSecond);
        for (TransferListener listener : listeners.toArray(new TransferListener[listeners.size()])) {
            listener.bandwidthChanged(bandwidth);
        }
    }

    /**
     * @return
     */
    public Date getTimestamp() {
        return timestamp;
    }

    /**
     * @return Rate in bytes per second allowed for this transfer
     */
    public float getBandwidth() {
        return bandwidth.getRate();
    }

    /**
     * @return The first <code>root</code> added to this transfer
     */
    public Path getRoot() {
        return roots.get(0);
    }

    /**
     * @return All <code>root</code>s added to this transfer
     */
    public List<Path> getRoots() {
        return this.roots;
    }

    public void setRoots(List<Path> roots) {
        this.roots = roots;
    }

    /**
     * Normalize path names and remove duplicates.
     */
    protected abstract void normalize();

    public Session getSession() {
        return this.session;
    }

    /**
     * @return The concatenation of the local filenames of all roots
     * @see #getRoots()
     */
    public String getName() {
        String name = StringUtils.EMPTY;
        for (Path next : this.roots) {
            name = name + next.getLocal().getName() + " ";
        }
        return name;
    }

    protected abstract static class TransferFilter implements PathFilter<Path> {
        /**
         * Called before the file will actually get transferred. Should prepare for the transfer
         * such as calculating its size.
         * Must only be called exactly once for each file.
         * Must only be called if #accept for the file returns true
         *
         * @param p
         * @see PathFilter#accept(AbstractPath)
         */
        public abstract void prepare(Path p);

        /**
         * Post processing.
         *
         * @param p
         */
        public abstract void complete(Path p);
    }

    /**
     * @param action
     * @return Null if the filter could not be determined and the transfer should be canceled instead
     */
    public TransferFilter filter(final TransferAction action) {
        if (action.equals(TransferAction.ACTION_CANCEL)) {
            return null;
        }
        throw new IllegalArgumentException("Unknown transfer action:" + action);
    }

    /**
     * @param resumeRequested
     * @param reloadRequested
     * @return
     */
    public abstract TransferAction action(final boolean resumeRequested, final boolean reloadRequested);

    /**
     * Lookup the path by reference in the session cache
     *
     * @param r
     * @return
     * @see ch.cyberduck.core.Cache#lookup(PathReference)
     */
    public Path lookup(PathReference r) {
        for (Path root : roots) {
            if (r.equals(root.<Object>getReference())) {
                return root;
            }
        }
        return this.cache().lookup(r);
    }

    /**
     * Returns the children of this path filtering it with the default regex filter
     *
     * @param parent The directory to list the children
     * @return A list of child items
     */
    public abstract AttributedList<Path> children(final Path parent);

    /**
     * @param item
     * @return True if the path is not skipped when transferring
     */
    public boolean isIncluded(Path item) {
        return item.status().isSelected() && !this.isSkipped(item);
    }

    /**
     * @param item
     * @return
     */
    public boolean isSkipped(Path item) {
        return false;
    }

    /**
     * Select the path to be included in the transfer
     *
     * @param item
     * @param selected
     */
    public void setSelected(Path item, final boolean selected) {
        item.status().setSelected(selected);
        if (item.attributes().isDirectory()) {
            if (item.isCached()) {
                for (Path child : this.children(item)) {
                    this.setSelected(child, selected);
                }
            }
        }
    }

    /**
     * The current path being transferred
     */
    private Path _current = null;

    /**
     * @param p
     * @param filter
     */
    private void transfer(final Path p, final TransferFilter filter) {
        if (!this.isIncluded(p)) {
            if (log.isInfoEnabled()) {
                log.info("Not included in transfer:" + p);
            }
            p.status().setComplete(true);
            return;
        }

        if (!this.check()) {
            return;
        }

        if (filter.accept(p)) {
            // Notification
            this.fireWillTransferPath(p);
            _current = p;
            // Reset transfer status
            p.status().reset();
            // Transfer
            transfer(p);
            // Post process of file
            filter.complete(p);
            _current = null;
            // Notification
            this.fireDidTransferPath(p);
        }

        if (!this.check()) {
            return;
        }

        if (p.attributes().isDirectory()) {
            p.status().reset();
            boolean failure = false;
            final AttributedList<Path> children = this.children(p);
            if (!children.attributes().isReadable()) {
                failure = true;
            }
            for (Path child : children) {
                this.transfer(child, filter);
                if (!child.status().isComplete()) {
                    failure = true;
                }
            }
            if (!failure) {
                p.status().setComplete(true);
            }
            this.cache().remove(p.getReference());
        }
    }

    /**
     * The actual transfer implementation
     *
     * @param file
     * @see ch.cyberduck.core.Path#download()
     * @see ch.cyberduck.core.Path#upload()
     */
    protected abstract void transfer(final Path file);

    /**
     * @param options
     */
    private void transfer(final TransferOptions options) {
        final Session session = this.getSession();
        try {
            try {
                log.debug("Checking connnection");
                // We manually open the connection here first as otherwise
                // every transfer will try again if it should fail
                session.check();
            } catch (IOException e) {
                log.warn(e.getMessage());
                return;
            }

            if (!this.check()) {
                return;
            }

            if (!options.invalidateCache) {
                // Do not invalidate cache entries during file transfers
                session.cache().setLifecycle(Cache.Lifecycle.FOREVER);
            }
            // Determine the filter to match files against
            final TransferAction action = this.action(options.resumeRequested, options.reloadRequested);
            if (action.equals(TransferAction.ACTION_CANCEL)) {
                if (log.isInfoEnabled()) {
                    log.info("Transfer canceled by user:" + this);
                }
                this.cancel();
                return;
            }

            this.clear(options);

            this.normalize();

            if (!this.check()) {
                return;
            }

            // Get the transfer filter from the concret transfer class
            final TransferFilter filter = this.filter(action);
            if (null == filter) {
                // The user has canceled choosing a transfer filter
                this.cancel();
                return;
            }

            // Reset the cached size of the transfer and progress value
            this.reset();

            // Calculate information about the files in advance to give progress information
            for (Path next : roots) {
                this.prepare(next, filter);
            }

            // Transfer all files sequentially
            for (Path next : roots) {
                this.transfer(next, filter);
            }
        } finally {
            this.clear(options);
            if (options.closeSession) {
                session.close();
            }
        }
    }

    /**
     * To be called before any file is actually transferred
     *
     * @param p
     * @param filter
     */
    private void prepare(Path p, final TransferFilter filter) {
        log.debug("prepare:" + p);
        if (!this.check()) {
            return;
        }

        if (!this.isIncluded(p)) {
            if (log.isInfoEnabled()) {
                log.info("Not included in transfer:" + p);
            }
            return;
        }

        // Only prepare the path it will be actually transferred
        if (filter.accept(p)) {
            if (log.isInfoEnabled()) {
                log.info("Accepted in transfer:" + p);
            }
            this.getSession()
                    .message(MessageFormat.format(Locale.localizedString("Prepare {0}", "Status"), p.getName()));
            filter.prepare(p);
        }

        if (p.attributes().isDirectory()) {
            // Call recursively for all children
            for (Path child : this.children(p)) {
                this.prepare(child, filter);
            }
        }
    }

    /**
     * @return False if the transfer has been canceled or the socket is
     *         no longer connected
     */
    protected boolean check() {
        if (log.isDebugEnabled()) {
            log.debug("check:" + this);
        }
        boolean connected = this.getSession().isConnected();
        if (!connected) {
            // Bail out if no more connected
            log.warn("Disconnected transfer in progress:" + this);
            return false;
        }
        // Bail out if canceled
        boolean canceled = this.isCanceled();
        if (canceled) {
            log.warn("Canceled transfer in progress:" + this);
        }
        return !canceled;
    }

    /**
     * Clear all cached values
     */
    protected void clear(final TransferOptions options) {
        log.debug("clear:" + options);
        if (options.closeSession) {
            // We have our own session independent of any browser.
            this.cache().clear();
        }
    }

    /**
     * @return The cache of the underlying session
     * @see Session#cache()
     */
    public Cache<Path> cache() {
        return this.getSession().cache();
    }

    /**
     * Use default transfer options
     *
     * @param prompt
     */
    public void start(TransferPrompt prompt) {
        this.start(prompt, TransferOptions.DEFAULT);
    }

    /**
     *
     */
    protected TransferPrompt prompt;

    /**
     * @param prompt
     * @param options
     */
    public void start(TransferPrompt prompt, final TransferOptions options) {
        if (log.isDebugEnabled()) {
            log.debug("start:" + prompt);
        }
        this.prompt = prompt;
        try {
            this.fireTransferWillStart();
            this.queue();
            if (this.isCanceled()) {
                // The transfer has been canceled while being queued
                return;
            }
            this.transfer(options);
        } finally {
            this.prompt = null;
            this.fireTransferDidEnd();
        }
    }

    private synchronized void queue() {
        if (log.isDebugEnabled()) {
            log.debug("queue:" + this);
        }
        Queue.instance().add(this);
    }

    /**
     * @see Session#interrupt()
     */
    public void interrupt() {
        if (log.isDebugEnabled()) {
            log.debug("interrupt:" + this);
        }
        this.getSession().interrupt();
    }

    /**
     * Marks all items in the queue as canceled. Canceled items will be
     * skipped when processed. If the transfer is already in a <code>canceled</code>
     * state, the underlying session's socket is interrupted to force exit.
     */
    public void cancel() {
        if (log.isDebugEnabled()) {
            log.debug("cancel:" + this);
        }
        if (_current != null) {
            _current.status().setCanceled();
        }
        if (this.isCanceled()) {
            // Called prevously; now force
            this.interrupt();
        }
        canceled = true;
        Queue.instance().remove(this);
    }

    /**
     * Called before remove from the transfer queue
     */
    public void cleanup() {
        ;
    }

    /**
     * Recalculate the size of the <code>queue</code>
     */
    protected void reset() {
        if (log.isDebugEnabled()) {
            log.debug("reset:" + this);
        }
        this.transferred = 0;
        this.size = 0;
        this.reset = true;
    }

    /**
     * @return
     */
    public boolean isReset() {
        return reset;
    }

    /**
     * @return The number of roots
     */
    public int numberOfRoots() {
        return this.roots.size();
    }

    /**
     * @return True if the bytes transferred equal the size of the queue and
     *         the bytes transfered is > 0
     */
    public boolean isComplete() {
        for (Path root : this.roots) {
            if (!root.status().isComplete()) {
                return false;
            }
        }
        return true;
    }

    /**
     * @return The sum of all file lengths in this transfer.
     */
    public double getSize() {
        return size;
    }

    public void setSize(double size) {
        this.size = size;
    }

    /**
     * @return The number of bytes transfered of all items in this <code>transfer</code>
     */
    public double getTransferred() {
        return transferred;
    }

    public void setTransferred(double transferred) {
        this.transferred = transferred;
    }
}