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