Home | History | Annotate | Download | only in smackx
      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 
     21 package org.jivesoftware.smackx;
     22 
     23 import org.jivesoftware.smack.Connection;
     24 import org.jivesoftware.smack.XMPPException;
     25 import org.jivesoftware.smack.packet.Message;
     26 import org.jivesoftware.smack.packet.Packet;
     27 import org.jivesoftware.smack.util.Cache;
     28 import org.jivesoftware.smack.util.StringUtils;
     29 import org.jivesoftware.smackx.packet.DiscoverInfo;
     30 import org.jivesoftware.smackx.packet.DiscoverItems;
     31 import org.jivesoftware.smackx.packet.MultipleAddresses;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Iterator;
     35 import 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  */
     44 public 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