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