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