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 <iq/> (RECOMMENDED) or <command/> 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}