001/**
002 *
003 * Copyright 2003-2006 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.smackx.address;
019
020import org.jivesoftware.smack.SmackException;
021import org.jivesoftware.smack.SmackException.NoResponseException;
022import org.jivesoftware.smack.SmackException.FeatureNotSupportedException;
023import org.jivesoftware.smack.SmackException.NotConnectedException;
024import org.jivesoftware.smack.XMPPConnection;
025import org.jivesoftware.smack.XMPPException.XMPPErrorException;
026import org.jivesoftware.smack.packet.Message;
027import org.jivesoftware.smack.packet.Stanza;
028import org.jivesoftware.smack.util.StringUtils;
029import org.jivesoftware.smackx.address.packet.MultipleAddresses;
030import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
031import org.jxmpp.jid.EntityBareJid;
032import org.jxmpp.jid.DomainBareJid;
033import org.jxmpp.jid.EntityFullJid;
034import org.jxmpp.jid.Jid;
035
036import java.util.ArrayList;
037import java.util.Collection;
038import java.util.List;
039
040/**
041 * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
042 * <a href="http://www.xmpp.org/extensions/jep-0033.html">XEP-33: Extended Stanza Addressing</a>.
043 * It also allows to send replies to packets that were sent to multiple recipients.
044 *
045 * @author Gaston Dombiak
046 */
047public class MultipleRecipientManager {
048
049    /**
050     * Sends the specified stanza(/packet) to the collection of specified recipients using the
051     * specified connection. If the server has support for XEP-33 then only one
052     * stanza(/packet) is going to be sent to the server with the multiple recipient instructions.
053     * However, if XEP-33 is not supported by the server then the client is going to send
054     * the stanza(/packet) to each recipient.
055     *
056     * @param connection the connection to use to send the packet.
057     * @param packet     the stanza(/packet) to send to the list of recipients.
058     * @param to         the collection of JIDs to include in the TO list or <tt>null</tt> if no TO
059     *                   list exists.
060     * @param cc         the collection of JIDs to include in the CC list or <tt>null</tt> if no CC
061     *                   list exists.
062     * @param bcc        the collection of JIDs to include in the BCC list or <tt>null</tt> if no BCC
063     *                   list exists.
064     * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
065     *         server does not support them.
066     * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
067     *                       some XEP-33 specific features were requested.
068     * @throws NoResponseException if there was no response from the server.
069     * @throws NotConnectedException 
070     * @throws InterruptedException 
071     */
072    public static void send(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to, Collection<? extends Jid> cc, Collection<? extends Jid> bcc) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException, InterruptedException
073   {
074        send(connection, packet, to, cc, bcc, null, null, false);
075    }
076
077    /**
078     * Sends the specified stanza(/packet) to the collection of specified recipients using the specified
079     * connection. If the server has support for XEP-33 then only one stanza(/packet) is going to be sent to
080     * the server with the multiple recipient instructions. However, if XEP-33 is not supported by
081     * the server then the client is going to send the stanza(/packet) to each recipient.
082     * 
083     * @param connection the connection to use to send the packet.
084     * @param packet the stanza(/packet) to send to the list of recipients.
085     * @param to the collection of JIDs to include in the TO list or <tt>null</tt> if no TO list exists.
086     * @param cc the collection of JIDs to include in the CC list or <tt>null</tt> if no CC list exists.
087     * @param bcc the collection of JIDs to include in the BCC list or <tt>null</tt> if no BCC list
088     *        exists.
089     * @param replyTo address to which all replies are requested to be sent or <tt>null</tt>
090     *        indicating that they can reply to any address.
091     * @param replyRoom JID of a MUC room to which responses should be sent or <tt>null</tt>
092     *        indicating that they can reply to any address.
093     * @param noReply true means that receivers should not reply to the message.
094     * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
095     *         some XEP-33 specific features were requested.
096     * @throws NoResponseException if there was no response from the server.
097     * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
098     *         server does not support them.
099     * @throws NotConnectedException 
100     * @throws InterruptedException 
101     */
102    public static void send(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to, Collection<? extends Jid> cc, Collection<? extends Jid> bcc,
103            Jid replyTo, Jid replyRoom, boolean noReply) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException, InterruptedException {
104        // Check if *only* 'to' is set and contains just *one* entry, in this case extended stanzas addressing is not
105        // required at all and we can send it just as normal stanza without needing to add the extension element
106        if (to != null && to.size() == 1 && (cc == null || cc.isEmpty()) && (bcc == null || bcc.isEmpty()) && !noReply
107                        && StringUtils.isNullOrEmpty(replyTo) && StringUtils.isNullOrEmpty(replyRoom)) {
108            Jid toJid = to.iterator().next();
109            packet.setTo(toJid);
110            connection.sendStanza(packet);
111            return;
112        }
113        DomainBareJid serviceAddress = getMultipleRecipienServiceAddress(connection);
114        if (serviceAddress != null) {
115            // Send packet to target users using multiple recipient service provided by the server
116            sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
117                    serviceAddress);
118        }
119        else {
120            // Server does not support XEP-33 so try to send the packet to each recipient
121            if (noReply || replyTo != null ||
122                    replyRoom != null) {
123                // Some specified XEP-33 features were requested so throw an exception alerting
124                // the user that this features are not available
125                throw new FeatureNotSupportedException("Extended Stanza Addressing");
126            }
127            // Send the packet to each individual recipient
128            sendToIndividualRecipients(connection, packet, to, cc, bcc);
129        }
130    }
131
132    /**
133     * Sends a reply to a previously received stanza(/packet) that was sent to multiple recipients. Before
134     * attempting to send the reply message some checkings are performed. If any of those checkings
135     * fail then an XMPPException is going to be thrown with the specific error detail.
136     *
137     * @param connection the connection to use to send the reply.
138     * @param original   the previously received stanza(/packet) that was sent to multiple recipients.
139     * @param reply      the new message to send as a reply.
140     * @throws SmackException 
141     * @throws XMPPErrorException 
142     * @throws InterruptedException 
143     */
144    public static void reply(XMPPConnection connection, Message original, Message reply) throws SmackException, XMPPErrorException, InterruptedException
145         {
146        MultipleRecipientInfo info = getMultipleRecipientInfo(original);
147        if (info == null) {
148            throw new SmackException("Original message does not contain multiple recipient info");
149        }
150        if (info.shouldNotReply()) {
151            throw new SmackException("Original message should not be replied");
152        }
153        if (info.getReplyRoom() != null) {
154            throw new SmackException("Reply should be sent through a room");
155        }
156        // Any <thread/> element from the initial message MUST be copied into the reply.
157        if (original.getThread() != null) {
158            reply.setThread(original.getThread());
159        }
160        MultipleAddresses.Address replyAddress = info.getReplyAddress();
161        if (replyAddress != null && replyAddress.getJid() != null) {
162            // Send reply to the reply_to address
163            reply.setTo(replyAddress.getJid());
164            connection.sendStanza(reply);
165        }
166        else {
167            // Send reply to multiple recipients
168            List<Jid> to = new ArrayList<>(info.getTOAddresses().size());
169            List<Jid> cc = new ArrayList<>(info.getCCAddresses().size());
170            for (MultipleAddresses.Address jid : info.getTOAddresses()) {
171                to.add(jid.getJid());
172            }
173            for (MultipleAddresses.Address jid : info.getCCAddresses()) {
174                cc.add(jid.getJid());
175            }
176            // Add original sender as a 'to' address (if not already present)
177            if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
178                to.add(original.getFrom());
179            }
180            // Remove the sender from the TO/CC list (try with bare JID too)
181            EntityFullJid from = connection.getUser();
182            if (!to.remove(from) && !cc.remove(from)) {
183                EntityBareJid bareJID = from.asEntityBareJid();
184                to.remove(bareJID);
185                cc.remove(bareJID);
186            }
187
188            send(connection, reply, to, cc, null, null, null, false);
189        }
190    }
191
192    /**
193     * Returns the {@link MultipleRecipientInfo} contained in the specified stanza(/packet) or
194     * <tt>null</tt> if none was found. Only packets sent to multiple recipients will
195     * contain such information.
196     *
197     * @param packet the stanza(/packet) to check.
198     * @return the MultipleRecipientInfo contained in the specified stanza(/packet) or <tt>null</tt>
199     *         if none was found.
200     */
201    public static MultipleRecipientInfo getMultipleRecipientInfo(Stanza packet) {
202        MultipleAddresses extension = (MultipleAddresses) packet
203                .getExtension(MultipleAddresses.ELEMENT, MultipleAddresses.NAMESPACE);
204        return extension == null ? null : new MultipleRecipientInfo(extension);
205    }
206
207    private static void sendToIndividualRecipients(XMPPConnection connection, Stanza packet,
208            Collection<? extends Jid> to, Collection<? extends Jid> cc, Collection<? extends Jid> bcc) throws NotConnectedException, InterruptedException {
209        if (to != null) {
210            for (Jid jid : to) {
211                packet.setTo(jid);
212                connection.sendStanza(new PacketCopy(packet.toXML()));
213            }
214        }
215        if (cc != null) {
216            for (Jid jid : cc) {
217                packet.setTo(jid);
218                connection.sendStanza(new PacketCopy(packet.toXML()));
219            }
220        }
221        if (bcc != null) {
222            for (Jid jid : bcc) {
223                packet.setTo(jid);
224                connection.sendStanza(new PacketCopy(packet.toXML()));
225            }
226        }
227    }
228
229    private static void sendThroughService(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to,
230            Collection<? extends Jid> cc, Collection<? extends Jid> bcc, Jid replyTo, Jid replyRoom, boolean noReply,
231            DomainBareJid serviceAddress) throws NotConnectedException, InterruptedException {
232        // Create multiple recipient extension
233        MultipleAddresses multipleAddresses = new MultipleAddresses();
234        if (to != null) {
235            for (Jid jid : to) {
236                multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null);
237            }
238        }
239        if (cc != null) {
240            for (Jid jid : cc) {
241                multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null);
242            }
243        }
244        if (bcc != null) {
245            for (Jid jid : bcc) {
246                multipleAddresses.addAddress(MultipleAddresses.Type.bcc, jid, null, null, false, null);
247            }
248        }
249        if (noReply) {
250            multipleAddresses.setNoReply();
251        }
252        else {
253            if (replyTo != null) {
254                multipleAddresses
255                        .addAddress(MultipleAddresses.Type.replyto, replyTo, null, null, false, null);
256            }
257            if (replyRoom != null) {
258                multipleAddresses.addAddress(MultipleAddresses.Type.replyroom, replyRoom, null, null,
259                        false, null);
260            }
261        }
262        // Set the multiple recipient service address as the target address
263        packet.setTo(serviceAddress);
264        // Add extension to packet
265        packet.addExtension(multipleAddresses);
266        // Send the packet
267        connection.sendStanza(packet);
268    }
269
270    /**
271     * Returns the address of the multiple recipients service. To obtain such address service
272     * discovery is going to be used on the connected server and if none was found then another
273     * attempt will be tried on the server items. The discovered information is going to be
274     * cached for 24 hours.
275     *
276     * @param connection the connection to use for disco. The connected server is going to be
277     *                   queried.
278     * @return the address of the multiple recipients service or <tt>null</tt> if none was found.
279     * @throws NoResponseException if there was no response from the server.
280     * @throws XMPPErrorException 
281     * @throws NotConnectedException 
282     * @throws InterruptedException 
283     */
284    private static DomainBareJid getMultipleRecipienServiceAddress(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
285        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
286        return sdm.findService(MultipleAddresses.NAMESPACE, true);
287    }
288
289    /**
290     * Stanza(/Packet) that holds the XML stanza to send. This class is useful when the same packet
291     * is needed to be sent to different recipients. Since using the same stanza(/packet) is not possible
292     * (i.e. cannot change the TO address of a queues stanza(/packet) to be sent) then this class was
293     * created to keep the XML stanza to send.
294     */
295    private static class PacketCopy extends Stanza {
296
297        private CharSequence text;
298
299        /**
300         * Create a copy of a stanza(/packet) with the text to send. The passed text must be a valid text to
301         * send to the server, no validation will be done on the passed text.
302         *
303         * @param text the whole text of the stanza(/packet) to send
304         */
305        public PacketCopy(CharSequence text) {
306            this.text = text;
307        }
308
309        @Override
310        public CharSequence toXML() {
311            return text;
312        }
313
314        @Override
315        public String toString() {
316            return toXML().toString();
317        }
318
319    }
320
321}