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