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.si.packet;
018
019import java.util.Date;
020
021import org.jivesoftware.smack.packet.ExtensionElement;
022import org.jivesoftware.smack.packet.IQ;
023import org.jivesoftware.smack.util.StringUtils;
024
025import org.jivesoftware.smackx.xdata.packet.DataForm;
026
027import org.jxmpp.util.XmppDateTime;
028
029/**
030 * The process by which two entities initiate a stream.
031 *
032 * @author Alexander Wenckus
033 */
034public class StreamInitiation extends IQ {
035
036    public static final String ELEMENT = "si";
037    public static final String NAMESPACE = "http://jabber.org/protocol/si";
038
039    private String id;
040
041    private String mimeType;
042
043    private File file;
044
045    private Feature featureNegotiation;
046
047    public StreamInitiation() {
048        super(ELEMENT, NAMESPACE);
049    }
050
051    /**
052     * The "id" attribute is an opaque identifier. This attribute MUST be
053     * present on type='set', and MUST be a valid string. This SHOULD NOT be
054     * sent back on type='result', since the <iq/> "id" attribute provides the
055     * only context needed. This value is generated by the Sender, and the same
056     * value MUST be used throughout a session when talking to the Receiver.
057     *
058     * @param id The "id" attribute.
059     */
060    public void setSessionID(final String id) {
061        this.id = id;
062    }
063
064    /**
065     * Uniquely identifies a stream initiation to the recipient.
066     *
067     * @return The "id" attribute.
068     * @see #setSessionID(String)
069     */
070    public String getSessionID() {
071        return id;
072    }
073
074    /**
075     * The "mime-type" attribute identifies the MIME-type for the data across
076     * the stream. This attribute MUST be a valid MIME-type as registered with
077     * the Internet Assigned Numbers Authority (IANA) [3] (specifically, as
078     * listed at <http://www.iana.org/assignments/media-types>). During
079     * negotiation, this attribute SHOULD be present, and is otherwise not
080     * required. If not included during negotiation, its value is assumed to be
081     * "binary/octet-stream".
082     *
083     * @param mimeType The valid mime-type.
084     */
085    public void setMimeType(final String mimeType) {
086        this.mimeType = mimeType;
087    }
088
089    /**
090     * Identifies the type of file that is desired to be transferred.
091     *
092     * @return The mime-type.
093     * @see #setMimeType(String)
094     */
095    public String getMimeType() {
096        return mimeType;
097    }
098
099    /**
100     * Sets the file which contains the information pertaining to the file to be
101     * transferred.
102     *
103     * @param file The file identified by the stream initiator to be sent.
104     */
105    public void setFile(final File file) {
106        this.file = file;
107    }
108
109    /**
110     * Returns the file containing the information about the request.
111     *
112     * @return Returns the file containing the information about the request.
113     */
114    public File getFile() {
115        return file;
116    }
117
118    /**
119     * Sets the data form which contains the valid methods of stream negotiation
120     * and transfer.
121     *
122     * @param form The dataform containing the methods.
123     */
124    public void setFeatureNegotiationForm(final DataForm form) {
125        this.featureNegotiation = new Feature(form);
126    }
127
128    /**
129     * Returns the data form which contains the valid methods of stream
130     * negotiation and transfer.
131     *
132     * @return Returns the data form which contains the valid methods of stream
133     *         negotiation and transfer.
134     */
135    public DataForm getFeatureNegotiationForm() {
136        return featureNegotiation.getData();
137    }
138
139    @Override
140    protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder buf) {
141        switch (getType()) {
142        case set:
143            buf.optAttribute("id", getSessionID());
144            buf.optAttribute("mime-type", getMimeType());
145            buf.attribute("profile", NAMESPACE + "/profile/file-transfer");
146            buf.rightAngleBracket();
147
148            // Add the file section if there is one.
149            buf.optElement(file);
150            break;
151        case result:
152            buf.rightAngleBracket();
153            break;
154        default:
155            throw new IllegalArgumentException("IQ Type not understood");
156        }
157        if (featureNegotiation != null) {
158            buf.append(featureNegotiation.toXML());
159        }
160        return buf;
161    }
162
163    /**
164     * <ul>
165     * <li>size: The size, in bytes, of the data to be sent.</li>
166     * <li>name: The name of the file that the Sender wishes to send.</li>
167     * <li>date: The last modification time of the file. This is specified
168     * using the DateTime profile as described in Jabber Date and Time Profiles.</li>
169     * <li>hash: The MD5 sum of the file contents.</li>
170     * </ul>
171     * <p>
172     * &lt;desc&gt; is used to provide a sender-generated description of the
173     * file so the receiver can better understand what is being sent. It MUST
174     * NOT be sent in the result.
175     * </p>
176     * <p>
177     * When &lt;range&gt; is sent in the offer, it should have no attributes.
178     * This signifies that the sender can do ranged transfers. When a Stream
179     * Initiation result is sent with the &lt;range&gt; element, it uses these
180     * attributes:
181     * </p>
182     * <ul>
183     * <li>offset: Specifies the position, in bytes, to start transferring the
184     * file data from. This defaults to zero (0) if not specified.</li>
185     * <li>length - Specifies the number of bytes to retrieve starting at
186     * offset. This defaults to the length of the file from offset to the end.</li>
187     * </ul>
188     * Both attributes are OPTIONAL on the &lt;range&gt; element. Sending no
189     * attributes is synonymous with not sending the &lt;range&gt; element. When
190     * no &lt;range&gt; element is sent in the Stream Initiation result, the
191     * Sender MUST send the complete file starting at offset 0. More generally,
192     * data is sent over the stream byte for byte starting at the offset
193     * position for the length specified.
194     *
195     * @author Alexander Wenckus
196     */
197    public static class File implements ExtensionElement {
198
199        private final String name;
200
201        private final long size;
202
203        private String hash;
204
205        private Date date;
206
207        private String desc;
208
209        private boolean isRanged;
210
211        /**
212         * Constructor providing the name of the file and its size.
213         *
214         * @param name The name of the file.
215         * @param size The size of the file in bytes.
216         */
217        public File(final String name, final long size) {
218            if (name == null) {
219                throw new NullPointerException("name cannot be null");
220            }
221
222            this.name = name;
223            this.size = size;
224        }
225
226        /**
227         * Returns the file's name.
228         *
229         * @return Returns the file's name.
230         */
231        public String getName() {
232            return name;
233        }
234
235        /**
236         * Returns the file's size.
237         *
238         * @return Returns the file's size.
239         */
240        public long getSize() {
241            return size;
242        }
243
244        /**
245         * Sets the MD5 sum of the file's contents.
246         *
247         * @param hash The MD5 sum of the file's contents.
248         */
249        public void setHash(final String hash) {
250            this.hash = hash;
251        }
252
253        /**
254         * Returns the MD5 sum of the file's contents.
255         *
256         * @return Returns the MD5 sum of the file's contents
257         */
258        public String getHash() {
259            return hash;
260        }
261
262        /**
263         * Sets the date that the file was last modified.
264         *
265         * @param date The date that the file was last modified.
266         */
267        public void setDate(Date date) {
268            this.date = date;
269        }
270
271        /**
272         * Returns the date that the file was last modified.
273         *
274         * @return Returns the date that the file was last modified.
275         */
276        public Date getDate() {
277            return date;
278        }
279
280        /**
281         * Sets the description of the file.
282         *
283         * @param desc The description of the file so that the file receiver can
284         *             know what file it is.
285         */
286        public void setDesc(final String desc) {
287            this.desc = desc;
288        }
289
290        /**
291         * Returns the description of the file.
292         *
293         * @return Returns the description of the file.
294         */
295        public String getDesc() {
296            return desc;
297        }
298
299        /**
300         * True if a range can be provided and false if it cannot.
301         *
302         * @param isRanged True if a range can be provided and false if it cannot.
303         */
304        public void setRanged(final boolean isRanged) {
305            this.isRanged = isRanged;
306        }
307
308        /**
309         * Returns whether or not the initiator can support a range for the file
310         * transfer.
311         *
312         * @return Returns whether or not the initiator can support a range for
313         *         the file transfer.
314         */
315        public boolean isRanged() {
316            return isRanged;
317        }
318
319        @Override
320        public String getElementName() {
321            return "file";
322        }
323
324        @Override
325        public String getNamespace() {
326            return "http://jabber.org/protocol/si/profile/file-transfer";
327        }
328
329        @Override
330        public String toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
331            StringBuilder buffer = new StringBuilder();
332
333            buffer.append('<').append(getElementName()).append(" xmlns=\"")
334                    .append(getNamespace()).append("\" ");
335
336            if (getName() != null) {
337                buffer.append("name=\"").append(StringUtils.escapeForXmlAttribute(getName())).append("\" ");
338            }
339
340            if (getSize() > 0) {
341                buffer.append("size=\"").append(getSize()).append("\" ");
342            }
343
344            if (getDate() != null) {
345                buffer.append("date=\"").append(XmppDateTime.formatXEP0082Date(date)).append("\" ");
346            }
347
348            if (getHash() != null) {
349                buffer.append("hash=\"").append(getHash()).append("\" ");
350            }
351
352            if ((desc != null && desc.length() > 0) || isRanged) {
353                buffer.append('>');
354                if (getDesc() != null && desc.length() > 0) {
355                    buffer.append("<desc>").append(StringUtils.escapeForXmlText(getDesc())).append("</desc>");
356                }
357                if (isRanged()) {
358                    buffer.append("<range/>");
359                }
360                buffer.append("</").append(getElementName()).append('>');
361            }
362            else {
363                buffer.append("/>");
364            }
365            return buffer.toString();
366        }
367    }
368
369    /**
370     * The feature negotiation portion of the StreamInitiation packet.
371     *
372     * @author Alexander Wenckus
373     *
374     */
375    public static class Feature implements ExtensionElement {
376
377        private final DataForm data;
378
379        /**
380         * The dataform can be provided as part of the constructor.
381         *
382         * @param data The dataform.
383         */
384        public Feature(final DataForm data) {
385            this.data = data;
386        }
387
388        /**
389         * Returns the dataform associated with the feature negotiation.
390         *
391         * @return Returns the dataform associated with the feature negotiation.
392         */
393        public DataForm getData() {
394            return data;
395        }
396
397        @Override
398        public String getNamespace() {
399            return "http://jabber.org/protocol/feature-neg";
400        }
401
402        @Override
403        public String getElementName() {
404            return "feature";
405        }
406
407        @Override
408        public String toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
409            StringBuilder buf = new StringBuilder();
410            buf
411                    .append("<feature xmlns=\"http://jabber.org/protocol/feature-neg\">");
412            buf.append(data.toXML());
413            buf.append("</feature>");
414            return buf.toString();
415        }
416    }
417}