StreamInitiation.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.si.packet;

import java.util.Date;

import javax.xml.namespace.QName;

import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.StringUtils;

import org.jivesoftware.smackx.xdata.packet.DataForm;

import org.jxmpp.util.XmppDateTime;

/**
 * The process by which two entities initiate a stream.
 *
 * @author Alexander Wenckus
 */
public class StreamInitiation extends IQ {

    public static final String ELEMENT = "si";
    public static final String NAMESPACE = "http://jabber.org/protocol/si";

    private String id;

    private String mimeType;

    private File file;

    private Feature featureNegotiation;

    public StreamInitiation() {
        super(ELEMENT, NAMESPACE);
    }

    /**
     * The "id" attribute is an opaque identifier. This attribute MUST be
     * present on type='set', and MUST be a valid string. This SHOULD NOT be
     * sent back on type='result', since the <iq/> "id" attribute provides the
     * only context needed. This value is generated by the Sender, and the same
     * value MUST be used throughout a session when talking to the Receiver.
     *
     * @param id The "id" attribute.
     */
    public void setSessionID(final String id) {
        this.id = id;
    }

    /**
     * Uniquely identifies a stream initiation to the recipient.
     *
     * @return The "id" attribute.
     * @see #setSessionID(String)
     */
    public String getSessionID() {
        return id;
    }

    /**
     * The "mime-type" attribute identifies the MIME-type for the data across
     * the stream. This attribute MUST be a valid MIME-type as registered with
     * the Internet Assigned Numbers Authority (IANA) [3] (specifically, as
     * listed at <http://www.iana.org/assignments/media-types>). During
     * negotiation, this attribute SHOULD be present, and is otherwise not
     * required. If not included during negotiation, its value is assumed to be
     * "binary/octet-stream".
     *
     * @param mimeType The valid mime-type.
     */
    public void setMimeType(final String mimeType) {
        this.mimeType = mimeType;
    }

    /**
     * Identifies the type of file that is desired to be transferred.
     *
     * @return The mime-type.
     * @see #setMimeType(String)
     */
    public String getMimeType() {
        return mimeType;
    }

    /**
     * Sets the file which contains the information pertaining to the file to be
     * transferred.
     *
     * @param file The file identified by the stream initiator to be sent.
     */
    public void setFile(final File file) {
        this.file = file;
    }

    /**
     * Returns the file containing the information about the request.
     *
     * @return Returns the file containing the information about the request.
     */
    public File getFile() {
        return file;
    }

    /**
     * Sets the data form which contains the valid methods of stream negotiation
     * and transfer.
     *
     * @param form The dataform containing the methods.
     */
    public void setFeatureNegotiationForm(final DataForm form) {
        this.featureNegotiation = new Feature(form);
    }

    /**
     * Returns the data form which contains the valid methods of stream
     * negotiation and transfer.
     *
     * @return Returns the data form which contains the valid methods of stream
     *         negotiation and transfer.
     */
    public DataForm getFeatureNegotiationForm() {
        return featureNegotiation.getData();
    }

