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 * Set the stanza id if none is set. 164 * 165 * @return the stanza id. 166 * @since 4.2 167 * @deprecated use {@link StanzaBuilder} instead. 168 */ 169 @Deprecated 170 // TODO: Remove in Smack 4.5. 171 public String setStanzaId() { 172 if (!hasStanzaIdSet()) { 173 setNewStanzaId(); 174 } 175 return getStanzaId(); 176 } 177 178 /** 179 * Throws an {@link IllegalArgumentException} if this stanza has no stanza ID set. 180 * 181 * @throws IllegalArgumentException if this stanza has no stanza ID set. 182 * @since 4.4. 183 */ 184 public final void throwIfNoStanzaId() { 185 if (hasStanzaIdSet()) { 186 return; 187 } 188 189 throw new IllegalArgumentException("The stanza has no RFC stanza ID set, although one is required"); 190 } 191 192 /** 193 * Ensure that a stanza ID is set. 194 * 195 * @return the stanza ID. 196 * @since 4.4 197 */ 198 // TODO: Remove this method once StanzaBuilder is ready. 199 protected String setNewStanzaId() { 200 if (usedStanzaIdSource != null) { 201 id = usedStanzaIdSource.getNewStanzaId(); 202 } 203 else { 204 id = StandardStanzaIdSource.DEFAULT.getNewStanzaId(); 205 } 206 207 return getStanzaId(); 208 } 209 210 @Override 211 public final Jid getTo() { 212 return to; 213 } 214 215 /** 216 * Sets who the packet is being sent "to". The XMPP protocol often makes 217 * the "to" attribute optional, so it does not always need to be set. 218 * 219 * @param to who the packet is being sent to. 220 */ 221 // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. 222 public void setTo(Jid to) { 223 this.to = to; 224 } 225 226 @Override 227 public final Jid getFrom() { 228 return from; 229 } 230 231 /** 232 * Sets who the packet is being sent "from". The XMPP protocol often 233 * makes the "from" attribute optional, so it does not always need to 234 * be set. 235 * 236 * @param from who the packet is being sent to. 237 */ 238 // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. 239 public void setFrom(Jid from) { 240 this.from = from; 241 } 242 243 @Override 244 public final StanzaError getError() { 245 return error; 246 } 247 248 /** 249 * Sets the error for this stanza. 250 * 251 * @param stanzaError the error that this stanza carries and hence signals. 252 */ 253 // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. 254 public void setError(StanzaError stanzaError) { 255 error = stanzaError; 256 } 257 258 /** 259 * Deprecated. 260 * @param stanzaError the stanza error. 261 * @deprecated use {@link StanzaBuilder} instead. 262 */ 263 @Deprecated 264 // TODO: Remove in Smack 4.5. 265 public void setError(StanzaError.Builder stanzaError) { 266 setError(stanzaError.build()); 267 } 268 269 @Override 270 public final String getLanguage() { 271 return language; 272 } 273 274 /** 275 * Sets the xml:lang of this Stanza. 276 * 277 * @param language the xml:lang of this Stanza. 278 * @deprecated use {@link StanzaBuilder#setLanguage(String)} instead. 279 */ 280 @Deprecated 281 // TODO: Remove in Smack 4.5. 282 public void setLanguage(String language) { 283 this.language = language; 284 } 285 286 @Override 287 public final List<XmlElement> getExtensions() { 288 synchronized (extensionElements) { 289 // No need to create a new list, values() will already create a new one for us 290 return extensionElements.values(); 291 } 292 } 293 294 public final MultiMap<QName, XmlElement> getExtensionsMap() { 295 return cloneExtensionsMap(); 296 } 297 298 final MultiMap<QName, XmlElement> cloneExtensionsMap() { 299 synchronized (extensionElements) { 300 return extensionElements.clone(); 301 } 302 } 303 304 /** 305 * Return a list of all extensions with the given element name <em>and</em> namespace. 306 * <p> 307 * Changes to the returned set will update the stanza extensions, if the returned set is not the empty set. 308 * </p> 309 * 310 * @param elementName the element name, must not be null. 311 * @param namespace the namespace of the element(s), must not be null. 312 * @return a set of all matching extensions. 313 * @since 4.1 314 */ 315 public final List<XmlElement> getExtensions(String elementName, String namespace) { 316 requireNotNullNorEmpty(elementName, "elementName must not be null nor empty"); 317 requireNotNullNorEmpty(namespace, "namespace must not be null nor empty"); 318 QName key = new QName(namespace, elementName); 319 return getExtensions(key); 320 } 321 322 @Override 323 public final List<XmlElement> getExtensions(QName qname) { 324 List<XmlElement> res; 325 synchronized (extensionElements) { 326 res = extensionElements.getAll(qname); 327 } 328 return Collections.unmodifiableList(res); 329 } 330 331 @Override 332 public final <E extends ExtensionElement> List<E> getExtensions(Class<E> extensionElementClass) { 333 synchronized (extensionElements) { 334 return XmppElementUtil.getElementsFrom(extensionElements, extensionElementClass); 335 } 336 } 337 338 /** 339 * Returns the first extension of this stanza that has the given namespace. 340 * <p> 341 * When possible, use {@link #getExtensionElement(String, String)} instead. 342 * </p> 343 * 344 * @param namespace the namespace of the extension that is desired. 345 * @return the stanza extension with the given namespace. 346 */ 347 // TODO: Mark this method as deprecated in favor of getExtension(QName). 348 public final XmlElement getExtension(String namespace) { 349 return PacketUtil.extensionElementFrom(getExtensions(), null, namespace); 350 } 351 352 /** 353 * Returns the first extension that matches the specified element name and 354 * namespace, or <code>null</code> if it doesn't exist. Extensions are 355 * are arbitrary XML elements in standard XMPP stanzas. 356 * <p> 357 * Note that it is recommended to use {@link #getExtension(Class)} instead of this method. 358 * The {@link #getExtension(Class)} is more robust, as it is type safe. 359 * </p> 360 * 361 * @param elementName the XML element name of the extension. 362 * @param namespace the XML element namespace of the extension. 363 * @return the extension, or <code>null</code> if it doesn't exist. 364 */ 365 public final XmlElement getExtensionElement(String elementName, String namespace) { 366 if (namespace == null) { 367 return null; 368 } 369 QName key = new QName(namespace, elementName); 370 XmlElement packetExtension = getExtension(key); 371 if (packetExtension == null) { 372 return null; 373 } 374 return packetExtension; 375 } 376 377 /** 378 * This method is deprecated. Use preferably {@link #getExtension(Class)} or {@link #getExtensionElement(String, String)}. 379 * 380 * @param <E> the type to cast to. 381 * @param elementName the XML element name of the extension. (May be null) 382 * @param namespace the XML element namespace of the extension. 383 * @return the extension, or <code>null</code> if it doesn't exist. 384 * @deprecated use {@link #getExtension(Class)} or {@link #getExtensionElement(String, String)} instead. 385 */ 386 // TODO: Remove in Smack 4.5. 387 @SuppressWarnings("unchecked") 388 @Deprecated 389 public final <E extends ExtensionElement> E getExtension(String elementName, String namespace) { 390 return (E) getExtensionElement(elementName, namespace); 391 } 392 393 @Override 394 public final XmlElement getExtension(QName qname) { 395 synchronized (extensionElements) { 396 return extensionElements.getFirst(qname); 397 } 398 } 399 400 /** 401 * Adds a stanza extension to the packet. Does nothing if extension is null. 402 * <p> 403 * Please note that although this method is not yet marked as deprecated, it is recommended to use 404 * {@link StanzaBuilder#addExtension(XmlElement)} instead. 405 * </p> 406 * 407 * @param extension a stanza extension. 408 */ 409 // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. 410 public final void addExtension(XmlElement extension) { 411 if (extension == null) return; 412 QName key = extension.getQName(); 413 synchronized (extensionElements) { 414 extensionElements.put(key, extension); 415 } 416 } 417 418 /** 419 * Add the given extension and override eventually existing extensions with the same name and 420 * namespace. 421 * <p> 422 * Please note that although this method is not yet marked as deprecated, it is recommended to use 423 * {@link StanzaBuilder#overrideExtension(XmlElement)} instead. 424 * </p> 425 * 426 * @param extension the extension element to add. 427 * @return one of the removed extensions or <code>null</code> if there are none. 428 * @since 4.1.2 429 */ 430 // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. 431 public final XmlElement overrideExtension(XmlElement extension) { 432 if (extension == null) return null; 433 synchronized (extensionElements) { 434 // Note that we need to use removeExtension(String, String) here. If would use 435 // removeExtension(ExtensionElement) then we would remove based on the equality of ExtensionElement, which 436 // is not what we want in this case. 437 XmlElement removedExtension = removeExtension(extension.getElementName(), extension.getNamespace()); 438 addExtension(extension); 439 return removedExtension; 440 } 441 } 442 443 /** 444 * Adds a collection of stanza extensions to the packet. Does nothing if extensions is null. 445 * 446 * @param extensions a collection of stanza extensions 447 */ 448 // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. 449 public final void addExtensions(Collection<? extends XmlElement> extensions) { 450 if (extensions == null) return; 451 for (XmlElement packetExtension : extensions) { 452 addExtension(packetExtension); 453 } 454 } 455 456 /** 457 * Check if a stanza extension with the given element and namespace exists. 458 * <p> 459 * The argument <code>elementName</code> may be null. 460 * </p> 461 * 462 * @param elementName TODO javadoc me please 463 * @param namespace TODO javadoc me please 464 * @return true if a stanza extension exists, false otherwise. 465 */ 466 public final boolean hasExtension(String elementName, String namespace) { 467 if (elementName == null) { 468 return hasExtension(namespace); 469 } 470 QName key = new QName(namespace, elementName); 471 synchronized (extensionElements) { 472 return extensionElements.containsKey(key); 473 } 474 } 475 476 // Overridden in order to avoid an extra copy. 477 @Override 478 public final boolean hasExtension(String namespace) { 479 synchronized (extensionElements) { 480 for (XmlElement packetExtension : extensionElements.values()) { 481 if (packetExtension.getNamespace().equals(namespace)) { 482 return true; 483 } 484 } 485 } 486 return false; 487 } 488 489 /** 490 * Remove the stanza extension with the given elementName and namespace. 491 * 492 * @param elementName TODO javadoc me please 493 * @param namespace TODO javadoc me please 494 * @return the removed stanza extension or null. 495 */ 496 // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. 497 public final XmlElement removeExtension(String elementName, String namespace) { 498 QName key = new QName(namespace, elementName); 499 synchronized (extensionElements) { 500 return extensionElements.remove(key); 501 } 502 } 503 504 /** 505 * Removes a stanza extension from the packet. 506 * 507 * @param extension the stanza extension to remove. 508 * @return the removed stanza extension or null. 509 * @deprecated use {@link StanzaBuilder} instead. 510 */ 511 @Deprecated 512 // TODO: Remove in Smack 4.5. 513 public final XmlElement removeExtension(XmlElement extension) { 514 QName key = extension.getQName(); 515 synchronized (extensionElements) { 516 List<XmlElement> list = extensionElements.getAll(key); 517 boolean removed = list.remove(extension); 518 if (removed) { 519 return extension; 520 } 521 } 522 return null; 523 } 524 525 /** 526 * Returns a short String describing the Stanza. This method is suited for log purposes. 527 */ 528 @Override 529 public abstract String toString(); 530 531 @Override 532 public final String getNamespace() { 533 return namespace; 534 } 535 536 /** 537 * Returns the default language used for all messages containing localized content. 538 * 539 * @return the default language 540 */ 541 public static String getDefaultLanguage() { 542 return DEFAULT_LANGUAGE; 543 } 544 545 /** 546 * Add to, from, and id attributes. 547 * 548 * @param xml the {@link XmlStringBuilder}. 549 */ 550 protected final void addCommonAttributes(XmlStringBuilder xml) { 551 xml.optAttribute("to", getTo()); 552 xml.optAttribute("from", getFrom()); 553 xml.optAttribute("id", getStanzaId()); 554 } 555 556 protected void logCommonAttributes(StringBuilder sb) { 557 if (getTo() != null) { 558 sb.append("to=").append(to).append(','); 559 } 560 if (getFrom() != null) { 561 sb.append("from=").append(from).append(','); 562 } 563 if (hasStanzaIdSet()) { 564 sb.append("id=").append(id).append(','); 565 } 566 } 567 568 /** 569 * Append an XMPPError is this stanza has one set. 570 * 571 * @param xml the XmlStringBuilder to append the error to. 572 */ 573 protected final void appendErrorIfExists(XmlStringBuilder xml) { 574 StanzaError error = getError(); 575 if (error != null) { 576 xml.append(error); 577 } 578 } 579 580 /** 581 * Return the provided non-empty language, or use this {@link XmlLangElement} language (if set). 582 * 583 * @param language the provided language, may be the empty string or <code>null</code>. 584 * @return the provided language or this element's language (if set). 585 */ 586 static String determineLanguage(XmlLangElement xmlLangElement, String language) { 587 if (language != null && !language.isEmpty()) { 588 return language; 589 } 590 591 return xmlLangElement.getLanguage(); 592 } 593}