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.Logger;
026
027import org.jivesoftware.smack.SmackException;
028import org.jivesoftware.smack.SmackException.IllegalStateChangeException;
029import org.jivesoftware.smack.XMPPException;
030import org.jivesoftware.smack.XMPPException.XMPPErrorException;
031import org.jivesoftware.smack.packet.StanzaError;
032import org.jivesoftware.smack.util.CloseableUtil;
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 TODO javadoc me please
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 TODO javadoc me please
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 TODO javadoc me please
116     *            The size in bytes of the file that will be transmitted.
117     * @param description TODO javadoc me please
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 if an XMPP protocol error was received.
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 if the calling thread was interrupted.
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 TODO javadoc me please
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 TODO javadoc me please
154     *            The size in bytes of the file that will be transmitted.
155     * @param description TODO javadoc me please
156     *            A description of the file that will be transmitted.
157     * @param progress TODO javadoc me please
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 if Smack detected an exceptional situation.
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                    CloseableUtil.maybeClose(inputStream, LOGGER);
262                    CloseableUtil.maybeClose(outputStream, LOGGER);
263                }
264                updateStatus(Status.in_progress, FileTransfer.Status.complete);
265                }
266
267        }, "File Transfer " + streamID);
268        transferThread.start();
269    }
270
271    /**
272     * This method handles the stream negotiation process and transmits the file
273     * to the remote user. It returns immediately and the progress of the file
274     * transfer can be monitored through several methods:
275     *
276     * <UL>
277     * <LI>{@link FileTransfer#getStatus()}
278     * <LI>{@link FileTransfer#getProgress()}
279     * <LI>{@link FileTransfer#isDone()}
280     * </UL>
281     *
282     * @param in the stream to transfer to the remote entity.
283     * @param fileName the name of the file that is transferred
284     * @param fileSize the size of the file that is transferred
285     * @param description a description for the file to transfer.
286     */
287    public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description) {
288        checkTransferThread();
289
290        setFileInfo(fileName, fileSize);
291        transferThread = new Thread(new Runnable() {
292            @Override
293            public void run() {
294                // Create packet filter.
295                try {
296                    outputStream = negotiateStream(fileName, fileSize, description);
297                } catch (XMPPErrorException e) {
298                    handleXMPPException(e);
299                    return;
300                }
301                catch (Exception e) {
302                    setException(e);
303                }
304                if (outputStream == null) {
305                    return;
306                }
307
308                if (!updateStatus(Status.negotiated, Status.in_progress)) {
309                    return;
310                }
311                try {
312                    writeToStream(in, outputStream);
313                } catch (IOException e) {
314                    setStatus(FileTransfer.Status.error);
315                    setException(e);
316                } finally {
317                    CloseableUtil.maybeClose(in, LOGGER);
318                    CloseableUtil.maybeClose(outputStream, LOGGER);
319                }
320                updateStatus(Status.in_progress, FileTransfer.Status.complete);
321                }
322
323        }, "File Transfer " + streamID);
324        transferThread.start();
325    }
326
327    public void setCallback(NegotiationProgress negotiationProcess) {
328        this.callback = negotiationProcess;
329    }
330
331    private void handleXMPPException(XMPPErrorException e) {
332        StanzaError error = e.getStanzaError();
333        if (error != null) {
334            switch (error.getCondition()) {
335            case forbidden:
336                setStatus(Status.refused);
337                return;
338            case bad_request:
339                setStatus(Status.error);
340                setError(Error.not_acceptable);
341                break;
342            default:
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, InterruptedException {
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        // Negotiate the stream
377        if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {
378            throw new IllegalStateChangeException();
379        }
380        outputStream = streamNegotiator.createOutgoingStream(streamID,
381                initiator, getPeer());
382
383        if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {
384            throw new IllegalStateChangeException();
385        }
386        return outputStream;
387    }
388
389    @Override
390    public void cancel() {
391        setStatus(Status.cancelled);
392    }
393
394    @Override
395    protected boolean updateStatus(Status oldStatus, Status newStatus) {
396        boolean isUpdated = super.updateStatus(oldStatus, newStatus);
397        if (callback != null && isUpdated) {
398            callback.statusUpdated(oldStatus, newStatus);
399        }
400        return isUpdated;
401    }
402
403    @Override
404    protected void setStatus(Status status) {
405        Status oldStatus = getStatus();
406        super.setStatus(status);
407        if (callback != null) {
408            callback.statusUpdated(oldStatus, status);
409        }
410    }
411
412    @Override
413    protected void setException(Exception exception) {
414        super.setException(exception);
415        if (callback != null) {
416            callback.errorEstablishingStream(exception);
417        }
418    }
419
420    /**
421     * A callback class to retrieve the status of an outgoing transfer
422     * negotiation process.
423     *
424     * @author Alexander Wenckus
425     *
426     */
427    public interface NegotiationProgress {
428
429        /**
430         * Called when the status changes.
431         *
432         * @param oldStatus the previous status of the file transfer.
433         * @param newStatus the new status of the file transfer.
434         */
435        void statusUpdated(Status oldStatus, Status newStatus);
436
437        /**
438         * Once the negotiation process is completed the output stream can be
439         * retrieved.
440         *
441         * @param stream the established stream which can be used to transfer the file to the remote
442         * entity
443         */
444        void outputStreamEstablished(OutputStream stream);
445
446        /**
447         * Called when an exception occurs during the negotiation progress.
448         *
449         * @param e the exception that occurred.
450         */
451        void errorEstablishingStream(Exception e);
452    }
453
454}