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.FileNotFoundException;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.util.concurrent.Callable;
026import java.util.concurrent.ExecutionException;
027import java.util.concurrent.FutureTask;
028import java.util.concurrent.TimeUnit;
029import java.util.concurrent.TimeoutException;
030import java.util.logging.Logger;
031
032import org.jivesoftware.smack.SmackException;
033import org.jivesoftware.smack.SmackException.NoResponseException;
034import org.jivesoftware.smack.XMPPException.XMPPErrorException;
035import org.jivesoftware.smack.util.CloseableUtil;
036
037
038/**
039 * An incoming file transfer is created when the
040 * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)}
041 * method is invoked. It is a file being sent to the local user from another
042 * user on the jabber network. There are two stages of the file transfer to be
043 * concerned with and they can be handled in different ways depending upon the
044 * method that is invoked on this class.
045 *
046 * The first way that a file is received is by calling the
047 * {@link #receiveFile()} method. This method, negotiates the appropriate stream
048 * method and then returns the InputStream to read the file
049 * data from.
050 *
051 * The second way that a file can be received through this class is by invoking
052 * the {@link #receiveFile(File)} method. This method returns immediately and
053 * takes as its parameter a file on the local file system where the file
054 * recieved from the transfer will be put.
055 *
056 * @author Alexander Wenckus
057 */
058public class IncomingFileTransfer extends FileTransfer {
059
060    private static final Logger LOGGER = Logger.getLogger(IncomingFileTransfer.class.getName());
061
062    private FileTransferRequest receiveRequest;
063
064    private InputStream inputStream;
065
066    protected IncomingFileTransfer(FileTransferRequest request,
067            FileTransferNegotiator transferNegotiator) {
068        super(request.getRequestor(), request.getStreamID(), transferNegotiator);
069        this.receiveRequest = request;
070    }
071
072    /**
073     * Negotiates the stream method to transfer the file over and then returns
074     * the negotiated stream.
075     *
076     * @return The negotiated InputStream from which to read the data.
077     * @throws SmackException if Smack detected an exceptional situation.
078     * @throws XMPPErrorException If there is an error in the negotiation process an exception
079     *                       is thrown.
080     * @throws InterruptedException if the calling thread was interrupted.
081     */
082    public InputStream receiveFile() throws SmackException, XMPPErrorException, InterruptedException {
083        if (inputStream != null) {
084            throw new IllegalStateException("Transfer already negotiated!");
085        }
086
087        try {
088            inputStream = negotiateStream();
089        }
090        catch (XMPPErrorException e) {
091            setException(e);
092            throw e;
093        }
094
095        return inputStream;
096    }
097
098    /**
099     * This method negotiates the stream and then transfer's the file over the negotiated stream.
100     * The transferred file will be saved at the provided location.
101     *
102     * This method will return immediately, file transfer progress can be monitored through several
103     * methods:
104     *
105     * <UL>
106     * <LI>{@link FileTransfer#getStatus()}</LI>
107     * <LI>{@link FileTransfer#getProgress()}</LI>
108     * <LI>{@link FileTransfer#isDone()}</LI>
109     * </UL>
110     *
111     * @param file The location to save the file.
112     * @throws SmackException when the file transfer fails
113     * @throws IOException if an I/O error occurred.
114     * @throws IllegalArgumentException This exception is thrown when the the provided file is
115     *         either null, or cannot be written to.
116     */
117    public void receiveFile(final File file) throws SmackException, IOException {
118        if (file == null) {
119            throw new IllegalArgumentException("File cannot be null");
120        }
121        if (!file.exists()) {
122                 file.createNewFile();
123            }
124        if (!file.canWrite()) {
125                throw new IllegalArgumentException("Cannot write to provided file");
126        }
127
128        Thread transferThread = new Thread(new Runnable() {
129            @Override
130            public void run() {
131                try {
132                    inputStream = negotiateStream();
133                }
134                catch (Exception e) {
135                    setStatus(FileTransfer.Status.error);
136                    setException(e);
137                    return;
138                }
139
140                OutputStream outputStream = null;
141                try {
142                    outputStream = new FileOutputStream(file);
143                    setStatus(Status.in_progress);
144                    writeToStream(inputStream, outputStream);
145                }
146                catch (FileNotFoundException e) {
147                    setStatus(Status.error);
148                    setError(Error.bad_file);
149                    setException(e);
150                }
151                catch (IOException e) {
152                    setStatus(Status.error);
153                    setError(Error.stream);
154                    setException(e);
155                }
156
157
158                if (getStatus().equals(Status.in_progress)) {
159                    setStatus(Status.complete);
160                }
161                CloseableUtil.maybeClose(inputStream, LOGGER);
162                CloseableUtil.maybeClose(outputStream, LOGGER);
163            }
164        }, "File Transfer " + streamID);
165        transferThread.start();
166    }
167
168    private InputStream negotiateStream() throws SmackException, XMPPErrorException, InterruptedException {
169        setStatus(Status.negotiating_transfer);
170        final StreamNegotiator streamNegotiator = negotiator
171                .selectStreamNegotiator(receiveRequest);
172        setStatus(Status.negotiating_stream);
173        FutureTask<InputStream> streamNegotiatorTask = new FutureTask<>(
174                new Callable<InputStream>() {
175
176                    @Override
177                    public InputStream call() throws Exception {
178                        return streamNegotiator
179                                .createIncomingStream(receiveRequest.getStreamInitiation());
180                    }
181                });
182        streamNegotiatorTask.run();
183        InputStream inputStream;
184        try {
185            inputStream = streamNegotiatorTask.get(15, TimeUnit.SECONDS);
186        }
187        catch (ExecutionException e) {
188            final Throwable cause = e.getCause();
189            if (cause instanceof XMPPErrorException) {
190                throw (XMPPErrorException) cause;
191            }
192            if (cause instanceof InterruptedException) {
193                throw (InterruptedException) cause;
194            }
195            if (cause instanceof NoResponseException) {
196                throw (NoResponseException) cause;
197            }
198            if (cause instanceof SmackException) {
199                throw (SmackException) cause;
200            }
201            throw new SmackException.SmackWrappedException("Error in execution", e);
202        }
203        catch (TimeoutException e) {
204            throw new SmackException.SmackWrappedException("Request timed out", e);
205        }
206        finally {
207            streamNegotiatorTask.cancel(true);
208        }
209        setStatus(Status.negotiated);
210        return inputStream;
211    }
212
213    @Override
214    public void cancel() {
215        setStatus(Status.cancelled);
216    }
217
218}