/* * Tigase Jabber/XMPP Server * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. Look for COPYING file in the top folder. * If not, see http://www.gnu.org/licenses/. * * $Rev$ * Last modified by $Author$ * $Date$ */ package tigase.server; //~--- non-JDK imports -------------------------------------------------------- import tigase.conf.ConfiguratorAbstract; import tigase.disco.ServiceEntity; import tigase.disco.ServiceIdentity; import tigase.disco.XMPPService; import tigase.stats.StatisticsList; import tigase.sys.TigaseRuntime; import tigase.util.UpdatesChecker; import tigase.xml.Element; import tigase.xmpp.Authorization; import tigase.xmpp.JID; import tigase.xmpp.PacketErrorTypeException; import tigase.xmpp.StanzaType; import static tigase.server.MessageRouterConfig.*; //~--- JDK imports ------------------------------------------------------------ import java.lang.management.ManagementFactory; import java.lang.management.MemoryUsage; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayDeque; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.logging.Level; import java.util.logging.Logger; //~--- classes ---------------------------------------------------------------- /** * Class MessageRouter * * * Created: Tue Nov 22 07:07:11 2005 * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class MessageRouter extends AbstractMessageReceiver implements MessageRouterIfc { // implements XMPPService { // public static final String INFO_XMLNS = // "http://jabber.org/protocol/disco#info"; // public static final String ITEMS_XMLNS = // "http://jabber.org/protocol/disco#items"; private static final Logger log = Logger.getLogger(MessageRouter.class.getName()); // ~--- fields --------------------------------------------------------------- private ConfiguratorAbstract config = null; // private static final long startupTime = System.currentTimeMillis(); // private Set<String> localAddresses = new CopyOnWriteArraySet<String>(); private String disco_name = DISCO_NAME_PROP_VAL; private boolean disco_show_version = DISCO_SHOW_VERSION_PROP_VAL; private ServiceEntity serviceEntity = null; private UpdatesChecker updates_checker = null; private Map<String, XMPPService> xmppServices = new ConcurrentHashMap<String, XMPPService>(); private Map<String, ComponentRegistrator> registrators = new ConcurrentHashMap<String, ComponentRegistrator>(); private Map<String, MessageReceiver> receivers = new ConcurrentHashMap<String, MessageReceiver>(); private boolean inProperties = false; private Map<JID, ServerComponent> components_byId = new ConcurrentHashMap<JID, ServerComponent>(); private Map<String, ServerComponent> components = new ConcurrentHashMap<String, ServerComponent>(); private Set<String> connectionManagerNames = new ConcurrentSkipListSet<String>(); // ~--- methods -------------------------------------------------------------- /** * Method description * * * @param component */ public void addComponent(ServerComponent component) { log.info("Adding component: " + component.getClass().getSimpleName()); for (ComponentRegistrator registr : registrators.values()) { if (registr != component) { if (log.isLoggable(Level.FINER)) { log.finer("Adding: " + component.getName() + " component to " + registr.getName() + " registrator."); } registr.addComponent(component); } // end of if (reg != component) } // end of for () components.put(component.getName(), component); components_byId.put(component.getComponentId(), component); if (component instanceof XMPPService) { xmppServices.put(component.getName(), (XMPPService) component); } } /** * Method description * * * @param registr */ public void addRegistrator(ComponentRegistrator registr) { log.log(Level.INFO, "Adding registrator: {0}", registr.getClass().getSimpleName()); registrators.put(registr.getName(), registr); addComponent(registr); for (ServerComponent comp : components.values()) { // if (comp != registr) { registr.addComponent(comp); // } // end of if (comp != registr) } // end of for (ServerComponent comp : components) } /** * Method description * * * @param receiver */ public void addRouter(MessageReceiver receiver) { log.info("Adding receiver: " + receiver.getClass().getSimpleName()); addComponent(receiver); receivers.put(receiver.getName(), receiver); } // ~--- get methods ---------------------------------------------------------- /** * Method description * * * @param params * * @return */ @Override public Map<String, Object> getDefaults(Map<String, Object> params) { Map<String, Object> defs = super.getDefaults(params); MessageRouterConfig.getDefaults(defs, params, getName()); return defs; } /** * Method description * * * @param node * @param jid * @param from * * @return */ @Override public Element getDiscoInfo(String node, JID jid, JID from) { Element query = serviceEntity.getDiscoInfo(null); if (log.isLoggable(Level.FINEST)) { log.finest("Returing disco-info: " + query.toString()); } return query; } // public List<Element> getDiscoItems(String node, String jid) { // return null; // } /** * Method description * * * @param list */ @Override public void getStatistics(StatisticsList list) { super.getStatistics(list); list.add(getName(), "Local hostname", getDefHostName().getDomain(), Level.INFO); TigaseRuntime runtime = TigaseRuntime.getTigaseRuntime(); list.add(getName(), "Uptime", runtime.getUptimeString(), Level.INFO); NumberFormat format = NumberFormat.getNumberInstance(); format.setMaximumFractionDigits(4); list.add(getName(), "Load average", format.format(runtime.getLoadAverage()), Level.FINE); list.add(getName(), "CPUs no", runtime.getCPUsNumber(), Level.FINEST); list.add(getName(), "Threads count", runtime.getThreadsNumber(), Level.FINEST); float cpuUsage = runtime.getCPUUsage(); float heapUsage = runtime.getHeapMemUsage(); float nonHeapUsage = runtime.getNonHeapMemUsage(); list.add(getName(), "CPU usage [%]", cpuUsage, Level.FINE); list.add(getName(), "HEAP usage [%]", heapUsage, Level.FINE); list.add(getName(), "NONHEAP usage [%]", nonHeapUsage, Level.FINE); format = NumberFormat.getNumberInstance(); format.setMaximumFractionDigits(1); // if (format instanceof DecimalFormat) { // DecimalFormat decf = (DecimalFormat)format; // decf.applyPattern(decf.toPattern()+"%"); // } list.add(getName(), "CPU usage", format.format(cpuUsage) + "%", Level.INFO); MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); MemoryUsage nonHeap = ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage(); format = NumberFormat.getIntegerInstance(); if (format instanceof DecimalFormat) { DecimalFormat decf = (DecimalFormat) format; decf.applyPattern(decf.toPattern() + " KB"); } list.add(getName(), "Max Heap mem", format.format(heap.getMax() / 1024), Level.INFO); list.add(getName(), "Used Heap", format.format(heap.getUsed() / 1024), Level.INFO); list.add(getName(), "Free Heap", format.format((heap.getMax() - heap.getUsed()) / 1024), Level.FINE); list.add(getName(), "Max NonHeap mem", format.format(nonHeap.getMax() / 1024), Level.FINE); list.add(getName(), "Used NonHeap", format.format(nonHeap.getUsed() / 1024), Level.FINE); list.add(getName(), "Free NonHeap", format.format((nonHeap.getMax() - nonHeap.getUsed()) / 1024), Level.FINE); } // ~--- methods -------------------------------------------------------------- // private String isToLocalComponent(String jid) { // String nick = JIDUtils.getNodeNick(jid); // if (nick == null) { // return null; // } // String host = JIDUtils.getNodeHost(jid); // if (isLocalDomain(host) && components.get(nick) != null) { // return nick; // } // return null; // } // private boolean isLocalDomain(String domain) { // return localAddresses.contains(domain); // } /** * Method description * * * @param packet */ @Override public void processPacket(Packet packet) { // We do not process packets with not destination address // Just dropping them and writing a log message if (packet.getTo() == null) { log.warning("Packet with TO attribute set to NULL: " + packet); return; } // end of if (packet.getTo() == null) // Intentionally comparing to static, final String // This is kind of a hack to handle packets addressed specifically for the // SessionManager // TODO: Replace the hack with a proper solution // if (packet.getTo() == NULL_ROUTING) { // log.info("NULL routing, it is normal if server doesn't know how to" // + " process packet: " + packet.toStringSecure()); // // try { // Packet error = // Authorization.FEATURE_NOT_IMPLEMENTED.getResponseMessage(packet, // "Feature not supported yet.", true); // // addOutPacketNB(error); // } catch (PacketErrorTypeException e) { // log.warning("Packet processing exception: " + e); // } // // return; // } // if (log.isLoggable(Level.FINER)) { // log.finer("Processing packet: " + packet.getElemName() // + ", type: " + packet.getType()); // } if (log.isLoggable(Level.FINEST)) { log.finest("Processing packet: " + packet); } // Detect inifinite loop if from == to // Maybe it is not needed anymore... // There is a need to process packets with the same from and to address // let't try to relax restriction and block all packets with error type // 2008-06-16 if (((packet.getType() == StanzaType.error) && (packet.getFrom() != null) && packet .getFrom().equals(packet.getTo()))) { if (log.isLoggable(Level.FINEST)) { log.finest("Possible infinite loop, dropping packet: " + packet); } return; } // Catch and process all service discovery packets here.... ServerComponent comp = (packet.getStanzaTo() == null) ? null : getLocalComponent(packet.getStanzaTo()); if (packet.isServiceDisco() && (packet.getType() == StanzaType.get) && (packet.getStanzaFrom() != null) && (((comp != null) && !(comp instanceof DisableDisco)) || isLocalDomain(packet .getStanzaTo().toString()))) { Queue<Packet> results = new ArrayDeque<Packet>(); processDiscoQuery(packet, results); if (results.size() > 0) { for (Packet res : results) { // No more recurrential calls!! addOutPacketNB(res); } // end of for () } return; } // It it is not a service discovery packet, we have to find a component to // process // the packet. The below block of code is to "quickly" find a component if // the // the packet is addressed to the component ID where the component ID is // either // of one below: // 1. component name + "@" + default domain name // 2. component name + "@" + any virtual host name // 3. component name + "." + default domain name // 4. component name + "." + any virtual host name // TODO: check the efficiency for packets addressed to c2s component comp = getLocalComponent(packet.getTo()); if (comp != null) { if (log.isLoggable(Level.FINEST)) { log.finest("1. Packet will be processed by: " + comp.getComponentId() + ", " + packet); } Queue<Packet> results = new ArrayDeque<Packet>(); if (comp == this) { // This is addressed to the MessageRouter itself. Has to be processed // separately to avoid recurential calls by the packet processing // method. processPacketMR(packet, results); } else { // All other components process the packet the same way. comp.processPacket(packet, results); } if (results.size() > 0) { for (Packet res : results) { // No more recurrential calls!! addOutPacketNB(res); // processPacket(res); } // end of for () } // If the component is found the processing ends here as there can be // only one component with specific ID. return; } // This packet is not processed yet // The packet can be addressed to just a domain, one of the virtual hosts // The code below finds all components which handle packets addressed // to a virtual domains (implement VHostListener and return 'true' from // handlesLocalDomains() method call) String host = packet.getTo().getDomain(); ServerComponent[] comps = getComponentsForLocalDomain(host); if (comps == null) { // Still no component found, now the most expensive lookup. // Checking regex routings provided by the component. comps = getServerComponentsForRegex(packet.getTo().getBareJID().toString()); } if ((comps == null) && !isLocalDomain(host)) { // None of the component want to process the packet. // If the packet is addressed to non-local domain then it is processed by // all components dealing with external world, like s2s comps = getComponentsForNonLocalDomain(host); } // Ok, if any component has been found then process the packet in a standard // way if (comps != null) { // Processing packet and handling results out Queue<Packet> results = new ArrayDeque<Packet>(); for (ServerComponent serverComponent : comps) { if (log.isLoggable(Level.FINEST)) { log.finest("2. Packet will be processed by: " + serverComponent.getComponentId() + ", " + packet); } serverComponent.processPacket(packet, results); if (results.size() > 0) { for (Packet res : results) { // No more recurrential calls!! addOutPacketNB(res); // processPacket(res); } // end of for () } } } else { // No components for the packet, sending an error back if (log.isLoggable(Level.FINEST)) { log.finest("There is no component for the packet, sending it back"); } try { addOutPacketNB(Authorization.SERVICE_UNAVAILABLE.getResponseMessage(packet, "There is no service found to process your request.", true)); } catch (PacketErrorTypeException e) { // This packet is to local domain, we don't want to send it out // drop packet :-( log.warning("Can't process packet to local domain, dropping..." + packet.toStringSecure()); } } } /** * Method description * * * @param packet * @param results */ public void processPacketMR(Packet packet, Queue<Packet> results) { Iq iq = null; if (packet instanceof Iq) { iq = (Iq) packet; } else { // Not a command for sure... log.warning("I expect command (Iq) packet here, instead I got: " + packet.toString()); return; } if (packet.getPermissions() != Permissions.ADMIN) { try { Packet res = Authorization.NOT_AUTHORIZED.getResponseMessage(packet, "You are not authorized for this action.", true); results.offer(res); // processPacket(res); } catch (PacketErrorTypeException e) { log.warning("Packet processing exception: " + e); } return; } if (log.isLoggable(Level.FINEST)) { log.finest("Command received: " + iq.toString()); } switch (iq.getCommand()) { case OTHER: if (iq.getStrCommand() != null) { if (iq.getStrCommand().startsWith("controll/")) { String[] spl = iq.getStrCommand().split("/"); String cmd = spl[1]; if (cmd.equals("stop")) { Packet result = iq.commandResult(Command.DataType.result); results.offer(result); // processPacket(result); new Timer("Stopping...", true).schedule(new TimerTask() { @Override public void run() { System.exit(0); } }, 2000); } } } break; default: break; } } /** * Method description * * * @return */ @Override public int processingInThreads() { return Runtime.getRuntime().availableProcessors() * 4; } /** * Method description * * * @return */ @Override public int processingOutThreads() { return 1; } // ~--- set methods ---------------------------------------------------------- /** * Method description * * * @param config */ @Override public void setConfig(ConfiguratorAbstract config) { components.put(getName(), this); this.config = config; addRegistrator(config); } /** * Method description * * * @param props */ @Override public void setProperties(Map<String, Object> props) { if (inProperties) { return; } else { inProperties = true; } // end of if (inProperties) else connectionManagerNames.add("c2s"); connectionManagerNames.add("bosh"); connectionManagerNames.add("s2s"); if (props.get(DISCO_SHOW_VERSION_PROP_KEY) != null) { disco_show_version = (Boolean) props.get(DISCO_SHOW_VERSION_PROP_KEY); } if (props.get(DISCO_NAME_PROP_KEY) != null) { disco_name = (String) props.get(DISCO_NAME_PROP_KEY); serviceEntity = new ServiceEntity("Tigase", "server", "Session manager"); serviceEntity.addIdentities(new ServiceIdentity[] { new ServiceIdentity("server", "im", disco_name + (disco_show_version ? (" ver. " + tigase.server.XMPPServer .getImplementationVersion()) : "")) }); serviceEntity.addFeatures(XMPPService.DEF_FEATURES); } try { super.setProperties(props); if (props.size() == 1) { // If props.size() == 1, it means this is a single property update and // MR does // not support it yet. return; } MessageRouterConfig conf = new MessageRouterConfig(props); String[] reg_names = conf.getRegistrNames(); for (String name : reg_names) { // First remove it and later, add it again if still active ComponentRegistrator cr = registrators.remove(name); String cls_name = (String) props.get(REGISTRATOR_PROP_KEY + name + ".class"); try { if ((cr == null) || !cr.getClass().getName().equals(cls_name)) { if (cr != null) { cr.release(); } cr = conf.getRegistrInstance(name); cr.setName(name); } // end of if (cr == null) addRegistrator(cr); } catch (Exception e) { e.printStackTrace(); } // end of try-catch } // end of for (String name: reg_names) String[] msgrcv_names = conf.getMsgRcvNames(); for (String name : msgrcv_names) { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Loading and registering message receiver: {0}", name); } // First remove it and later, add it again if still active ServerComponent mr = receivers.remove(name); String cls_name = (String) props.get(MSG_RECEIVERS_PROP_KEY + name + ".class"); try { if ((mr == null) || !mr.getClass().getName().equals(cls_name)) { if (mr != null) { mr.release(); } mr = conf.getMsgRcvInstance(name); mr.setName(name); if (mr instanceof MessageReceiver) { ((MessageReceiver) mr).setParent(this); ((MessageReceiver) mr).start(); } } // end of if (cr == null) if (mr instanceof MessageReceiver) { addRouter((MessageReceiver) mr); } else { addComponent(mr); } } // end of try catch (Exception e) { e.printStackTrace(); } // end of try-catch } // end of for (String name: reg_names) // for (MessageReceiver mr : tmp_rec.values()) { // mr.release(); // } // end of for () // // tmp_rec.clear(); if ((Boolean) props.get(UPDATES_CHECKING_PROP_KEY)) { installUpdatesChecker((Long) props.get(UPDATES_CHECKING_INTERVAL_PROP_KEY)); } else { log.log(Level.INFO, "Disabling updates checker."); stopUpdatesChecker(); } } finally { inProperties = false; } // end of try-finally for (ServerComponent comp : components.values()) { log.log(Level.INFO, "Initialization completed notification to: {0}", comp.getName()); comp.initializationCompleted(); } // log.info("Initialization completed notification to: " + // config.getName()); // config.initializationCompleted(); } // ~--- get methods ---------------------------------------------------------- @Override protected Integer getMaxQueueSize(int def) { return def * 10; } private ServerComponent[] getComponentsForLocalDomain(String domain) { return vHostManager.getComponentsForLocalDomain(domain); } private ServerComponent[] getComponentsForNonLocalDomain(String domain) { return vHostManager.getComponentsForNonLocalDomain(domain); } private ServerComponent getLocalComponent(JID jid) { // Fast lookup in the server components to find a candidate // by the component ID (JID). If the packet is addressed directly // to the component ID then this is where the processing must happen. // Normally the component id is: component name + "@" + default hostname // However the component may "choose" to have any ID. ServerComponent comp = components_byId.get(jid); if (comp != null) { return comp; } // Note, component ID consists of the component name + default hostname // which can be different from a virtual host. There might be many // virtual hosts and the packet can be addressed to the component by // the component name + virtual host name // Code below, tries to find a destination by the component name + any // active virtual hostname. if (jid.getLocalpart() != null) { comp = components.get(jid.getLocalpart()); if ((comp != null) && (isLocalDomain(jid.getDomain()) || jid.getDomain().equals( getDefHostName().getDomain()))) { return comp; } } // Instead of a component ID built of: component name + "@" domain name // Some components have an ID of: component name + "." domain name // Code below tries to find a packet receiver if the address have the other // type of form. int idx = jid.getDomain().indexOf('.'); if (idx > 0) { String cmpName = jid.getDomain().substring(0, idx); String basename = jid.getDomain().substring(idx + 1); comp = components.get(cmpName); if ((comp != null) && (isLocalDomain(basename) || basename.equals(getDefHostName().getDomain()))) { return comp; } } return null; } @Override public int hashCodeForPacket(Packet packet) { // This is actually quite tricky part. We want to both avoid // packet reordering and also even packets distribution among // different threads. // If packet comes from a connection manager we must use packetFrom // address. However if the packet comes from SM, PubSub or other similar // component all packets would end-up in the same queue. // So, kind of a workaround here.... // TODO: develop a proper solution discovering which components are // connection managers and use their names here instead of static names. if (packet.getPacketFrom() != null && packet.getPacketFrom().getLocalpart() != null) { if (connectionManagerNames.contains(packet.getPacketFrom().getLocalpart())) { return packet.getPacketFrom().hashCode(); } } if (packet.getPacketTo() != null && packet.getPacketTo().getLocalpart() != null) { if (connectionManagerNames.contains(packet.getPacketTo().getLocalpart())) { return packet.getPacketTo().hashCode(); } } if (packet.getStanzaTo() != null) { return packet.getStanzaTo().getBareJID().hashCode(); } if ((packet.getPacketFrom() != null) && !getComponentId().equals(packet.getPacketFrom())) { // This comes from connection manager so the best way is to get hashcode // by the connectionId, which is in the getFrom() return packet.getPacketFrom().hashCode(); } if (packet.getPacketTo() != null && !getComponentId().equals(packet.getPacketTo())) { return packet.getPacketTo().hashCode(); } // If not, then a better way is to get hashCode from the elemTo address // as this would be by the destination address user name: return 1; } private ServerComponent[] getServerComponentsForRegex(String id) { LinkedHashSet<ServerComponent> comps = new LinkedHashSet<ServerComponent>(); for (MessageReceiver mr : receivers.values()) { if (log.isLoggable(Level.FINEST)) { log.finest("Checking routings for: " + mr.getName()); } if (mr.isInRegexRoutings(id)) { comps.add(mr); } } if (comps.size() > 0) { return comps.toArray(new ServerComponent[comps.size()]); } else { return null; } } // ~--- methods -------------------------------------------------------------- private void installUpdatesChecker(long interval) { stopUpdatesChecker(); updates_checker = new UpdatesChecker(interval, this, "This is automated message generated by updates checking module.\n" + " You can disable this function changing configuration option: " + "'/" + getName() + "/" + UPDATES_CHECKING_PROP_KEY + "' or adjust" + " updates checking interval time changing option: " + "'/" + getName() + "/" + UPDATES_CHECKING_INTERVAL_PROP_KEY + "' which" + " now set to " + interval + " days."); updates_checker.start(); } private void processDiscoQuery(final Packet packet, final Queue<Packet> results) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Processing disco query by: {0}", packet.toStringSecure()); } JID toJid = packet.getStanzaTo(); JID fromJid = packet.getStanzaFrom(); String node = packet.getAttribute("/iq/query", "node"); Element query = packet.getElement().getChild("query").clone(); if (packet.isXMLNS("/iq/query", INFO_XMLNS)) { if (isLocalDomain(toJid.toString()) && (node == null)) { query = getDiscoInfo(node, toJid, fromJid); for (XMPPService comp : xmppServices.values()) { // Buggy custom component may throw exceptions here (NPE most likely) // which may cause service disco problems for well-behaving components // too // So this is kind of a protection try { List<Element> features = comp.getDiscoFeatures(fromJid); if (features != null) { query.addChildren(features); } } catch (Exception e) { log.log(Level.WARNING, "Component service disco problem: " + comp.getName(), e); } } } else { for (XMPPService comp : xmppServices.values()) { // Buggy custom component may throw exceptions here (NPE most likely) // which may cause service disco problems for well-behaving components // too // So this is kind of a protection try { // if (jid.startsWith(comp.getName() + ".")) { Element resp = comp.getDiscoInfo(node, toJid, fromJid); if (resp != null) { query.addChildren(resp.getChildren()); } } catch (Exception e) { log.log(Level.WARNING, "Component service disco problem: " + comp.getName(), e); } // } } } } if (packet.isXMLNS("/iq/query", ITEMS_XMLNS)) { boolean localDomain = isLocalDomain(toJid.toString()); if (localDomain) { for (XMPPService comp : xmppServices.values()) { // Buggy custom component may throw exceptions here (NPE most likely) // which may cause service disco problems for well-behaving components // too // So this is kind of a protection try { // if (localDomain || (nick != null && comp.getName().equals(nick))) // { List<Element> items = comp.getDiscoItems(node, toJid, fromJid); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Localdomain: {0}, DiscoItems processed by: {1}, items: {2}", new Object[] { toJid, comp.getComponentId(), (items == null) ? null : items.toString() }); } if ((items != null) && (items.size() > 0)) { query.addChildren(items); } } catch (Exception e) { log.log(Level.WARNING, "Component service disco problem: " + comp.getName(), e); } } // end of for () } else { ServerComponent comp = getLocalComponent(toJid); if ((comp != null) && (comp instanceof XMPPService)) { List<Element> items = ((XMPPService) comp).getDiscoItems(node, toJid, fromJid); if (log.isLoggable(Level.FINEST)) { log.log( Level.FINEST, "DiscoItems processed by: {0}, items: {1}", new Object[] { comp.getComponentId(), (items == null) ? null : items.toString() }); } if ((items != null) && (items.size() > 0)) { query.addChildren(items); } } } } results.offer(packet.okResult(query, 0)); } private void stopUpdatesChecker() { if (updates_checker != null) { updates_checker.interrupt(); updates_checker = null; } } } // ~ Formatted in Sun Code Convention // ~ Formatted by Jindent --- http://www.jindent.com