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.StanzaError; 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 negotiation 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 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 negotiation 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 StanzaError error = e.getStanzaError(); 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}