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