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.Packet;
028import org.jivesoftware.smack.util.Cache;
029import org.jivesoftware.smack.util.StringUtils;
030import org.jivesoftware.smackx.address.packet.MultipleAddresses;
031import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
032import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
033import org.jivesoftware.smackx.disco.packet.DiscoverItems;
034
035import java.util.ArrayList;
036import java.util.Iterator;
037import java.util.List;
038import java.util.logging.Level;
039import java.util.logging.Logger;
040
041/**
042 * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
043 * <a href="http://www.xmpp.org/extensions/jep-0033.html">XEP-33: Extended Stanza Addressing</a>.
044 * It also allows to send replies to packets that were sent to multiple recipients.
045 *
046 * @author Gaston Dombiak
047 */
048public class MultipleRecipientManager {
049
050    private static final Logger LOGGER = Logger.getLogger(MultipleRecipientManager.class.getName());
051
052    /**
053     * Create a cache to hold the 100 most recently accessed elements for a period of
054     * 24 hours.
055     */
056    private static Cache<String, String> services = new Cache<String, String>(100, 24 * 60 * 60 * 1000);
057
058    /**
059     * Sends the specified packet to the list of specified recipients using the
060     * specified connection. If the server has support for XEP-33 then only one
061     * packet is going to be sent to the server with the multiple recipient instructions.
062     * However, if XEP-33 is not supported by the server then the client is going to send
063     * the packet to each recipient.
064     *
065     * @param connection the connection to use to send the packet.
066     * @param packet     the packet to send to the list of recipients.
067     * @param to         the list of JIDs to include in the TO list or <tt>null</tt> if no TO
068     *                   list exists.
069     * @param cc         the list of JIDs to include in the CC list or <tt>null</tt> if no CC
070     *                   list exists.
071     * @param bcc        the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
072     *                   list exists.
073     * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
074     *         server does not support them.
075     * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
076     *                       some XEP-33 specific features were requested.
077     * @throws NoResponseException if there was no response from the server.
078     * @throws NotConnectedException 
079     */
080    public static void send(XMPPConnection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException
081   {
082        send(connection, packet, to, cc, bcc, null, null, false);
083    }
084
085    /**
086     * Sends the specified packet to the list of specified recipients using the specified
087     * connection. If the server has support for XEP-33 then only one packet is going to be sent to
088     * the server with the multiple recipient instructions. However, if XEP-33 is not supported by
089     * the server then the client is going to send the packet to each recipient.
090     * 
091     * @param connection the connection to use to send the packet.
092     * @param packet the packet to send to the list of recipients.
093     * @param to the list of JIDs to include in the TO list or <tt>null</tt> if no TO list exists.
094     * @param cc the list of JIDs to include in the CC list or <tt>null</tt> if no CC list exists.
095     * @param bcc the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC list
096     *        exists.
097     * @param replyTo address to which all replies are requested to be sent or <tt>null</tt>
098     *        indicating that they can reply to any address.
099     * @param replyRoom JID of a MUC room to which responses should be sent or <tt>null</tt>
100     *        indicating that they can reply to any address.
101     * @param noReply true means that receivers should not reply to the message.
102     * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
103     *         some XEP-33 specific features were requested.
104     * @throws NoResponseException if there was no response from the server.
105     * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
106     *         server does not support them.
107     * @throws NotConnectedException 
108     */
109    public static void send(XMPPConnection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc,
110            String replyTo, String replyRoom, boolean noReply) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException {
111        String serviceAddress = getMultipleRecipienServiceAddress(connection);
112        if (serviceAddress != null) {
113            // Send packet to target users using multiple recipient service provided by the server
114            sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
115                    serviceAddress);
116        }
117        else {
118            // Server does not support XEP-33 so try to send the packet to each recipient
119            if (noReply || (replyTo != null && replyTo.trim().length() > 0) ||
120                    (replyRoom != null && replyRoom.trim().length() > 0)) {
121                // Some specified XEP-33 features were requested so throw an exception alerting
122                // the user that this features are not available
123                throw new FeatureNotSupportedException("Extended Stanza Addressing");
124            }
125            // Send the packet to each individual recipient
126            sendToIndividualRecipients(connection, packet, to, cc, bcc);
127        }
128    }
129
130    /**
131     * Sends a reply to a previously received packet that was sent to multiple recipients. Before
132     * attempting to send the reply message some checkings are performed. If any of those checkings
133     * fail then an XMPPException is going to be thrown with the specific error detail.
134     *
135     * @param connection the connection to use to send the reply.
136     * @param original   the previously received packet that was sent to multiple recipients.
137     * @param reply      the new message to send as a reply.
138     * @throws SmackException 
139     * @throws XMPPErrorException 
140     */
141    public static void reply(XMPPConnection connection, Message original, Message reply) throws SmackException, XMPPErrorException
142         {
143        MultipleRecipientInfo info = getMultipleRecipientInfo(original);
144        if (info == null) {
145            throw new SmackException("Original message does not contain multiple recipient info");
146        }
147        if (info.shouldNotReply()) {
148            throw new SmackException("Original message should not be replied");
149        }
150        if (info.getReplyRoom() != null) {
151            throw new SmackException("Reply should be sent through a room");
152        }
153        // Any <thread/> element from the initial message MUST be copied into the reply.
154        if (original.getThread() != null) {
155            reply.setThread(original.getThread());
156        }
157        MultipleAddresses.Address replyAddress = info.getReplyAddress();
158        if (replyAddress != null && replyAddress.getJid() != null) {
159            // Send reply to the reply_to address
160            reply.setTo(replyAddress.getJid());
161            connection.sendPacket(reply);
162        }
163        else {
164            // Send reply to multiple recipients
165            List<String> to = new ArrayList<String>();
166            List<String> cc = new ArrayList<String>();
167            for (Iterator<MultipleAddresses.Address> it = info.getTOAddresses().iterator(); it.hasNext();) {
168                String jid = it.next().getJid();
169                to.add(jid);
170            }
171            for (Iterator<MultipleAddresses.Address> it = info.getCCAddresses().iterator(); it.hasNext();) {
172                String jid = it.next().getJid();
173                cc.add(jid);
174            }
175            // Add original sender as a 'to' address (if not already present)
176            if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
177                to.add(original.getFrom());
178            }
179            // Remove the sender from the TO/CC list (try with bare JID too)
180            String from = connection.getUser();
181            if (!to.remove(from) && !cc.remove(from)) {
182                String bareJID = StringUtils.parseBareAddress(from);
183                to.remove(bareJID);
184                cc.remove(bareJID);
185            }
186
187            String serviceAddress = getMultipleRecipienServiceAddress(connection);
188            if (serviceAddress != null) {
189                // Send packet to target users using multiple recipient service provided by the server
190                sendThroughService(connection, reply, to, cc, null, null, null, false,
191                        serviceAddress);
192            }
193            else {
194                // Server does not support XEP-33 so try to send the packet to each recipient
195                sendToIndividualRecipients(connection, reply, to, cc, null);
196            }
197        }
198    }
199
200    /**
201     * Returns the {@link MultipleRecipientInfo} contained in the specified packet or
202     * <tt>null</tt> if none was found. Only packets sent to multiple recipients will
203     * contain such information.
204     *
205     * @param packet the packet to check.
206     * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt>
207     *         if none was found.
208     */
209    public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) {
210        MultipleAddresses extension = (MultipleAddresses) packet
211                .getExtension(MultipleAddresses.ELEMENT, MultipleAddresses.NAMESPACE);
212        return extension == null ? null : new MultipleRecipientInfo(extension);
213    }
214
215    private static void sendToIndividualRecipients(XMPPConnection connection, Packet packet,
216            List<String> to, List<String> cc, List<String> bcc) throws NotConnectedException {
217        if (to != null) {
218            for (Iterator<String> it = to.iterator(); it.hasNext();) {
219                String jid = it.next();
220                packet.setTo(jid);
221                connection.sendPacket(new PacketCopy(packet.toXML()));
222            }
223        }
224        if (cc != null) {
225            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
226                String jid = it.next();
227                packet.setTo(jid);
228                connection.sendPacket(new PacketCopy(packet.toXML()));
229            }
230        }
231        if (bcc != null) {
232            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
233                String jid = it.next();
234                packet.setTo(jid);
235                connection.sendPacket(new PacketCopy(packet.toXML()));
236            }
237        }
238    }
239
240    private static void sendThroughService(XMPPConnection connection, Packet packet, List<String> to,
241            List<String> cc, List<String> bcc, String replyTo, String replyRoom, boolean noReply,
242            String serviceAddress) throws NotConnectedException {
243        // Create multiple recipient extension
244        MultipleAddresses multipleAddresses = new MultipleAddresses();
245        if (to != null) {
246            for (Iterator<String> it = to.iterator(); it.hasNext();) {
247                String jid = it.next();
248                multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null);
249            }
250        }
251        if (cc != null) {
252            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
253                String jid = it.next();
254                multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null);
255            }
256        }
257        if (bcc != null) {
258            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
259                String jid = it.next();
260                multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null);
261            }
262        }
263        if (noReply) {
264            multipleAddresses.setNoReply();
265        }
266        else {
267            if (replyTo != null && replyTo.trim().length() > 0) {
268                multipleAddresses
269                        .addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null);
270            }
271            if (replyRoom != null && replyRoom.trim().length() > 0) {
272                multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, replyRoom, null, null,
273                        false, null);
274            }
275        }
276        // Set the multiple recipient service address as the target address
277        packet.setTo(serviceAddress);
278        // Add extension to packet
279        packet.addExtension(multipleAddresses);
280        // Send the packet
281        connection.sendPacket(packet);
282    }
283
284    /**
285     * Returns the address of the multiple recipients service. To obtain such address service
286     * discovery is going to be used on the connected server and if none was found then another
287     * attempt will be tried on the server items. The discovered information is going to be
288     * cached for 24 hours.
289     *
290     * @param connection the connection to use for disco. The connected server is going to be
291     *                   queried.
292     * @return the address of the multiple recipients service or <tt>null</tt> if none was found.
293     * @throws NoResponseException if there was no response from the server.
294     * @throws XMPPErrorException 
295     * @throws NotConnectedException 
296     */
297    private static String getMultipleRecipienServiceAddress(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException {
298        String serviceName = connection.getServiceName();
299        String serviceAddress = (String) services.get(serviceName);
300        if (serviceAddress == null) {
301            ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
302            // Send the disco packet to the server itself
303            DiscoverInfo info = sdm.discoverInfo(serviceName);
304            // Check if the server supports XEP-33
305            if (info.containsFeature(MultipleAddresses.NAMESPACE)) {
306                serviceAddress = serviceName;
307            }
308            else {
309                // Get the disco items and send the disco packet to each server item
310                DiscoverItems items = sdm.discoverItems(serviceName);
311                for (DiscoverItems.Item item : items.getItems()) {
312                    try {
313                        info = sdm.discoverInfo(item.getEntityID(), item.getNode());
314                    }
315                    catch (XMPPErrorException|NoResponseException e) {
316                        // Don't throw this exceptions if one of the server's items fail
317                        LOGGER.log(Level.WARNING,
318                                        "Exception while discovering info of " + item.getEntityID()
319                                                        + " node: " + item.getNode(), e);
320                        continue;
321                    }
322                    if (info.containsFeature(MultipleAddresses.NAMESPACE)) {
323                        serviceAddress = serviceName;
324                        break;
325                    }
326                }
327            }
328            // Use the empty string to indicate that no service is known for this connection
329            serviceAddress = serviceAddress == null ? "" : serviceAddress;
330            // Cache the discovered information
331            services.put(serviceName, serviceAddress);
332        }
333
334        return "".equals(serviceAddress) ? null : serviceAddress;
335    }
336
337    /**
338     * Packet that holds the XML stanza to send. This class is useful when the same packet
339     * is needed to be sent to different recipients. Since using the same packet is not possible
340     * (i.e. cannot change the TO address of a queues packet to be sent) then this class was
341     * created to keep the XML stanza to send.
342     */
343    private static class PacketCopy extends Packet {
344
345        private CharSequence text;
346
347        /**
348         * Create a copy of a packet with the text to send. The passed text must be a valid text to
349         * send to the server, no validation will be done on the passed text.
350         *
351         * @param text the whole text of the packet to send
352         */
353        public PacketCopy(CharSequence text) {
354            this.text = text;
355        }
356
357        @Override
358        public CharSequence toXML() {
359            return text;
360        }
361
362    }
363
364}