/** * $RCSfile$ * $Revision: $ * $Date: $ * * Copyright 2003-2006 Jive Software. * * All rights reserved. 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; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.util.Cache; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverItems; import org.jivesoftware.smackx.packet.MultipleAddresses; /** * A MultipleRecipientManager allows to send packets to multiple recipients by * making use of <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33: * Extended Stanza Addressing</a>. It also allows to send replies to packets * that were sent to multiple recipients. * * @author Gaston Dombiak */ public class MultipleRecipientManager { /** * Packet that holds the XML stanza to send. This class is useful when the * same packet is needed to be sent to different recipients. Since using the * same packet is not possible (i.e. cannot change the TO address of a * queues packet to be sent) then this class was created to keep the XML * stanza to send. */ private static class PacketCopy extends Packet { private final String text; /** * Create a copy of a packet with the text to send. The passed text must * be a valid text to send to the server, no validation will be done on * the passed text. * * @param text * the whole text of the packet to send */ public PacketCopy(String text) { this.text = text; } @Override public String toXML() { return text; } } /** * Create a cache to hold the 100 most recently accessed elements for a * period of 24 hours. */ private static Cache<String, String> services = new Cache<String, String>( 100, 24 * 60 * 60 * 1000); /** * 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 <tt>null</tt> * if none was found. */ private static String getMultipleRecipienServiceAddress( Connection connection) { final String serviceName = connection.getServiceName(); String serviceAddress = services.get(serviceName); if (serviceAddress == null) { synchronized (services) { serviceAddress = services.get(serviceName); if (serviceAddress == null) { // Send the disco packet to the server itself try { DiscoverInfo info = ServiceDiscoveryManager .getInstanceFor(connection).discoverInfo( serviceName); // Check if the server supports JEP-33 if (info.containsFeature("http://jabber.org/protocol/address")) { serviceAddress = serviceName; } else { // Get the disco items and send the disco packet to // each server item final DiscoverItems items = ServiceDiscoveryManager .getInstanceFor(connection).discoverItems( serviceName); for (final Iterator<?> it = items.getItems(); it .hasNext();) { final DiscoverItems.Item item = (DiscoverItems.Item) it .next(); info = ServiceDiscoveryManager.getInstanceFor( connection).discoverInfo( item.getEntityID(), item.getNode()); if (info.containsFeature("http://jabber.org/protocol/address")) { serviceAddress = serviceName; break; } } } // Cache the discovered information services.put(serviceName, serviceAddress == null ? "" : serviceAddress); } catch (final XMPPException e) { e.printStackTrace(); } } } } return "".equals(serviceAddress) ? null : serviceAddress; } /** * Returns the {@link MultipleRecipientInfo} contained in the specified * packet or <tt>null</tt> if none was found. Only packets sent to multiple * recipients will contain such information. * * @param packet * the packet to check. * @return the MultipleRecipientInfo contained in the specified packet or * <tt>null</tt> if none was found. */ public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) { final MultipleAddresses extension = (MultipleAddresses) packet .getExtension("addresses", "http://jabber.org/protocol/address"); return extension == null ? null : new MultipleRecipientInfo(extension); } /** * Sends a reply to a previously received packet that was sent to multiple * recipients. Before attempting to send the reply message some checkings * are performed. If any of those checkings fail 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 packet that was sent to multiple * recipients. * @param reply * the new message to send as a reply. * @throws XMPPException * if the original message was not sent to multiple recipients, * or the original message cannot be replied or reply should be * sent to a room. */ public static void reply(Connection connection, Message original, Message reply) throws XMPPException { final MultipleRecipientInfo info = getMultipleRecipientInfo(original); if (info == null) { throw new XMPPException( "Original message does not contain multiple recipient info"); } if (info.shouldNotReply()) { throw new XMPPException("Original message should not be replied"); } if (info.getReplyRoom() != null) { throw new XMPPException("Reply should be sent through a room"); } // Any <thread/> element from the initial message MUST be copied into // the reply. if (original.getThread() != null) { reply.setThread(original.getThread()); } final MultipleAddresses.Address replyAddress = info.getReplyAddress(); if (replyAddress != null && replyAddress.getJid() != null) { // Send reply to the reply_to address reply.setTo(replyAddress.getJid()); connection.sendPacket(reply); } else { // Send reply to multiple recipients final List<String> to = new ArrayList<String>(); final List<String> cc = new ArrayList<String>(); for (final Object name : info.getTOAddresses()) { final String jid = ((MultipleAddresses.Address) name).getJid(); to.add(jid); } for (final Object name : info.getCCAddresses()) { final String jid = ((MultipleAddresses.Address) name).getJid(); cc.add(jid); } // 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) final String from = connection.getUser(); if (!to.remove(from) && !cc.remove(from)) { final String bareJID = StringUtils.parseBareAddress(from); to.remove(bareJID); cc.remove(bareJID); } final String serviceAddress = getMultipleRecipienServiceAddress(connection); if (serviceAddress != null) { // Send packet to target users using multiple recipient service // provided by the server sendThroughService(connection, reply, to, cc, null, null, null, false, serviceAddress); } else { // Server does not support JEP-33 so try to send the packet to // each recipient sendToIndividualRecipients(connection, reply, to, cc, null); } } } /** * Sends the specified packet to the list of specified recipients using the * specified connection. If the server has support for JEP-33 then only one * packet is going to be sent to the server with the multiple recipient * instructions. However, if JEP-33 is not supported by the server then the * client is going to send the packet to each recipient. * * @param connection * the connection to use to send the packet. * @param packet * the packet to send to the list of recipients. * @param to * the list of JIDs to include in the TO list or <tt>null</tt> if * no TO list exists. * @param cc * the list of JIDs to include in the CC list or <tt>null</tt> if * no CC list exists. * @param bcc * the list of JIDs to include in the BCC list or <tt>null</tt> * if no BCC list exists. * @throws XMPPException * if server does not support JEP-33: Extended Stanza Addressing * and some JEP-33 specific features were requested. */ public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<?> bcc) throws XMPPException { send(connection, packet, to, cc, bcc, null, null, false); } /** * Sends the specified packet to the list of specified recipients using the * specified connection. If the server has support for JEP-33 then only one * packet is going to be sent to the server with the multiple recipient * instructions. However, if JEP-33 is not supported by the server then the * client is going to send the packet to each recipient. * * @param connection * the connection to use to send the packet. * @param packet * the packet to send to the list of recipients. * @param to * the list of JIDs to include in the TO list or <tt>null</tt> if * no TO list exists. * @param cc * the list of JIDs to include in the CC list or <tt>null</tt> if * no CC list exists. * @param bcc * the list of JIDs to include in the BCC list or <tt>null</tt> * if no BCC list exists. * @param replyTo * address to which all replies are requested to be sent or * <tt>null</tt> indicating that they can reply to any address. * @param replyRoom * JID of a MUC room to which responses should be sent or * <tt>null</tt> indicating that they can reply to any address. * @param noReply * true means that receivers should not reply to the message. * @throws XMPPException * if server does not support JEP-33: Extended Stanza Addressing * and some JEP-33 specific features were requested. */ public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<?> bcc, String replyTo, String replyRoom, boolean noReply) throws XMPPException { final String serviceAddress = getMultipleRecipienServiceAddress(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 JEP-33 so try to send the packet to each // recipient if (noReply || (replyTo != null && replyTo.trim().length() > 0) || (replyRoom != null && replyRoom.trim().length() > 0)) { // Some specified JEP-33 features were requested so throw an // exception alerting // the user that this features are not available throw new XMPPException( "Extended Stanza Addressing not supported by server"); } // Send the packet to each individual recipient sendToIndividualRecipients(connection, packet, to, cc, bcc); } } private static void sendThroughService(Connection connection, Packet packet, List<String> to, List<String> cc, List<?> bcc, String replyTo, String replyRoom, boolean noReply, String serviceAddress) { // Create multiple recipient extension final MultipleAddresses multipleAddresses = new MultipleAddresses(); if (to != null) { for (final String jid : to) { multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null); } } if (cc != null) { for (final String jid : cc) { multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null); } } if (bcc != null) { for (final Object name : bcc) { final String jid = (String) name; multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null); } } if (noReply) { multipleAddresses.setNoReply(); } else { if (replyTo != null && replyTo.trim().length() > 0) { multipleAddresses.addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null); } if (replyRoom != null && replyRoom.trim().length() > 0) { multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, 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.sendPacket(packet); } private static void sendToIndividualRecipients(Connection connection, Packet packet, List<String> to, List<String> cc, List<?> bcc) { if (to != null) { for (final String jid : to) { packet.setTo(jid); connection.sendPacket(new PacketCopy(packet.toXML())); } } if (cc != null) { for (final String jid : cc) { packet.setTo(jid); connection.sendPacket(new PacketCopy(packet.toXML())); } } if (bcc != null) { for (final Object name : bcc) { final String jid = (String) name; packet.setTo(jid); connection.sendPacket(new PacketCopy(packet.toXML())); } } } }