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}