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