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