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