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.InputStream;
020import java.io.OutputStream;
021
022import org.jivesoftware.smack.Manager;
023import org.jivesoftware.smack.SmackException;
024import org.jivesoftware.smack.SmackException.NoResponseException;
025import org.jivesoftware.smack.SmackException.NotConnectedException;
026import org.jivesoftware.smack.XMPPConnection;
027import org.jivesoftware.smack.XMPPException;
028import org.jivesoftware.smack.XMPPException.XMPPErrorException;
029import org.jivesoftware.smack.packet.IQ;
030import org.jivesoftware.smack.packet.Stanza;
031import org.jivesoftware.smack.util.EventManger;
032import org.jivesoftware.smack.util.EventManger.Callback;
033
034import org.jivesoftware.smackx.si.packet.StreamInitiation;
035import org.jivesoftware.smackx.xdata.FormField;
036import org.jivesoftware.smackx.xdata.ListSingleFormField;
037import org.jivesoftware.smackx.xdata.packet.DataForm;
038
039import org.jxmpp.jid.Jid;
040
041/**
042 * After the file transfer negotiation process is completed according to
043 * XEP-0096, the negotiation process is passed off to a particular stream
044 * negotiator. The stream negotiator will then negotiate the chosen stream and
045 * return the stream to transfer the file.
046 *
047 * @author Alexander Wenckus
048 */
049public abstract class StreamNegotiator extends Manager {
050
051    protected StreamNegotiator(XMPPConnection connection) {
052        super(connection);
053    }
054
055    /**
056     * A event manager for stream initiation requests send to us.
057     * <p>
058     * Those are typical XEP-45 Open or XEP-65 Bytestream IQ requests. The even key is in the format
059     * "initiationFrom + '\t' + streamId"
060     * </p>
061     */
062    // TODO This field currently being static is considered a quick hack. Ideally this should take
063    // the local connection into account, for example by changing the key to
064    // "localJid + '\t' + initiationFrom + '\t' + streamId" or making the field non-static (but then
065    // you need to provide access to the InitiationListeners, which could get tricky)
066    protected static final EventManger<String, IQ, SmackException.NotConnectedException> initationSetEvents = new EventManger<>();
067
068    /**
069     * Creates the initiation acceptance stanza to forward to the stream
070     * initiator.
071     *
072     * @param streamInitiationOffer The offer from the stream initiator to connect for a stream.
073     * @param namespace            The namespace that relates to the accepted means of transfer.
074     * @return The response to be forwarded to the initiator.
075     */
076    protected static StreamInitiation createInitiationAccept(
077            StreamInitiation streamInitiationOffer, String namespace) {
078        StreamInitiation response = new StreamInitiation();
079        response.setTo(streamInitiationOffer.getFrom());
080        response.setFrom(streamInitiationOffer.getTo());
081        response.setType(IQ.Type.result);
082        response.setStanzaId(streamInitiationOffer.getStanzaId());
083
084        DataForm.Builder form = DataForm.builder();
085        ListSingleFormField.Builder field = FormField.listSingleBuilder(
086                FileTransferNegotiator.STREAM_DATA_FIELD_NAME);
087        field.setValue(namespace);
088        form.addField(field.build());
089
090        response.setFeatureNegotiationForm(form.build());
091        return response;
092    }
093
094    protected final IQ initiateIncomingStream(final XMPPConnection connection, StreamInitiation initiation)
095                    throws NoResponseException, XMPPErrorException, NotConnectedException {
096        final StreamInitiation response = createInitiationAccept(initiation,
097                getNamespace());
098
099        newStreamInitiation(initiation.getFrom(), initiation.getSessionID());
100
101        final String eventKey = initiation.getFrom().toString() + '\t' + initiation.getSessionID();
102        IQ streamMethodInitiation;
103        try {
104            streamMethodInitiation = initationSetEvents.performActionAndWaitForEvent(eventKey, connection.getReplyTimeout(), new Callback<NotConnectedException>() {
105                @Override
106                public void action() throws NotConnectedException {
107                    try {
108                        connection.sendStanza(response);
109                    }
110                    catch (InterruptedException e) {
111                        // Ignore
112                    }
113                }
114            });
115        }
116        catch (InterruptedException e) {
117            // TODO remove this try/catch once merged into 4.2's master branch
118            throw new IllegalStateException(e);
119        }
120
121        if (streamMethodInitiation == null) {
122            throw NoResponseException.newWith(connection, "stream initiation");
123        }
124        XMPPErrorException.ifHasErrorThenThrow(streamMethodInitiation);
125        return streamMethodInitiation;
126    }
127
128    /**
129     * Signal that a new stream initiation arrived. The negotiator may needs to prepare for it.
130     *
131     * @param from     The initiator of the file transfer.
132     * @param streamID The stream ID related to the transfer.
133     */
134    protected abstract void newStreamInitiation(Jid from, String streamID);
135
136
137    abstract InputStream negotiateIncomingStream(Stanza streamInitiation) throws XMPPErrorException,
138            InterruptedException, SmackException;
139
140    /**
141     * This method handles the file stream download negotiation process. The
142     * appropriate stream negotiator's initiate incoming stream is called after
143     * an appropriate file transfer method is selected. The manager will respond
144     * to the initiator with the selected means of transfer, then it will handle
145     * any negotiation specific to the particular transfer method. This method
146     * returns the InputStream, ready to transfer the file.
147     *
148     * @param initiation The initiation that triggered this download.
149     * @return After the negotiation process is complete, the InputStream to
150     *         write a file to is returned.
151     * @throws XMPPErrorException If an error occurs during this process an XMPPException is
152     *                       thrown.
153     * @throws InterruptedException If thread is interrupted.
154     * @throws SmackException if Smack detected an exceptional situation.
155     */
156    public abstract InputStream createIncomingStream(StreamInitiation initiation)
157            throws XMPPErrorException, InterruptedException, SmackException;
158
159    /**
160     * This method handles the file upload stream negotiation process. The
161     * particular stream negotiator is determined during the file transfer
162     * negotiation process. This method returns the OutputStream to transmit the
163     * file to the remote user.
164     *
165     * @param streamID  The streamID that uniquely identifies the file transfer.
166     * @param initiator The fully-qualified JID of the initiator of the file transfer.
167     * @param target    The fully-qualified JID of the target or receiver of the file
168     *                  transfer.
169     * @return The negotiated stream ready for data.
170     * @throws SmackException if Smack detected an exceptional situation.
171     * @throws XMPPException if an XMPP protocol error was received.
172     * @throws InterruptedException if the calling thread was interrupted.
173     */
174    public abstract OutputStream createOutgoingStream(String streamID,
175            Jid initiator, Jid target) throws SmackException, XMPPException, InterruptedException;
176
177    /**
178     * Returns the XMPP namespace reserved for this particular type of file
179     * transfer.
180     *
181     * @return Returns the XMPP namespace reserved for this particular type of
182     *         file transfer.
183     */
184    public abstract String getNamespace();
185
186    public static void signal(String eventKey, IQ eventValue) {
187        initationSetEvents.signalEvent(eventKey, eventValue);
188    }
189}