001/**
002 *
003 * Copyright 2003-2007 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 */
017
018package org.jivesoftware.smack.packet;
019
020import static org.jivesoftware.smack.util.StringUtils.requireNotNullOrEmpty;
021
022import org.jivesoftware.smack.packet.id.StanzaIdUtil;
023import org.jivesoftware.smack.util.MultiMap;
024import org.jivesoftware.smack.util.PacketUtil;
025import org.jivesoftware.smack.util.XmlStringBuilder;
026import org.jxmpp.jid.Jid;
027import org.jxmpp.jid.impl.JidCreate;
028import org.jxmpp.stringprep.XmppStringprepException;
029import org.jxmpp.util.XmppStringUtils;
030
031import java.util.Collection;
032import java.util.List;
033import java.util.Locale;
034
035/**
036 * Base class for XMPP Stanzas, which are called Stanza(/Packet) in older versions of Smack (i.e. < 4.1).
037 * <p>
038 * Every stanza has a unique ID (which is automatically generated, but can be overridden). Stanza
039 * IDs are required for IQ stanzas and recommended for presence and message stanzas. Optionally, the
040 * "to" and "from" fields can be set.
041 * </p>
042 * <p>
043 * XMPP Stanzas are {@link Message}, {@link IQ} and {@link Presence}. Which therefore subclass this
044 * class. <b>If you think you need to subclass this class, then you are doing something wrong.</b>
045 * </p>
046 *
047 * @author Matt Tucker
048 * @see <a href="http://xmpp.org/rfcs/rfc6120.html#stanzas">RFC 6120 ยง 8. XML Stanzas</a>
049 */
050public abstract class Stanza implements TopLevelStreamElement {
051
052    public static final String TEXT = "text";
053    public static final String ITEM = "item";
054
055    protected static final String DEFAULT_LANGUAGE =
056            java.util.Locale.getDefault().getLanguage().toLowerCase(Locale.US);
057
058    private final MultiMap<String, ExtensionElement> packetExtensions = new MultiMap<>();
059
060    private String id = null;
061    private Jid to;
062    private Jid from;
063    private XMPPError error = null;
064
065    /**
066     * Optional value of the 'xml:lang' attribute of the outermost element of
067     * the stanza.
068     * <p>
069     * Such an attribute is defined for all stanza types. For IQ, see for
070     * example XEP-50 3.7:
071     * "The requester SHOULD provide its locale information using the "xml:lang
072     * " attribute on either the <iq/> (RECOMMENDED) or <command/> element."
073     * </p>
074     */
075    protected String language;
076
077    protected Stanza() {
078        this(StanzaIdUtil.newStanzaId());
079    }
080
081    protected Stanza(String stanzaId) {
082        setStanzaId(stanzaId);
083    }
084
085    protected Stanza(Stanza p) {
086        id = p.getStanzaId();
087        to = p.getTo();
088        from = p.getFrom();
089        error = p.error;
090
091        // Copy extensions
092        for (ExtensionElement pe : p.getExtensions()) {
093            addExtension(pe);
094        }
095    }
096
097    /**
098     * Returns the unique ID of the stanza. The returned value could be <code>null</code>.
099     *
100     * @return the packet's unique ID or <code>null</code> if the id is not available.
101     */
102    public String getStanzaId() {
103        return id;
104    }
105
106    /**
107     * Get the Stanza ID.
108     * @return the stanza id.
109     * @deprecated use {@link #getStanzaId()} instead.
110     */
111    @Deprecated
112    public String getPacketID() {
113        return getStanzaId();
114    }
115
116    /**
117     * Sets the unique ID of the packet. To indicate that a stanza(/packet) has no id
118     * pass <code>null</code> as the packet's id value.
119     *
120     * @param id the unique ID for the packet.
121     */
122    public void setStanzaId(String id) {
123        if (id != null) {
124            requireNotNullOrEmpty(id, "id must either be null or not the empty String");
125        }
126        this.id = id;
127    }
128
129    /**
130     * Set the stanza ID.
131     * @param packetID
132     * @deprecated use {@link #setStanzaId(String)} instead.
133     */
134    @Deprecated
135    public void setPacketID(String packetID) {
136        setStanzaId(packetID);
137    }
138
139    /**
140     * Check if this stanza has an ID set.
141     *
142     * @return true if the stanza ID is set, false otherwise.
143     * @since 4.1
144     */
145    public boolean hasStanzaIdSet() {
146        // setStanzaId ensures that the id is either null or not empty,
147        // so we can assume that it is set if it's not null.
148        return id != null;
149    }
150
151    /**
152     * Set the stanza id if none is set.
153     * 
154     * @return the stanza id.
155     * @since 4.2
156     */
157    public String setStanzaId() {
158        if (!hasStanzaIdSet()) {
159            setStanzaId(StanzaIdUtil.newStanzaId());
160        }
161        return getStanzaId();
162    }
163
164    /**
165     * Returns who the stanza(/packet) is being sent "to", or <tt>null</tt> if
166     * the value is not set. The XMPP protocol often makes the "to"
167     * attribute optional, so it does not always need to be set.<p>
168     *
169     * @return who the stanza(/packet) is being sent to, or <tt>null</tt> if the
170     *      value has not been set.
171     */
172    public Jid getTo() {
173        return to;
174    }
175
176    /**
177     * Sets who the stanza(/packet) is being sent "to". The XMPP protocol often makes
178     * the "to" attribute optional, so it does not always need to be set.
179     *
180     * @param to who the stanza(/packet) is being sent to.
181     * @throws IllegalArgumentException if to is not a valid JID String.
182     * @deprecated use {@link #setTo(Jid)} instead.
183     */
184    @Deprecated
185    public void setTo(String to) {
186        Jid jid;
187        try {
188            jid = JidCreate.from(to);
189        }
190        catch (XmppStringprepException e) {
191            throw new IllegalArgumentException(e);
192        }
193        setTo(jid);
194    }
195
196    /**
197     * Sets who the packet is being sent "to". The XMPP protocol often makes
198     * the "to" attribute optional, so it does not always need to be set.
199     *
200     * @param to who the packet is being sent to.
201     */
202    public void setTo(Jid to) {
203        this.to = to;
204    }
205
206    /**
207     * Returns who the stanza(/packet) is being sent "from" or <tt>null</tt> if
208     * the value is not set. The XMPP protocol often makes the "from"
209     * attribute optional, so it does not always need to be set.<p>
210     *
211     * @return who the stanza(/packet) is being sent from, or <tt>null</tt> if the
212     *      value has not been set.
213     */
214    public Jid getFrom() {
215        return from;
216    }
217
218    /**
219     * Sets who the stanza(/packet) is being sent "from". The XMPP protocol often
220     * makes the "from" attribute optional, so it does not always need to
221     * be set.
222     *
223     * @param from who the stanza(/packet) is being sent to.
224     * @throws IllegalArgumentException if from is not a valid JID String.
225     * @deprecated use {@link #setFrom(Jid)} instead.
226     */
227    @Deprecated
228    public void setFrom(String from) {
229        Jid jid;
230        try {
231            jid = JidCreate.from(from);
232        }
233        catch (XmppStringprepException e) {
234            throw new IllegalArgumentException(e);
235        }
236        setFrom(jid);
237    }
238
239    /**
240     * Sets who the packet is being sent "from". The XMPP protocol often
241     * makes the "from" attribute optional, so it does not always need to
242     * be set.
243     *
244     * @param from who the packet is being sent to.
245     */
246    public void setFrom(Jid from) {
247        this.from = from;
248    }
249
250    /**
251     * Returns the error associated with this packet, or <tt>null</tt> if there are
252     * no errors.
253     *
254     * @return the error sub-packet or <tt>null</tt> if there isn't an error.
255     */
256    public XMPPError getError() {
257        return error;
258    }
259
260    /**
261     * Sets the error for this packet.
262     *
263     * @param error the error to associate with this packet.
264     * @deprecated use {@link #setError(org.jivesoftware.smack.packet.XMPPError.Builder)} instead.
265     */
266    @Deprecated
267    public void setError(XMPPError error) {
268        this.error = error;
269    }
270
271    /**
272     * Sets the error for this stanza.
273     *
274     * @param xmppErrorBuilder the error to associate with this stanza.
275     */
276    public void setError(XMPPError.Builder xmppErrorBuilder) {
277        if (xmppErrorBuilder == null) {
278            return;
279        }
280        xmppErrorBuilder.setStanza(this);
281        error = xmppErrorBuilder.build();
282    }
283
284    /**
285     * Returns the xml:lang of this Stanza, or null if one has not been set.
286     *
287     * @return the xml:lang of this Stanza, or null.
288     */
289    public String getLanguage() {
290        return language;
291    }
292
293    /**
294     * Sets the xml:lang of this Stanza.
295     *
296     * @param language the xml:lang of this Stanza.
297     */
298    public void setLanguage(String language) {
299        this.language = language;
300    }
301
302    /**
303     * Returns a list of all extension elements of this stanza.
304     *
305     * @return a list of all extension elements of this stanza.
306     */
307    public List<ExtensionElement> getExtensions() {
308        synchronized (packetExtensions) {
309            // No need to create a new list, values() will already create a new one for us
310            return packetExtensions.values();
311        }
312    }
313
314    /**
315     * Return a list of all extensions with the given element name <em>and</em> namespace.
316     * <p>
317     * Changes to the returned set will update the stanza(/packet) extensions, if the returned set is not the empty set.
318     * </p>
319     *
320     * @param elementName the element name, must not be null.
321     * @param namespace the namespace of the element(s), must not be null.
322     * @return a set of all matching extensions.
323     * @since 4.1
324     */
325    public List<ExtensionElement> getExtensions(String elementName, String namespace) {
326        requireNotNullOrEmpty(elementName, "elementName must not be null or empty");
327        requireNotNullOrEmpty(namespace, "namespace must not be null or empty");
328        String key = XmppStringUtils.generateKey(elementName, namespace);
329        return packetExtensions.getAll(key);
330    }
331
332    /**
333     * Returns the first extension of this stanza(/packet) that has the given namespace.
334     * <p>
335     * When possible, use {@link #getExtension(String,String)} instead.
336     * </p>
337     *
338     * @param namespace the namespace of the extension that is desired.
339     * @return the stanza(/packet) extension with the given namespace.
340     */
341    public ExtensionElement getExtension(String namespace) {
342        return PacketUtil.extensionElementFrom(getExtensions(), null, namespace);
343    }
344
345    /**
346     * Returns the first extension that matches the specified element name and
347     * namespace, or <tt>null</tt> if it doesn't exist. If the provided elementName is null,
348     * only the namespace is matched. Extensions are
349     * are arbitrary XML elements in standard XMPP stanzas.
350     *
351     * @param elementName the XML element name of the extension. (May be null)
352     * @param namespace the XML element namespace of the extension.
353     * @return the extension, or <tt>null</tt> if it doesn't exist.
354     */
355    @SuppressWarnings("unchecked")
356    public <PE extends ExtensionElement> PE getExtension(String elementName, String namespace) {
357        if (namespace == null) {
358            return null;
359        }
360        String key = XmppStringUtils.generateKey(elementName, namespace);
361        ExtensionElement packetExtension;
362        synchronized (packetExtensions) {
363            packetExtension = packetExtensions.getFirst(key);
364        }
365        if (packetExtension == null) {
366            return null;
367        }
368        return (PE) packetExtension;
369    }
370
371    /**
372     * Adds a stanza(/packet) extension to the packet. Does nothing if extension is null.
373     *
374     * @param extension a stanza(/packet) extension.
375     */
376    public void addExtension(ExtensionElement extension) {
377        if (extension == null) return;
378        String key = XmppStringUtils.generateKey(extension.getElementName(), extension.getNamespace());
379        synchronized (packetExtensions) {
380            packetExtensions.put(key, extension);
381        }
382    }
383
384    /**
385     * Add the given extension and override eventually existing extensions with the same name and
386     * namespace.
387     *
388     * @param extension the extension element to add.
389     * @return one of the removed extensions or <code>null</code> if there are none.
390     * @since 4.1.2
391     */
392    public ExtensionElement overrideExtension(ExtensionElement extension) {
393        if (extension == null) return null;
394        synchronized (packetExtensions) {
395            ExtensionElement removedExtension = removeExtension(extension);
396            addExtension(extension);
397            return removedExtension;
398        }
399    }
400
401    /**
402     * Adds a collection of stanza(/packet) extensions to the packet. Does nothing if extensions is null.
403     * 
404     * @param extensions a collection of stanza(/packet) extensions
405     */
406    public void addExtensions(Collection<ExtensionElement> extensions) {
407        if (extensions == null) return;
408        for (ExtensionElement packetExtension : extensions) {
409            addExtension(packetExtension);
410        }
411    }
412
413    /**
414     * Check if a stanza(/packet) extension with the given element and namespace exists.
415     * <p>
416     * The argument <code>elementName</code> may be null.
417     * </p>
418     *
419     * @param elementName
420     * @param namespace
421     * @return true if a stanza(/packet) extension exists, false otherwise.
422     */
423    public boolean hasExtension(String elementName, String namespace) {
424        if (elementName == null) {
425            return hasExtension(namespace);
426        }
427        String key = XmppStringUtils.generateKey(elementName, namespace);
428        synchronized (packetExtensions) {
429            return packetExtensions.containsKey(key);
430        }
431    }
432
433    /**
434     * Check if a stanza(/packet) extension with the given namespace exists.
435     * 
436     * @param namespace
437     * @return true if a stanza(/packet) extension exists, false otherwise.
438     */
439    public boolean hasExtension(String namespace) {
440        synchronized (packetExtensions) {
441            for (ExtensionElement packetExtension : packetExtensions.values()) {
442                if (packetExtension.getNamespace().equals(namespace)) {
443                    return true;
444                }
445            }
446        }
447        return false;
448    }
449
450    /**
451     * Remove the stanza(/packet) extension with the given elementName and namespace.
452     *
453     * @param elementName
454     * @param namespace
455     * @return the removed stanza(/packet) extension or null.
456     */
457    public ExtensionElement removeExtension(String elementName, String namespace) {
458        String key = XmppStringUtils.generateKey(elementName, namespace);
459        synchronized (packetExtensions) {
460            return packetExtensions.remove(key);
461        }
462    }
463
464    /**
465     * Removes a stanza(/packet) extension from the packet.
466     *
467     * @param extension the stanza(/packet) extension to remove.
468     * @return the removed stanza(/packet) extension or null.
469     */
470    public ExtensionElement removeExtension(ExtensionElement extension)  {
471        return removeExtension(extension.getElementName(), extension.getNamespace());
472    }
473
474    /**
475     * Returns a short String describing the Stanza. This method is suited for log purposes.
476     */
477    @Override
478    public abstract String toString();
479
480    /**
481     * Returns the extension sub-packets (including properties data) as an XML
482     * String, or the Empty String if there are no stanza(/packet) extensions.
483     *
484     * @return the extension sub-packets as XML or the Empty String if there
485     * are no stanza(/packet) extensions.
486     */
487    protected final XmlStringBuilder getExtensionsXML() {
488        XmlStringBuilder xml = new XmlStringBuilder();
489        // Add in all standard extension sub-packets.
490        for (ExtensionElement extension : getExtensions()) {
491            xml.append(extension.toXML());
492        }
493        return xml;
494    }
495
496    /**
497     * Returns the default language used for all messages containing localized content.
498     * 
499     * @return the default language
500     */
501    public static String getDefaultLanguage() {
502        return DEFAULT_LANGUAGE;
503    }
504
505    /**
506     * Add to, from, id and 'xml:lang' attributes
507     *
508     * @param xml
509     */
510    protected void addCommonAttributes(XmlStringBuilder xml) {
511        xml.optAttribute("to", getTo());
512        xml.optAttribute("from", getFrom());
513        xml.optAttribute("id", getStanzaId());
514        xml.xmllangAttribute(getLanguage());
515    }
516
517    protected void logCommonAttributes(StringBuilder sb) {
518        if (getTo() != null) {
519            sb.append("to=").append(to).append(',');
520        }
521        if (getFrom() != null) {
522            sb.append("from=").append(from).append(',');
523        }
524        if (hasStanzaIdSet()) {
525            sb.append("id=").append(id).append(',');
526        }
527    }
528
529    /**
530     * Append an XMPPError is this stanza(/packet) has one set.
531     *
532     * @param xml the XmlStringBuilder to append the error to.
533     */
534    protected void appendErrorIfExists(XmlStringBuilder xml) {
535        XMPPError error = getError();
536        if (error != null) {
537            xml.append(error.toXML());
538        }
539    }
540}