Home | History | Annotate | Download | only in filetransfer
      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  */
     20 package org.jivesoftware.smackx.filetransfer;
     21 
     22 import org.jivesoftware.smack.XMPPException;
     23 import org.jivesoftware.smack.packet.XMPPError;
     24 
     25 import 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  */
     35 public 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