    @Override
    protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder buf) {
        switch (getType()) {
        case set:
            buf.optAttribute("id", getSessionID());
            buf.optAttribute("mime-type", getMimeType());
            buf.attribute("profile", NAMESPACE + "/profile/file-transfer");
            buf.rightAngleBracket();

            // Add the file section if there is one.
            buf.optElement(file);
            break;
        case result:
            buf.rightAngleBracket();
            break;
        default:
            throw new IllegalArgumentException("IQ Type not understood");
        }
        if (featureNegotiation != null) {
            buf.append(featureNegotiation.toXML());
        }
        return buf;
    }

    /**
     * <ul>
     * <li>size: The size, in bytes, of the data to be sent.</li>
     * <li>name: The name of the file that the Sender wishes to send.</li>
     * <li>date: The last modification time of the file. This is specified
     * using the DateTime profile as described in Jabber Date and Time Profiles.</li>
     * <li>hash: The MD5 sum of the file contents.</li>
     * </ul>
     * <p>
     * &lt;desc&gt; is used to provide a sender-generated description of the
     * file so the receiver can better understand what is being sent. It MUST
     * NOT be sent in the result.
     * </p>
     * <p>
     * When &lt;range&gt; is sent in the offer, it should have no attributes.
     * This signifies that the sender can do ranged transfers. When a Stream
     * Initiation result is sent with the &lt;range&gt; element, it uses these
     * attributes:
     * </p>
     * <ul>
     * <li>offset: Specifies the position, in bytes, to start transferring the
     * file data from. This defaults to zero (0) if not specified.</li>
     * <li>length - Specifies the number of bytes to retrieve starting at
     * offset. This defaults to the length of the file from offset to the end.</li>
     * </ul>
     * Both attributes are OPTIONAL on the &lt;range&gt; element. Sending no
     * attributes is synonymous with not sending the &lt;range&gt; element. When
     * no &lt;range&gt; element is sent in the Stream Initiation result, the
     * Sender MUST send the complete file starting at offset 0. More generally,
     * data is sent over the stream byte for byte starting at the offset
     * position for the length specified.
     *
     * @author Alexander Wenckus
     */
    public static class File implements ExtensionElement {

        public static final String ELEMENT = "file";
        public static final String NAMESPACE = "http://jabber.org/protocol/si/profile/file-transfer";
        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);

        private final String name;

        private final long size;

        private String hash;

        private Date date;

        private String desc;

        private boolean isRanged;

        /**
         * Constructor providing the name of the file and its size.
         *
         * @param name The name of the file.
         * @param size The size of the file in bytes.
         */
        public File(final String name, final long size) {
            if (name == null) {
                throw new NullPointerException("name cannot be null");
            }

            this.name = name;
            this.size = size;
        }

        /**
         * Returns the file's name.
         *
         * @return Returns the file's name.
         */
        public String getName() {
            return name;
        }

        /**
         * Returns the file's size.
         *
         * @return Returns the file's size.
         */
        public long getSize() {
            return size;
        }

        /**
         * Sets the MD5 sum of the file's contents.
         *
         * @param hash The MD5 sum of the file's contents.
         */
        public void setHash(final String hash) {
            this.hash = hash;
        }

        /**
         * Returns the MD5 sum of the file's contents.
         *
         * @return Returns the MD5 sum of the file's contents
         */
        public String getHash() {
            return hash;
        }

        /**
         * Sets the date that the file was last modified.
         *
         * @param date The date that the file was last modified.
         */
        public void setDate(Date date) {
            this.date = date;
        }

        /**
         * Returns the date that the file was last modified.
         *
         * @return Returns the date that the file was last modified.
         */
        public Date getDate() {
            return date;
        }

        /**
         * Sets the description of the file.
         *
         * @param desc The description of the file so that the file receiver can
         *             know what file it is.
         */
        public void setDesc(final String desc) {
            this.desc = desc;
        }

        /**
         * Returns the description of the file.
         *
         * @return Returns the description of the file.
         */
        public String getDesc() {
            return desc;
        }

        /**
         * True if a range can be provided and false if it cannot.
         *
         * @param isRanged True if a range can be provided and false if it cannot.
         */
        public void setRanged(final boolean isRanged) {
            this.isRanged = isRanged;
        }

        /**
         * Returns whether or not the initiator can support a range for the file
         * transfer.
         *
         * @return Returns whether or not the initiator can support a range for
         *         the file transfer.
         */
        public boolean isRanged() {
            return isRanged;
        }

        @Override
        public String getElementName() {
            return QNAME.getLocalPart();
        }

        @Override
        public String getNamespace() {
            return QNAME.getNamespaceURI();
        }

        @Override
        public String toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
            StringBuilder buffer = new StringBuilder();

            buffer.append('<').append(getElementName()).append(" xmlns=\"")
                    .append(getNamespace()).append("\" ");

            if (getName() != null) {
                buffer.append("name=\"").append(StringUtils.escapeForXmlAttribute(getName())).append("\" ");
            }

            if (getSize() > 0) {
                buffer.append("size=\"").append(getSize()).append("\" ");
            }

            if (getDate() != null) {
                buffer.append("date=\"").append(XmppDateTime.formatXEP0082Date(date)).append("\" ");
            }

            if (getHash() != null) {
                buffer.append("hash=\"").append(getHash()).append("\" ");
            }

            if ((desc != null && desc.length() > 0) || isRanged) {
                buffer.append('>');
                if (getDesc() != null && desc.length() > 0) {
                    buffer.append("<desc>").append(StringUtils.escapeForXmlText(getDesc())).append("</desc>");
                }
                if (isRanged()) {
                    buffer.append("<range/>");
                }
                buffer.append("</").append(getElementName()).append('>');
            }
            else {
                buffer.append("/>");
            }
            return buffer.toString();
        }
    }

    /**
     * The feature negotiation portion of the StreamInitiation packet.
     *
     * @author Alexander Wenckus
     *
     */
    public static class Feature implements ExtensionElement {

        public static final QName QNAME = new QName("http://jabber.org/protocol/feature-neg", "feature");

        private final DataForm data;

        /**
         * The dataform can be provided as part of the constructor.
         *
         * @param data The dataform.
         */
        public Feature(final DataForm data) {
            this.data = data;
        }

        /**
         * Returns the dataform associated with the feature negotiation.
         *
         * @return Returns the dataform associated with the feature negotiation.
         */
        public DataForm getData() {
            return data;
        }

        @Override
        public String getElementName() {
            return QNAME.getLocalPart();
        }

        @Override
        public String getNamespace() {
            return QNAME.getNamespaceURI();
        }

        @Override
        public String toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
            StringBuilder buf = new StringBuilder();
            buf
                    .append("<feature xmlns=\"http://jabber.org/protocol/feature-neg\">");
            buf.append(data.toXML());
            buf.append("</feature>");
            return buf.toString();
        }
    }
}