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