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; 029import org.jxmpp.jid.Jid; 030import org.jxmpp.jid.impl.JidCreate; 031import org.jxmpp.stringprep.XmppStringprepException; 032 033/** 034 * Represents XMPP message packets. A message can be one of several types: 035 * 036 * <ul> 037 * <li>Message.Type.NORMAL -- (Default) a normal text message used in email like interface. 038 * <li>Message.Type.CHAT -- a typically short text message used in line-by-line chat interfaces. 039 * <li>Message.Type.GROUP_CHAT -- a chat message sent to a groupchat server for group chats. 040 * <li>Message.Type.HEADLINE -- a text message to be displayed in scrolling marquee displays. 041 * <li>Message.Type.ERROR -- indicates a messaging error. 042 * </ul> 043 * 044 * For each message type, different message fields are typically used as follows: 045 * <p> 046 * <table border="1"> 047 * <caption>Message Types</caption> 048 * <tr><td> </td><td colspan="5"><b>Message type</b></td></tr> 049 * <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> 050 * <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> 051 * <tr><td><i>thread</i></td> <td>OPTIONAL</td><td>SHOULD</td><td>OPTIONAL</td><td>OPTIONAL</td><td>SHOULD NOT</td></tr> 052 * <tr><td><i>body</i></td> <td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD NOT</td></tr> 053 * <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> 054 * </table> 055 * 056 * @author Matt Tucker 057 */ 058public final class Message extends Stanza implements TypedCloneable<Message> { 059 060 public static final String ELEMENT = "message"; 061 public static final String BODY = "body"; 062 063 private Type type; 064 private String thread = null; 065 066 private final Set<Subject> subjects = new HashSet<Subject>(); 067 private final Set<Body> bodies = new HashSet<Body>(); 068 069 /** 070 * Creates a new, "normal" message. 071 */ 072 public Message() { 073 } 074 075 /** 076 * Creates a new "normal" message to the specified recipient. 077 * 078 * @param to the recipient of the message. 079 */ 080 public Message(Jid to) { 081 setTo(to); 082 } 083 084 /** 085 * Creates a new message of the specified type to a recipient. 086 * 087 * @param to the user to send the message to. 088 * @param type the message type. 089 */ 090 public Message(Jid to, Type type) { 091 this(to); 092 setType(type); 093 } 094 095 /** 096 * Creates a new message to the specified recipient and with the specified body. 097 * 098 * @param to the user to send the message to. 099 * @param body the body of the message. 100 */ 101 public Message(Jid to, String body) { 102 this(to); 103 setBody(body); 104 } 105 106 /** 107 * Creates a new message to the specified recipient and with the specified body. 108 * 109 * @param to the user to send the message to. 110 * @param body the body of the message. 111 * @throws XmppStringprepException if 'to' is not a valid XMPP address. 112 */ 113 public Message(String to, String body) throws XmppStringprepException { 114 this(JidCreate.from(to), body); 115 } 116 117 /** 118 * Creates a new message with the specified recipient and extension element. 119 * 120 * @param to 121 * @param extensionElement 122 * @since 4.2 123 */ 124 public Message(Jid to, ExtensionElement extensionElement) { 125 this(to); 126 addExtension(extensionElement); 127 } 128 129 /** 130 * Copy constructor. 131 * <p> 132 * This does not perform a deep clone, as extension elements are shared between the new and old 133 * instance. 134 * </p> 135 * 136 * @param other 137 */ 138 public Message(Message other) { 139 super(other); 140 this.type = other.type; 141 this.thread = other.thread; 142 this.subjects.addAll(other.subjects); 143 this.bodies.addAll(other.bodies); 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 (language.equals(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(null); 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 : bodies) { 317 if (language.equals(body.language)) { 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 return Collections.unmodifiableSet(bodies); 333 } 334 335 /** 336 * Sets the body of the message. 337 * 338 * @param body the body of the message. 339 * @see #setBody(String) 340 * @since 4.2 341 */ 342 public void setBody(CharSequence body) { 343 String bodyString; 344 if (body != null) { 345 bodyString = body.toString(); 346 } else { 347 bodyString = null; 348 } 349 setBody(bodyString); 350 } 351 352 /** 353 * Sets the body of the message. The body is the main message contents. 354 * 355 * @param body the body of the message. 356 */ 357 public void setBody(String body) { 358 if (body == null) { 359 removeBody(""); // use empty string because #removeBody(null) is ambiguous 360 return; 361 } 362 addBody(null, body); 363 } 364 365 /** 366 * Adds a body with a corresponding language. 367 * 368 * @param language the language of the body being added. 369 * @param body the body being added to the message. 370 * @return the new {@link org.jivesoftware.smack.packet.Message.Body} 371 * @throws NullPointerException if the body is null, a null pointer exception is thrown 372 * @since 3.0.2 373 */ 374 public Body addBody(String language, String body) { 375 language = determineLanguage(language); 376 Body messageBody = new Body(language, body); 377 bodies.add(messageBody); 378 return messageBody; 379 } 380 381 /** 382 * Removes the body with the given language from the message. 383 * 384 * @param language the language of the body which is to be removed 385 * @return true if a body was removed and false if it was not. 386 */ 387 public boolean removeBody(String language) { 388 language = determineLanguage(language); 389 for (Body body : bodies) { 390 if (language.equals(body.language)) { 391 return bodies.remove(body); 392 } 393 } 394 return false; 395 } 396 397 /** 398 * Removes the body from the message and returns true if the body was removed. 399 * 400 * @param body the body being removed from the message. 401 * @return true if the body was successfully removed and false if it was not. 402 * @since 3.0.2 403 */ 404 public boolean removeBody(Body body) { 405 return bodies.remove(body); 406 } 407 408 /** 409 * Returns all the languages being used for the bodies, not including the default body. 410 * 411 * @return the languages being used for the bodies. 412 * @since 3.0.2 413 */ 414 public List<String> getBodyLanguages() { 415 Body defaultBody = getMessageBody(null); 416 List<String> languages = new ArrayList<String>(); 417 for (Body body : bodies) { 418 if (!body.equals(defaultBody)) { 419 languages.add(body.language); 420 } 421 } 422 return Collections.unmodifiableList(languages); 423 } 424 425 /** 426 * Returns the thread id of the message, which is a unique identifier for a sequence 427 * of "chat" messages. If no thread id is set, <tt>null</tt> will be returned. 428 * 429 * @return the thread id of the message, or <tt>null</tt> if it doesn't exist. 430 */ 431 public String getThread() { 432 return thread; 433 } 434 435 /** 436 * Sets the thread id of the message, which is a unique identifier for a sequence 437 * of "chat" messages. 438 * 439 * @param thread the thread id of the message. 440 */ 441 public void setThread(String thread) { 442 this.thread = thread; 443 } 444 445 private String determineLanguage(String language) { 446 447 // empty string is passed by #setSubject() and #setBody() and is the same as null 448 language = "".equals(language) ? null : language; 449 450 // if given language is null check if message language is set 451 if (language == null && this.language != null) { 452 return this.language; 453 } 454 else if (language == null) { 455 return getDefaultLanguage(); 456 } 457 else { 458 return language; 459 } 460 461 } 462 463 @Override 464 public String toString() { 465 StringBuilder sb = new StringBuilder(); 466 sb.append("Message Stanza ["); 467 logCommonAttributes(sb); 468 if (type != null) { 469 sb.append("type=").append(type).append(','); 470 } 471 sb.append(']'); 472 return sb.toString(); 473 } 474 475 @Override 476 public XmlStringBuilder toXML() { 477 XmlStringBuilder buf = new XmlStringBuilder(); 478 buf.halfOpenElement(ELEMENT); 479 addCommonAttributes(buf); 480 buf.optAttribute("type", type); 481 buf.rightAngleBracket(); 482 483 // Add the subject in the default language 484 Subject defaultSubject = getMessageSubject(null); 485 if (defaultSubject != null) { 486 buf.element("subject", defaultSubject.subject); 487 } 488 // Add the subject in other languages 489 for (Subject subject : getSubjects()) { 490 // Skip the default language 491 if(subject.equals(defaultSubject)) 492 continue; 493 buf.halfOpenElement("subject").xmllangAttribute(subject.language).rightAngleBracket(); 494 buf.escape(subject.subject); 495 buf.closeElement("subject"); 496 } 497 // Add the body in the default language 498 Body defaultBody = getMessageBody(null); 499 if (defaultBody != null) { 500 buf.element("body", defaultBody.message); 501 } 502 // Add the bodies in other languages 503 for (Body body : getBodies()) { 504 // Skip the default language 505 if(body.equals(defaultBody)) 506 continue; 507 buf.halfOpenElement(BODY).xmllangAttribute(body.getLanguage()).rightAngleBracket(); 508 buf.escape(body.getMessage()); 509 buf.closeElement(BODY); 510 } 511 buf.optElement("thread", thread); 512 // Append the error subpacket if the message type is an error. 513 if (type == Type.error) { 514 appendErrorIfExists(buf); 515 } 516 // Add packet extensions, if any are defined. 517 buf.append(getExtensionsXML()); 518 buf.closeElement(ELEMENT); 519 return buf; 520 } 521 522 /** 523 * Creates and returns a copy of this message stanza. 524 * <p> 525 * This does not perform a deep clone, as extension elements are shared between the new and old 526 * instance. 527 * </p> 528 * @return a clone of this message. 529 */ 530 @Override 531 public Message clone() { 532 return new Message(this); 533 } 534 535 /** 536 * Represents a message subject, its language and the content of the subject. 537 */ 538 public static final class Subject { 539 540 private final String subject; 541 private final String language; 542 543 private Subject(String language, String subject) { 544 if (language == null) { 545 throw new NullPointerException("Language cannot be null."); 546 } 547 if (subject == null) { 548 throw new NullPointerException("Subject cannot be null."); 549 } 550 this.language = language; 551 this.subject = subject; 552 } 553 554 /** 555 * Returns the language of this message subject. 556 * 557 * @return the language of this message subject. 558 */ 559 public String getLanguage() { 560 return language; 561 } 562 563 /** 564 * Returns the subject content. 565 * 566 * @return the content of the subject. 567 */ 568 public String getSubject() { 569 return subject; 570 } 571 572 573 @Override 574 public int hashCode() { 575 final int prime = 31; 576 int result = 1; 577 result = prime * result + this.language.hashCode(); 578 result = prime * result + this.subject.hashCode(); 579 return result; 580 } 581 582 @Override 583 public boolean equals(Object obj) { 584 if (this == obj) { 585 return true; 586 } 587 if (obj == null) { 588 return false; 589 } 590 if (getClass() != obj.getClass()) { 591 return false; 592 } 593 Subject other = (Subject) obj; 594 // simplified comparison because language and subject are always set 595 return this.language.equals(other.language) && this.subject.equals(other.subject); 596 } 597 598 } 599 600 /** 601 * Represents a message body, its language and the content of the message. 602 */ 603 public static final class Body { 604 605 private final String message; 606 private final String language; 607 608 private Body(String language, String message) { 609 if (language == null) { 610 throw new NullPointerException("Language cannot be null."); 611 } 612 if (message == null) { 613 throw new NullPointerException("Message cannot be null."); 614 } 615 this.language = language; 616 this.message = message; 617 } 618 619 /** 620 * Returns the language of this message body. 621 * 622 * @return the language of this message body. 623 */ 624 public String getLanguage() { 625 return language; 626 } 627 628 /** 629 * Returns the message content. 630 * 631 * @return the content of the message. 632 */ 633 public String getMessage() { 634 return message; 635 } 636 637 @Override 638 public int hashCode() { 639 final int prime = 31; 640 int result = 1; 641 result = prime * result + this.language.hashCode(); 642 result = prime * result + this.message.hashCode(); 643 return result; 644 } 645 646 @Override 647 public boolean equals(Object obj) { 648 if (this == obj) { 649 return true; 650 } 651 if (obj == null) { 652 return false; 653 } 654 if (getClass() != obj.getClass()) { 655 return false; 656 } 657 Body other = (Body) obj; 658 // simplified comparison because language and message are always set 659 return this.language.equals(other.language) && this.message.equals(other.message); 660 } 661 662 } 663 664 /** 665 * Represents the type of a message. 666 */ 667 public enum Type { 668 669 /** 670 * (Default) a normal text message used in email like interface. 671 */ 672 normal, 673 674 /** 675 * Typically short text message used in line-by-line chat interfaces. 676 */ 677 chat, 678 679 /** 680 * Chat message sent to a groupchat server for group chats. 681 */ 682 groupchat, 683 684 /** 685 * Text message to be displayed in scrolling marquee displays. 686 */ 687 headline, 688 689 /** 690 * indicates a messaging error. 691 */ 692 error; 693 694 /** 695 * Converts a String into the corresponding types. Valid String values that can be converted 696 * to types are: "normal", "chat", "groupchat", "headline" and "error". 697 * 698 * @param string the String value to covert. 699 * @return the corresponding Type. 700 * @throws IllegalArgumentException when not able to parse the string parameter 701 * @throws NullPointerException if the string is null 702 */ 703 public static Type fromString(String string) { 704 return Type.valueOf(string.toLowerCase(Locale.US)); 705 } 706 707 } 708}