1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2006 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20package org.jivesoftware.smackx.filetransfer;
21
22import org.jivesoftware.smack.XMPPException;
23import org.jivesoftware.smack.packet.XMPPError;
24
25import java.io.*;
26
27/**
28 * Handles the sending of a file to another user. File transfer's in jabber have
29 * several steps and there are several methods in this class that handle these
30 * steps differently.
31 *
32 * @author Alexander Wenckus
33 *
34 */
35public class OutgoingFileTransfer extends FileTransfer {
36
37	private static int RESPONSE_TIMEOUT = 60 * 1000;
38    private NegotiationProgress callback;
39
40    /**
41     * Returns the time in milliseconds after which the file transfer
42     * negotiation process will timeout if the other user has not responded.
43     *
44     * @return Returns the time in milliseconds after which the file transfer
45     *         negotiation process will timeout if the remote user has not
46     *         responded.
47     */
48    public static int getResponseTimeout() {
49        return RESPONSE_TIMEOUT;
50    }
51
52	/**
53	 * Sets the time in milliseconds after which the file transfer negotiation
54	 * process will timeout if the other user has not responded.
55	 *
56	 * @param responseTimeout
57	 *            The timeout time in milliseconds.
58	 */
59	public static void setResponseTimeout(int responseTimeout) {
60		RESPONSE_TIMEOUT = responseTimeout;
61	}
62
63	private OutputStream outputStream;
64
65	private String initiator;
66
67	private Thread transferThread;
68
69	protected OutgoingFileTransfer(String initiator, String target,
70			String streamID, FileTransferNegotiator transferNegotiator) {
71		super(target, streamID, transferNegotiator);
72		this.initiator = initiator;
73	}
74
75	protected void setOutputStream(OutputStream stream) {
76		if (outputStream == null) {
77			this.outputStream = stream;
78		}
79	}
80
81	/**
82	 * Returns the output stream connected to the peer to transfer the file. It
83	 * is only available after it has been successfully negotiated by the
84	 * {@link StreamNegotiator}.
85	 *
86	 * @return Returns the output stream connected to the peer to transfer the
87	 *         file.
88	 */
89	protected OutputStream getOutputStream() {
90		if (getStatus().equals(FileTransfer.Status.negotiated)) {
91			return outputStream;
92		} else {
93			return null;
94		}
95	}
96
97	/**
98	 * This method handles the negotiation of the file transfer and the stream,
99	 * 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	 */
115	public synchronized OutputStream sendFile(String fileName, long fileSize,
116			String description) throws XMPPException {
117		if (isDone() || outputStream != null) {
118			throw new IllegalStateException(
119					"The negotation process has already"
120							+ " been attempted on this file transfer");
121		}
122		try {
123			setFileInfo(fileName, fileSize);
124			this.outputStream = negotiateStream(fileName, fileSize, description);
125		} catch (XMPPException e) {
126			handleXMPPException(e);
127			throw e;
128		}
129		return outputStream;
130	}
131
132	/**
133	 * This methods handles the transfer and stream negotiation process. It
134	 * returns immediately and its progress will be updated through the
135	 * {@link NegotiationProgress} callback.
136	 *
137	 * @param fileName
138	 *            The name of the file that will be transmitted. It is
139	 *            preferable for this name to have an extension as it will be
140	 *            used to determine the type of file it is.
141	 * @param fileSize
142	 *            The size in bytes of the file that will be transmitted.
143	 * @param description
144	 *            A description of the file that will be transmitted.
145	 * @param progress
146	 *            A callback to monitor the progress of the file transfer
147	 *            negotiation process and to retrieve the OutputStream when it
148	 *            is complete.
149	 */
150	public synchronized void sendFile(final String fileName,
151			final long fileSize, final String description,
152			final NegotiationProgress progress)
153    {
154        if(progress == null) {
155            throw new IllegalArgumentException("Callback progress cannot be null.");
156        }
157        checkTransferThread();
158		if (isDone() || outputStream != null) {
159			throw new IllegalStateException(
160					"The negotation process has already"
161							+ " been attempted for this file transfer");
162		}
163        setFileInfo(fileName, fileSize);
164        this.callback = progress;
165        transferThread = new Thread(new Runnable() {
166			public void run() {
167				try {
168					OutgoingFileTransfer.this.outputStream = negotiateStream(
169							fileName, fileSize, description);
170                    progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream);
171                }
172                catch (XMPPException e) {
173					handleXMPPException(e);
174				}
175			}
176		}, "File Transfer Negotiation " + streamID);
177		transferThread.start();
178	}
179
180	private void checkTransferThread() {
181		if (transferThread != null && transferThread.isAlive() || isDone()) {
182			throw new IllegalStateException(
183					"File transfer in progress or has already completed.");
184		}
185	}
186
187    /**
188	 * This method handles the stream negotiation process and transmits the file
189	 * to the remote user. It returns immediately and the progress of the file
190	 * transfer can be monitored through several methods:
191	 *
192	 * <UL>
193	 * <LI>{@link FileTransfer#getStatus()}
194	 * <LI>{@link FileTransfer#getProgress()}
195	 * <LI>{@link FileTransfer#isDone()}
196	 * </UL>
197	 *
198     * @param file the file to transfer to the remote entity.
199     * @param description a description for the file to transfer.
200	 * @throws XMPPException
201	 *             If there is an error during the negotiation process or the
202	 *             sending of the file.
203	 */
204	public synchronized void sendFile(final File file, final String description)
205			throws XMPPException {
206		checkTransferThread();
207		if (file == null || !file.exists() || !file.canRead()) {
208			throw new IllegalArgumentException("Could not read file");
209		} else {
210			setFileInfo(file.getAbsolutePath(), file.getName(), file.length());
211		}
212
213		transferThread = new Thread(new Runnable() {
214			public void run() {
215				try {
216					outputStream = negotiateStream(file.getName(), file
217							.length(), description);
218				} catch (XMPPException e) {
219					handleXMPPException(e);
220					return;
221				}
222				if (outputStream == null) {
223					return;
224				}
225
226                if (!updateStatus(Status.negotiated, Status.in_progress)) {
227					return;
228				}
229
230				InputStream inputStream = null;
231				try {
232					inputStream = new FileInputStream(file);
233					writeToStream(inputStream, outputStream);
234				} catch (FileNotFoundException e) {
235					setStatus(FileTransfer.Status.error);
236					setError(Error.bad_file);
237					setException(e);
238				} catch (XMPPException e) {
239					setStatus(FileTransfer.Status.error);
240					setException(e);
241				} finally {
242					try {
243						if (inputStream != null) {
244							inputStream.close();
245						}
246
247						outputStream.flush();
248						outputStream.close();
249					} catch (IOException e) {
250                        /* Do Nothing */
251					}
252				}
253                updateStatus(Status.in_progress, FileTransfer.Status.complete);
254				}
255
256		}, "File Transfer " + streamID);
257		transferThread.start();
258	}
259
260    /**
261	 * This method handles the stream negotiation process and transmits the file
262	 * to the remote user. It returns immediately and the progress of the file
263	 * transfer can be monitored through several methods:
264	 *
265	 * <UL>
266	 * <LI>{@link FileTransfer#getStatus()}
267	 * <LI>{@link FileTransfer#getProgress()}
268	 * <LI>{@link FileTransfer#isDone()}
269	 * </UL>
270	 *
271     * @param in the stream to transfer to the remote entity.
272     * @param fileName the name of the file that is transferred
273     * @param fileSize the size of the file that is transferred
274     * @param description a description for the file to transfer.
275	 */
276	public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description){
277		checkTransferThread();
278
279		setFileInfo(fileName, fileSize);
280		transferThread = new Thread(new Runnable() {
281			public void run() {
282                setFileInfo(fileName, fileSize);
283                //Create packet filter
284                try {
285					outputStream = negotiateStream(fileName, fileSize, description);
286				} catch (XMPPException e) {
287					handleXMPPException(e);
288					return;
289				} catch (IllegalStateException e) {
290					setStatus(FileTransfer.Status.error);
291					setException(e);
292				}
293				if (outputStream == null) {
294					return;
295				}
296
297                if (!updateStatus(Status.negotiated, Status.in_progress)) {
298					return;
299				}
300				try {
301					writeToStream(in, outputStream);
302				} catch (XMPPException e) {
303					setStatus(FileTransfer.Status.error);
304					setException(e);
305				} catch (IllegalStateException e) {
306					setStatus(FileTransfer.Status.error);
307					setException(e);
308				} finally {
309					try {
310						if (in != null) {
311							in.close();
312						}
313
314						outputStream.flush();
315						outputStream.close();
316					} catch (IOException e) {
317                        /* Do Nothing */
318					}
319				}
320                updateStatus(Status.in_progress, FileTransfer.Status.complete);
321				}
322
323		}, "File Transfer " + streamID);
324		transferThread.start();
325	}
326
327	private void handleXMPPException(XMPPException e) {
328		XMPPError error = e.getXMPPError();
329		if (error != null) {
330			int code = error.getCode();
331			if (code == 403) {
332				setStatus(Status.refused);
333				return;
334			}
335            else if (code == 400) {
336				setStatus(Status.error);
337				setError(Error.not_acceptable);
338            }
339            else {
340                setStatus(FileTransfer.Status.error);
341            }
342        }
343
344        setException(e);
345	}
346
347	/**
348	 * Returns the amount of bytes that have been sent for the file transfer. Or
349	 * -1 if the file transfer has not started.
350	 * <p>
351	 * Note: This method is only useful when the {@link #sendFile(File, String)}
352	 * method is called, as it is the only method that actually transmits the
353	 * file.
354	 *
355	 * @return Returns the amount of bytes that have been sent for the file
356	 *         transfer. Or -1 if the file transfer has not started.
357	 */
358	public long getBytesSent() {
359		return amountWritten;
360	}
361
362	private OutputStream negotiateStream(String fileName, long fileSize,
363			String description) throws XMPPException {
364		// Negotiate the file transfer profile
365
366        if (!updateStatus(Status.initial, Status.negotiating_transfer)) {
367            throw new XMPPException("Illegal state change");
368        }
369		StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(
370				getPeer(), streamID, fileName, fileSize, description,
371				RESPONSE_TIMEOUT);
372
373		if (streamNegotiator == null) {
374			setStatus(Status.error);
375			setError(Error.no_response);
376			return null;
377		}
378
379        // Negotiate the stream
380        if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {
381            throw new XMPPException("Illegal state change");
382        }
383		outputStream = streamNegotiator.createOutgoingStream(streamID,
384                initiator, getPeer());
385
386        if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {
387            throw new XMPPException("Illegal state change");
388		}
389		return outputStream;
390	}
391
392	public void cancel() {
393		setStatus(Status.cancelled);
394	}
395
396    @Override
397    protected boolean updateStatus(Status oldStatus, Status newStatus) {
398        boolean isUpdated = super.updateStatus(oldStatus, newStatus);
399        if(callback != null && isUpdated) {
400            callback.statusUpdated(oldStatus, newStatus);
401        }
402        return isUpdated;
403    }
404
405    @Override
406    protected void setStatus(Status status) {
407        Status oldStatus = getStatus();
408        super.setStatus(status);
409        if(callback != null) {
410            callback.statusUpdated(oldStatus, status);
411        }
412    }
413
414    @Override
415    protected void setException(Exception exception) {
416        super.setException(exception);
417        if(callback != null) {
418            callback.errorEstablishingStream(exception);
419        }
420    }
421
422    /**
423	 * A callback class to retrieve the status of an outgoing transfer
424	 * negotiation process.
425	 *
426	 * @author Alexander Wenckus
427	 *
428	 */
429	public interface NegotiationProgress {
430
431		/**
432		 * Called when the status changes
433         *
434         * @param oldStatus the previous status of the file transfer.
435         * @param newStatus the new status of the file transfer.
436         */
437		void statusUpdated(Status oldStatus, Status newStatus);
438
439		/**
440		 * Once the negotiation process is completed the output stream can be
441		 * retrieved.
442         *
443         * @param stream the established stream which can be used to transfer the file to the remote
444         * entity
445		 */
446		void outputStreamEstablished(OutputStream stream);
447
448        /**
449         * Called when an exception occurs during the negotiation progress.
450         *
451         * @param e the exception that occurred.
452         */
453        void errorEstablishingStream(Exception e);
454    }
455
456}
457