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