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 java.util.List; 021import java.util.Locale; 022 023import javax.xml.namespace.QName; 024 025import org.jivesoftware.smack.XMPPConnection; 026import org.jivesoftware.smack.util.EqualsUtil; 027import org.jivesoftware.smack.util.HashCode; 028import org.jivesoftware.smack.util.Objects; 029import org.jivesoftware.smack.util.StringUtils; 030import org.jivesoftware.smack.util.XmlStringBuilder; 031 032import org.jxmpp.jid.Jid; 033import org.jxmpp.jid.impl.JidCreate; 034import org.jxmpp.stringprep.XmppStringprepException; 035 036/** 037 * Represents XMPP message packets. A message can be one of several types: 038 * 039 * <ul> 040 * <li>Message.Type.NORMAL -- (Default) a normal text message used in email like interface. 041 * <li>Message.Type.CHAT -- a typically short text message used in line-by-line chat interfaces. 042 * <li>Message.Type.GROUP_CHAT -- a chat message sent to a groupchat server for group chats. 043 * <li>Message.Type.HEADLINE -- a text message to be displayed in scrolling marquee displays. 044 * <li>Message.Type.ERROR -- indicates a messaging error. 045 * </ul> 046 * 047 * For each message type, different message fields are typically used as follows: 048 * <table border="1"> 049 * <caption>Message Types</caption> 050 * <tr><td> </td><td colspan="5"><b>Message type</b></td></tr> 051 * <tr><td><i>Field</i></td><td><b>Normal</b></td><td><b>Chat</b></td><td><b>Group Chat</b></td><td><b>Headline</b></td><td><b>XMPPError</b></td></tr> 052 * <tr><td><i>subject</i></td> <td>SHOULD</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td></tr> 053 * <tr><td><i>thread</i></td> <td>OPTIONAL</td><td>SHOULD</td><td>OPTIONAL</td><td>OPTIONAL</td><td>SHOULD NOT</td></tr> 054 * <tr><td><i>body</i></td> <td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD NOT</td></tr> 055 * <tr><td><i>error</i></td> <td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST</td></tr> 056 * </table> 057 * 058 * @author Matt Tucker 059 */ 060public final class Message extends MessageOrPresence<MessageBuilder> 061 implements MessageView { 062 063 public static final String ELEMENT = "message"; 064 public static final String BODY = "body"; 065 066 private Type type; 067 068 /** 069 * Creates a new, "normal" message. 070 * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. 071 */ 072 @Deprecated 073 // TODO: Remove in Smack 4.5. 074 public Message() { 075 } 076 077 /** 078 * Creates a new "normal" message to the specified recipient. 079 * 080 * @param to the recipient of the message. 081 * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. 082 */ 083 @Deprecated 084 // TODO: Remove in Smack 4.5. 085 public Message(Jid to) { 086 setTo(to); 087 } 088 089 /** 090 * Creates a new message of the specified type to a recipient. 091 * 092 * @param to the user to send the message to. 093 * @param type the message type. 094 * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. 095 */ 096 @Deprecated 097 // TODO: Remove in Smack 4.5. 098 public Message(Jid to, Type type) { 099 this(to); 100 setType(type); 101 } 102 103 /** 104 * Creates a new message to the specified recipient and with the specified body. 105 * 106 * @param to the user to send the message to. 107 * @param body the body of the message. 108 * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. 109 */ 110 @Deprecated 111 // TODO: Remove in Smack 4.5. 112 public Message(Jid to, String body) { 113 this(to); 114 setBody(body); 115 } 116 117 /** 118 * Creates a new message to the specified recipient and with the specified body. 119 * 120 * @param to the user to send the message to. 121 * @param body the body of the message. 122 * @throws XmppStringprepException if 'to' is not a valid XMPP address. 123 * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. 124 */ 125 @Deprecated 126 // TODO: Remove in Smack 4.5. 127 public Message(String to, String body) throws XmppStringprepException { 128 this(JidCreate.from(to), body); 129 } 130 131 /** 132 * Creates a new message with the specified recipient and extension element. 133 * 134 * @param to TODO javadoc me please 135 * @param extensionElement TODO javadoc me please 136 * @since 4.2 137 * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. 138 */ 139 @Deprecated 140 // TODO: Remove in Smack 4.5. 141 public Message(Jid to, ExtensionElement extensionElement) { 142 this(to); 143 addExtension(extensionElement); 144 } 145 146 Message(MessageBuilder messageBuilder) { 147 super(messageBuilder); 148 type = messageBuilder.type; 149 } 150 151 /** 152 * Copy constructor. 153 * <p> 154 * This does not perform a deep clone, as extension elements are shared between the new and old 155 * instance. 156 * </p> 157 * 158 * @param other TODO javadoc me please 159 */ 160 public Message(Message other) { 161 super(other); 162 this.type = other.type; 163 } 164 165 @Override 166 public Type getType() { 167 if (type == null) { 168 return Type.normal; 169 } 170 return type; 171 } 172 173 /** 174 * Sets the type of the message. 175 * 176 * @param type the type of the message. 177 * @deprecated use {@link StanzaBuilder} instead. 178 */ 179 @Deprecated 180 // TODO: Remove in Smack 4.5. 181 public void setType(Type type) { 182 this.type = type; 183 } 184 185 /** 186 * Sets the subject of the message. The subject is a short description of 187 * message contents. 188 * 189 * @param subject the subject of the message. 190 * @deprecated use {@link StanzaBuilder} instead. 191 */ 192 @Deprecated 193 // TODO: Remove when stanza builder is ready. 194 public void setSubject(String subject) { 195 if (subject == null) { 196 removeSubject(""); // use empty string because #removeSubject(null) is ambiguous 197 return; 198 } 199 addSubject(null, subject); 200 } 201 202 /** 203 * Adds a subject with a corresponding language. 204 * 205 * @param language the language of the subject being added. 206 * @param subject the subject being added to the message. 207 * @return the new {@link org.jivesoftware.smack.packet.Message.Subject} 208 * @throws NullPointerException if the subject is null, a null pointer exception is thrown 209 */ 210 @Deprecated 211 // TODO: Remove when stanza builder is ready. 212 public Subject addSubject(String language, String subject) { 213 language = Stanza.determineLanguage(this, language); 214 215 List<Subject> currentSubjects = getExtensions(Subject.class); 216 for (Subject currentSubject : currentSubjects) { 217 if (language.equals(currentSubject.getLanguage())) { 218 throw new IllegalArgumentException("Subject with the language " + language + " already exists"); 219 } 220 } 221 222 Subject messageSubject = new Subject(language, subject); 223 addExtension(messageSubject); 224 return messageSubject; 225 } 226 227 /** 228 * Removes the subject with the given language from the message. 229 * 230 * @param language the language of the subject which is to be removed 231 * @return true if a subject was removed and false if it was not. 232 */ 233 @Deprecated 234 // TODO: Remove when stanza builder is ready. 235 public boolean removeSubject(String language) { 236 language = Stanza.determineLanguage(this, language); 237 for (Subject subject : getExtensions(Subject.class)) { 238 if (language.equals(subject.language)) { 239 return removeSubject(subject); 240 } 241 } 242 return false; 243 } 244 245 /** 246 * Removes the subject from the message and returns true if the subject was removed. 247 * 248 * @param subject the subject being removed from the message. 249 * @return true if the subject was successfully removed and false if it was not. 250 */ 251 @Deprecated 252 // TODO: Remove when stanza builder is ready. 253 public boolean removeSubject(Subject subject) { 254 return removeExtension(subject) != null; 255 } 256 257 /** 258 * Sets the body of the message. 259 * 260 * @param body the body of the message. 261 * @see #setBody(String) 262 * @since 4.2 263 * @deprecated use {@link StanzaBuilder} instead. 264 */ 265 @Deprecated 266 // TODO: Remove when stanza builder is ready. 267 public void setBody(CharSequence body) { 268 String bodyString; 269 if (body != null) { 270 bodyString = body.toString(); 271 } else { 272 bodyString = null; 273 } 274 setBody(bodyString); 275 } 276 277 /** 278 * Sets the body of the message. The body is the main message contents. 279 * 280 * @param body the body of the message. 281 * @deprecated use {@link StanzaBuilder} instead. 282 */ 283 @Deprecated 284 // TODO: Remove when stanza builder is ready. 285 public void setBody(String body) { 286 if (body == null) { 287 removeBody(""); // use empty string because #removeBody(null) is ambiguous 288 return; 289 } 290 addBody(null, body); 291 } 292 293 /** 294 * Adds a body with a corresponding language. 295 * 296 * @param language the language of the body being added. 297 * @param body the body being added to the message. 298 * @return the new {@link org.jivesoftware.smack.packet.Message.Body} 299 * @throws NullPointerException if the body is null, a null pointer exception is thrown 300 * @since 3.0.2 301 * @deprecated use {@link StanzaBuilder} instead. 302 */ 303 @Deprecated 304 // TODO: Remove when stanza builder is ready. 305 public Body addBody(String language, String body) { 306 language = Stanza.determineLanguage(this, language); 307 308 removeBody(language); 309 310 Body messageBody = new Body(language, body); 311 addExtension(messageBody); 312 return messageBody; 313 } 314 315 /** 316 * Removes the body with the given language from the message. 317 * 318 * @param language the language of the body which is to be removed 319 * @return true if a body was removed and false if it was not. 320 * @deprecated use {@link StanzaBuilder} instead. 321 */ 322 @Deprecated 323 // TODO: Remove when stanza builder is ready. 324 public boolean removeBody(String language) { 325 language = Stanza.determineLanguage(this, language); 326 for (Body body : getBodies()) { 327 String bodyLanguage = body.getLanguage(); 328 if (Objects.equals(bodyLanguage, language)) { 329 removeExtension(body); 330 return true; 331 } 332 } 333 return false; 334 } 335 336 /** 337 * Removes the body from the message and returns true if the body was removed. 338 * 339 * @param body the body being removed from the message. 340 * @return true if the body was successfully removed and false if it was not. 341 * @since 3.0.2 342 * @deprecated use {@link StanzaBuilder} instead. 343 */ 344 @Deprecated 345 // TODO: Remove when stanza builder is ready. 346 public boolean removeBody(Body body) { 347 ExtensionElement removedElement = removeExtension(body); 348 return removedElement != null; 349 } 350 351 /** 352 * Sets the thread id of the message, which is a unique identifier for a sequence 353 * of "chat" messages. 354 * 355 * @param thread the thread id of the message. 356 * @deprecated use {@link StanzaBuilder} instead. 357 */ 358 @Deprecated 359 // TODO: Remove when stanza builder is ready. 360 public void setThread(String thread) { 361 addExtension(new Message.Thread(thread)); 362 } 363 364 @Override 365 public String getElementName() { 366 return ELEMENT; 367 } 368 369 @Override 370 public MessageBuilder asBuilder() { 371 return StanzaBuilder.buildMessageFrom(this, getStanzaId()); 372 } 373 374 @Override 375 public MessageBuilder asBuilder(String id) { 376 return StanzaBuilder.buildMessageFrom(this, id); 377 } 378 379 @Override 380 public MessageBuilder asBuilder(XMPPConnection connection) { 381 return connection.getStanzaFactory().buildMessageStanzaFrom(this); 382 } 383 384 @Override 385 public String toString() { 386 StringBuilder sb = new StringBuilder(); 387 sb.append("Message Stanza ["); 388 logCommonAttributes(sb); 389 if (type != null) { 390 sb.append("type=").append(type).append(','); 391 } 392 sb.append(']'); 393 return sb.toString(); 394 } 395 396 @Override 397 public XmlStringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { 398 XmlStringBuilder buf = new XmlStringBuilder(this, enclosingXmlEnvironment); 399 addCommonAttributes(buf); 400 buf.optAttribute("type", type); 401 buf.rightAngleBracket(); 402 403 // Append the error subpacket if the message type is an error. 404 if (type == Type.error) { 405 appendErrorIfExists(buf); 406 } 407 408 // Add extension elements, if any are defined. 409 buf.append(getExtensions()); 410 411 buf.closeElement(ELEMENT); 412 return buf; 413 } 414 415 /** 416 * Creates and returns a copy of this message stanza. 417 * <p> 418 * This does not perform a deep clone, as extension elements are shared between the new and old 419 * instance. 420 * </p> 421 * @return a clone of this message. 422 * @deprecated use {@link #asBuilder()} instead. 423 */ 424 // TODO: Remove in Smack 4.5. 425 @Deprecated 426 @Override 427 public Message clone() { 428 return new Message(this); 429 } 430 431 /** 432 * Represents a message subject, its language and the content of the subject. 433 */ 434 public static final class Subject implements ExtensionElement { 435 436 public static final String ELEMENT = "subject"; 437 public static final String NAMESPACE = StreamOpen.CLIENT_NAMESPACE; 438 439 public static final QName QNAME = new QName(NAMESPACE, ELEMENT); 440 441 private final String subject; 442 private final String language; 443 444 public Subject(String language, String subject) { 445 if (subject == null) { 446 throw new NullPointerException("Subject cannot be null."); 447 } 448 this.language = language; 449 this.subject = subject; 450 } 451 452 @Override 453 public String getLanguage() { 454 return language; 455 } 456 457 /** 458 * Returns the subject content. 459 * 460 * @return the content of the subject. 461 */ 462 public String getSubject() { 463 return subject; 464 } 465 466 private final HashCode.Cache hashCodeCache = new HashCode.Cache(); 467 468 @Override 469 public int hashCode() { 470 return hashCodeCache.getHashCode(c -> 471 c.append(language) 472 .append(subject) 473 ); 474 } 475 476 @Override 477 public boolean equals(Object obj) { 478 return EqualsUtil.equals(this, obj, (e, o) -> 479 e.append(language, o.language) 480 .append(subject, o.subject) 481 ); 482 } 483 484 @Override 485 public String getElementName() { 486 return ELEMENT; 487 } 488 489 @Override 490 public String getNamespace() { 491 return NAMESPACE; 492 } 493 494 @Override 495 public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { 496 XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); 497 xml.rightAngleBracket(); 498 xml.escape(subject); 499 xml.closeElement(getElementName()); 500 return xml; 501 } 502 503 } 504 505 /** 506 * Represents a message body, its language and the content of the message. 507 */ 508 public static final class Body implements ExtensionElement { 509 510 public static final String ELEMENT = "body"; 511 public static final String NAMESPACE = StreamOpen.CLIENT_NAMESPACE; 512 public static final QName QNAME = new QName(NAMESPACE, ELEMENT); 513 514 enum BodyElementNamespace { 515 client(StreamOpen.CLIENT_NAMESPACE), 516 server(StreamOpen.SERVER_NAMESPACE), 517 ; 518 519 private final String xmlNamespace; 520 521 BodyElementNamespace(String xmlNamespace) { 522 this.xmlNamespace = xmlNamespace; 523 } 524 525 public String getNamespace() { 526 return xmlNamespace; 527 } 528 } 529 530 private final String message; 531 private final String language; 532 private final BodyElementNamespace namespace; 533 534 public Body(String language, String message) { 535 this(language, message, BodyElementNamespace.client); 536 } 537 538 public Body(String language, String message, BodyElementNamespace namespace) { 539 if (message == null) { 540 throw new NullPointerException("Message cannot be null."); 541 } 542 this.language = language; 543 this.message = message; 544 this.namespace = Objects.requireNonNull(namespace); 545 } 546 547 @Override 548 public String getLanguage() { 549 return language; 550 } 551 552 /** 553 * Returns the message content. 554 * 555 * @return the content of the message. 556 */ 557 public String getMessage() { 558 return message; 559 } 560 561 private final HashCode.Cache hashCodeCache = new HashCode.Cache(); 562 563 @Override 564 public int hashCode() { 565 return hashCodeCache.getHashCode(c -> 566 c.append(language) 567 .append(message) 568 ); 569 } 570 571 @Override 572 public boolean equals(Object obj) { 573 return EqualsUtil.equals(this, obj, (e, o) -> 574 e.append(language, o.language) 575 .append(message, o.message) 576 ); 577 } 578 579 @Override 580 public String getElementName() { 581 return ELEMENT; 582 } 583 584 @Override 585 public String getNamespace() { 586 return namespace.xmlNamespace; 587 } 588 589 @Override 590 public XmlStringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { 591 XmlStringBuilder xml = new XmlStringBuilder(this, enclosingXmlEnvironment); 592 xml.rightAngleBracket(); 593 xml.text(message); 594 xml.closeElement(getElementName()); 595 return xml; 596 } 597 598 } 599 600 @SuppressWarnings("JavaLangClash") 601 public static class Thread implements ExtensionElement { 602 public static final String ELEMENT = "thread"; 603 public static final String NAMESPACE = StreamOpen.CLIENT_NAMESPACE; 604 public static final QName QNAME = new QName(NAMESPACE, ELEMENT); 605 606 public static final String PARENT_ATTRIBUTE_NAME = "parent"; 607 608 private final String thread; 609 private final String parent; 610 611 public Thread(String thread) { 612 this(thread, null); 613 } 614 615 public Thread(String thread, String parent) { 616 this.thread = StringUtils.requireNotNullNorEmpty(thread, "thread must not be null nor empty"); 617 this.parent = StringUtils.requireNullOrNotEmpty(parent, "parent must be null or not empty"); 618 } 619 620 public String getThread() { 621 return thread; 622 } 623 624 public String getParent() { 625 return parent; 626 } 627 628 @Override 629 public String getElementName() { 630 return ELEMENT; 631 } 632 633 @Override 634 public String getNamespace() { 635 return NAMESPACE; 636 } 637 638 @Override 639 public QName getQName() { 640 return QNAME; 641 } 642 643 @Override 644 public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { 645 XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment); 646 xml.optAttribute(PARENT_ATTRIBUTE_NAME, parent); 647 xml.rightAngleBracket(); 648 xml.escape(thread); 649 xml.closeElement(this); 650 return xml; 651 } 652 } 653 654 /** 655 * Represents the type of a message. 656 */ 657 public enum Type { 658 659 /** 660 * (Default) a normal text message used in email like interface. 661 */ 662 normal, 663 664 /** 665 * Typically short text message used in line-by-line chat interfaces. 666 */ 667 chat, 668 669 /** 670 * Chat message sent to a groupchat server for group chats. 671 */ 672 groupchat, 673 674 /** 675 * Text message to be displayed in scrolling marquee displays. 676 */ 677 headline, 678 679 /** 680 * indicates a messaging error. 681 */ 682 error; 683 684 /** 685 * Converts a String into the corresponding types. Valid String values that can be converted 686 * to types are: "normal", "chat", "groupchat", "headline" and "error". 687 * 688 * @param string the String value to covert. 689 * @return the corresponding Type. 690 * @throws IllegalArgumentException when not able to parse the string parameter 691 * @throws NullPointerException if the string is null 692 */ 693 public static Type fromString(String string) { 694 return Type.valueOf(string.toLowerCase(Locale.US)); 695 } 696 697 } 698}