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