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