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