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.List;
021import java.util.Locale;
022
023import javax.xml.namespace.QName;
024
025import org.jivesoftware.smack.XMPPConnection;
026import org.jivesoftware.smack.util.EqualsUtil;
027import org.jivesoftware.smack.util.HashCode;
028import org.jivesoftware.smack.util.Objects;
029import org.jivesoftware.smack.util.StringUtils;
030import org.jivesoftware.smack.util.XmlStringBuilder;
031
032import org.jxmpp.jid.Jid;
033import org.jxmpp.jid.impl.JidCreate;
034import org.jxmpp.stringprep.XmppStringprepException;
035
036/**
037 * Represents XMPP message packets. A message can be one of several types:
038 *
039 * <ul>
040 *      <li>Message.Type.NORMAL -- (Default) a normal text message used in email like interface.
041 *      <li>Message.Type.CHAT -- a typically short text message used in line-by-line chat interfaces.
042 *      <li>Message.Type.GROUP_CHAT -- a chat message sent to a groupchat server for group chats.
043 *      <li>Message.Type.HEADLINE -- a text message to be displayed in scrolling marquee displays.
044 *      <li>Message.Type.ERROR -- indicates a messaging error.
045 * </ul>
046 *
047 * For each message type, different message fields are typically used as follows:
048 * <table border="1">
049 * <caption>Message Types</caption>
050 * <tr><td>&nbsp;</td><td colspan="5"><b>Message type</b></td></tr>
051 * <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>
052 * <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>
053 * <tr><td><i>thread</i></td>  <td>OPTIONAL</td><td>SHOULD</td><td>OPTIONAL</td><td>OPTIONAL</td><td>SHOULD NOT</td></tr>
054 * <tr><td><i>body</i></td>    <td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD NOT</td></tr>
055 * <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>
056 * </table>
057 *
058 * @author Matt Tucker
059 */
060public final class Message extends MessageOrPresence<MessageBuilder>
061                implements MessageView {
062
063    public static final String ELEMENT = "message";
064    public static final String BODY = "body";
065
066    private Type type;
067
068    /**
069     * Creates a new, "normal" message.
070     * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead.
071     */
072    @Deprecated
073    // TODO: Remove in Smack 4.5.
074    public Message() {
075    }
076
077    /**
078     * Creates a new "normal" message to the specified recipient.
079     *
080     * @param to the recipient of the message.
081     * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead.
082     */
083    @Deprecated
084    // TODO: Remove in Smack 4.5.
085    public Message(Jid to) {
086        setTo(to);
087    }
088
089    /**
090     * Creates a new message of the specified type to a recipient.
091     *
092     * @param to the user to send the message to.
093     * @param type the message type.
094     * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead.
095     */
096    @Deprecated
097    // TODO: Remove in Smack 4.5.
098    public Message(Jid to, Type type) {
099        this(to);
100        setType(type);
101    }
102
103    /**
104     * Creates a new message to the specified recipient and with the specified body.
105     *
106     * @param to the user to send the message to.
107     * @param body the body of the message.
108     * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead.
109     */
110    @Deprecated
111    // TODO: Remove in Smack 4.5.
112    public Message(Jid to, String body) {
113        this(to);
114        setBody(body);
115    }
116
117    /**
118     * Creates a new message to the specified recipient and with the specified body.
119     *
120     * @param to the user to send the message to.
121     * @param body the body of the message.
122     * @throws XmppStringprepException if 'to' is not a valid XMPP address.
123     * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead.
124     */
125    @Deprecated
126    // TODO: Remove in Smack 4.5.
127    public Message(String to, String body) throws XmppStringprepException {
128        this(JidCreate.from(to), body);
129    }
130
131    /**
132     * Creates a new message with the specified recipient and extension element.
133     *
134     * @param to TODO javadoc me please
135     * @param extensionElement TODO javadoc me please
136     * @since 4.2
137     * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead.
138     */
139    @Deprecated
140    // TODO: Remove in Smack 4.5.
141    public Message(Jid to, ExtensionElement extensionElement) {
142        this(to);
143        addExtension(extensionElement);
144    }
145
146    Message(MessageBuilder messageBuilder) {
147        super(messageBuilder);
148        type = messageBuilder.type;
149    }
150
151    /**
152     * Copy constructor.
153     * <p>
154     * This does not perform a deep clone, as extension elements are shared between the new and old
155     * instance.
156     * </p>
157     *
158     * @param other TODO javadoc me please
159     */
160    public Message(Message other) {
161        super(other);
162        this.type = other.type;
163    }
164
165    @Override
166    public Type getType() {
167        if (type == null) {
168            return Type.normal;
169        }
170        return type;
171    }
172
173    /**
174     * Sets the type of the message.
175     *
176     * @param type the type of the message.
177     * @deprecated use {@link StanzaBuilder} instead.
178     */
179    @Deprecated
180    // TODO: Remove in Smack 4.5.
181    public void setType(Type type) {
182        this.type = type;
183    }
184
185    /**
186     * Sets the subject of the message. The subject is a short description of
187     * message contents.
188     *
189     * @param subject the subject of the message.
190     * @deprecated use {@link StanzaBuilder} instead.
191     */
192    @Deprecated
193    // TODO: Remove when stanza builder is ready.
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    @Deprecated
211    // TODO: Remove when stanza builder is ready.
212    public Subject addSubject(String language, String subject) {
213        language = Stanza.determineLanguage(this, language);
214
215        List<Subject> currentSubjects = getExtensions(Subject.class);
216        for (Subject currentSubject : currentSubjects) {
217            if (language.equals(currentSubject.getLanguage())) {
218                throw new IllegalArgumentException("Subject with the language " + language + " already exists");
219            }
220        }
221
222        Subject messageSubject = new Subject(language, subject);
223        addExtension(messageSubject);
224        return messageSubject;
225    }
226
227    /**
228     * Removes the subject with the given language from the message.
229     *
230     * @param language the language of the subject which is to be removed
231     * @return true if a subject was removed and false if it was not.
232     */
233    @Deprecated
234    // TODO: Remove when stanza builder is ready.
235    public boolean removeSubject(String language) {
236        language = Stanza.determineLanguage(this, language);
237        for (Subject subject : getExtensions(Subject.class)) {
238            if (language.equals(subject.language)) {
239                return removeSubject(subject);
240            }
241        }
242        return false;
243    }
244
245    /**
246     * Removes the subject from the message and returns true if the subject was removed.
247     *
248     * @param subject the subject being removed from the message.
249     * @return true if the subject was successfully removed and false if it was not.
250     */
251    @Deprecated
252    // TODO: Remove when stanza builder is ready.
253    public boolean removeSubject(Subject subject) {
254        return removeExtension(subject) != null;
255    }
256
257    /**
258     * Sets the body of the message.
259     *
260     * @param body the body of the message.
261     * @see #setBody(String)
262     * @since 4.2
263     * @deprecated use {@link StanzaBuilder} instead.
264     */
265    @Deprecated
266    // TODO: Remove when stanza builder is ready.
267    public void setBody(CharSequence body) {
268        String bodyString;
269        if (body != null) {
270            bodyString = body.toString();
271        } else {
272            bodyString = null;
273        }
274        setBody(bodyString);
275    }
276
277    /**
278     * Sets the body of the message. The body is the main message contents.
279     *
280     * @param body the body of the message.
281     * @deprecated use {@link StanzaBuilder} instead.
282     */
283    @Deprecated
284    // TODO: Remove when stanza builder is ready.
285    public void setBody(String body) {
286        if (body == null) {
287            removeBody(""); // use empty string because #removeBody(null) is ambiguous
288            return;
289        }
290        addBody(null, body);
291    }
292
293    /**
294     * Adds a body with a corresponding language.
295     *
296     * @param language the language of the body being added.
297     * @param body the body being added to the message.
298     * @return the new {@link org.jivesoftware.smack.packet.Message.Body}
299     * @throws NullPointerException if the body is null, a null pointer exception is thrown
300     * @since 3.0.2
301     * @deprecated use {@link StanzaBuilder} instead.
302     */
303    @Deprecated
304    // TODO: Remove when stanza builder is ready.
305    public Body addBody(String language, String body) {
306        language = Stanza.determineLanguage(this, language);
307
308        removeBody(language);
309
310        Body messageBody = new Body(language, body);
311        addExtension(messageBody);
312        return messageBody;
313    }
314
315    /**
316     * Removes the body with the given language from the message.
317     *
318     * @param language the language of the body which is to be removed
319     * @return true if a body was removed and false if it was not.
320     * @deprecated use {@link StanzaBuilder} instead.
321     */
322    @Deprecated
323    // TODO: Remove when stanza builder is ready.
324    public boolean removeBody(String language) {
325        language = Stanza.determineLanguage(this, language);
326        for (Body body : getBodies()) {
327            String bodyLanguage = body.getLanguage();
328            if (Objects.equals(bodyLanguage, language)) {
329                removeExtension(body);
330                return true;
331            }
332        }
333        return false;
334    }
335
336    /**
337     * Removes the body from the message and returns true if the body was removed.
338     *
339     * @param body the body being removed from the message.
340     * @return true if the body was successfully removed and false if it was not.
341     * @since 3.0.2
342     * @deprecated use {@link StanzaBuilder} instead.
343     */
344    @Deprecated
345    // TODO: Remove when stanza builder is ready.
346    public boolean removeBody(Body body) {
347        XmlElement removedElement = removeExtension(body);
348        return removedElement != null;
349    }
350
351    /**
352     * Sets the thread id of the message, which is a unique identifier for a sequence
353     * of "chat" messages.
354     *
355     * @param thread the thread id of the message.
356     * @deprecated use {@link StanzaBuilder} instead.
357     */
358    @Deprecated
359    // TODO: Remove when stanza builder is ready.
360    public void setThread(String thread) {
361        addExtension(new Message.Thread(thread));
362    }
363
364    @Override
365    public String getElementName() {
366        return ELEMENT;
367    }
368
369    @Override
370    public MessageBuilder asBuilder() {
371        return StanzaBuilder.buildMessageFrom(this, getStanzaId());
372    }
373
374    @Override
375    public MessageBuilder asBuilder(String id) {
376        return StanzaBuilder.buildMessageFrom(this, id);
377    }
378
379    @Override
380    public MessageBuilder asBuilder(XMPPConnection connection) {
381        return connection.getStanzaFactory().buildMessageStanzaFrom(this);
382    }
383
384    @Override
385    public String toString() {
386        StringBuilder sb = new StringBuilder();
387        sb.append("Message Stanza [");
388        logCommonAttributes(sb);
389        if (type != null) {
390            sb.append("type=").append(type).append(',');
391        }
392        sb.append(']');
393        return sb.toString();
394    }
395
396    @Override
397    public XmlStringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) {
398        XmlStringBuilder buf = new XmlStringBuilder(this, enclosingXmlEnvironment);
399        addCommonAttributes(buf);
400        buf.optAttribute("type", type);
401        buf.rightAngleBracket();
402
403        // Append the error subpacket if the message type is an error.
404        if (type == Type.error) {
405            appendErrorIfExists(buf);
406        }
407
408        // Add extension elements, if any are defined.
409        buf.append(getExtensions());
410
411        buf.closeElement(ELEMENT);
412        return buf;
413    }
414
415    /**
416     * Creates and returns a copy of this message stanza.
417     * <p>
418     * This does not perform a deep clone, as extension elements are shared between the new and old
419     * instance.
420     * </p>
421     * @return a clone of this message.
422     * @deprecated use {@link #asBuilder()} instead.
423     */
424    // TODO: Remove in Smack 4.5.
425    @Deprecated
426    @Override
427    public Message clone() {
428        return new Message(this);
429    }
430
431    /**
432     * Represents a message subject, its language and the content of the subject.
433     */
434    public static final class Subject implements ExtensionElement {
435
436        public static final String ELEMENT = "subject";
437        public static final String NAMESPACE = StreamOpen.CLIENT_NAMESPACE;
438
439        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
440
441        private final String subject;
442        private final String language;
443
444        public Subject(String language, String subject) {
445            if (subject == null) {
446                throw new NullPointerException("Subject cannot be null.");
447            }
448            this.language = language;
449            this.subject = subject;
450        }
451
452        @Override
453        public String getLanguage() {
454            return language;
455        }
456
457        /**
458         * Returns the subject content.
459         *
460         * @return the content of the subject.
461         */
462        public String getSubject() {
463            return subject;
464        }
465
466        private final HashCode.Cache hashCodeCache = new HashCode.Cache();
467
468        @Override
469        public int hashCode() {
470            return hashCodeCache.getHashCode(c ->
471                c.append(language)
472                 .append(subject)
473            );
474        }
475
476        @Override
477        public boolean equals(Object obj) {
478            return EqualsUtil.equals(this, obj, (e, o) ->
479                e.append(language, o.language)
480                 .append(subject, o.subject)
481            );
482        }
483
484        @Override
485        public String getElementName() {
486            return ELEMENT;
487        }
488
489        @Override
490        public String getNamespace() {
491            return NAMESPACE;
492        }
493
494        @Override
495        public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
496            XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace);
497            xml.rightAngleBracket();
498            xml.escape(subject);
499            xml.closeElement(getElementName());
500            return xml;
501        }
502
503    }
504
505    /**
506     * Represents a message body, its language and the content of the message.
507     */
508    public static final class Body implements ExtensionElement {
509
510        public static final String ELEMENT = "body";
511        public static final String NAMESPACE = StreamOpen.CLIENT_NAMESPACE;
512        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
513
514        enum BodyElementNamespace {
515            client(StreamOpen.CLIENT_NAMESPACE),
516            server(StreamOpen.SERVER_NAMESPACE),
517            ;
518
519            private final String xmlNamespace;
520
521            BodyElementNamespace(String xmlNamespace) {
522                this.xmlNamespace = xmlNamespace;
523            }
524
525            public String getNamespace() {
526                return xmlNamespace;
527            }
528        }
529
530        private final String message;
531        private final String language;
532        private final BodyElementNamespace namespace;
533
534        public Body(String language, String message) {
535            this(language, message, BodyElementNamespace.client);
536        }
537
538        public Body(String language, String message, BodyElementNamespace namespace) {
539            if (message == null) {
540                throw new NullPointerException("Message cannot be null.");
541            }
542            this.language = language;
543            this.message = message;
544            this.namespace = Objects.requireNonNull(namespace);
545        }
546
547        @Override
548        public String getLanguage() {
549            return language;
550        }
551
552        /**
553         * Returns the message content.
554         *
555         * @return the content of the message.
556         */
557        public String getMessage() {
558            return message;
559        }
560
561        private final HashCode.Cache hashCodeCache = new HashCode.Cache();
562
563        @Override
564        public int hashCode() {
565            return hashCodeCache.getHashCode(c ->
566                c.append(language)
567                .append(message)
568            );
569        }
570
571        @Override
572        public boolean equals(Object obj) {
573            return EqualsUtil.equals(this, obj, (e, o) ->
574                e.append(language, o.language)
575                 .append(message, o.message)
576            );
577        }
578
579        @Override
580        public String getElementName() {
581            return ELEMENT;
582        }
583
584        @Override
585        public String getNamespace() {
586            return namespace.xmlNamespace;
587        }
588
589        @Override
590        public XmlStringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) {
591            XmlStringBuilder xml = new XmlStringBuilder(this, enclosingXmlEnvironment);
592            xml.rightAngleBracket();
593            xml.text(message);
594            xml.closeElement(getElementName());
595            return xml;
596        }
597
598    }
599
600    @SuppressWarnings("JavaLangClash")
601    public static class Thread implements ExtensionElement {
602        public static final String ELEMENT = "thread";
603        public static final String NAMESPACE = StreamOpen.CLIENT_NAMESPACE;
604        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
605
606        public static final String PARENT_ATTRIBUTE_NAME = "parent";
607
608        private final String thread;
609        private final String parent;
610
611        public Thread(String thread) {
612            this(thread, null);
613        }
614
615        public Thread(String thread, String parent) {
616            this.thread = StringUtils.requireNotNullNorEmpty(thread, "thread must not be null nor empty");
617            this.parent = StringUtils.requireNullOrNotEmpty(parent, "parent must be null or not empty");
618        }
619
620        public String getThread() {
621            return thread;
622        }
623
624        public String getParent() {
625            return parent;
626        }
627
628        @Override
629        public String getElementName() {
630            return ELEMENT;
631        }
632
633        @Override
634        public String getNamespace() {
635            return NAMESPACE;
636        }
637
638        @Override
639        public QName getQName() {
640            return QNAME;
641        }
642
643        @Override
644        public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
645            XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
646            xml.optAttribute(PARENT_ATTRIBUTE_NAME, parent);
647            xml.rightAngleBracket();
648            xml.escape(thread);
649            xml.closeElement(this);
650            return xml;
651        }
652    }
653
654    /**
655     * Represents the type of a message.
656     */
657    public enum Type {
658
659        /**
660         * (Default) a normal text message used in email like interface.
661         */
662        normal,
663
664        /**
665         * Typically short text message used in line-by-line chat interfaces.
666         */
667        chat,
668
669        /**
670         * Chat message sent to a groupchat server for group chats.
671         */
672        groupchat,
673
674        /**
675         * Text message to be displayed in scrolling marquee displays.
676         */
677        headline,
678
679        /**
680         * indicates a messaging error.
681         */
682        error;
683
684        /**
685         * Converts a String into the corresponding types. Valid String values that can be converted
686         * to types are: "normal", "chat", "groupchat", "headline" and "error".
687         *
688         * @param string the String value to covert.
689         * @return the corresponding Type.
690         * @throws IllegalArgumentException when not able to parse the string parameter
691         * @throws NullPointerException if the string is null
692         */
693        public static Type fromString(String string) {
694            return Type.valueOf(string.toLowerCase(Locale.US));
695        }
696
697    }
698}