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