aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/MultipleRecipientManager.java
blob: 83afaceb8639b026549ffcfd0274e630d0787d06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2006 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smackx;

import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.Cache;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverItems;
import org.jivesoftware.smackx.packet.MultipleAddresses;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
 * <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33: Extended Stanza Addressing</a>.
 * It also allows to send replies to packets that were sent to multiple recipients.
 *
 * @author Gaston Dombiak
 */
public class MultipleRecipientManager {

    /**
     * Create a cache to hold the 100 most recently accessed elements for a period of
     * 24 hours.
     */
    private static Cache<String, String> services = new Cache<String, String>(100, 24 * 60 * 60 * 1000);

    /**
     * Sends the specified packet to the list of specified recipients using the
     * specified connection. If the server has support for JEP-33 then only one
     * packet is going to be sent to the server with the multiple recipient instructions.
     * However, if JEP-33 is not supported by the server then the client is going to send
     * the packet to each recipient.
     *
     * @param connection the connection to use to send the packet.
     * @param packet     the packet to send to the list of recipients.
     * @param to         the list of JIDs to include in the TO list or <tt>null</tt> if no TO
     *                   list exists.
     * @param cc         the list of JIDs to include in the CC list or <tt>null</tt> if no CC
     *                   list exists.
     * @param bcc        the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
     *                   list exists.
     * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
     *                       some JEP-33 specific features were requested.
     */
    public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc)
            throws XMPPException {
        send(connection, packet, to, cc, bcc, null, null, false);
    }

    /**
     * Sends the specified packet to the list of specified recipients using the
     * specified connection. If the server has support for JEP-33 then only one
     * packet is going to be sent to the server with the multiple recipient instructions.
     * However, if JEP-33 is not supported by the server then the client is going to send
     * the packet to each recipient.
     *
     * @param connection the connection to use to send the packet.
     * @param packet     the packet to send to the list of recipients.
     * @param to         the list of JIDs to include in the TO list or <tt>null</tt> if no TO
     *                   list exists.
     * @param cc         the list of JIDs to include in the CC list or <tt>null</tt> if no CC
     *                   list exists.
     * @param bcc        the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
     *                   list exists.
     * @param replyTo    address to which all replies are requested to be sent or <tt>null</tt>
     *                   indicating that they can reply to any address.
     * @param replyRoom  JID of a MUC room to which responses should be sent or <tt>null</tt>
     *                   indicating that they can reply to any address.
     * @param noReply    true means that receivers should not reply to the message.
     * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
     *                       some JEP-33 specific features were requested.
     */
    public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc,
            String replyTo, String replyRoom, boolean noReply) throws XMPPException {
        String serviceAddress = getMultipleRecipienServiceAddress(connection);
        if (serviceAddress != null) {
            // Send packet to target users using multiple recipient service provided by the server
            sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
                    serviceAddress);
        }
        else {
            // Server does not support JEP-33 so try to send the packet to each recipient
            if (noReply || (replyTo != null && replyTo.trim().length() > 0) ||
                    (replyRoom != null && replyRoom.trim().length() > 0)) {
                // Some specified JEP-33 features were requested so throw an exception alerting
                // the user that this features are not available
                throw new XMPPException("Extended Stanza Addressing not supported by server");
            }
            // Send the packet to each individual recipient
            sendToIndividualRecipients(connection, packet, to, cc, bcc);
        }
    }

    /**
     * Sends a reply to a previously received packet that was sent to multiple recipients. Before
     * attempting to send the reply message some checkings are performed. If any of those checkings
     * fail then an XMPPException is going to be thrown with the specific error detail.
     *
     * @param connection the connection to use to send the reply.
     * @param original   the previously received packet that was sent to multiple recipients.
     * @param reply      the new message to send as a reply.
     * @throws XMPPException if the original message was not sent to multiple recipients, or the
     *                       original message cannot be replied or reply should be sent to a room.
     */
    public static void reply(Connection connection, Message original, Message reply)
            throws XMPPException {
        MultipleRecipientInfo info = getMultipleRecipientInfo(original);
        if (info == null) {
            throw new XMPPException("Original message does not contain multiple recipient info");
        }
        if (info.shouldNotReply()) {
            throw new XMPPException("Original message should not be replied");
        }
        if (info.getReplyRoom() != null) {
            throw new XMPPException("Reply should be sent through a room");
        }
        // Any <thread/> element from the initial message MUST be copied into the reply.
        if (original.getThread() != null) {
            reply.setThread(original.getThread());
        }
        MultipleAddresses.Address replyAddress = info.getReplyAddress();
        if (replyAddress != null && replyAddress.getJid() != null) {
            // Send reply to the reply_to address
            reply.setTo(replyAddress.getJid());
            connection.sendPacket(reply);
        }
        else {
            // Send reply to multiple recipients
            List<String> to = new ArrayList<String>();
            List<String> cc = new ArrayList<String>();
            for (Iterator<MultipleAddresses.Address> it = info.getTOAddresses().iterator(); it.hasNext();) {
                String jid = it.next().getJid();
                to.add(jid);
            }
            for (Iterator<MultipleAddresses.Address> it = info.getCCAddresses().iterator(); it.hasNext();) {
                String jid = it.next().getJid();
                cc.add(jid);
            }
            // Add original sender as a 'to' address (if not already present)
            if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
                to.add(original.getFrom());
            }
            // Remove the sender from the TO/CC list (try with bare JID too)
            String from = connection.getUser();
            if (!to.remove(from) && !cc.remove(from)) {
                String bareJID = StringUtils.parseBareAddress(from);
                to.remove(bareJID);
                cc.remove(bareJID);
            }

            String serviceAddress = getMultipleRecipienServiceAddress(connection);
            if (serviceAddress != null) {
                // Send packet to target users using multiple recipient service provided by the server
                sendThroughService(connection, reply, to, cc, null, null, null, false,
                        serviceAddress);
            }
            else {
                // Server does not support JEP-33 so try to send the packet to each recipient
                sendToIndividualRecipients(connection, reply, to, cc, null);
            }
        }
    }

    /**
     * Returns the {@link MultipleRecipientInfo} contained in the specified packet or
     * <tt>null</tt> if none was found. Only packets sent to multiple recipients will
     * contain such information.
     *
     * @param packet the packet to check.
     * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt>
     *         if none was found.
     */
    public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) {
        MultipleAddresses extension = (MultipleAddresses) packet
                .getExtension("addresses", "http://jabber.org/protocol/address");
        return extension == null ? null : new MultipleRecipientInfo(extension);
    }

    private static void sendToIndividualRecipients(Connection connection, Packet packet,
            List<String> to, List<String> cc, List<String> bcc) {
        if (to != null) {
            for (Iterator<String> it = to.iterator(); it.hasNext();) {
                String jid = it.next();
                packet.setTo(jid);
                connection.sendPacket(new PacketCopy(packet.toXML()));
            }
        }
        if (cc != null) {
            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
                String jid = it.next();
                packet.setTo(jid);
                connection.sendPacket(new PacketCopy(packet.toXML()));
            }
        }
        if (bcc != null) {
            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
                String jid = it.next();
                packet.setTo(jid);
                connection.sendPacket(new PacketCopy(packet.toXML()));
            }
        }
    }

    private static void sendThroughService(Connection connection, Packet packet, List<String> to,
            List<String> cc, List<String> bcc, String replyTo, String replyRoom, boolean noReply,
            String serviceAddress) {
        // Create multiple recipient extension
        MultipleAddresses multipleAddresses = new MultipleAddresses();
        if (to != null) {
            for (Iterator<String> it = to.iterator(); it.hasNext();) {
                String jid = it.next();
                multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null);
            }
        }
        if (cc != null) {
            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
                String jid = it.next();
                multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null);
            }
        }
        if (bcc != null) {
            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
                String jid = it.next();
                multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null);
            }
        }
        if (noReply) {
            multipleAddresses.setNoReply();
        }
        else {
            if (replyTo != null && replyTo.trim().length() > 0) {
                multipleAddresses
                        .addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null);
            }
            if (replyRoom != null && replyRoom.trim().length() > 0) {
                multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, replyRoom, null, null,
                        false, null);
            }
        }
        // Set the multiple recipient service address as the target address
        packet.setTo(serviceAddress);
        // Add extension to packet
        packet.addExtension(multipleAddresses);
        // Send the packet
        connection.sendPacket(packet);
    }

    /**
     * Returns the address of the multiple recipients service. To obtain such address service
     * discovery is going to be used on the connected server and if none was found then another
     * attempt will be tried on the server items. The discovered information is going to be
     * cached for 24 hours.
     *
     * @param connection the connection to use for disco. The connected server is going to be
     *                   queried.
     * @return the address of the multiple recipients service or <tt>null</tt> if none was found.
     */
    private static String getMultipleRecipienServiceAddress(Connection connection) {
        String serviceName = connection.getServiceName();
        String serviceAddress = (String) services.get(serviceName);
        if (serviceAddress == null) {
            synchronized (services) {
                serviceAddress = (String) services.get(serviceName);
                if (serviceAddress == null) {

                    // Send the disco packet to the server itself
                    try {
                        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection)
                                .discoverInfo(serviceName);
                        // Check if the server supports JEP-33
                        if (info.containsFeature("http://jabber.org/protocol/address")) {
                            serviceAddress = serviceName;
                        }
                        else {
                            // Get the disco items and send the disco packet to each server item
                            DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection)
                                    .discoverItems(serviceName);
                            for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
                                DiscoverItems.Item item = it.next();
                                info = ServiceDiscoveryManager.getInstanceFor(connection)
                                        .discoverInfo(item.getEntityID(), item.getNode());
                                if (info.containsFeature("http://jabber.org/protocol/address")) {
                                    serviceAddress = serviceName;
                                    break;
                                }
                            }

                        }
                        // Cache the discovered information
                        services.put(serviceName, serviceAddress == null ? "" : serviceAddress);
                    }
                    catch (XMPPException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return "".equals(serviceAddress) ? null : serviceAddress;
    }

    /**
     * Packet that holds the XML stanza to send. This class is useful when the same packet
     * is needed to be sent to different recipients. Since using the same packet is not possible
     * (i.e. cannot change the TO address of a queues packet to be sent) then this class was
     * created to keep the XML stanza to send.
     */
    private static class PacketCopy extends Packet {

        private String text;

        /**
         * Create a copy of a packet with the text to send. The passed text must be a valid text to
         * send to the server, no validation will be done on the passed text.
         *
         * @param text the whole text of the packet to send
         */
        public PacketCopy(String text) {
            this.text = text;
        }

        public String toXML() {
            return text;
        }

    }

}