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