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