/** * * Copyright 2003-2006 Jive Software. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.address; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.jivesoftware.smack.SmackException.FeatureNotSupportedException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaBuilder; import org.jivesoftware.smack.packet.StanzaFactory; import org.jivesoftware.smack.packet.StanzaView; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.address.packet.MultipleAddresses; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.Jid; /** * A MultipleRecipientManager allows to send packets to multiple recipients by making use of * XEP-33: Extended Stanza Addressing. * It also allows to send replies to packets that were sent to multiple recipients. * * @author Gaston Dombiak */ public class MultipleRecipientManager { /** * Sends the specified stanza to the collection of specified recipients using the * specified connection. If the server has support for XEP-33 then only one * stanza is going to be sent to the server with the multiple recipient instructions. * However, if XEP-33 is not supported by the server then the client is going to send * the stanza to each recipient. * * @param connection the connection to use to send the packet. * @param packet the stanza to send to the list of recipients. * @param to the collection of JIDs to include in the TO list or null if no TO * list exists. * @param cc the collection of JIDs to include in the CC list or null if no CC * list exists. * @param bcc the collection of JIDs to include in the BCC list or null if no BCC * list exists. * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the * server does not support them. * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and * some XEP-33 specific features were requested. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public static void send(XMPPConnection connection, Stanza packet, Collection to, Collection cc, Collection bcc) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException, InterruptedException { send(connection, packet, to, cc, bcc, null, null, false); } /** * Sends the specified stanza to the collection of specified recipients using the specified * connection. If the server has support for XEP-33 then only one stanza is going to be sent to * the server with the multiple recipient instructions. However, if XEP-33 is not supported by * the server then the client is going to send the stanza to each recipient. * * @param connection the connection to use to send the packet. * @param packet the stanza to send to the list of recipients. * @param to the collection of JIDs to include in the TO list or null if no TO list exists. * @param cc the collection of JIDs to include in the CC list or null if no CC list exists. * @param bcc the collection of JIDs to include in the BCC list or null if no BCC list * exists. * @param replyTo address to which all replies are requested to be sent or null * indicating that they can reply to any address. * @param replyRoom JID of a MUC room to which responses should be sent or null * indicating that they can reply to any address. * @param noReply true means that receivers should not reply to the message. * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and * some XEP-33 specific features were requested. * @throws NoResponseException if there was no response from the server. * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the * server does not support them. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public static void send(XMPPConnection connection, Stanza packet, Collection to, Collection cc, Collection bcc, Jid replyTo, Jid replyRoom, boolean noReply) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException, InterruptedException { // Check if *only* 'to' is set and contains just *one* entry, in this case extended stanzas addressing is not // required at all and we can send it just as normal stanza without needing to add the extension element if (to != null && to.size() == 1 && (cc == null || cc.isEmpty()) && (bcc == null || bcc.isEmpty()) && !noReply && StringUtils.isNullOrEmpty(replyTo) && StringUtils.isNullOrEmpty(replyRoom)) { Jid toJid = to.iterator().next(); packet.setTo(toJid); connection.sendStanza(packet); return; } DomainBareJid serviceAddress = getMultipleRecipientServiceAddress(connection); if (serviceAddress != null) { // Send packet to target users using multiple recipient service provided by the server sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply, serviceAddress); } else { // Server does not support XEP-33 so try to send the packet to each recipient if (noReply || replyTo != null || replyRoom != null) { // Some specified XEP-33 features were requested so throw an exception alerting // the user that this features are not available throw new FeatureNotSupportedException("Extended Stanza Addressing"); } // Send the packet to each individual recipient sendToIndividualRecipients(connection, packet, to, cc, bcc); } } /** * Sends a reply to a previously received stanza that was sent to multiple recipients. Before * attempting to send the reply message some checks are performed. If any of those checks * fails, then an XMPPException is going to be thrown with the specific error detail. * * @param connection the connection to use to send the reply. * @param original the previously received stanza that was sent to multiple recipients. * @param reply the new message to send as a reply. * @throws XMPPErrorException if there was an XMPP error returned. * @throws InterruptedException if the calling thread was interrupted. * @throws NotConnectedException if the XMPP connection is not connected. * @throws FeatureNotSupportedException if a requested feature is not supported by the remote entity. * @throws NoResponseException if there was no response from the remote entity. */ public static void reply(XMPPConnection connection, Message original, Message reply) throws XMPPErrorException, InterruptedException, NotConnectedException, NoResponseException, FeatureNotSupportedException { MultipleRecipientInfo info = getMultipleRecipientInfo(original); if (info == null) { throw new IllegalArgumentException("Original message does not contain multiple recipient info"); } if (info.shouldNotReply()) { throw new IllegalArgumentException("Original message should not be replied"); } if (info.getReplyRoom() != null) { throw new IllegalArgumentException("Reply should be sent through a room"); } // Any element from the initial message MUST be copied into the reply. if (original.getThread() != null) { reply.asBuilder().setThread(original.getThread()).build(); } MultipleAddresses.Address replyAddress = info.getReplyAddress(); if (replyAddress != null && replyAddress.getJid() != null) { // Send reply to the reply_to address reply.setTo(replyAddress.getJid()); connection.sendStanza(reply); } else { // Send reply to multiple recipients List to = new ArrayList<>(info.getTOAddresses().size()); List cc = new ArrayList<>(info.getCCAddresses().size()); for (MultipleAddresses.Address jid : info.getTOAddresses()) { to.add(jid.getJid()); } for (MultipleAddresses.Address jid : info.getCCAddresses()) { cc.add(jid.getJid()); } // Add original sender as a 'to' address (if not already present) if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) { to.add(original.getFrom()); } // Remove the sender from the TO/CC list (try with bare JID too) EntityFullJid from = connection.getUser(); if (!to.remove(from) && !cc.remove(from)) { EntityBareJid bareJID = from.asEntityBareJid(); to.remove(bareJID); cc.remove(bareJID); } send(connection, reply, to, cc, null, null, null, false); } } /** * Returns the {@link MultipleRecipientInfo} contained in the specified stanza or * null if none was found. Only packets sent to multiple recipients will * contain such information. * * @param packet the stanza to check. * @return the MultipleRecipientInfo contained in the specified stanza or null * if none was found. */ public static MultipleRecipientInfo getMultipleRecipientInfo(Stanza packet) { MultipleAddresses extension = packet.getExtension(MultipleAddresses.class); return extension == null ? null : new MultipleRecipientInfo(extension); } private static void sendToIndividualRecipients(XMPPConnection connection, StanzaView stanza, Collection to, Collection cc, Collection bcc) throws NotConnectedException, InterruptedException { final StanzaFactory stanzaFactory = connection.getStanzaFactory(); final StanzaBuilder stanzaBuilder; if (stanza instanceof Message) { Message message = (Message) stanza; stanzaBuilder = stanzaFactory.buildMessageStanzaFrom(message); } else if (stanza instanceof Presence) { Presence presence = (Presence) stanza; stanzaBuilder = stanzaFactory.buildPresenceStanzaFrom(presence); } else if (stanza instanceof IQ) { throw new IllegalArgumentException("IQ stanzas have no supported fallback in case no XEP-0033 service is available"); } else { throw new AssertionError(); } if (to == null) to = Collections.emptyList(); if (cc == null) cc = Collections.emptyList(); if (bcc == null) bcc = Collections.emptyList(); final int numRecipients = to.size() + cc.size() + bcc.size(); final List recipients = new ArrayList<>(numRecipients); recipients.addAll(to); recipients.addAll(cc); recipients.addAll(bcc); final List stanzasToSend = new ArrayList<>(numRecipients); for (Jid recipient : recipients) { Stanza stanzaToSend = stanzaBuilder.to(recipient).build(); stanzasToSend.add(stanzaToSend); } // TODO: Use XMPPConnection.sendStanzas(Collection) once this method exists. for (Stanza stanzaToSend : stanzasToSend) { connection.sendStanza(stanzaToSend); } } private static void sendThroughService(XMPPConnection connection, Stanza packet, Collection to, Collection cc, Collection bcc, Jid replyTo, Jid replyRoom, boolean noReply, DomainBareJid serviceAddress) throws NotConnectedException, InterruptedException { // Create multiple recipient extension MultipleAddresses multipleAddresses = new MultipleAddresses(); if (to != null) { for (Jid jid : to) { multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null); } } if (cc != null) { for (Jid jid : cc) { multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null); } } if (bcc != null) { for (Jid jid : bcc) { multipleAddresses.addAddress(MultipleAddresses.Type.bcc, jid, null, null, false, null); } } if (noReply) { multipleAddresses.setNoReply(); } else { if (replyTo != null) { multipleAddresses .addAddress(MultipleAddresses.Type.replyto, replyTo, null, null, false, null); } if (replyRoom != null) { multipleAddresses.addAddress(MultipleAddresses.Type.replyroom, replyRoom, null, null, false, null); } } // Set the multiple recipient service address as the target address packet.setTo(serviceAddress); // Add extension to packet packet.addExtension(multipleAddresses); // Send the packet connection.sendStanza(packet); } /** * Returns the address of the multiple recipients service. To obtain such address service * discovery is going to be used on the connected server and if none was found then another * attempt will be tried on the server items. The discovered information is going to be * cached for 24 hours. * * @param connection the connection to use for disco. The connected server is going to be * queried. * @return the address of the multiple recipients service or null if none was found. * @throws NoResponseException if there was no response from the server. * @throws XMPPErrorException if there was an XMPP error returned. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ private static DomainBareJid getMultipleRecipientServiceAddress(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); return sdm.findService(MultipleAddresses.NAMESPACE, true); } }