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