FileTransferManager.java

/**
 *
 * Copyright 2003-2006 Jive Software.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.filetransfer;

import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.si.packet.StreamInitiation;
import org.jxmpp.jid.FullJid;

import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * The file transfer manager class handles the sending and recieving of files.
 * To send a file invoke the {@link #createOutgoingFileTransfer(FullJid)} method.
 * <p>
 * And to recieve a file add a file transfer listener to the manager. The
 * listener will notify you when there is a new file transfer request. To create
 * the {@link IncomingFileTransfer} object accept the transfer, or, if the
 * transfer is not desirable reject it.
 * 
 * @author Alexander Wenckus
 * 
 */
public class FileTransferManager extends Manager {

    private static final Map<XMPPConnection, FileTransferManager> INSTANCES = new WeakHashMap<XMPPConnection, FileTransferManager>();

    public static synchronized FileTransferManager getInstanceFor(XMPPConnection connection) {
        FileTransferManager fileTransferManager = INSTANCES.get(connection);
        if (fileTransferManager == null) {
            fileTransferManager = new FileTransferManager(connection);
            INSTANCES.put(connection, fileTransferManager);
        }
        return fileTransferManager;
    }

	private final FileTransferNegotiator fileTransferNegotiator;

	private final List<FileTransferListener> listeners = new CopyOnWriteArrayList<FileTransferListener>();

	/**
	 * Creates a file transfer manager to initiate and receive file transfers.
	 * 
	 * @param connection
	 *            The XMPPConnection that the file transfers will use.
	 */
	private FileTransferManager(XMPPConnection connection) {
		super(connection);
		this.fileTransferNegotiator = FileTransferNegotiator
				.getInstanceFor(connection);
        connection.registerIQRequestHandler(new AbstractIqRequestHandler(StreamInitiation.ELEMENT,
                        StreamInitiation.NAMESPACE, IQ.Type.set, Mode.async) {
            @Override
            public IQ handleIQRequest(IQ packet) {
                StreamInitiation si = (StreamInitiation) packet;
                final FileTransferRequest request = new FileTransferRequest(FileTransferManager.this, si);
                for (final FileTransferListener listener : listeners) {
                            listener.fileTransferRequest(request);
                }
                return null;
            }
        });
	}

	/**
	 * Add a file transfer listener to listen to incoming file transfer
	 * requests.
	 * 
	 * @param li
	 *            The listener
	 * @see #removeFileTransferListener(FileTransferListener)
	 * @see FileTransferListener
	 */
	public void addFileTransferListener(final FileTransferListener li) {
		listeners.add(li);
	}

	/**
	 * Removes a file transfer listener.
	 * 
	 * @param li
	 *            The file transfer listener to be removed
	 * @see FileTransferListener
	 */
	public void removeFileTransferListener(final FileTransferListener li) {
		listeners.remove(li);
	}

	/**
	 * Creates an OutgoingFileTransfer to send a file to another user.
	 * 
	 * @param userID
	 *            The fully qualified jabber ID (i.e. full JID) with resource of the user to
	 *            send the file to.
	 * @return The send file object on which the negotiated transfer can be run.
	 * @exception IllegalArgumentException if userID is null or not a full JID
	 */
	public OutgoingFileTransfer createOutgoingFileTransfer(FullJid userID) {
        // We need to create outgoing file transfers with a full JID since this method will later
        // use XEP-0095 to negotiate the stream. This is done with IQ stanzas that need to be addressed to a full JID
        // in order to reach an client entity.
        if (userID == null) {
            throw new IllegalArgumentException("userID was null");
        }

		return new OutgoingFileTransfer(connection().getUser(), userID,
				fileTransferNegotiator.getNextStreamID(),
				fileTransferNegotiator);
	}

	/**
	 * When the file transfer request is acceptable, this method should be
	 * invoked. It will create an IncomingFileTransfer which allows the
	 * transmission of the file to procede.
	 * 
	 * @param request
	 *            The remote request that is being accepted.
	 * @return The IncomingFileTransfer which manages the download of the file
	 *         from the transfer initiator.
	 */
	protected IncomingFileTransfer createIncomingFileTransfer(
			FileTransferRequest request) {
		if (request == null) {
			throw new NullPointerException("RecieveRequest cannot be null");
		}

		IncomingFileTransfer transfer = new IncomingFileTransfer(request,
                fileTransferNegotiator);
		transfer.setFileInfo(request.getFileName(), request.getFileSize());

		return transfer;
	}

	/**
	 * Reject an incoming file transfer.
	 * <p>
	 * Specified in XEP-95 4.2 and 3.2 Example 8
	 * </p>
	 * @param request
	 * @throws NotConnectedException
	 * @throws InterruptedException 
	 */
	protected void rejectIncomingFileTransfer(FileTransferRequest request) throws NotConnectedException, InterruptedException {
		StreamInitiation initiation = request.getStreamInitiation();

        // Reject as specified in XEP-95 4.2. Note that this is not to be confused with the Socks 5
        // Bytestream rejection as specified in XEP-65 5.3.1 Example 13, which says that
        // 'not-acceptable' should be returned. This is done by Smack in
        // Socks5BytestreamManager.replyRejectPacket(IQ).
        IQ rejection = IQ.createErrorResponse(initiation, new XMPPError(
                        XMPPError.Condition.forbidden));
        connection().sendStanza(rejection);
	}
}