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.XMPPError;
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 negotation 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    {
166        if (progress == null) {
167            throw new IllegalArgumentException("Callback progress cannot be null.");
168        }
169        checkTransferThread();
170        if (isDone() || outputStream != null) {
171            throw new IllegalStateException(
172                    "The negotation process has already"
173                            + " been attempted for this file transfer");
174        }
175        setFileInfo(fileName, fileSize);
176        this.callback = progress;
177        transferThread = new Thread(new Runnable() {
178            @Override
179            public void run() {
180                try {
181                    OutgoingFileTransfer.this.outputStream = negotiateStream(
182                            fileName, fileSize, description);
183                    progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream);
184                }
185                catch (XMPPErrorException e) {
186                    handleXMPPException(e);
187                }
188                catch (Exception e) {
189                    setException(e);
190                }
191            }
192        }, "File Transfer Negotiation " + streamID);
193        transferThread.start();
194    }
195
196    private void checkTransferThread() {
197        if ((transferThread != null && transferThread.isAlive()) || isDone()) {
198            throw new IllegalStateException(
199                    "File transfer in progress or has already completed.");
200        }
201    }
202
203    /**
204     * This method handles the stream negotiation process and transmits the file
205     * to the remote user. It returns immediately and the progress of the file
206     * transfer can be monitored through several methods:
207     *
208     * <UL>
209     * <LI>{@link FileTransfer#getStatus()}
210     * <LI>{@link FileTransfer#getProgress()}
211     * <LI>{@link FileTransfer#isDone()}
212     * </UL>
213     *
214     * @param file the file to transfer to the remote entity.
215     * @param description a description for the file to transfer.
216     * @throws SmackException
217     *             If there is an error during the negotiation process or the
218     *             sending of the file.
219     */
220    public synchronized void sendFile(final File file, final String description)
221            throws SmackException {
222        checkTransferThread();
223        if (file == null || !file.exists() || !file.canRead()) {
224            throw new IllegalArgumentException("Could not read file");
225        } else {
226            setFileInfo(file.getAbsolutePath(), file.getName(), file.length());
227        }
228
229        transferThread = new Thread(new Runnable() {
230            @Override
231            public void run() {
232                try {
233                    outputStream = negotiateStream(file.getName(), file
234                            .length(), description);
235                } catch (XMPPErrorException e) {
236                    handleXMPPException(e);
237                    return;
238                }
239                catch (Exception e) {
240                    setException(e);
241                }
242                if (outputStream == null) {
243                    return;
244                }
245
246                if (!updateStatus(Status.negotiated, Status.in_progress)) {
247                    return;
248                }
249
250                InputStream inputStream = null;
251                try {
252                    inputStream = new FileInputStream(file);
253                    writeToStream(inputStream, outputStream);
254                } catch (FileNotFoundException e) {
255                    setStatus(FileTransfer.Status.error);
256                    setError(Error.bad_file);
257                    setException(e);
258                } catch (IOException e) {
259                    setStatus(FileTransfer.Status.error);
260                    setException(e);
261                } finally {
262                        if (inputStream != null) {
263                            try {
264                                inputStream.close();
265                            } catch (IOException e) {
266                                LOGGER.log(Level.WARNING, "Closing input stream", e);
267                            }
268                        }
269
270                        try {
271                            outputStream.close();
272                        } catch (IOException e) {
273                            LOGGER.log(Level.WARNING, "Closing output stream", e);
274                        }
275                }
276                updateStatus(Status.in_progress, FileTransfer.Status.complete);
277                }
278
279        }, "File Transfer " + streamID);
280        transferThread.start();
281    }
282
283    /**
284     * This method handles the stream negotiation process and transmits the file
285     * to the remote user. It returns immediately and the progress of the file
286     * transfer can be monitored through several methods:
287     *
288     * <UL>
289     * <LI>{@link FileTransfer#getStatus()}
290     * <LI>{@link FileTransfer#getProgress()}
291     * <LI>{@link FileTransfer#isDone()}
292     * </UL>
293     *
294     * @param in the stream to transfer to the remote entity.
295     * @param fileName the name of the file that is transferred
296     * @param fileSize the size of the file that is transferred
297     * @param description a description for the file to transfer.
298     */
299    public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description) {
300        checkTransferThread();
301
302        setFileInfo(fileName, fileSize);
303        transferThread = new Thread(new Runnable() {
304            @Override
305            public void run() {
306                // Create packet filter.
307                try {
308                    outputStream = negotiateStream(fileName, fileSize, description);
309                } catch (XMPPErrorException e) {
310                    handleXMPPException(e);
311                    return;
312                }
313                catch (Exception e) {
314                    setException(e);
315                }
316                if (outputStream == null) {
317                    return;
318                }
319
320                if (!updateStatus(Status.negotiated, Status.in_progress)) {
321                    return;
322                }
323                try {
324                    writeToStream(in, outputStream);
325                } catch (IOException e) {
326                    setStatus(FileTransfer.Status.error);
327                    setException(e);
328                } finally {
329                    try {
330                        if (in != null) {
331                            in.close();
332                        }
333
334                        outputStream.flush();
335                        outputStream.close();
336                    } catch (IOException e) {
337                        /* Do Nothing */
338                    }
339                }
340                updateStatus(Status.in_progress, FileTransfer.Status.complete);
341                }
342
343        }, "File Transfer " + streamID);
344        transferThread.start();
345    }
346
347    private void handleXMPPException(XMPPErrorException e) {
348        XMPPError error = e.getXMPPError();
349        if (error != null) {
350            switch (error.getCondition()) {
351            case forbidden:
352                setStatus(Status.refused);
353                return;
354            case bad_request:
355                setStatus(Status.error);
356                setError(Error.not_acceptable);
357                break;
358            default:
359                setStatus(FileTransfer.Status.error);
360            }
361        }
362
363        setException(e);
364    }
365
366    /**
367     * Returns the amount of bytes that have been sent for the file transfer. Or
368     * -1 if the file transfer has not started.
369     * <p>
370     * Note: This method is only useful when the {@link #sendFile(File, String)}
371     * method is called, as it is the only method that actually transmits the
372     * file.
373     *
374     * @return Returns the amount of bytes that have been sent for the file
375     *         transfer. Or -1 if the file transfer has not started.
376     */
377    public long getBytesSent() {
378        return amountWritten;
379    }
380
381    private OutputStream negotiateStream(String fileName, long fileSize,
382            String description) throws SmackException, XMPPException, InterruptedException {
383        // Negotiate the file transfer profile
384
385        if (!updateStatus(Status.initial, Status.negotiating_transfer)) {
386            throw new IllegalStateChangeException();
387        }
388        StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(
389                getPeer(), streamID, fileName, fileSize, description,
390                RESPONSE_TIMEOUT);
391
392        // Negotiate the stream
393        if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {
394            throw new IllegalStateChangeException();
395        }
396        outputStream = streamNegotiator.createOutgoingStream(streamID,
397                initiator, getPeer());
398
399        if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {
400            throw new IllegalStateChangeException();
401        }
402        return outputStream;
403    }
404
405    @Override
406    public void cancel() {
407        setStatus(Status.cancelled);
408    }
409
410    @Override
411    protected boolean updateStatus(Status oldStatus, Status newStatus) {
412        boolean isUpdated = super.updateStatus(oldStatus, newStatus);
413        if (callback != null && isUpdated) {
414            callback.statusUpdated(oldStatus, newStatus);
415        }
416        return isUpdated;
417    }
418
419    @Override
420    protected void setStatus(Status status) {
421        Status oldStatus = getStatus();
422        super.setStatus(status);
423        if (callback != null) {
424            callback.statusUpdated(oldStatus, status);
425        }
426    }
427
428    @Override
429    protected void setException(Exception exception) {
430        super.setException(exception);
431        if (callback != null) {
432            callback.errorEstablishingStream(exception);
433        }
434    }
435
436    /**
437     * A callback class to retrieve the status of an outgoing transfer
438     * negotiation process.
439     *
440     * @author Alexander Wenckus
441     *
442     */
443    public interface NegotiationProgress {
444
445        /**
446         * Called when the status changes.
447         *
448         * @param oldStatus the previous status of the file transfer.
449         * @param newStatus the new status of the file transfer.
450         */
451        void statusUpdated(Status oldStatus, Status newStatus);
452
453        /**
454         * Once the negotiation process is completed the output stream can be
455         * retrieved.
456         *
457         * @param stream the established stream which can be used to transfer the file to the remote
458         * entity
459         */
460        void outputStreamEstablished(OutputStream stream);
461
462        /**
463         * Called when an exception occurs during the negotiation progress.
464         *
465         * @param e the exception that occurred.
466         */
467        void errorEstablishingStream(Exception e);
468    }
469
470}