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