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