001/**
002 *
003 * Copyright 2003-2006 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.filetransfer;
018
019import org.jivesoftware.smack.Manager;
020import org.jivesoftware.smack.SmackException.NotConnectedException;
021import org.jivesoftware.smack.XMPPConnection;
022import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
023import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
024import org.jivesoftware.smack.packet.IQ;
025import org.jivesoftware.smack.packet.XMPPError;
026import org.jivesoftware.smackx.si.packet.StreamInitiation;
027import org.jxmpp.jid.EntityFullJid;
028
029import java.util.List;
030import java.util.Map;
031import java.util.WeakHashMap;
032import java.util.concurrent.CopyOnWriteArrayList;
033
034/**
035 * The file transfer manager class handles the sending and recieving of files.
036 * To send a file invoke the {@link #createOutgoingFileTransfer(EntityFullJid)} method.
037 * <p>
038 * And to recieve a file add a file transfer listener to the manager. The
039 * listener will notify you when there is a new file transfer request. To create
040 * the {@link IncomingFileTransfer} object accept the transfer, or, if the
041 * transfer is not desirable reject it.
042 * 
043 * @author Alexander Wenckus
044 * 
045 */
046public final class FileTransferManager extends Manager {
047
048    private static final Map<XMPPConnection, FileTransferManager> INSTANCES = new WeakHashMap<XMPPConnection, FileTransferManager>();
049
050    public static synchronized FileTransferManager getInstanceFor(XMPPConnection connection) {
051        FileTransferManager fileTransferManager = INSTANCES.get(connection);
052        if (fileTransferManager == null) {
053            fileTransferManager = new FileTransferManager(connection);
054            INSTANCES.put(connection, fileTransferManager);
055        }
056        return fileTransferManager;
057    }
058
059    private final FileTransferNegotiator fileTransferNegotiator;
060
061    private final List<FileTransferListener> listeners = new CopyOnWriteArrayList<FileTransferListener>();
062
063    /**
064     * Creates a file transfer manager to initiate and receive file transfers.
065     * 
066     * @param connection
067     *            The XMPPConnection that the file transfers will use.
068     */
069    private FileTransferManager(XMPPConnection connection) {
070        super(connection);
071        this.fileTransferNegotiator = FileTransferNegotiator
072                .getInstanceFor(connection);
073        connection.registerIQRequestHandler(new AbstractIqRequestHandler(StreamInitiation.ELEMENT,
074                        StreamInitiation.NAMESPACE, IQ.Type.set, Mode.async) {
075            @Override
076            public IQ handleIQRequest(IQ packet) {
077                StreamInitiation si = (StreamInitiation) packet;
078                final FileTransferRequest request = new FileTransferRequest(FileTransferManager.this, si);
079                for (final FileTransferListener listener : listeners) {
080                            listener.fileTransferRequest(request);
081                }
082                return null;
083            }
084        });
085    }
086
087    /**
088     * Add a file transfer listener to listen to incoming file transfer
089     * requests.
090     * 
091     * @param li
092     *            The listener
093     * @see #removeFileTransferListener(FileTransferListener)
094     * @see FileTransferListener
095     */
096    public void addFileTransferListener(final FileTransferListener li) {
097        listeners.add(li);
098    }
099
100    /**
101     * Removes a file transfer listener.
102     * 
103     * @param li
104     *            The file transfer listener to be removed
105     * @see FileTransferListener
106     */
107    public void removeFileTransferListener(final FileTransferListener li) {
108        listeners.remove(li);
109    }
110
111    /**
112     * Creates an OutgoingFileTransfer to send a file to another user.
113     * 
114     * @param userID
115     *            The fully qualified jabber ID (i.e. full JID) with resource of the user to
116     *            send the file to.
117     * @return The send file object on which the negotiated transfer can be run.
118     * @exception IllegalArgumentException if userID is null or not a full JID
119     */
120    public OutgoingFileTransfer createOutgoingFileTransfer(EntityFullJid userID) {
121        // We need to create outgoing file transfers with a full JID since this method will later
122        // use XEP-0095 to negotiate the stream. This is done with IQ stanzas that need to be addressed to a full JID
123        // in order to reach an client entity.
124        if (userID == null) {
125            throw new IllegalArgumentException("userID was null");
126        }
127
128        return new OutgoingFileTransfer(connection().getUser(), userID,
129                FileTransferNegotiator.getNextStreamID(),
130                fileTransferNegotiator);
131    }
132
133    /**
134     * When the file transfer request is acceptable, this method should be
135     * invoked. It will create an IncomingFileTransfer which allows the
136     * transmission of the file to procede.
137     * 
138     * @param request
139     *            The remote request that is being accepted.
140     * @return The IncomingFileTransfer which manages the download of the file
141     *         from the transfer initiator.
142     */
143    protected IncomingFileTransfer createIncomingFileTransfer(
144            FileTransferRequest request) {
145        if (request == null) {
146            throw new NullPointerException("RecieveRequest cannot be null");
147        }
148
149        IncomingFileTransfer transfer = new IncomingFileTransfer(request,
150                fileTransferNegotiator);
151        transfer.setFileInfo(request.getFileName(), request.getFileSize());
152
153        return transfer;
154    }
155
156    /**
157     * Reject an incoming file transfer.
158     * <p>
159     * Specified in XEP-95 4.2 and 3.2 Example 8
160     * </p>
161     * @param request
162     * @throws NotConnectedException
163     * @throws InterruptedException 
164     */
165    protected void rejectIncomingFileTransfer(FileTransferRequest request) throws NotConnectedException, InterruptedException {
166        StreamInitiation initiation = request.getStreamInitiation();
167
168        // Reject as specified in XEP-95 4.2. Note that this is not to be confused with the Socks 5
169        // Bytestream rejection as specified in XEP-65 5.3.1 Example 13, which says that
170        // 'not-acceptable' should be returned. This is done by Smack in
171        // Socks5BytestreamManager.replyRejectPacket(IQ).
172        IQ rejection = IQ.createErrorResponse(initiation, XMPPError.getBuilder(
173                        XMPPError.Condition.forbidden));
174        connection().sendStanza(rejection);
175    }
176}