StreamNegotiator.java

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

import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.util.EventManger;
import org.jivesoftware.smack.util.EventManger.Callback;
import org.jivesoftware.smackx.si.packet.StreamInitiation;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid;

import java.io.InputStream;
import java.io.OutputStream;

/**
 * After the file transfer negotiation process is completed according to
 * XEP-0096, the negotiation process is passed off to a particular stream
 * negotiator. The stream negotiator will then negotiate the chosen stream and
 * return the stream to transfer the file.
 *
 * @author Alexander Wenckus
 */
public abstract class StreamNegotiator {

    /**
     * A event manager for stream initiation requests send to us.
     * <p>
     * Those are typical XEP-45 Open or XEP-65 Bytestream IQ requests. The even key is in the format
     * "initiationFrom + '\t' + streamId"
     * </p>
     */
    // TODO This field currently being static is considered a quick hack. Ideally this should take
    // the local connection into account, for example by changing the key to
    // "localJid + '\t' + initiationFrom + '\t' + streamId" or making the field non-static (but then
    // you need to provide access to the InitiationListeners, which could get tricky)
    protected static final EventManger<String, IQ, SmackException.NotConnectedException> initationSetEvents = new EventManger<>();

    /**
     * Creates the initiation acceptance packet to forward to the stream
     * initiator.
     *
     * @param streamInitiationOffer The offer from the stream initiator to connect for a stream.
     * @param namespaces            The namespace that relates to the accepted means of transfer.
     * @return The response to be forwarded to the initiator.
     */
    protected static StreamInitiation createInitiationAccept(
            StreamInitiation streamInitiationOffer, String[] namespaces)
    {
        StreamInitiation response = new StreamInitiation();
        response.setTo(streamInitiationOffer.getFrom());
        response.setFrom(streamInitiationOffer.getTo());
        response.setType(IQ.Type.result);
        response.setStanzaId(streamInitiationOffer.getStanzaId());

        DataForm form = new DataForm(DataForm.Type.submit);
        FormField field = new FormField(
                FileTransferNegotiator.STREAM_DATA_FIELD_NAME);
        for (String namespace : namespaces) {
            field.addValue(namespace);
        }
        form.addField(field);

        response.setFeatureNegotiationForm(form);
        return response;
    }

    protected final IQ initiateIncomingStream(final XMPPConnection connection, StreamInitiation initiation)
				   throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        final StreamInitiation response = createInitiationAccept(initiation,
                getNamespaces());

        newStreamInitiation(initiation.getFrom(), initiation.getSessionID());

        final String eventKey = initiation.getFrom().toString() + '\t' + initiation.getSessionID();
        IQ streamMethodInitiation;
        try {
            streamMethodInitiation = initationSetEvents.performActionAndWaitForEvent(eventKey, connection.getPacketReplyTimeout(), new Callback<NotConnectedException>() {
                @Override
                public void action() throws NotConnectedException {
                    try {
                        connection.sendStanza(response);
                    }
                    catch (InterruptedException e) {
                        // Ignore
                    }
                }
            });
        }
        catch (InterruptedException e) {
            // TODO remove this try/catch once merged into 4.2's master branch
            throw new IllegalStateException(e);
        }

        if (streamMethodInitiation == null) {
            throw NoResponseException.newWith(connection);
        }
        XMPPErrorException.ifHasErrorThenThrow(streamMethodInitiation);
        return streamMethodInitiation;
    }

    /**
     * Signal that a new stream initiation arrived. The negotiator may needs to prepare for it.
     *
     * @param from     The initiator of the file transfer.
     * @param streamID The stream ID related to the transfer.
     */
    protected abstract void newStreamInitiation(Jid from, String streamID);


    abstract InputStream negotiateIncomingStream(Stanza streamInitiation) throws XMPPErrorException,
            InterruptedException, NoResponseException, SmackException;

    /**
     * This method handles the file stream download negotiation process. The
     * appropriate stream negotiator's initiate incoming stream is called after
     * an appropriate file transfer method is selected. The manager will respond
     * to the initiator with the selected means of transfer, then it will handle
     * any negotiation specific to the particular transfer method. This method
     * returns the InputStream, ready to transfer the file.
     *
     * @param initiation The initiation that triggered this download.
     * @return After the negotiation process is complete, the InputStream to
     *         write a file to is returned.
     * @throws XMPPErrorException If an error occurs during this process an XMPPException is
     *                       thrown.
     * @throws InterruptedException If thread is interrupted.
     * @throws SmackException 
     */
    public abstract InputStream createIncomingStream(StreamInitiation initiation)
            throws XMPPErrorException, InterruptedException, NoResponseException, SmackException;

    /**
     * This method handles the file upload stream negotiation process. The
     * particular stream negotiator is determined during the file transfer
     * negotiation process. This method returns the OutputStream to transmit the
     * file to the remote user.
     *
     * @param streamID  The streamID that uniquely identifies the file transfer.
     * @param initiator The fully-qualified JID of the initiator of the file transfer.
     * @param target    The fully-qualified JID of the target or receiver of the file
     *                  transfer.
     * @return The negotiated stream ready for data.
     * @throws XMPPErrorException If an error occurs during the negotiation process an
     *                       exception will be thrown.
     * @throws SmackException 
     * @throws XMPPException 
     * @throws InterruptedException 
     */
    public abstract OutputStream createOutgoingStream(String streamID,
            Jid initiator, Jid target) throws XMPPErrorException, NoResponseException, SmackException, XMPPException, InterruptedException;

    /**
     * Returns the XMPP namespace reserved for this particular type of file
     * transfer.
     *
     * @return Returns the XMPP namespace reserved for this particular type of
     *         file transfer.
     */
    public abstract String[] getNamespaces();

    public static void signal(String eventKey, IQ eventValue) {
        initationSetEvents.signalEvent(eventKey, eventValue);
    }
}