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