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.SmackException;
020import org.jivesoftware.smack.SmackException.IllegalStateChangeException;
021import org.jivesoftware.smack.XMPPException;
022import org.jivesoftware.smack.XMPPException.XMPPErrorException;
023import org.jivesoftware.smack.packet.XMPPError;
024
025import java.io.*;
026
027/**
028 * Handles the sending of a file to another user. File transfer's in jabber have
029 * several steps and there are several methods in this class that handle these
030 * steps differently.
031 *
032 * @author Alexander Wenckus
033 *
034 */
035public class OutgoingFileTransfer extends FileTransfer {
036
037        private static int RESPONSE_TIMEOUT = 60 * 1000;
038    private NegotiationProgress callback;
039
040    /**
041     * Returns the time in milliseconds after which the file transfer
042     * negotiation process will timeout if the other user has not responded.
043     *
044     * @return Returns the time in milliseconds after which the file transfer
045     *         negotiation process will timeout if the remote user has not
046     *         responded.
047     */
048    public static int getResponseTimeout() {
049        return RESPONSE_TIMEOUT;
050    }
051
052        /**
053         * Sets the time in milliseconds after which the file transfer negotiation
054         * process will timeout if the other user has not responded.
055         *
056         * @param responseTimeout
057         *            The timeout time in milliseconds.
058         */
059        public static void setResponseTimeout(int responseTimeout) {
060                RESPONSE_TIMEOUT = responseTimeout;
061        }
062
063        private OutputStream outputStream;
064
065        private String initiator;
066
067        private Thread transferThread;
068
069        protected OutgoingFileTransfer(String initiator, String target,
070                        String streamID, FileTransferNegotiator transferNegotiator) {
071                super(target, streamID, transferNegotiator);
072                this.initiator = initiator;
073        }
074
075        protected void setOutputStream(OutputStream stream) {
076                if (outputStream == null) {
077                        this.outputStream = stream;
078                }
079        }
080
081        /**
082         * Returns the output stream connected to the peer to transfer the file. It
083         * is only available after it has been successfully negotiated by the
084         * {@link StreamNegotiator}.
085         *
086         * @return Returns the output stream connected to the peer to transfer the
087         *         file.
088         */
089        protected OutputStream getOutputStream() {
090                if (getStatus().equals(FileTransfer.Status.negotiated)) {
091                        return outputStream;
092                } else {
093                        return null;
094                }
095        }
096
097        /**
098         * This method handles the negotiation of the file transfer and the stream,
099         * it only returns the created stream after the negotiation has been completed.
100         *
101         * @param fileName
102         *            The name of the file that will be transmitted. It is
103         *            preferable for this name to have an extension as it will be
104         *            used to determine the type of file it is.
105         * @param fileSize
106         *            The size in bytes of the file that will be transmitted.
107         * @param description
108         *            A description of the file that will be transmitted.
109         * @return The OutputStream that is connected to the peer to transmit the
110         *         file.
111         * @throws XMPPException
112         *             Thrown if an error occurs during the file transfer
113         *             negotiation process.
114         * @throws SmackException if there was no response from the server.
115         */
116        public synchronized OutputStream sendFile(String fileName, long fileSize,
117                        String description) throws XMPPException, SmackException {
118                if (isDone() || outputStream != null) {
119                        throw new IllegalStateException(
120                                        "The negotation process has already"
121                                                        + " been attempted on this file transfer");
122                }
123                try {
124                        setFileInfo(fileName, fileSize);
125                        this.outputStream = negotiateStream(fileName, fileSize, description);
126                } catch (XMPPErrorException e) {
127                        handleXMPPException(e);
128                        throw e;
129                }
130                return outputStream;
131        }
132
133        /**
134         * This methods handles the transfer and stream negotiation process. It
135         * returns immediately and its progress will be updated through the
136         * {@link NegotiationProgress} callback.
137         *
138         * @param fileName
139         *            The name of the file that will be transmitted. It is
140         *            preferable for this name to have an extension as it will be
141         *            used to determine the type of file it is.
142         * @param fileSize
143         *            The size in bytes of the file that will be transmitted.
144         * @param description
145         *            A description of the file that will be transmitted.
146         * @param progress
147         *            A callback to monitor the progress of the file transfer
148         *            negotiation process and to retrieve the OutputStream when it
149         *            is complete.
150         */
151        public synchronized void sendFile(final String fileName,
152                        final long fileSize, final String description,
153                        final NegotiationProgress progress)
154    {
155        if(progress == null) {
156            throw new IllegalArgumentException("Callback progress cannot be null.");
157        }
158        checkTransferThread();
159                if (isDone() || outputStream != null) {
160                        throw new IllegalStateException(
161                                        "The negotation process has already"
162                                                        + " been attempted for this file transfer");
163                }
164        setFileInfo(fileName, fileSize);
165        this.callback = progress;
166        transferThread = new Thread(new Runnable() {
167                        public void run() {
168                                try {
169                                        OutgoingFileTransfer.this.outputStream = negotiateStream(
170                                                        fileName, fileSize, description);
171                    progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream);
172                }
173                catch (XMPPErrorException e) {
174                                        handleXMPPException(e);
175                                }
176                catch (Exception e) {
177                    setException(e);
178                }
179                        }
180                }, "File Transfer Negotiation " + streamID);
181                transferThread.start();
182        }
183
184        private void checkTransferThread() {
185                if (transferThread != null && transferThread.isAlive() || isDone()) {
186                        throw new IllegalStateException(
187                                        "File transfer in progress or has already completed.");
188                }
189        }
190
191    /**
192         * This method handles the stream negotiation process and transmits the file
193         * to the remote user. It returns immediately and the progress of the file
194         * transfer can be monitored through several methods:
195         *
196         * <UL>
197         * <LI>{@link FileTransfer#getStatus()}
198         * <LI>{@link FileTransfer#getProgress()}
199         * <LI>{@link FileTransfer#isDone()}
200         * </UL>
201         *
202     * @param file the file to transfer to the remote entity.
203     * @param description a description for the file to transfer.
204         * @throws SmackException
205         *             If there is an error during the negotiation process or the
206         *             sending of the file.
207         */
208        public synchronized void sendFile(final File file, final String description)
209                        throws SmackException {
210                checkTransferThread();
211                if (file == null || !file.exists() || !file.canRead()) {
212                        throw new IllegalArgumentException("Could not read file");
213                } else {
214                        setFileInfo(file.getAbsolutePath(), file.getName(), file.length());
215                }
216
217                transferThread = new Thread(new Runnable() {
218                        public void run() {
219                                try {
220                                        outputStream = negotiateStream(file.getName(), file
221                                                        .length(), description);
222                                } catch (XMPPErrorException e) {
223                                        handleXMPPException(e);
224                                        return;
225                                }
226                catch (Exception e) {
227                    setException(e);
228                }
229                                if (outputStream == null) {
230                                        return;
231                                }
232
233                if (!updateStatus(Status.negotiated, Status.in_progress)) {
234                                        return;
235                                }
236
237                                InputStream inputStream = null;
238                                try {
239                                        inputStream = new FileInputStream(file);
240                                        writeToStream(inputStream, outputStream);
241                                } catch (FileNotFoundException e) {
242                                        setStatus(FileTransfer.Status.error);
243                                        setError(Error.bad_file);
244                                        setException(e);
245                                } catch (SmackException e) {
246                                        setStatus(FileTransfer.Status.error);
247                                        setException(e);
248                                } finally {
249                                        try {
250                                                if (inputStream != null) {
251                                                        inputStream.close();
252                                                }
253
254                                                outputStream.flush();
255                                                outputStream.close();
256                                        } catch (IOException e) {
257                        /* Do Nothing */
258                                        }
259                                }
260                updateStatus(Status.in_progress, FileTransfer.Status.complete);
261                                }
262
263                }, "File Transfer " + streamID);
264                transferThread.start();
265        }
266
267    /**
268         * This method handles the stream negotiation process and transmits the file
269         * to the remote user. It returns immediately and the progress of the file
270         * transfer can be monitored through several methods:
271         *
272         * <UL>
273         * <LI>{@link FileTransfer#getStatus()}
274         * <LI>{@link FileTransfer#getProgress()}
275         * <LI>{@link FileTransfer#isDone()}
276         * </UL>
277         *
278     * @param in the stream to transfer to the remote entity.
279     * @param fileName the name of the file that is transferred
280     * @param fileSize the size of the file that is transferred
281     * @param description a description for the file to transfer.
282         */
283        public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description){
284                checkTransferThread();
285
286                setFileInfo(fileName, fileSize);
287                transferThread = new Thread(new Runnable() {
288                        public void run() {
289                //Create packet filter
290                try {
291                                        outputStream = negotiateStream(fileName, fileSize, description);
292                                } catch (XMPPErrorException e) {
293                                        handleXMPPException(e);
294                                        return;
295                                }
296                catch (Exception e) {
297                    setException(e);
298                }
299                                if (outputStream == null) {
300                                        return;
301                                }
302
303                if (!updateStatus(Status.negotiated, Status.in_progress)) {
304                                        return;
305                                }
306                                try {
307                                        writeToStream(in, outputStream);
308                                } catch (SmackException e) {
309                                        setStatus(FileTransfer.Status.error);
310                                        setException(e);
311                                } finally {
312                                        try {
313                                                if (in != null) {
314                                                        in.close();
315                                                }
316
317                                                outputStream.flush();
318                                                outputStream.close();
319                                        } catch (IOException e) {
320                        /* Do Nothing */
321                                        }
322                                }
323                updateStatus(Status.in_progress, FileTransfer.Status.complete);
324                                }
325
326                }, "File Transfer " + streamID);
327                transferThread.start();
328        }
329
330        private void handleXMPPException(XMPPErrorException e) {
331                XMPPError error = e.getXMPPError();
332                if (error != null) {
333                        String condition = error.getCondition();
334                        if (XMPPError.Condition.forbidden.equals(condition)) {
335                                setStatus(Status.refused);
336                                return;
337                        }
338            else if (XMPPError.Condition.bad_request.equals(condition)) {
339                                setStatus(Status.error);
340                                setError(Error.not_acceptable);
341            }
342            else {
343                setStatus(FileTransfer.Status.error);
344            }
345        }
346
347        setException(e);
348        }
349
350        /**
351         * Returns the amount of bytes that have been sent for the file transfer. Or
352         * -1 if the file transfer has not started.
353         * <p>
354         * Note: This method is only useful when the {@link #sendFile(File, String)}
355         * method is called, as it is the only method that actually transmits the
356         * file.
357         *
358         * @return Returns the amount of bytes that have been sent for the file
359         *         transfer. Or -1 if the file transfer has not started.
360         */
361        public long getBytesSent() {
362                return amountWritten;
363        }
364
365        private OutputStream negotiateStream(String fileName, long fileSize,
366                        String description) throws SmackException, XMPPException {
367                // Negotiate the file transfer profile
368
369        if (!updateStatus(Status.initial, Status.negotiating_transfer)) {
370            throw new IllegalStateChangeException();
371        }
372                StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(
373                                getPeer(), streamID, fileName, fileSize, description,
374                                RESPONSE_TIMEOUT);
375
376                if (streamNegotiator == null) {
377                        setStatus(Status.error);
378                        setError(Error.no_response);
379                        return null;
380                }
381
382        // Negotiate the stream
383        if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {
384            throw new IllegalStateChangeException();
385        }
386                outputStream = streamNegotiator.createOutgoingStream(streamID,
387                initiator, getPeer());
388
389        if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {
390            throw new IllegalStateChangeException();
391                }
392                return outputStream;
393        }
394
395        public void cancel() {
396                setStatus(Status.cancelled);
397        }
398
399    @Override
400    protected boolean updateStatus(Status oldStatus, Status newStatus) {
401        boolean isUpdated = super.updateStatus(oldStatus, newStatus);
402        if(callback != null && isUpdated) {
403            callback.statusUpdated(oldStatus, newStatus);
404        }
405        return isUpdated;
406    }
407
408    @Override
409    protected void setStatus(Status status) {
410        Status oldStatus = getStatus();
411        super.setStatus(status);
412        if(callback != null) {
413            callback.statusUpdated(oldStatus, status);
414        }
415    }
416
417    @Override
418    protected void setException(Exception exception) {
419        super.setException(exception);
420        if(callback != null) {
421            callback.errorEstablishingStream(exception);
422        }
423    }
424
425    /**
426         * A callback class to retrieve the status of an outgoing transfer
427         * negotiation process.
428         *
429         * @author Alexander Wenckus
430         *
431         */
432        public interface NegotiationProgress {
433
434                /**
435                 * Called when the status changes
436         *
437         * @param oldStatus the previous status of the file transfer.
438         * @param newStatus the new status of the file transfer.
439         */
440                void statusUpdated(Status oldStatus, Status newStatus);
441
442                /**
443                 * Once the negotiation process is completed the output stream can be
444                 * retrieved.
445         *
446         * @param stream the established stream which can be used to transfer the file to the remote
447         * entity
448                 */
449                void outputStreamEstablished(OutputStream stream);
450
451        /**
452         * Called when an exception occurs during the negotiation progress.
453         *
454         * @param e the exception that occurred.
455         */
456        void errorEstablishingStream(Exception e);
457    }
458
459}