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