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.util.XmppStringUtils;
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(String)} 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 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(String userID) {
121        if (userID == null) {
122            throw new IllegalArgumentException("userID was null");
123        }
124        // We need to create outgoing file transfers with a full JID since this method will later
125        // use XEP-0095 to negotiate the stream. This is done with IQ stanzas that need to be addressed to a full JID
126        // in order to reach an client entity.
127        else if (!XmppStringUtils.isFullJID(userID)) {
128            throw new IllegalArgumentException("The provided user id was not a full JID (i.e. with resource part)");
129        }
130
131                return new OutgoingFileTransfer(connection().getUser(), userID,
132                                fileTransferNegotiator.getNextStreamID(),
133                                fileTransferNegotiator);
134        }
135
136        /**
137         * When the file transfer request is acceptable, this method should be
138         * invoked. It will create an IncomingFileTransfer which allows the
139         * transmission of the file to procede.
140         * 
141         * @param request
142         *            The remote request that is being accepted.
143         * @return The IncomingFileTransfer which manages the download of the file
144         *         from the transfer initiator.
145         */
146        protected IncomingFileTransfer createIncomingFileTransfer(
147                        FileTransferRequest request) {
148                if (request == null) {
149                        throw new NullPointerException("RecieveRequest cannot be null");
150                }
151
152                IncomingFileTransfer transfer = new IncomingFileTransfer(request,
153                fileTransferNegotiator);
154                transfer.setFileInfo(request.getFileName(), request.getFileSize());
155
156                return transfer;
157        }
158
159        /**
160         * Reject an incoming file transfer.
161         * <p>
162         * Specified in XEP-95 4.2 and 3.2 Example 8
163         * </p>
164         * @param request
165         * @throws NotConnectedException
166         */
167        protected void rejectIncomingFileTransfer(FileTransferRequest request) throws NotConnectedException {
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, new XMPPError(
175                        XMPPError.Condition.forbidden));
176        connection().sendStanza(rejection);
177        }
178}