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.PacketListener;
020import org.jivesoftware.smack.SmackException.NotConnectedException;
021import org.jivesoftware.smack.XMPPConnection;
022import org.jivesoftware.smack.filter.AndFilter;
023import org.jivesoftware.smack.filter.IQTypeFilter;
024import org.jivesoftware.smack.filter.PacketTypeFilter;
025import org.jivesoftware.smack.packet.IQ;
026import org.jivesoftware.smack.packet.Packet;
027import org.jivesoftware.smack.packet.XMPPError;
028import org.jivesoftware.smack.util.StringUtils;
029import org.jivesoftware.smackx.si.packet.StreamInitiation;
030
031import java.util.ArrayList;
032import java.util.List;
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 {
047
048        private final FileTransferNegotiator fileTransferNegotiator;
049
050        private List<FileTransferListener> listeners;
051
052        private XMPPConnection connection;
053
054        /**
055         * Creates a file transfer manager to initiate and receive file transfers.
056         * 
057         * @param connection
058         *            The XMPPConnection that the file transfers will use.
059         */
060        public FileTransferManager(XMPPConnection connection) {
061                this.connection = connection;
062                this.fileTransferNegotiator = FileTransferNegotiator
063                                .getInstanceFor(connection);
064        }
065
066        /**
067         * Add a file transfer listener to listen to incoming file transfer
068         * requests.
069         * 
070         * @param li
071         *            The listener
072         * @see #removeFileTransferListener(FileTransferListener)
073         * @see FileTransferListener
074         */
075        public void addFileTransferListener(final FileTransferListener li) {
076                if (listeners == null) {
077                        initListeners();
078                }
079                synchronized (this.listeners) {
080                        listeners.add(li);
081                }
082        }
083
084        private void initListeners() {
085                listeners = new ArrayList<FileTransferListener>();
086
087                connection.addPacketListener(new PacketListener() {
088                        public void processPacket(Packet packet) {
089                                fireNewRequest((StreamInitiation) packet);
090                        }
091                }, new AndFilter(new PacketTypeFilter(StreamInitiation.class),
092                                new IQTypeFilter(IQ.Type.SET)));
093        }
094
095        protected void fireNewRequest(StreamInitiation initiation) {
096                FileTransferListener[] listeners = null;
097                synchronized (this.listeners) {
098                        listeners = new FileTransferListener[this.listeners.size()];
099                        this.listeners.toArray(listeners);
100                }
101                FileTransferRequest request = new FileTransferRequest(this, initiation);
102                for (int i = 0; i < listeners.length; i++) {
103                        listeners[i].fileTransferRequest(request);
104                }
105        }
106
107        /**
108         * Removes a file transfer listener.
109         * 
110         * @param li
111         *            The file transfer listener to be removed
112         * @see FileTransferListener
113         */
114        public void removeFileTransferListener(final FileTransferListener li) {
115                if (listeners == null) {
116                        return;
117                }
118                synchronized (this.listeners) {
119                        listeners.remove(li);
120                }
121        }
122
123        /**
124         * Creates an OutgoingFileTransfer to send a file to another user.
125         * 
126         * @param userID
127         *            The fully qualified jabber ID (i.e. full JID) with resource of the user to
128         *            send the file to.
129         * @return The send file object on which the negotiated transfer can be run.
130         * @exception IllegalArgumentException if userID is null or not a full JID
131         */
132        public OutgoingFileTransfer createOutgoingFileTransfer(String userID) {
133        if (userID == null) {
134            throw new IllegalArgumentException("userID was null");
135        }
136        // We need to create outgoing file transfers with a full JID since this method will later
137        // use XEP-0095 to negotiate the stream. This is done with IQ stanzas that need to be addressed to a full JID
138        // in order to reach an client entity.
139        else if (!StringUtils.isFullJID(userID)) {
140            throw new IllegalArgumentException("The provided user id was not a full JID (i.e. with resource part)");
141        }
142
143                return new OutgoingFileTransfer(connection.getUser(), userID,
144                                fileTransferNegotiator.getNextStreamID(),
145                                fileTransferNegotiator);
146        }
147
148        /**
149         * When the file transfer request is acceptable, this method should be
150         * invoked. It will create an IncomingFileTransfer which allows the
151         * transmission of the file to procede.
152         * 
153         * @param request
154         *            The remote request that is being accepted.
155         * @return The IncomingFileTransfer which manages the download of the file
156         *         from the transfer initiator.
157         */
158        protected IncomingFileTransfer createIncomingFileTransfer(
159                        FileTransferRequest request) {
160                if (request == null) {
161                        throw new NullPointerException("RecieveRequest cannot be null");
162                }
163
164                IncomingFileTransfer transfer = new IncomingFileTransfer(request,
165                fileTransferNegotiator);
166                transfer.setFileInfo(request.getFileName(), request.getFileSize());
167
168                return transfer;
169        }
170
171        /**
172         * Reject an incoming file transfer.
173         * <p>
174         * Specified in XEP-95 4.2 and 3.2 Example 8
175         * </p>
176         * @param request
177         * @throws NotConnectedException
178         */
179        protected void rejectIncomingFileTransfer(FileTransferRequest request) throws NotConnectedException {
180                StreamInitiation initiation = request.getStreamInitiation();
181
182                IQ rejection = FileTransferNegotiator.createIQ(
183                                initiation.getPacketID(), initiation.getFrom(), initiation
184                                                .getTo(), IQ.Type.ERROR);
185        // Reject as specified in XEP-95 4.2. Note that this is not to be confused with the Socks 5
186        // Bytestream rejection as specified in XEP-65 5.3.1 Example 13, which says that
187        // 'not-acceptable' should be returned. This is done by Smack in
188        // Socks5BytestreamManager.replyRejectPacket(IQ).
189                rejection.setError(new XMPPError(XMPPError.Condition.forbidden));
190                connection.sendPacket(rejection);
191        }
192}