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