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