blob: 83afaceb8639b026549ffcfd0274e630d0787d06 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
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
21package org.jivesoftware.smackx;
22
23import org.jivesoftware.smack.Connection;
24import org.jivesoftware.smack.XMPPException;
25import org.jivesoftware.smack.packet.Message;
26import org.jivesoftware.smack.packet.Packet;
27import org.jivesoftware.smack.util.Cache;
28import org.jivesoftware.smack.util.StringUtils;
29import org.jivesoftware.smackx.packet.DiscoverInfo;
30import org.jivesoftware.smackx.packet.DiscoverItems;
31import org.jivesoftware.smackx.packet.MultipleAddresses;
32
33import java.util.ArrayList;
34import java.util.Iterator;
35import 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 */
44public 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}