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