OutgoingFileTransfer.java

  1. /**
  2.  *
  3.  * Copyright 2003-2006 Jive Software.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.filetransfer;

  18. import java.io.File;
  19. import java.io.FileInputStream;
  20. import java.io.FileNotFoundException;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.io.OutputStream;
  24. import java.util.logging.Level;
  25. import java.util.logging.Logger;

  26. import org.jivesoftware.smack.SmackException;
  27. import org.jivesoftware.smack.SmackException.IllegalStateChangeException;
  28. import org.jivesoftware.smack.XMPPException;
  29. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  30. import org.jivesoftware.smack.packet.XMPPError;
  31. import org.jxmpp.jid.Jid;

  32. /**
  33.  * Handles the sending of a file to another user. File transfer's in jabber have
  34.  * several steps and there are several methods in this class that handle these
  35.  * steps differently.
  36.  *
  37.  * @author Alexander Wenckus
  38.  *
  39.  */
  40. public class OutgoingFileTransfer extends FileTransfer {
  41.     private static final Logger LOGGER = Logger.getLogger(OutgoingFileTransfer.class.getName());

  42.     private static int RESPONSE_TIMEOUT = 60 * 1000;
  43.     private NegotiationProgress callback;

  44.     /**
  45.      * Returns the time in milliseconds after which the file transfer
  46.      * negotiation process will timeout if the other user has not responded.
  47.      *
  48.      * @return Returns the time in milliseconds after which the file transfer
  49.      *         negotiation process will timeout if the remote user has not
  50.      *         responded.
  51.      */
  52.     public static int getResponseTimeout() {
  53.         return RESPONSE_TIMEOUT;
  54.     }

  55.     /**
  56.      * Sets the time in milliseconds after which the file transfer negotiation
  57.      * process will timeout if the other user has not responded.
  58.      *
  59.      * @param responseTimeout
  60.      *            The timeout time in milliseconds.
  61.      */
  62.     public static void setResponseTimeout(int responseTimeout) {
  63.         RESPONSE_TIMEOUT = responseTimeout;
  64.     }

  65.     private OutputStream outputStream;

  66.     private Jid initiator;

  67.     private Thread transferThread;

  68.     protected OutgoingFileTransfer(Jid initiator, Jid target,
  69.             String streamID, FileTransferNegotiator transferNegotiator) {
  70.         super(target, streamID, transferNegotiator);
  71.         this.initiator = initiator;
  72.     }

  73.     protected void setOutputStream(OutputStream stream) {
  74.         if (outputStream == null) {
  75.             this.outputStream = stream;
  76.         }
  77.     }

  78.     /**
  79.      * Returns the output stream connected to the peer to transfer the file. It
  80.      * is only available after it has been successfully negotiated by the
  81.      * {@link StreamNegotiator}.
  82.      *
  83.      * @return Returns the output stream connected to the peer to transfer the
  84.      *         file.
  85.      */
  86.     protected OutputStream getOutputStream() {
  87.         if (getStatus().equals(FileTransfer.Status.negotiated)) {
  88.             return outputStream;
  89.         } else {
  90.             return null;
  91.         }
  92.     }

  93.     /**
  94.      * This method handles the negotiation of the file transfer and the stream,
  95.      * it only returns the created stream after the negotiation has been completed.
  96.      *
  97.      * @param fileName
  98.      *            The name of the file that will be transmitted. It is
  99.      *            preferable for this name to have an extension as it will be
  100.      *            used to determine the type of file it is.
  101.      * @param fileSize
  102.      *            The size in bytes of the file that will be transmitted.
  103.      * @param description
  104.      *            A description of the file that will be transmitted.
  105.      * @return The OutputStream that is connected to the peer to transmit the
  106.      *         file.
  107.      * @throws XMPPException
  108.      *             Thrown if an error occurs during the file transfer
  109.      *             negotiation process.
  110.      * @throws SmackException if there was no response from the server.
  111.      * @throws InterruptedException
  112.      */
  113.     public synchronized OutputStream sendFile(String fileName, long fileSize,
  114.             String description) throws XMPPException, SmackException, InterruptedException {
  115.         if (isDone() || outputStream != null) {
  116.             throw new IllegalStateException(
  117.                     "The negotation process has already"
  118.                             + " been attempted on this file transfer");
  119.         }
  120.         try {
  121.             setFileInfo(fileName, fileSize);
  122.             this.outputStream = negotiateStream(fileName, fileSize, description);
  123.         } catch (XMPPErrorException e) {
  124.             handleXMPPException(e);
  125.             throw e;
  126.         }
  127.         return outputStream;
  128.     }

  129.     /**
  130.      * This methods handles the transfer and stream negotiation process. It
  131.      * returns immediately and its progress will be updated through the
  132.      * {@link NegotiationProgress} callback.
  133.      *
  134.      * @param fileName
  135.      *            The name of the file that will be transmitted. It is
  136.      *            preferable for this name to have an extension as it will be
  137.      *            used to determine the type of file it is.
  138.      * @param fileSize
  139.      *            The size in bytes of the file that will be transmitted.
  140.      * @param description
  141.      *            A description of the file that will be transmitted.
  142.      * @param progress
  143.      *            A callback to monitor the progress of the file transfer
  144.      *            negotiation process and to retrieve the OutputStream when it
  145.      *            is complete.
  146.      */
  147.     public synchronized void sendFile(final String fileName,
  148.             final long fileSize, final String description,
  149.             final NegotiationProgress progress)
  150.     {
  151.         if(progress == null) {
  152.             throw new IllegalArgumentException("Callback progress cannot be null.");
  153.         }
  154.         checkTransferThread();
  155.         if (isDone() || outputStream != null) {
  156.             throw new IllegalStateException(
  157.                     "The negotation process has already"
  158.                             + " been attempted for this file transfer");
  159.         }
  160.         setFileInfo(fileName, fileSize);
  161.         this.callback = progress;
  162.         transferThread = new Thread(new Runnable() {
  163.             public void run() {
  164.                 try {
  165.                     OutgoingFileTransfer.this.outputStream = negotiateStream(
  166.                             fileName, fileSize, description);
  167.                     progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream);
  168.                 }
  169.                 catch (XMPPErrorException e) {
  170.                     handleXMPPException(e);
  171.                 }
  172.                 catch (Exception e) {
  173.                     setException(e);
  174.                 }
  175.             }
  176.         }, "File Transfer Negotiation " + streamID);
  177.         transferThread.start();
  178.     }

  179.     private void checkTransferThread() {
  180.         if (transferThread != null && transferThread.isAlive() || isDone()) {
  181.             throw new IllegalStateException(
  182.                     "File transfer in progress or has already completed.");
  183.         }
  184.     }

  185.     /**
  186.      * This method handles the stream negotiation process and transmits the file
  187.      * to the remote user. It returns immediately and the progress of the file
  188.      * transfer can be monitored through several methods:
  189.      *
  190.      * <UL>
  191.      * <LI>{@link FileTransfer#getStatus()}
  192.      * <LI>{@link FileTransfer#getProgress()}
  193.      * <LI>{@link FileTransfer#isDone()}
  194.      * </UL>
  195.      *
  196.      * @param file the file to transfer to the remote entity.
  197.      * @param description a description for the file to transfer.
  198.      * @throws SmackException
  199.      *             If there is an error during the negotiation process or the
  200.      *             sending of the file.
  201.      */
  202.     public synchronized void sendFile(final File file, final String description)
  203.             throws SmackException {
  204.         checkTransferThread();
  205.         if (file == null || !file.exists() || !file.canRead()) {
  206.             throw new IllegalArgumentException("Could not read file");
  207.         } else {
  208.             setFileInfo(file.getAbsolutePath(), file.getName(), file.length());
  209.         }

  210.         transferThread = new Thread(new Runnable() {
  211.             public void run() {
  212.                 try {
  213.                     outputStream = negotiateStream(file.getName(), file
  214.                             .length(), description);
  215.                 } catch (XMPPErrorException e) {
  216.                     handleXMPPException(e);
  217.                     return;
  218.                 }
  219.                 catch (Exception e) {
  220.                     setException(e);
  221.                 }
  222.                 if (outputStream == null) {
  223.                     return;
  224.                 }

  225.                 if (!updateStatus(Status.negotiated, Status.in_progress)) {
  226.                     return;
  227.                 }

  228.                 InputStream inputStream = null;
  229.                 try {
  230.                     inputStream = new FileInputStream(file);
  231.                     writeToStream(inputStream, outputStream);
  232.                 } catch (FileNotFoundException e) {
  233.                     setStatus(FileTransfer.Status.error);
  234.                     setError(Error.bad_file);
  235.                     setException(e);
  236.                 } catch (IOException e) {
  237.                     setStatus(FileTransfer.Status.error);
  238.                     setException(e);
  239.                 } finally {
  240.                         if (inputStream != null) {
  241.                             try {
  242.                                 inputStream.close();
  243.                             } catch (IOException e) {
  244.                                 LOGGER.log(Level.WARNING, "Closing input stream", e);
  245.                             }
  246.                         }

  247.                         try {
  248.                             outputStream.close();
  249.                         } catch (IOException e) {
  250.                             LOGGER.log(Level.WARNING, "Closing output stream", e);
  251.                         }
  252.                 }
  253.                 updateStatus(Status.in_progress, FileTransfer.Status.complete);
  254.                 }

  255.         }, "File Transfer " + streamID);
  256.         transferThread.start();
  257.     }

  258.     /**
  259.      * This method handles the stream negotiation process and transmits the file
  260.      * to the remote user. It returns immediately and the progress of the file
  261.      * transfer can be monitored through several methods:
  262.      *
  263.      * <UL>
  264.      * <LI>{@link FileTransfer#getStatus()}
  265.      * <LI>{@link FileTransfer#getProgress()}
  266.      * <LI>{@link FileTransfer#isDone()}
  267.      * </UL>
  268.      *
  269.      * @param in the stream to transfer to the remote entity.
  270.      * @param fileName the name of the file that is transferred
  271.      * @param fileSize the size of the file that is transferred
  272.      * @param description a description for the file to transfer.
  273.      */
  274.     public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description){
  275.         checkTransferThread();

  276.         setFileInfo(fileName, fileSize);
  277.         transferThread = new Thread(new Runnable() {
  278.             public void run() {
  279.                 //Create packet filter
  280.                 try {
  281.                     outputStream = negotiateStream(fileName, fileSize, description);
  282.                 } catch (XMPPErrorException e) {
  283.                     handleXMPPException(e);
  284.                     return;
  285.                 }
  286.                 catch (Exception e) {
  287.                     setException(e);
  288.                 }
  289.                 if (outputStream == null) {
  290.                     return;
  291.                 }

  292.                 if (!updateStatus(Status.negotiated, Status.in_progress)) {
  293.                     return;
  294.                 }
  295.                 try {
  296.                     writeToStream(in, outputStream);
  297.                 } catch (IOException e) {
  298.                     setStatus(FileTransfer.Status.error);
  299.                     setException(e);
  300.                 } finally {
  301.                     try {
  302.                         if (in != null) {
  303.                             in.close();
  304.                         }

  305.                         outputStream.flush();
  306.                         outputStream.close();
  307.                     } catch (IOException e) {
  308.                         /* Do Nothing */
  309.                     }
  310.                 }
  311.                 updateStatus(Status.in_progress, FileTransfer.Status.complete);
  312.                 }

  313.         }, "File Transfer " + streamID);
  314.         transferThread.start();
  315.     }

  316.     private void handleXMPPException(XMPPErrorException e) {
  317.         XMPPError error = e.getXMPPError();
  318.         if (error != null) {
  319.             switch (error.getCondition()) {
  320.             case forbidden:
  321.                 setStatus(Status.refused);
  322.                 return;
  323.             case bad_request:
  324.                 setStatus(Status.error);
  325.                 setError(Error.not_acceptable);
  326.                 break;
  327.             default:
  328.                 setStatus(FileTransfer.Status.error);
  329.             }
  330.         }

  331.         setException(e);
  332.     }

  333.     /**
  334.      * Returns the amount of bytes that have been sent for the file transfer. Or
  335.      * -1 if the file transfer has not started.
  336.      * <p>
  337.      * Note: This method is only useful when the {@link #sendFile(File, String)}
  338.      * method is called, as it is the only method that actually transmits the
  339.      * file.
  340.      *
  341.      * @return Returns the amount of bytes that have been sent for the file
  342.      *         transfer. Or -1 if the file transfer has not started.
  343.      */
  344.     public long getBytesSent() {
  345.         return amountWritten;
  346.     }

  347.     private OutputStream negotiateStream(String fileName, long fileSize,
  348.             String description) throws SmackException, XMPPException, InterruptedException {
  349.         // Negotiate the file transfer profile

  350.         if (!updateStatus(Status.initial, Status.negotiating_transfer)) {
  351.             throw new IllegalStateChangeException();
  352.         }
  353.         StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(
  354.                 getPeer(), streamID, fileName, fileSize, description,
  355.                 RESPONSE_TIMEOUT);

  356.         // Negotiate the stream
  357.         if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {
  358.             throw new IllegalStateChangeException();
  359.         }
  360.         outputStream = streamNegotiator.createOutgoingStream(streamID,
  361.                 initiator, getPeer());

  362.         if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {
  363.             throw new IllegalStateChangeException();
  364.         }
  365.         return outputStream;
  366.     }

  367.     public void cancel() {
  368.         setStatus(Status.cancelled);
  369.     }

  370.     @Override
  371.     protected boolean updateStatus(Status oldStatus, Status newStatus) {
  372.         boolean isUpdated = super.updateStatus(oldStatus, newStatus);
  373.         if(callback != null && isUpdated) {
  374.             callback.statusUpdated(oldStatus, newStatus);
  375.         }
  376.         return isUpdated;
  377.     }

  378.     @Override
  379.     protected void setStatus(Status status) {
  380.         Status oldStatus = getStatus();
  381.         super.setStatus(status);
  382.         if(callback != null) {
  383.             callback.statusUpdated(oldStatus, status);
  384.         }
  385.     }

  386.     @Override
  387.     protected void setException(Exception exception) {
  388.         super.setException(exception);
  389.         if(callback != null) {
  390.             callback.errorEstablishingStream(exception);
  391.         }
  392.     }

  393.     /**
  394.      * A callback class to retrieve the status of an outgoing transfer
  395.      * negotiation process.
  396.      *
  397.      * @author Alexander Wenckus
  398.      *
  399.      */
  400.     public interface NegotiationProgress {

  401.         /**
  402.          * Called when the status changes
  403.          *
  404.          * @param oldStatus the previous status of the file transfer.
  405.          * @param newStatus the new status of the file transfer.
  406.          */
  407.         void statusUpdated(Status oldStatus, Status newStatus);

  408.         /**
  409.          * Once the negotiation process is completed the output stream can be
  410.          * retrieved.
  411.          *
  412.          * @param stream the established stream which can be used to transfer the file to the remote
  413.          * entity
  414.          */
  415.         void outputStreamEstablished(OutputStream stream);

  416.         /**
  417.          * Called when an exception occurs during the negotiation progress.
  418.          *
  419.          * @param e the exception that occurred.
  420.          */
  421.         void errorEstablishingStream(Exception e);
  422.     }

  423. }