1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2006 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smackx;
22
23import org.jivesoftware.smack.Connection;
24import org.jivesoftware.smack.XMPPException;
25import org.jivesoftware.smack.packet.Message;
26import org.jivesoftware.smack.packet.Packet;
27import org.jivesoftware.smack.util.Cache;
28import org.jivesoftware.smack.util.StringUtils;
29import org.jivesoftware.smackx.packet.DiscoverInfo;
30import org.jivesoftware.smackx.packet.DiscoverItems;
31import org.jivesoftware.smackx.packet.MultipleAddresses;
32
33import java.util.ArrayList;
34import java.util.Iterator;
35import java.util.List;
36
37/**
38 * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
39 * <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33: Extended Stanza Addressing</a>.
40 * It also allows to send replies to packets that were sent to multiple recipients.
41 *
42 * @author Gaston Dombiak
43 */
44public class MultipleRecipientManager {
45
46    /**
47     * Create a cache to hold the 100 most recently accessed elements for a period of
48     * 24 hours.
49     */
50    private static Cache<String, String> services = new Cache<String, String>(100, 24 * 60 * 60 * 1000);
51
52    /**
53     * Sends the specified packet to the list of specified recipients using the
54     * specified connection. If the server has support for JEP-33 then only one
55     * packet is going to be sent to the server with the multiple recipient instructions.
56     * However, if JEP-33 is not supported by the server then the client is going to send
57     * the packet to each recipient.
58     *
59     * @param connection the connection to use to send the packet.
60     * @param packet     the packet to send to the list of recipients.
61     * @param to         the list of JIDs to include in the TO list or <tt>null</tt> if no TO
62     *                   list exists.
63     * @param cc         the list of JIDs to include in the CC list or <tt>null</tt> if no CC
64     *                   list exists.
65     * @param bcc        the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
66     *                   list exists.
67     * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
68     *                       some JEP-33 specific features were requested.
69     */
70    public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc)
71            throws XMPPException {
72        send(connection, packet, to, cc, bcc, null, null, false);
73    }
74
75    /**
76     * Sends the specified packet to the list of specified recipients using the
77     * specified connection. If the server has support for JEP-33 then only one
78     * packet is going to be sent to the server with the multiple recipient instructions.
79     * However, if JEP-33 is not supported by the server then the client is going to send
80     * the packet to each recipient.
81     *
82     * @param connection the connection to use to send the packet.
83     * @param packet     the packet to send to the list of recipients.
84     * @param to         the list of JIDs to include in the TO list or <tt>null</tt> if no TO
85     *                   list exists.
86     * @param cc         the list of JIDs to include in the CC list or <tt>null</tt> if no CC
87     *                   list exists.
88     * @param bcc        the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
89     *                   list exists.
90     * @param replyTo    address to which all replies are requested to be sent or <tt>null</tt>
91     *                   indicating that they can reply to any address.
92     * @param replyRoom  JID of a MUC room to which responses should be sent or <tt>null</tt>
93     *                   indicating that they can reply to any address.
94     * @param noReply    true means that receivers should not reply to the message.
95     * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
96     *                       some JEP-33 specific features were requested.
97     */
98    public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc,
99            String replyTo, String replyRoom, boolean noReply) throws XMPPException {
100        String serviceAddress = getMultipleRecipienServiceAddress(connection);
101        if (serviceAddress != null) {
102            // Send packet to target users using multiple recipient service provided by the server
103            sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
104                    serviceAddress);
105        }
106        else {
107            // Server does not support JEP-33 so try to send the packet to each recipient
108            if (noReply || (replyTo != null && replyTo.trim().length() > 0) ||
109                    (replyRoom != null && replyRoom.trim().length() > 0)) {
110                // Some specified JEP-33 features were requested so throw an exception alerting
111                // the user that this features are not available
112                throw new XMPPException("Extended Stanza Addressing not supported by server");
113            }
114            // Send the packet to each individual recipient
115            sendToIndividualRecipients(connection, packet, to, cc, bcc);
116        }
117    }
118
119    /**
120     * Sends a reply to a previously received packet that was sent to multiple recipients. Before
121     * attempting to send the reply message some checkings are performed. If any of those checkings
122     * fail then an XMPPException is going to be thrown with the specific error detail.
123     *
124     * @param connection the connection to use to send the reply.
125     * @param original   the previously received packet that was sent to multiple recipients.
126     * @param reply      the new message to send as a reply.
127     * @throws XMPPException if the original message was not sent to multiple recipients, or the
128     *                       original message cannot be replied or reply should be sent to a room.
129     */
130    public static void reply(Connection connection, Message original, Message reply)
131            throws XMPPException {
132        MultipleRecipientInfo info = getMultipleRecipientInfo(original);
133        if (info == null) {
134            throw new XMPPException("Original message does not contain multiple recipient info");
135        }
136        if (info.shouldNotReply()) {
137            throw new XMPPException("Original message should not be replied");
138        }
139        if (info.getReplyRoom() != null) {
140            throw new XMPPException("Reply should be sent through a room");
141        }
142        // Any <thread/> element from the initial message MUST be copied into the reply.
143        if (original.getThread() != null) {
144            reply.setThread(original.getThread());
145        }
146        MultipleAddresses.Address replyAddress = info.getReplyAddress();
147        if (replyAddress != null && replyAddress.getJid() != null) {
148            // Send reply to the reply_to address
149            reply.setTo(replyAddress.getJid());
150            connection.sendPacket(reply);
151        }
152        else {
153            // Send reply to multiple recipients
154            List<String> to = new ArrayList<String>();
155            List<String> cc = new ArrayList<String>();
156            for (Iterator<MultipleAddresses.Address> it = info.getTOAddresses().iterator(); it.hasNext();) {
157                String jid = it.next().getJid();
158                to.add(jid);
159            }
160            for (Iterator<MultipleAddresses.Address> it = info.getCCAddresses().iterator(); it.hasNext();) {
161                String jid = it.next().getJid();
162                cc.add(jid);
163            }
164            // Add original sender as a 'to' address (if not already present)
165            if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
166                to.add(original.getFrom());
167            }
168            // Remove the sender from the TO/CC list (try with bare JID too)
169            String from = connection.getUser();
170            if (!to.remove(from) && !cc.remove(from)) {
171                String bareJID = StringUtils.parseBareAddress(from);
172                to.remove(bareJID);
173                cc.remove(bareJID);
174            }
175
176            String serviceAddress = getMultipleRecipienServiceAddress(connection);
177            if (serviceAddress != null) {
178                // Send packet to target users using multiple recipient service provided by the server
179                sendThroughService(connection, reply, to, cc, null, null, null, false,
180                        serviceAddress);
181            }
182            else {
183                // Server does not support JEP-33 so try to send the packet to each recipient
184                sendToIndividualRecipients(connection, reply, to, cc, null);
185            }
186        }
187    }
188
189    /**
190     * Returns the {@link MultipleRecipientInfo} contained in the specified packet or
191     * <tt>null</tt> if none was found. Only packets sent to multiple recipients will
192     * contain such information.
193     *
194     * @param packet the packet to check.
195     * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt>
196     *         if none was found.
197     */
198    public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) {
199        MultipleAddresses extension = (MultipleAddresses) packet
200                .getExtension("addresses", "http://jabber.org/protocol/address");
201        return extension == null ? null : new MultipleRecipientInfo(extension);
202    }
203
204    private static void sendToIndividualRecipients(Connection connection, Packet packet,
205            List<String> to, List<String> cc, List<String> bcc) {
206        if (to != null) {
207            for (Iterator<String> it = to.iterator(); it.hasNext();) {
208                String jid = it.next();
209                packet.setTo(jid);
210                connection.sendPacket(new PacketCopy(packet.toXML()));
211            }
212        }
213        if (cc != null) {
214            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
215                String jid = it.next();
216                packet.setTo(jid);
217                connection.sendPacket(new PacketCopy(packet.toXML()));
218            }
219        }
220        if (bcc != null) {
221            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
222                String jid = it.next();
223                packet.setTo(jid);
224                connection.sendPacket(new PacketCopy(packet.toXML()));
225            }
226        }
227    }
228
229    private static void sendThroughService(Connection connection, Packet packet, List<String> to,
230            List<String> cc, List<String> bcc, String replyTo, String replyRoom, boolean noReply,
231            String serviceAddress) {
232        // Create multiple recipient extension
233        MultipleAddresses multipleAddresses = new MultipleAddresses();
234        if (to != null) {
235            for (Iterator<String> it = to.iterator(); it.hasNext();) {
236                String jid = it.next();
237                multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null);
238            }
239        }
240        if (cc != null) {
241            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
242                String jid = it.next();
243                multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null);
244            }
245        }
246        if (bcc != null) {
247            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
248                String jid = it.next();
249                multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null);
250            }
251        }
252        if (noReply) {
253            multipleAddresses.setNoReply();
254        }
255        else {
256            if (replyTo != null && replyTo.trim().length() > 0) {
257                multipleAddresses
258                        .addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null);
259            }
260            if (replyRoom != null && replyRoom.trim().length() > 0) {
261                multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, replyRoom, null, null,
262                        false, null);
263            }
264        }
265        // Set the multiple recipient service address as the target address
266        packet.setTo(serviceAddress);
267        // Add extension to packet
268        packet.addExtension(multipleAddresses);
269        // Send the packet
270        connection.sendPacket(packet);
271    }
272
273    /**
274     * Returns the address of the multiple recipients service. To obtain such address service
275     * discovery is going to be used on the connected server and if none was found then another
276     * attempt will be tried on the server items. The discovered information is going to be
277     * cached for 24 hours.
278     *
279     * @param connection the connection to use for disco. The connected server is going to be
280     *                   queried.
281     * @return the address of the multiple recipients service or <tt>null</tt> if none was found.
282     */
283    private static String getMultipleRecipienServiceAddress(Connection connection) {
284        String serviceName = connection.getServiceName();
285        String serviceAddress = (String) services.get(serviceName);
286        if (serviceAddress == null) {
287            synchronized (services) {
288                serviceAddress = (String) services.get(serviceName);
289                if (serviceAddress == null) {
290
291                    // Send the disco packet to the server itself
292                    try {
293                        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection)
294                                .discoverInfo(serviceName);
295                        // Check if the server supports JEP-33
296                        if (info.containsFeature("http://jabber.org/protocol/address")) {
297                            serviceAddress = serviceName;
298                        }
299                        else {
300                            // Get the disco items and send the disco packet to each server item
301                            DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection)
302                                    .discoverItems(serviceName);
303                            for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
304                                DiscoverItems.Item item = it.next();
305                                info = ServiceDiscoveryManager.getInstanceFor(connection)
306                                        .discoverInfo(item.getEntityID(), item.getNode());
307                                if (info.containsFeature("http://jabber.org/protocol/address")) {
308                                    serviceAddress = serviceName;
309                                    break;
310                                }
311                            }
312
313                        }
314                        // Cache the discovered information
315                        services.put(serviceName, serviceAddress == null ? "" : serviceAddress);
316                    }
317                    catch (XMPPException e) {
318                        e.printStackTrace();
319                    }
320                }
321            }
322        }
323
324        return "".equals(serviceAddress) ? null : serviceAddress;
325    }
326
327    /**
328     * Packet that holds the XML stanza to send. This class is useful when the same packet
329     * is needed to be sent to different recipients. Since using the same packet is not possible
330     * (i.e. cannot change the TO address of a queues packet to be sent) then this class was
331     * created to keep the XML stanza to send.
332     */
333    private static class PacketCopy extends Packet {
334
335        private String text;
336
337        /**
338         * Create a copy of a packet with the text to send. The passed text must be a valid text to
339         * send to the server, no validation will be done on the passed text.
340         *
341         * @param text the whole text of the packet to send
342         */
343        public PacketCopy(String text) {
344            this.text = text;
345        }
346
347        public String toXML() {
348            return text;
349        }
350
351    }
352
353}
354