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