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