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>&nbsp;</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}