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.Logger; 026 027import org.jivesoftware.smack.SmackException; 028import org.jivesoftware.smack.SmackException.IllegalStateChangeException; 029import org.jivesoftware.smack.XMPPException; 030import org.jivesoftware.smack.XMPPException.XMPPErrorException; 031import org.jivesoftware.smack.packet.StanzaError; 032import org.jivesoftware.smack.util.CloseableUtil; 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 TODO javadoc me please 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 TODO javadoc me please 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 TODO javadoc me please 116 * The size in bytes of the file that will be transmitted. 117 * @param description TODO javadoc me please 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 if an XMPP protocol error was received. 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 if the calling thread was interrupted. 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 TODO javadoc me please 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 TODO javadoc me please 154 * The size in bytes of the file that will be transmitted. 155 * @param description TODO javadoc me please 156 * A description of the file that will be transmitted. 157 * @param progress TODO javadoc me please 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 if Smack detected an exceptional situation. 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 CloseableUtil.maybeClose(inputStream, LOGGER); 262 CloseableUtil.maybeClose(outputStream, LOGGER); 263 } 264 updateStatus(Status.in_progress, FileTransfer.Status.complete); 265 } 266 267 }, "File Transfer " + streamID); 268 transferThread.start(); 269 } 270 271 /** 272 * This method handles the stream negotiation process and transmits the file 273 * to the remote user. It returns immediately and the progress of the file 274 * transfer can be monitored through several methods: 275 * 276 * <UL> 277 * <LI>{@link FileTransfer#getStatus()} 278 * <LI>{@link FileTransfer#getProgress()} 279 * <LI>{@link FileTransfer#isDone()} 280 * </UL> 281 * 282 * @param in the stream to transfer to the remote entity. 283 * @param fileName the name of the file that is transferred 284 * @param fileSize the size of the file that is transferred 285 * @param description a description for the file to transfer. 286 */ 287 public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description) { 288 checkTransferThread(); 289 290 setFileInfo(fileName, fileSize); 291 transferThread = new Thread(new Runnable() { 292 @Override 293 public void run() { 294 // Create packet filter. 295 try { 296 outputStream = negotiateStream(fileName, fileSize, description); 297 } catch (XMPPErrorException e) { 298 handleXMPPException(e); 299 return; 300 } 301 catch (Exception e) { 302 setException(e); 303 } 304 if (outputStream == null) { 305 return; 306 } 307 308 if (!updateStatus(Status.negotiated, Status.in_progress)) { 309 return; 310 } 311 try { 312 writeToStream(in, outputStream); 313 } catch (IOException e) { 314 setStatus(FileTransfer.Status.error); 315 setException(e); 316 } finally { 317 CloseableUtil.maybeClose(in, LOGGER); 318 CloseableUtil.maybeClose(outputStream, LOGGER); 319 } 320 updateStatus(Status.in_progress, FileTransfer.Status.complete); 321 } 322 323 }, "File Transfer " + streamID); 324 transferThread.start(); 325 } 326 327 private void handleXMPPException(XMPPErrorException e) { 328 StanzaError error = e.getStanzaError(); 329 if (error != null) { 330 switch (error.getCondition()) { 331 case forbidden: 332 setStatus(Status.refused); 333 return; 334 case bad_request: 335 setStatus(Status.error); 336 setError(Error.not_acceptable); 337 break; 338 default: 339 setStatus(FileTransfer.Status.error); 340 } 341 } 342 343 setException(e); 344 } 345 346 /** 347 * Returns the amount of bytes that have been sent for the file transfer. Or 348 * -1 if the file transfer has not started. 349 * <p> 350 * Note: This method is only useful when the {@link #sendFile(File, String)} 351 * method is called, as it is the only method that actually transmits the 352 * file. 353 * 354 * @return Returns the amount of bytes that have been sent for the file 355 * transfer. Or -1 if the file transfer has not started. 356 */ 357 public long getBytesSent() { 358 return amountWritten; 359 } 360 361 private OutputStream negotiateStream(String fileName, long fileSize, 362 String description) throws SmackException, XMPPException, InterruptedException { 363 // Negotiate the file transfer profile 364 365 if (!updateStatus(Status.initial, Status.negotiating_transfer)) { 366 throw new IllegalStateChangeException(); 367 } 368 StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer( 369 getPeer(), streamID, fileName, fileSize, description, 370 RESPONSE_TIMEOUT); 371 372 // Negotiate the stream 373 if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) { 374 throw new IllegalStateChangeException(); 375 } 376 outputStream = streamNegotiator.createOutgoingStream(streamID, 377 initiator, getPeer()); 378 379 if (!updateStatus(Status.negotiating_stream, Status.negotiated)) { 380 throw new IllegalStateChangeException(); 381 } 382 return outputStream; 383 } 384 385 @Override 386 public void cancel() { 387 setStatus(Status.cancelled); 388 } 389 390 @Override 391 protected boolean updateStatus(Status oldStatus, Status newStatus) { 392 boolean isUpdated = super.updateStatus(oldStatus, newStatus); 393 if (callback != null && isUpdated) { 394 callback.statusUpdated(oldStatus, newStatus); 395 } 396 return isUpdated; 397 } 398 399 @Override 400 protected void setStatus(Status status) { 401 Status oldStatus = getStatus(); 402 super.setStatus(status); 403 if (callback != null) { 404 callback.statusUpdated(oldStatus, status); 405 } 406 } 407 408 @Override 409 protected void setException(Exception exception) { 410 super.setException(exception); 411 if (callback != null) { 412 callback.errorEstablishingStream(exception); 413 } 414 } 415 416 /** 417 * A callback class to retrieve the status of an outgoing transfer 418 * negotiation process. 419 * 420 * @author Alexander Wenckus 421 * 422 */ 423 public interface NegotiationProgress { 424 425 /** 426 * Called when the status changes. 427 * 428 * @param oldStatus the previous status of the file transfer. 429 * @param newStatus the new status of the file transfer. 430 */ 431 void statusUpdated(Status oldStatus, Status newStatus); 432 433 /** 434 * Once the negotiation process is completed the output stream can be 435 * retrieved. 436 * 437 * @param stream the established stream which can be used to transfer the file to the remote 438 * entity 439 */ 440 void outputStreamEstablished(OutputStream stream); 441 442 /** 443 * Called when an exception occurs during the negotiation progress. 444 * 445 * @param e the exception that occurred. 446 */ 447 void errorEstablishingStream(Exception e); 448 } 449 450}