MultipleRecipientManager.java

  1. /**
  2.  *
  3.  * Copyright 2003-2006 Jive Software.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.jivesoftware.smackx.address;

  18. import org.jivesoftware.smack.SmackException;
  19. import org.jivesoftware.smack.SmackException.NoResponseException;
  20. import org.jivesoftware.smack.SmackException.FeatureNotSupportedException;
  21. import org.jivesoftware.smack.SmackException.NotConnectedException;
  22. import org.jivesoftware.smack.XMPPConnection;
  23. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  24. import org.jivesoftware.smack.packet.Message;
  25. import org.jivesoftware.smack.packet.Stanza;
  26. import org.jivesoftware.smack.util.StringUtils;
  27. import org.jivesoftware.smackx.address.packet.MultipleAddresses;
  28. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  29. import org.jxmpp.jid.BareJid;
  30. import org.jxmpp.jid.DomainBareJid;
  31. import org.jxmpp.jid.FullJid;
  32. import org.jxmpp.jid.Jid;

  33. import java.util.ArrayList;
  34. import java.util.Collection;
  35. import java.util.List;

  36. /**
  37.  * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
  38.  * <a href="http://www.xmpp.org/extensions/jep-0033.html">XEP-33: Extended Stanza Addressing</a>.
  39.  * It also allows to send replies to packets that were sent to multiple recipients.
  40.  *
  41.  * @author Gaston Dombiak
  42.  */
  43. public class MultipleRecipientManager {

  44.     /**
  45.      * Sends the specified packet to the collection of specified recipients using the
  46.      * specified connection. If the server has support for XEP-33 then only one
  47.      * packet is going to be sent to the server with the multiple recipient instructions.
  48.      * However, if XEP-33 is not supported by the server then the client is going to send
  49.      * the packet to each recipient.
  50.      *
  51.      * @param connection the connection to use to send the packet.
  52.      * @param packet     the packet to send to the list of recipients.
  53.      * @param to         the collection of JIDs to include in the TO list or <tt>null</tt> if no TO
  54.      *                   list exists.
  55.      * @param cc         the collection of JIDs to include in the CC list or <tt>null</tt> if no CC
  56.      *                   list exists.
  57.      * @param bcc        the collection of JIDs to include in the BCC list or <tt>null</tt> if no BCC
  58.      *                   list exists.
  59.      * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
  60.      *         server does not support them.
  61.      * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
  62.      *                       some XEP-33 specific features were requested.
  63.      * @throws NoResponseException if there was no response from the server.
  64.      * @throws NotConnectedException
  65.      * @throws InterruptedException
  66.      */
  67.     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
  68.    {
  69.         send(connection, packet, to, cc, bcc, null, null, false);
  70.     }

  71.     /**
  72.      * Sends the specified packet to the collection of specified recipients using the specified
  73.      * connection. If the server has support for XEP-33 then only one packet is going to be sent to
  74.      * the server with the multiple recipient instructions. However, if XEP-33 is not supported by
  75.      * the server then the client is going to send the packet to each recipient.
  76.      *
  77.      * @param connection the connection to use to send the packet.
  78.      * @param packet the packet to send to the list of recipients.
  79.      * @param to the collection of JIDs to include in the TO list or <tt>null</tt> if no TO list exists.
  80.      * @param cc the collection of JIDs to include in the CC list or <tt>null</tt> if no CC list exists.
  81.      * @param bcc the collection of JIDs to include in the BCC list or <tt>null</tt> if no BCC list
  82.      *        exists.
  83.      * @param replyTo address to which all replies are requested to be sent or <tt>null</tt>
  84.      *        indicating that they can reply to any address.
  85.      * @param replyRoom JID of a MUC room to which responses should be sent or <tt>null</tt>
  86.      *        indicating that they can reply to any address.
  87.      * @param noReply true means that receivers should not reply to the message.
  88.      * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
  89.      *         some XEP-33 specific features were requested.
  90.      * @throws NoResponseException if there was no response from the server.
  91.      * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
  92.      *         server does not support them.
  93.      * @throws NotConnectedException
  94.      * @throws InterruptedException
  95.      */
  96.     public static void send(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to, Collection<? extends Jid> cc, Collection<? extends Jid> bcc,
  97.             Jid replyTo, Jid replyRoom, boolean noReply) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException, InterruptedException {
  98.         // Check if *only* 'to' is set and contains just *one* entry, in this case extended stanzas addressing is not
  99.         // required at all and we can send it just as normal stanza without needing to add the extension element
  100.         if (to != null && to.size() == 1 && (cc == null || cc.isEmpty()) && (bcc == null || bcc.isEmpty()) && !noReply
  101.                         && StringUtils.isNullOrEmpty(replyTo) && StringUtils.isNullOrEmpty(replyRoom)) {
  102.             Jid toJid = to.iterator().next();
  103.             packet.setTo(toJid);
  104.             connection.sendStanza(packet);
  105.             return;
  106.         }
  107.         DomainBareJid serviceAddress = getMultipleRecipienServiceAddress(connection);
  108.         if (serviceAddress != null) {
  109.             // Send packet to target users using multiple recipient service provided by the server
  110.             sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
  111.                     serviceAddress);
  112.         }
  113.         else {
  114.             // Server does not support XEP-33 so try to send the packet to each recipient
  115.             if (noReply || replyTo != null ||
  116.                     replyRoom != null) {
  117.                 // Some specified XEP-33 features were requested so throw an exception alerting
  118.                 // the user that this features are not available
  119.                 throw new FeatureNotSupportedException("Extended Stanza Addressing");
  120.             }
  121.             // Send the packet to each individual recipient
  122.             sendToIndividualRecipients(connection, packet, to, cc, bcc);
  123.         }
  124.     }

  125.     /**
  126.      * Sends a reply to a previously received packet that was sent to multiple recipients. Before
  127.      * attempting to send the reply message some checkings are performed. If any of those checkings
  128.      * fail then an XMPPException is going to be thrown with the specific error detail.
  129.      *
  130.      * @param connection the connection to use to send the reply.
  131.      * @param original   the previously received packet that was sent to multiple recipients.
  132.      * @param reply      the new message to send as a reply.
  133.      * @throws SmackException
  134.      * @throws XMPPErrorException
  135.      * @throws InterruptedException
  136.      */
  137.     public static void reply(XMPPConnection connection, Message original, Message reply) throws SmackException, XMPPErrorException, InterruptedException
  138.          {
  139.         MultipleRecipientInfo info = getMultipleRecipientInfo(original);
  140.         if (info == null) {
  141.             throw new SmackException("Original message does not contain multiple recipient info");
  142.         }
  143.         if (info.shouldNotReply()) {
  144.             throw new SmackException("Original message should not be replied");
  145.         }
  146.         if (info.getReplyRoom() != null) {
  147.             throw new SmackException("Reply should be sent through a room");
  148.         }
  149.         // Any <thread/> element from the initial message MUST be copied into the reply.
  150.         if (original.getThread() != null) {
  151.             reply.setThread(original.getThread());
  152.         }
  153.         MultipleAddresses.Address replyAddress = info.getReplyAddress();
  154.         if (replyAddress != null && replyAddress.getJid() != null) {
  155.             // Send reply to the reply_to address
  156.             reply.setTo(replyAddress.getJid());
  157.             connection.sendStanza(reply);
  158.         }
  159.         else {
  160.             // Send reply to multiple recipients
  161.             List<Jid> to = new ArrayList<>(info.getTOAddresses().size());
  162.             List<Jid> cc = new ArrayList<>(info.getCCAddresses().size());
  163.             for (MultipleAddresses.Address jid : info.getTOAddresses()) {
  164.                 to.add(jid.getJid());
  165.             }
  166.             for (MultipleAddresses.Address jid : info.getCCAddresses()) {
  167.                 cc.add(jid.getJid());
  168.             }
  169.             // Add original sender as a 'to' address (if not already present)
  170.             if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
  171.                 to.add(original.getFrom());
  172.             }
  173.             // Remove the sender from the TO/CC list (try with bare JID too)
  174.             FullJid from = connection.getUser();
  175.             if (!to.remove(from) && !cc.remove(from)) {
  176.                 BareJid bareJID = from.asBareJid();
  177.                 to.remove(bareJID);
  178.                 cc.remove(bareJID);
  179.             }

  180.             send(connection, reply, to, cc, null, null, null, false);
  181.         }
  182.     }

  183.     /**
  184.      * Returns the {@link MultipleRecipientInfo} contained in the specified packet or
  185.      * <tt>null</tt> if none was found. Only packets sent to multiple recipients will
  186.      * contain such information.
  187.      *
  188.      * @param packet the packet to check.
  189.      * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt>
  190.      *         if none was found.
  191.      */
  192.     public static MultipleRecipientInfo getMultipleRecipientInfo(Stanza packet) {
  193.         MultipleAddresses extension = (MultipleAddresses) packet
  194.                 .getExtension(MultipleAddresses.ELEMENT, MultipleAddresses.NAMESPACE);
  195.         return extension == null ? null : new MultipleRecipientInfo(extension);
  196.     }

  197.     private static void sendToIndividualRecipients(XMPPConnection connection, Stanza packet,
  198.             Collection<? extends Jid> to, Collection<? extends Jid> cc, Collection<? extends Jid> bcc) throws NotConnectedException, InterruptedException {
  199.         if (to != null) {
  200.             for (Jid jid : to) {
  201.                 packet.setTo(jid);
  202.                 connection.sendStanza(new PacketCopy(packet.toXML()));
  203.             }
  204.         }
  205.         if (cc != null) {
  206.             for (Jid jid : cc) {
  207.                 packet.setTo(jid);
  208.                 connection.sendStanza(new PacketCopy(packet.toXML()));
  209.             }
  210.         }
  211.         if (bcc != null) {
  212.             for (Jid jid : bcc) {
  213.                 packet.setTo(jid);
  214.                 connection.sendStanza(new PacketCopy(packet.toXML()));
  215.             }
  216.         }
  217.     }

  218.     private static void sendThroughService(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to,
  219.             Collection<? extends Jid> cc, Collection<? extends Jid> bcc, Jid replyTo, Jid replyRoom, boolean noReply,
  220.             DomainBareJid serviceAddress) throws NotConnectedException, InterruptedException {
  221.         // Create multiple recipient extension
  222.         MultipleAddresses multipleAddresses = new MultipleAddresses();
  223.         if (to != null) {
  224.             for (Jid jid : to) {
  225.                 multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null);
  226.             }
  227.         }
  228.         if (cc != null) {
  229.             for (Jid jid : cc) {
  230.                 multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null);
  231.             }
  232.         }
  233.         if (bcc != null) {
  234.             for (Jid jid : bcc) {
  235.                 multipleAddresses.addAddress(MultipleAddresses.Type.bcc, jid, null, null, false, null);
  236.             }
  237.         }
  238.         if (noReply) {
  239.             multipleAddresses.setNoReply();
  240.         }
  241.         else {
  242.             if (replyTo != null) {
  243.                 multipleAddresses
  244.                         .addAddress(MultipleAddresses.Type.replyto, replyTo, null, null, false, null);
  245.             }
  246.             if (replyRoom != null) {
  247.                 multipleAddresses.addAddress(MultipleAddresses.Type.replyroom, replyRoom, null, null,
  248.                         false, null);
  249.             }
  250.         }
  251.         // Set the multiple recipient service address as the target address
  252.         packet.setTo(serviceAddress);
  253.         // Add extension to packet
  254.         packet.addExtension(multipleAddresses);
  255.         // Send the packet
  256.         connection.sendStanza(packet);
  257.     }

  258.     /**
  259.      * Returns the address of the multiple recipients service. To obtain such address service
  260.      * discovery is going to be used on the connected server and if none was found then another
  261.      * attempt will be tried on the server items. The discovered information is going to be
  262.      * cached for 24 hours.
  263.      *
  264.      * @param connection the connection to use for disco. The connected server is going to be
  265.      *                   queried.
  266.      * @return the address of the multiple recipients service or <tt>null</tt> if none was found.
  267.      * @throws NoResponseException if there was no response from the server.
  268.      * @throws XMPPErrorException
  269.      * @throws NotConnectedException
  270.      * @throws InterruptedException
  271.      */
  272.     private static DomainBareJid getMultipleRecipienServiceAddress(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  273.         ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
  274.         List<DomainBareJid> services = sdm.findServices(MultipleAddresses.NAMESPACE, true, true);
  275.         if (services.size() > 0) {
  276.             return services.get(0);
  277.         }
  278.         return null;
  279.     }

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

  287.         private CharSequence text;

  288.         /**
  289.          * Create a copy of a packet with the text to send. The passed text must be a valid text to
  290.          * send to the server, no validation will be done on the passed text.
  291.          *
  292.          * @param text the whole text of the packet to send
  293.          */
  294.         public PacketCopy(CharSequence text) {
  295.             this.text = text;
  296.         }

  297.         @Override
  298.         public CharSequence toXML() {
  299.             return text;
  300.         }

  301.     }

  302. }