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