/**
 *
 * Copyright 2009 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.smack;

import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.log4j.Logger;
import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream;
import org.jivesoftware.smack.compression.JzlibInputOutputStream;
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.filter.PacketFilter;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;

/**
 * The abstract Connection class provides an interface for connections to a XMPP
 * server and implements shared methods which are used by the different types of
 * connections (e.g. XMPPConnection or BoshConnection).
 *
 * To create a connection to a XMPP server a simple usage of this API might look
 * like the following:
 *
 *

 * // Create a connection to the igniterealtime.org XMPP server.
 * Connection con = new XMPPConnection("igniterealtime.org");
 * // Connect to the server
 * con.connect();
 * // Most servers require you to login before performing other tasks.
 * con.login("jsmith", "mypass");
 * // Start a new conversation with John Doe and send him a message.
 * Chat chat = connection.getChatManager().createChat("jdoe@igniterealtime.org", new MessageListener() {
 *

 *     public void processMessage(Chat chat, Message message) {
 *         // Print out any messages we get back to standard out.
 *         System.out.println("Received message: " + message);
 *     }
 * });
 * chat.sendMessage("Howdy!");
 * // Disconnect from the server
 * con.disconnect();
 *

 *

 * Connections can be reused between connections. This means that an Connection
 * may be connected, disconnected and then connected again. Listeners of the
 * Connection will be retained accross connections.
 *

 *

 * If a connected Connection gets disconnected abruptly then it will try to
 * reconnect again. To stop the reconnection process, use {@link #disconnect()}.
 * Once stopped you can use {@link #connect()} to manually connect to the
 * server.
 *
 * @see XMPPConnection
 * @author Matt Tucker
 * @author Guenther Niess
 */
public abstract class Connection {
    private static Logger log = Logger.getLogger(Connection.class.getName());

    /**
     * Counter to uniquely identify connections that are created.
     */
    private final static AtomicInteger connectionCounter = new AtomicInteger(0);

    /**
     * A set of listeners which will be invoked if a new connection is created.
     */
    private final static Set<ConnectionCreationListener> connectionEstablishedListeners = new CopyOnWriteArraySet<ConnectionCreationListener>();

    protected final static List<XMPPInputOutputStream> compressionHandlers = new ArrayList<XMPPInputOutputStream>(
            2);

    /**
     * Value that indicates whether debugging is enabled. When enabled, a debug
     * window will apear for each new connection that will contain the following
     * information:
     *

     *
Client Traffic -- raw XML traffic generated by Smack and sent to the
     * server.
     *
Server Traffic -- raw XML traffic sent by the server to the client.
     *
Interpreted Packets -- shows XML packets from the server as parsed by
     * Smack.
     *

     *

     * Debugging can be enabled by setting this field to true, or by setting the
     * Java system property <tt>smack.debugEnabled</tt> to true. The system
     * property can be set on the command line such as
     * "java SomeApp -Dsmack.debugEnabled=true".
     */
    public static boolean DEBUG_ENABLED = false;

    static {
        // Use try block since we may not have permission to get a system
        // property (for example, when an applet).
        try {
            DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled");
        } catch (Exception e) {
            // Ignore.
        }
        // Ensure the SmackConfiguration class is loaded by calling a method in
        // it.
        SmackConfiguration.getVersion();
        // Add the Java7 compression handler first, since it's preferred
        compressionHandlers.add(new Java7ZlibInputOutputStream());
        // If we don't have access to the Java7 API use the JZlib compression
        // handler
        compressionHandlers.add(new JzlibInputOutputStream());
    }

    /**
     * A collection of ConnectionListeners which listen for connection closing
     * and reconnection events.
     */
    protected final Collection<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<ConnectionListener>();

    /**
     * A collection of PacketCollectors which collects packets for a specified
     * filter and perform blocking and polling operations on the result queue.
     */
    protected final Collection<PacketCollector> collectors = new ConcurrentLinkedQueue<PacketCollector>();

    /**
     * List of PacketListeners that will be notified when a new packet was
     * received.
     */
    protected final Map<PacketListener, ListenerWrapper> recvListeners = new ConcurrentHashMap<PacketListener, ListenerWrapper>();

    /**
     * List of PacketListeners that will be notified when a new packet was sent.
     */
    protected final Map<PacketListener, ListenerWrapper> sendListeners = new ConcurrentHashMap<PacketListener, ListenerWrapper>();

    /**
     * List of PacketInterceptors that will be notified when a new packet is
     * about to be sent to the server. These interceptors may modify the packet
     * before it is being actually sent to the server.
     */
    protected final Map<PacketInterceptor, InterceptorWrapper> interceptors = new ConcurrentHashMap<PacketInterceptor, InterceptorWrapper>();

    /**
     * The ChatManager keeps track of references to all current chats.
     */
    private ChatManager chatManager = null;

    /**
     * The SmackDebugger allows to log and debug XML traffic.
     */
    protected SmackDebugger debugger = null;

    /**
     * The Reader which is used for the debugger.
     */
    protected Reader reader;

    /**
     * The Writer which is used for the debugger.
     */
    protected Writer writer;

    /**
     * The SASLAuthentication manager that is responsible for authenticating
     * with the server.
     */
    protected SASLAuthentication saslAuthentication = new SASLAuthentication(
            this);

    /**
     * A number to uniquely identify connections that are created. This is
     * distinct from the connection ID, which is a value sent by the server once
     * a connection is made.
     */
    protected final int connectionCounterValue = connectionCounter
            .getAndIncrement();

    /**
     * Holds the initial configuration used while creating the connection.
     */
    protected final ConnectionConfiguration config;

    /**
     * Holds the Caps Node information for the used XMPP service (i.e. the XMPP
     * server)
     */
    private String serviceCapsNode;

    /**
     * Stores whether the server supports rosterVersioning
     */
    private boolean rosterVersioningSupported = false;

    protected XMPPInputOutputStream compressionHandler;

    /**
     * Create a new Connection to a XMPP server.
     *
     * @param configuration
     *            The configuration which is used to establish the connection.
     */
    protected Connection(ConnectionConfiguration configuration) {
        config = configuration;
    }

    /**
     * Returns the configuration used to connect to the server.
     *
     * @return the configuration used to connect to the server.
     */
    protected ConnectionConfiguration getConfiguration() {
        return config;
    }

    /**
     * Returns the name of the service provided by the XMPP server for this
     * connection. This is also called XMPP domain of the connected server.
     * After authenticating with the server the returned value may be different.
     *
     * @return the name of the service provided by the XMPP server.
     */
    public String getServiceName() {
        return config.getServiceName();
    }

    /**
     * Returns the host name of the server where the XMPP server is running.
     * This would be the IP address of the server or a name that may be resolved
     * by a DNS server.
     *
     * @return the host name of the server where the XMPP server is running.
     */
    public String getHost() {
        return config.getHost();
    }

    /**
     * Returns the port number of the XMPP server for this connection. The
     * default port for normal connections is 5222. The default port for SSL
     * connections is 5223.
     *
     * @return the port number of the XMPP server.
     */
    public int getPort() {
        return config.getPort();
    }

    /**
     * Returns the full XMPP address of the user that is logged in to the
     * connection or <tt>null</tt> if not logged in yet. An XMPP address is in
     * the form username@server/resource.
     *
     * @return the full XMPP address of the user logged in.
     */
    public abstract String getUser();

    /**
     * Returns the connection ID for this connection, which is the value set by
     * the server when opening a XMPP stream. If the server does not set a
     * connection ID, this value will be null. This value will be <tt>null</tt>
     * if not connected to the server.
     *
     * @return the ID of this connection returned from the XMPP server or
     *         <tt>null</tt> if not connected to the server.
     */
    public abstract String getConnectionID();

    /**
     * Returns true if currently connected to the XMPP server.
     *
     * @return true if connected.
     */
    public abstract boolean isConnected();

    /**
     * Returns true if currently authenticated by successfully calling the login
     * method.
     *
     * @return true if authenticated.
     */
    public abstract boolean isAuthenticated();

    /**
     * Returns true if currently authenticated anonymously.
     *
     * @return true if authenticated anonymously.
     */
    public abstract boolean isAnonymous();

    /**
     * Returns true if the connection to the server has successfully negotiated
     * encryption.
     *
     * @return true if a secure connection to the server.
     */
    public abstract boolean isSecureConnection();

    /**
     * Returns if the reconnection mechanism is allowed to be used. By default
     * reconnection is allowed.
     *
     * @return true if the reconnection mechanism is allowed to be used.
     */
    protected boolean isReconnectionAllowed() {
        return config.isReconnectionAllowed();
    }

    /**
     * Returns true if network traffic is being compressed. When using stream
     * compression network traffic can be reduced up to 90%. Therefore, stream
     * compression is ideal when using a slow speed network connection. However,
     * the server will need to use more CPU time in order to un/compress network
     * data so under high load the server performance might be affected.
     *
     * @return true if network traffic is being compressed.
     */
    public abstract boolean isUsingCompression();

    /**
     * Establishes a connection to the XMPP server and performs an automatic
     * login only if the previous connection state was logged (authenticated).
     * It basically creates and maintains a connection to the server.
     *

     *

     * Listeners will be preserved from a previous connection if the
     * reconnection occurs after an abrupt termination.
     *
     * @throws XMPPException
     *             if an error occurs while trying to establish the connection.
     */
    public abstract void connect() throws XMPPException;

    /**
     * Logs in to the server using the strongest authentication mode supported
     * by the server, then sets presence to available. If the server supports
     * SASL authentication then the user will be authenticated using SASL if not
     * Non-SASL authentication will be tried. If more than five seconds (default
     * timeout) elapses in each step of the authentication process without a
     * response from the server, or if an error occurs, a XMPPException will be
     * thrown.
     *

     *
     * Before logging in (i.e. authenticate) to the server the connection must
     * be connected.
     *
     * It is possible to log in without sending an initial available presence by
     * using {@link ConnectionConfiguration#setSendPresence(boolean)}. If this
     * connection is not interested in loading its roster upon login then use
     * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}. Finally,
     * if you want to not pass a password and instead use a more advanced
     * mechanism while using SASL then you may be interested in using
     * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}
     * . For more advanced login settings see {@link ConnectionConfiguration}.
     *
     * @param username
     *            the username.
     * @param password
     *            the password or <tt>null</tt> if using a CallbackHandler.
     * @throws XMPPException
     *             if an error occurs.
     */
    public void login(String username, String password) throws XMPPException {
        login(username, password, "Smack");
    }

    /**
     * Logs in to the server using the strongest authentication mode supported
     * by the server, then sets presence to available. If the server supports
     * SASL authentication then the user will be authenticated using SASL if not
     * Non-SASL authentication will be tried. If more than five seconds (default
     * timeout) elapses in each step of the authentication process without a
     * response from the server, or if an error occurs, a XMPPException will be
     * thrown.
     *

     *
     * Before logging in (i.e. authenticate) to the server the connection must
     * be connected.
     *
     * It is possible to log in without sending an initial available presence by
     * using {@link ConnectionConfiguration#setSendPresence(boolean)}. If this
     * connection is not interested in loading its roster upon login then use
     * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}. Finally,
     * if you want to not pass a password and instead use a more advanced
     * mechanism while using SASL then you may be interested in using
     * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}
     * . For more advanced login settings see {@link ConnectionConfiguration}.
     *
     * @param username
     *            the username.
     * @param password
     *            the password or <tt>null</tt> if using a CallbackHandler.
     * @param resource
     *            the resource.
     * @throws XMPPException
     *             if an error occurs.
     * @throws IllegalStateException
     *             if not connected to the server, or already logged in to the
     *             serrver.
     */
    public abstract void login(String username, String password, String resource)
            throws XMPPException;

    /**
     * Logs in to the server anonymously. Very few servers are configured to
     * support anonymous authentication, so it's fairly likely logging in
     * anonymously will fail. If anonymous login does succeed, your XMPP address
     * will likely be in the form "123ABC@server/789XYZ" or "server/123ABC"
     * (where "123ABC" and "789XYZ" is a random value generated by the server).
     *
     * @throws XMPPException
     *             if an error occurs or anonymous logins are not supported by
     *             the server.
     * @throws IllegalStateException
     *             if not connected to the server, or already logged in to the
     *             serrver.
     */
    public abstract void loginAnonymously() throws XMPPException;

    /**
     * Sends the specified packet to the server.
     *
     * @param packet
     *            the packet to send.
     */
    public abstract void sendPacket(Packet packet);

    /**
     * Returns a chat manager instance for this connection. The ChatManager
     * manages all incoming and outgoing chats on the current connection.
     *
     * @return a chat manager instance for this connection.
     */
    public synchronized ChatManager getChatManager() {
        if (this.chatManager == null) {
            this.chatManager = new ChatManager(this);
        }
        return this.chatManager;
    }

    /**
     * Returns the roster for the user.
     *

     * This method will never return null, instead if the user has
     * not yet logged into the server or is logged in anonymously all modifying
     * methods of the returned roster object like
     * {@link UserRoster#createEntry(String, String, String[])},
     * {@link UserRoster#removeEntry(RosterEntry)} , etc. except adding or
     * removing {@link RosterListener}s will throw an IllegalStateException.
     *
     * @return the user's roster.
     */
    public abstract UserRoster getRoster();

    /**
     * Returns the SASLAuthentication manager that is responsible for
     * authenticating with the server.
     *
     * @return the SASLAuthentication manager that is responsible for
     *         authenticating with the server.
     */
    public SASLAuthentication getSASLAuthentication() {
        return saslAuthentication;
    }

    /**
     * Closes the connection by setting presence to unavailable then closing the
     * connection to the XMPP server. The Connection can still be used for
     * connecting to the server again.
     *

     *

     * This method cleans up all resources used by the connection. Therefore,
     * the roster, listeners and other stateful objects cannot be re-used by
     * simply calling connect() on this connection again. This is unlike the
     * behavior during unexpected disconnects (and subsequent connections). In
     * that case, all state is preserved to allow for more seamless error
     * recovery.
     */
    public void disconnect() {
        disconnect(new Presence(Presence.Type.unavailable));
    }

    /**
     * Closes the connection. A custom unavailable presence is sent to the
     * server, followed by closing the stream. The Connection can still be used
     * for connecting to the server again. A custom unavilable presence is
     * useful for communicating offline presence information such as
     * "On vacation". Typically, just the status text of the presence packet is
     * set with online information, but most XMPP servers will deliver the full
     * presence packet with whatever data is set.
     *

     *

     * This method cleans up all resources used by the connection. Therefore,
     * the roster, listeners and other stateful objects cannot be re-used by
     * simply calling connect() on this connection again. This is unlike the
     * behavior during unexpected disconnects (and subsequent connections). In
     * that case, all state is preserved to allow for more seamless error
     * recovery.
     *
     * @param unavailablePresence
     *            the presence packet to send during shutdown.
     */
    public abstract void disconnect(Presence unavailablePresence);

    /**
     * Adds a new listener that will be notified when new Connections are
     * created. Note that newly created connections will not be actually
     * connected to the server.
     *
     * @param connectionCreationListener
     *            a listener interested on new connections.
     */
    public static void addConnectionCreationListener(
            ConnectionCreationListener connectionCreationListener) {
        connectionEstablishedListeners.add(connectionCreationListener);
    }

    /**
     * Removes a listener that was interested in connection creation events.
     *
     * @param connectionCreationListener
     *            a listener interested on new connections.
     */
    public static void removeConnectionCreationListener(
            ConnectionCreationListener connectionCreationListener) {
        connectionEstablishedListeners.remove(connectionCreationListener);
    }

    /**
     * Get the collection of listeners that are interested in connection
     * creation events.
     *
     * @return a collection of listeners interested on new connections.
     */
    protected static Collection<ConnectionCreationListener> getConnectionCreationListeners() {
        return Collections
                .unmodifiableCollection(connectionEstablishedListeners);
    }

    /**
     * Adds a connection listener to this connection that will be notified when
     * the connection closes or fails.
     *
     * @param connectionListener
     *            a connection listener.
     */
    public void addConnectionListener(ConnectionListener connectionListener) {
        if (connectionListener == null) {
            return;
        }
        if (!connectionListeners.contains(connectionListener)) {
            connectionListeners.add(connectionListener);
        }
    }

    /**
     * Removes a connection listener from this connection.
     *
     * @param connectionListener
     *            a connection listener.
     */
    public void removeConnectionListener(ConnectionListener connectionListener) {
        connectionListeners.remove(connectionListener);
    }

    /**
     * Get the collection of listeners that are interested in connection events.
     *
     * @return a collection of listeners interested on connection events.
     */
    protected Collection<ConnectionListener> getConnectionListeners() {
        return connectionListeners;
    }

    /**
     * Creates a new packet collector for this connection. A packet filter
     * determines which packets will be accumulated by the collector. A
     * PacketCollector is more suitable to use than a {@link PacketListener}
     * when you need to wait for a specific result.
     *
     * @param packetFilter
     *            the packet filter to use.
     * @return a new packet collector.
     */
    public PacketCollector createPacketCollector(PacketFilter packetFilter) {
        PacketCollector collector = new PacketCollector(this, packetFilter);
        // Add the collector to the list of active collectors.
        collectors.add(collector);
        return collector;
    }

    /**
     * Remove a packet collector of this connection.
     *
     * @param collector
     *            a packet collectors which was created for this connection.
     */
    protected void removePacketCollector(PacketCollector collector) {
        collectors.remove(collector);
    }

    /**
     * Get the collection of all packet collectors for this connection.
     *
     * @return a collection of packet collectors for this connection.
     */
    protected Collection<PacketCollector> getPacketCollectors() {
        return collectors;
    }

    /**
     * Registers a packet listener with this connection. A packet listener will
     * be invoked only when an incoming packet is received. A packet filter
     * determines which packets will be delivered to the listener. If the same
     * packet listener is added again with a different filter, only the new
     * filter will be used.
     *
     *

     * NOTE: If you want get a similar callback for outgoing packets, see
     * {@link #addPacketInterceptor(PacketInterceptor, PacketFilter)}.
     *
     * @param packetListener
     *            the packet listener to notify of new received packets.
     * @param packetFilter
     *            the packet filter to use.
     */
    public void addPacketListener(PacketListener packetListener,
            PacketFilter packetFilter) {
        if (packetListener == null) {
            throw new NullPointerException("Packet listener is null.");
        }
        ListenerWrapper wrapper = new ListenerWrapper(packetListener,
                packetFilter);
        recvListeners.put(packetListener, wrapper);
    }

    /**
     * Removes a packet listener for received packets from this connection.
     *
     * @param packetListener
     *            the packet listener to remove.
     */
    public void removePacketListener(PacketListener packetListener) {
        recvListeners.remove(packetListener);
    }

    /**
     * Get a map of all packet listeners for received packets of this
     * connection.
     *
     * @return a map of all packet listeners for received packets.
     */
    protected Map<PacketListener, ListenerWrapper> getPacketListeners() {
        return recvListeners;
    }

    /**
     * Registers a packet listener with this connection. The listener will be
     * notified of every packet that this connection sends. A packet filter
     * determines which packets will be delivered to the listener. Note that the
     * thread that writes packets will be used to invoke the listeners.
     * Therefore, each packet listener should complete all operations quickly or
     * use a different thread for processing.
     *
     * @param packetListener
     *            the packet listener to notify of sent packets.
     * @param packetFilter
     *            the packet filter to use.
     */
    public void addPacketSendingListener(PacketListener packetListener,
            PacketFilter packetFilter) {
        if (packetListener == null) {
            throw new NullPointerException("Packet listener is null.");
        }
        ListenerWrapper wrapper = new ListenerWrapper(packetListener,
                packetFilter);
        sendListeners.put(packetListener, wrapper);
    }

    /**
     * Removes a packet listener for sending packets from this connection.
     *
     * @param packetListener
     *            the packet listener to remove.
     */
    public void removePacketSendingListener(PacketListener packetListener) {
        sendListeners.remove(packetListener);
    }

    /**
     * Get a map of all packet listeners for sending packets of this connection.
     *
     * @return a map of all packet listeners for sent packets.
     */
    protected Map<PacketListener, ListenerWrapper> getPacketSendingListeners() {
        return sendListeners;
    }

    /**
     * Process all packet listeners for sending packets.
     *
     * @param packet
     *            the packet to process.
     */
    protected void firePacketSendingListeners(Packet packet) {
        // Notify the listeners of the new sent packet
        for (ListenerWrapper listenerWrapper : sendListeners.values()) {
            listenerWrapper.notifyListener(packet);
        }
    }

    /**
     * Registers a packet interceptor with this connection. The interceptor will
     * be invoked every time a packet is about to be sent by this connection.
     * Interceptors may modify the packet to be sent. A packet filter determines
     * which packets will be delivered to the interceptor.
     *
     *

     * NOTE: For a similar functionality on incoming packets, see
     * {@link #addPacketListener(PacketListener, PacketFilter)}.
     *
     * @param packetInterceptor
     *            the packet interceptor to notify of packets about to be sent.
     * @param packetFilter
     *            the packet filter to use.
     */
    public void addPacketInterceptor(PacketInterceptor packetInterceptor,
            PacketFilter packetFilter) {
        if (packetInterceptor == null) {
            throw new NullPointerException("Packet interceptor is null.");
        }
        interceptors.put(packetInterceptor, new InterceptorWrapper(
                packetInterceptor, packetFilter));
    }

    /**
     * Removes a packet interceptor.
     *
     * @param packetInterceptor
     *            the packet interceptor to remove.
     */
    public void removePacketInterceptor(PacketInterceptor packetInterceptor) {
        interceptors.remove(packetInterceptor);
    }

    /**
     * Get a map of all packet interceptors for sending packets of this
     * connection.
     *
     * @return a map of all packet interceptors for sending packets.
     */
    protected Map<PacketInterceptor, InterceptorWrapper> getPacketInterceptors() {
        return interceptors;
    }

    /**
     * Process interceptors. Interceptors may modify the packet that is about to
     * be sent. Since the thread that requested to send the packet will invoke
     * all interceptors, it is important that interceptors perform their work as
     * soon as possible so that the thread does not remain blocked for a long
     * period.
     *
     * @param packet
     *            the packet that is going to be sent to the server
     */
    protected void firePacketInterceptors(Packet packet) {
        if (packet != null) {
            for (InterceptorWrapper interceptorWrapper : interceptors.values()) {
                interceptorWrapper.notifyListener(packet);
            }
        }
    }

    /**
     * Initialize the {@link #debugger}. You can specify a customized
     * {@link SmackDebugger} by setup the system property
     * smack.debuggerClass to the implementation.
     *
     * @throws IllegalStateException
     *             if the reader or writer isn't yet initialized.
     * @throws IllegalArgumentException
     *             if the SmackDebugger can't be loaded.
     */
    protected void initDebugger() {
        if (reader == null || writer == null) {
            throw new NullPointerException(
                    "Reader or writer isn't initialized.");
        }
        // If debugging is enabled, we open a window and write out all network
        // traffic.
        if (config.isDebuggerEnabled()) {
            if (debugger == null) {
                // Detect the debugger class to use.
                String className = null;
                // Use try block since we may not have permission to get a
                // system
                // property (for example, when an applet).
                try {
                    className = System.getProperty("smack.debuggerClass");
                } catch (Throwable t) {
                    // Ignore.
                }
                Class<?> debuggerClass = null;
                if (className != null) {
                    try {
                        debuggerClass = Class.forName(className);
                    } catch (Exception e) {
                        log.warn("Unabled to instantiate debugger class "
                                + className);
                    }
                }
                if (debuggerClass == null) {
                    try {
                        debuggerClass = Class
                                .forName("org.jivesoftware.smackx.debugger.EnhancedDebugger");
                    } catch (Exception ex) {
                        try {
                            debuggerClass = Class
                                    .forName("org.jivesoftware.smack.debugger.LiteDebugger");
                        } catch (Exception ex2) {
                            log.warn("Unabled to instantiate either Smack debugger class");
                        }
                    }
                }
                // Create a new debugger instance. If an exception occurs then
                // disable the debugging
                // option
                try {
                    Constructor<?> constructor = debuggerClass.getConstructor(
                            Connection.class, Writer.class, Reader.class);
                    debugger = (SmackDebugger) constructor.newInstance(this,
                            writer, reader);
                    reader = debugger.getReader();
                    writer = debugger.getWriter();
                } catch (Exception e) {
                    throw new IllegalArgumentException(
                            "Can't initialize the configured debugger!", e);
                }
            } else {
                // Obtain new reader and writer from the existing debugger
                reader = debugger.newConnectionReader(reader);
                writer = debugger.newConnectionWriter(writer);
            }
        }
    }

    /**
     * Set the servers Entity Caps node
     *
     * Connection holds this information in order to avoid a dependency to
     * smackx where EntityCapsManager lives from smack.
     *
     * @param node
     */
    protected void setServiceCapsNode(String node) {
        serviceCapsNode = node;
    }

    /**
     * Retrieve the servers Entity Caps node
     *
     * Connection holds this information in order to avoid a dependency to
     * smackx where EntityCapsManager lives from smack.
     *
     * @return
     */
    public String getServiceCapsNode() {
        return serviceCapsNode;
    }

    /**
     * Returns true if the server supports roster versioning as defined in
     * XEP-0237.
     *
     * @return true if the server supports roster versioning
     */
    public boolean isRosterVersioningSupported() {
        return rosterVersioningSupported;
    }

    /**
     * Indicates that the server supports roster versioning as defined in
     * XEP-0237.
     */
    protected void setRosterVersioningSupported() {
        rosterVersioningSupported = true;
    }

    /**
     * A wrapper class to associate a packet filter with a listener.
     */
    protected static class ListenerWrapper {

        private PacketListener packetListener;
        private PacketFilter packetFilter;

        /**
         * Create a class which associates a packet filter with a listener.
         *
         * @param packetListener
         *            the packet listener.
         * @param packetFilter
         *            the associated filter or null if it listen for all
         *            packets.
         */
        public ListenerWrapper(PacketListener packetListener,
                PacketFilter packetFilter) {
            this.packetListener = packetListener;
            this.packetFilter = packetFilter;
        }

        /**
         * Notify and process the packet listener if the filter matches the
         * packet.
         *
         * @param packet
         *            the packet which was sent or received.
         */
        public void notifyListener(Packet packet) {
            if (packetFilter == null || packetFilter.accept(packet)) {
                packetListener.processPacket(packet);
            }
        }
    }

    /**
     * A wrapper class to associate a packet filter with an interceptor.
     */
    protected static class InterceptorWrapper {

        private PacketInterceptor packetInterceptor;
        private PacketFilter packetFilter;

        /**
         * Create a class which associates a packet filter with an interceptor.
         *
         * @param packetInterceptor
         *            the interceptor.
         * @param packetFilter
         *            the associated filter or null if it intercepts all
         *            packets.
         */
        public InterceptorWrapper(PacketInterceptor packetInterceptor,
                PacketFilter packetFilter) {
            this.packetInterceptor = packetInterceptor;
            this.packetFilter = packetFilter;
        }

        public boolean equals(Object object) {
            if (object == null) {
                return false;
            }
            if (object instanceof InterceptorWrapper) {
                return ((InterceptorWrapper) object).packetInterceptor
                        .equals(this.packetInterceptor);
            } else if (object instanceof PacketInterceptor) {
                return object.equals(this.packetInterceptor);
            }
            return false;
        }

        /**
         * Notify and process the packet interceptor if the filter matches the
         * packet.
         *
         * @param packet
         *            the packet which will be sent.
         */
        public void notifyListener(Packet packet) {
            if (packetFilter == null || packetFilter.accept(packet)) {
                packetInterceptor.interceptPacket(packet);
            }
        }
    }
}


package uk.co.tfd.sm.util.http;

import java.lang.reflect.Array;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.co.tfd.sm.resource.types.BooleanType;
import uk.co.tfd.sm.resource.types.CalendarType;
import uk.co.tfd.sm.resource.types.DoubleType;
import uk.co.tfd.sm.resource.types.IntegerType;
import uk.co.tfd.sm.resource.types.LongType;
import uk.co.tfd.sm.resource.types.RequestParameterType;
import uk.co.tfd.sm.resource.types.StringType;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;

public class RequestUtils {

	private static final Logger LOGGER = LoggerFactory
	.getLogger(RequestUtils.class);
	@SuppressWarnings("unchecked")
	private static final Class<RequestParameterType<?>>[] TYPE_CLASSES = new Class[] {
		StringType.class,
		IntegerType.class,
		LongType.class,
		BooleanType.class,
		CalendarType.class,
		DoubleType.class};
	private static final Map<String, RequestParameterType<?>> TYPES = createScalarTypes();

	private static Map<String, RequestParameterType<?>> createScalarTypes() {
		Builder<String, RequestParameterType<?>> b = ImmutableMap.builder();
		for (Class<RequestParameterType<?>> typeClass : TYPE_CLASSES) {
			try {
				RequestParameterType<?> o = typeClass.newInstance();
				b.put(o.getType(), o);
			} catch (InstantiationException e) {
				LOGGER.error(e.getMessage(), e);
			} catch (IllegalAccessException e) {
				LOGGER.error(e.getMessage(), e);
			}
		}
		return b.build();
	}

	/**
	 * @param name
	 *            the property name complete with type hints operations or
	 *            stream names.
	 * @return Just the property name excluding [] markers and all type, stream
	 *         hints.
	 */
	public static String propertyName(String name) {
		String[] parts = StringUtils.split(name, "@", 2);
		if (parts == null || parts.length == 0 ) {
			return null;
		}
		String propertyName = parts[0];
		if (propertyName.endsWith("[]")) {
			propertyName = propertyName.substring(0, propertyName.length() - 2);
		}
		return propertyName;
	}

	/**
	 * Creates the most suitable type.
	 *
	 * @param name
	 *            the name. Types are specified as name@Type where Type is the
	 *            name of the type. If the name ends with a [] the type is
	 *            forced to be an array.
	 * @param value
	 *            the value the value. If its an array, the type is assumed to
	 *            be an array provided there is more than one element. If there
	 *            is one element and the name ends with [] an array is created,
	 *            otherwise a scalar is created.
	 * @return an instance
	 */
	public static Object toValue(String name, Object value) {
		String[] parts = StringUtils.split(name, "@", 2);
		String fieldName = null;
		String fieldType = "String";
		if (parts.length == 2) {
			fieldType = parts[1];
			fieldName = parts[0];
		} else if (parts.length == 1) {
			fieldName = parts[0];
		} else {
			throw new IllegalArgumentException("Invalid property name");
		}
		try {
			int l = Array.getLength(value);
			RequestParameterType<?> rpt = TYPES.get(fieldType);
			if (rpt == null) {
				rpt = TYPES.get(RequestParameterType.STRING);
			}
			if (!fieldName.endsWith("[]") && l == 1) {
				return rpt.newInstance(Array.get(value, 0));
			}
			Class<?> componentType = rpt.getComponentType();
			Object[] a = (Object[]) Array.newInstance(componentType, l);
			for (int i = 0; i < l; i++) {
				a[i] = rpt.newInstance(Array.get(value, i));
			}
			return a;
		} catch (IllegalArgumentException e) {
			RequestParameterType<?> rpt = TYPES.get(fieldType);
			if (rpt == null) {
				rpt = TYPES.get(RequestParameterType.STRING);
			}
			return rpt.newInstance(value);
		}
	}

	/**
	 * @param name
	 *            the property name, with the stream specified as
	 *            name@StreamName. Only file uploads have stream names.
	 * @return the stream name or null if there is none.
	 */
	public static String getStreamName(String name) {

		String[] parts = StringUtils.split(name, "@", 3);
		if (parts != null && parts.length >= 2) {
			return parts[1];
		}
		return null;
	}

	/**
	 * @param name the property name
	 * @return the name of the file taking into account any alternative name
	 */
	public static String getFileName(String name) {
		String[] parts = StringUtils.split(name, "@", 2);
		if (parts != null && parts.length > 1) {
			return parts[0];
		}
		return name;
	}

	public static void accumulate(Map<String, Object> toAdd, String propertyName,
			Object value) {

		Object o = toAdd.get(propertyName);
		if (o == null) {
			toAdd.put(propertyName, value);
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("Saved {} {}", propertyName, value);
			}
		} else {
			int sl = 1;
			try {
				sl = Array.getLength(o);
			} catch (IllegalArgumentException e) {
				Object[] newO = (Object[]) Array.newInstance(o.getClass(), 1);
				newO[0] = o;
				o = newO;
			}
			int vl = 1;
			try {
				vl = Array.getLength(value);
			} catch (IllegalArgumentException e) {
				Object[] newO = (Object[]) Array.newInstance(value.getClass(),
						1);
				newO[0] = value;
				value = newO;
			}
			Object type = Array.get(o, 0);
			Object[] newArray = (Object[]) Array.newInstance(type.getClass(),
					sl + vl);
			System.arraycopy(o, 0, newArray, 0, sl);
			System.arraycopy(value, 0, newArray, sl, vl);
			toAdd.put(propertyName, newArray);
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("Appended {} {} {}", new Object[] { propertyName,
						value, newArray });
			}
		}
	}

}


/**
 * $RCSfile$
 * $Revision: 10099 $
 * $Date: 2008-03-20 16:46:17 -0700 (Thu, 20 Mar 2008) $
 *
 * Copyright 2005 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.whack;

import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;
import org.dom4j.io.XPPPacketReader;
import org.jivesoftware.whack.util.StringUtils;
import org.jivesoftware.whack.util.TaskEngine;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmpp.component.Component;
import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.StreamError;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * ExternalComponents are responsible for connecting and authenticating with a
 * remote server and for sending and processing received packets. In fact, an
 * ExternalComponent is a wrapper on a Component that provides remote connection
 * capabilities. The actual processing of the packets is done by the wrapped
 * Component.
 *
 * @author Gaston Dombiak
 */
public class ExternalComponent implements Component {

    /**
     * The utf-8 charset for decoding and encoding XMPP packet streams.
     */
    private static String CHARSET = "UTF-8";

    private Component component;
    private ExternalComponentManager manager;

    private Socket socket;
    private XMLWriter xmlSerializer;
    private XmlPullParserFactory factory = null;
    private XPPPacketReader reader = null;
    private Writer writer = null;
    private boolean shutdown = false;
    private boolean reconnecting = false;

    private KeepAliveTask keepAliveTask;
    private TimeoutTask timeoutTask;
    /**
     * Timestamp when the last stanza was sent to the server. This information
     * is used by the keep alive process to only send heartbeats when the
     * connection has been idle.
     */
    private long lastActive = System.currentTimeMillis();

    private String connectionID;
    /**
     * Hold the full domain of this component. The full domain is composed by
     * the subdomain plus the domain of the server. E.g.
     * conference.jivesoftware.com. The domain may change after a connection has
     * been established with the server.
     */
    private String domain;
    /**
     * Holds the subdomain that is associated to this component. The subdomain
     * is the initial part of the domain. The subdomain cannot be affected after
     * establishing a connection with the server. E.g. conference.
     */
    private String subdomain;
    /**
     * Holds the IP address or host name where the connection must be made.
     */
    private String host;
    private int port;

    /**
     * Pool of threads that are available for processing the requests.
     */
    private ThreadPoolExecutor threadPool;
    /**
     * Thread that will read the XML from the socket and ask this component to
     * process the read packets.
     */
    private SocketReadThread readerThread;

    private Map<String, IQResultListener> resultListeners = new ConcurrentHashMap<String, IQResultListener>();
    private Map<String, Long> resultTimeout = new ConcurrentHashMap<String, Long>();

    public ExternalComponent(Component component,
            ExternalComponentManager manager) {
        // Be default create a pool of 25 threads to process the received
        // requests
        this(component, manager, 25);
    }

    public ExternalComponent(Component component,
            ExternalComponentManager manager, int maxThreads) {
        this.component = component;
        this.manager = manager;

        // Create a pool of threads that will process requests received by this
        // component. If more
        // threads are required then the command will be executed on the
        // SocketReadThread process
        threadPool = new ThreadPoolExecutor(maxThreads, maxThreads, 15,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }

    /**
     * Generates a connection with the server and tries to authenticate. If an
     * error occurs in any of the steps then a ComponentException is thrown.
     *
     * @param host
     *            the host to connect with.
     * @param port
     *            the port to use.
     * @param subdomain
     *            the subdomain that this component will be handling.
     * @throws ComponentException
     *             if an error happens during the connection and authentication
     *             steps.
     */
    public void connect(String host, int port, String subdomain)
            throws ComponentException {
        try {
            // Open a socket to the server
            this.socket = new Socket();
            socket.connect(new InetSocketAddress(host, port),
                    manager.getConnectTimeout());
            if (manager.getServerName() != null) {
                this.domain = subdomain + "." + manager.getServerName();
            } else {
                this.domain = subdomain;
            }
            this.subdomain = subdomain;
            // Keep these variables that will be used in case a reconnection is
            // required
            this.host = host;
            this.port = port;

            try {
                factory = XmlPullParserFactory.newInstance();
                reader = new XPPPacketReader();
                reader.setXPPFactory(factory);

                reader.getXPPParser()
                        .setInput(
                                new InputStreamReader(socket.getInputStream(),
                                        CHARSET));

                // Get a writer for sending the open stream tag
                writer = new BufferedWriter(new OutputStreamWriter(
                        socket.getOutputStream(), CHARSET));
                // Open the stream.
                StringBuilder stream = new StringBuilder();
                stream.append("<stream:stream");
                stream.append(" xmlns=\"jabber:component:accept\"");
                stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
                if (manager.isMultipleAllowed(subdomain)) {
                    stream.append(" allowMultiple=\"true\"");
                }
                stream.append(" to=\"").append(domain).append("\">");
                writer.write(stream.toString());
                writer.flush();
                stream = null;

                // Get the answer from the server
                XmlPullParser xpp = reader.getXPPParser();
                for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
                    eventType = xpp.next();
                }

                // Set the streamID returned from the server
                connectionID = xpp.getAttributeValue("", "id");
                if (xpp.getAttributeValue("", "from") != null) {
                    this.domain = xpp.getAttributeValue("", "from");
                }
                xmlSerializer = new XMLWriter(writer);

                // Handshake with the server
                stream = new StringBuilder();
                stream.append("<handshake>");
                stream.append(StringUtils.hash(connectionID
                        + manager.getSecretKey(subdomain)));
                stream.append("</handshake>");
                writer.write(stream.toString());
                writer.flush();
                stream = null;

                // Get the answer from the server
                try {
                    Element doc = reader.parseDocument().getRootElement();
                    if ("error".equals(doc.getName())) {
                        StreamError error = new StreamError(doc);
                        // Close the connection
                        socket.close();
                        socket = null;
                        // throw the exception with the wrapped error
                        throw new ComponentException(error);
                    }
                    // Everything went fine
                    // Start keep alive thread to send every 30 seconds of
                    // inactivity a heart beat
                    keepAliveTask = new KeepAliveTask();
                    TaskEngine.getInstance().scheduleAtFixedRate(keepAliveTask,
                            15000, 30000);

                    timeoutTask = new TimeoutTask();
                    TaskEngine.getInstance().scheduleAtFixedRate(timeoutTask,
                            2000, 2000);

                } catch (DocumentException e) {
                    try {
                        socket.close();
                    } catch (IOException ioe) {
                        // Do nothing
                    }
                    throw new ComponentException(e);
                } catch (XmlPullParserException e) {
                    try {
                        socket.close();
                    } catch (IOException ioe) {
                        // Do nothing
                    }
                    throw new ComponentException(e);
                }
            } catch (XmlPullParserException e) {
                try {
                    socket.close();
                } catch (IOException ioe) {
                    // Do nothing
                }
                throw new ComponentException(e);
            }
        } catch (UnknownHostException uhe) {
            try {
                if (socket != null)
                    socket.close();
            } catch (IOException e) {
                // Do nothing
            }
            throw new ComponentException(uhe);
        } catch (IOException ioe) {
            try {
                if (socket != null)
                    socket.close();
            } catch (IOException e) {
                // Do nothing
            }
            throw new ComponentException(ioe);
        }
    }

    public Component getComponent() {
        return component;
    }

    public String getName() {
        return component.getName();
    }

    public String getDescription() {
        return component.getDescription();
    }

    /**
     * Returns the domain provided by this component in the connected server.
     * The domain is composed by the subdomain plus the domain of the server.
     * E.g. conference.jivesoftware.com. The domain may change after a
     * connection has been established with the server.
     *
     * @return the domain provided by this component in the connected server.
     */
    public String getDomain() {
        return domain;
    }

    /**
     * Returns the subdomain provided by this component in the connected server.
     * E.g. conference.
     *
     * @return the subdomain provided by this component in the connected server.
     */
    public String getSubdomain() {
        return subdomain;
    }

    /**
     * Returns the ComponentManager that created this component.
     *
     * @return the ComponentManager that created this component.
     */
    ExternalComponentManager getManager() {
        return manager;
    }

    public void processPacket(final Packet packet) {
        threadPool.execute(new Runnable() {
            public void run() {
                if (packet instanceof IQ) {
                    IQ iq = (IQ) packet;
                    IQ.Type iqType = iq.getType();
                    if (IQ.Type.result == iqType || IQ.Type.error == iqType) {
                        // The server got an answer to an IQ packet that was
                        // sent from the component
                        IQResultListener iqResultListener = resultListeners
                                .remove(iq.getID());
                        resultTimeout.remove(iq.getID());
                        if (iqResultListener != null) {
                            try {
                                iqResultListener.receivedAnswer(iq);
                            } catch (Exception e) {
                                manager.getLog()
                                        .error("Error processing answer of remote entity",
                                                e);
                            }
                            return;
                        }
                    }
                }
                component.processPacket(packet);
            }
        });
    }

    public void send(Packet packet) {
        synchronized (writer) {
            try {
                xmlSerializer.write(packet.getElement());
                xmlSerializer.flush();
                // Keep track of the last time a stanza was sent to the server
                lastActive = System.currentTimeMillis();
            } catch (IOException e) {
                // Log the exception
                manager.getLog().error(e);
                if (!shutdown) {
                    // Connection was lost so try to reconnect
                    connectionLost();
                }
            }
        }
    }

    public void initialize(JID jid, ComponentManager componentManager)
            throws ComponentException {
        component.initialize(jid, componentManager);
    }

    public void start() {
        // Everything went fine so start reading packets from the server
        readerThread = new SocketReadThread(this, reader);
        readerThread.setDaemon(true);
        readerThread.start();
        // Notify the component that it will be notified of new received packets
        component.start();
    }

    public void shutdown() {
        shutdown = true;
        disconnect();
    }

    private void disconnect() {
        if (readerThread != null) {
            readerThread.shutdown();
        }
        threadPool.shutdown();
        TaskEngine.getInstance().cancelScheduledTask(keepAliveTask);
        TaskEngine.getInstance().cancelScheduledTask(timeoutTask);
        if (socket != null && !socket.isClosed()) {
            try {
                synchronized (writer) {
                    try {
                        writer.write("</stream:stream>");
                        xmlSerializer.flush();
                    } catch (IOException e) {
                        // Do nothing
                    }
                }
            } catch (Exception e) {
                // Do nothing
            }
            try {
                socket.close();
            } catch (Exception e) {
                manager.getLog().error(e);
            }
        }
    }

    /**
     * Notification message that the connection with the server was lost
     * unexpectedly. We will try to reestablish the connection for ever until
     * the connection has been reestablished or this thread has been stopped.
     */
    public void connectionLost() {
        // Ensure that only one thread will try to reconnect.
        synchronized (this) {
            if (reconnecting) {
                return;
            }
            reconnecting = true;
        }
        readerThread = null;
        boolean isConnected = false;
        if (!shutdown) {
            // Notify the component that connection was lost so it needs to
            // shutdown. The component is
            // still registered in the local component manager but just not
            // connected to the server
            component.shutdown();
        }
        while (!isConnected && !shutdown) {
            try {
                connect(host, port, subdomain);
                isConnected = true;
                // It may be possible that while a new connection was being
                // established the
                // component was required to shutdown so in this case we need to
                // close the new
                // connection
                if (shutdown) {
                    disconnect();
                } else {
                    // Component is back again working so start it up again
                    start();
                }
            } catch (ComponentException e) {
                manager.getLog().error(
                        "Error trying to reconnect with the server", e);
                // Wait for 5 seconds until the next retry
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e1) {
                    // Do nothing
                }
            }
        }
        reconnecting = false;
    }

    /**
     * Adds an {@link IQResultListener} that will be invoked when an IQ result
     * is sent to the server itself and is of type result or error. This is a
     * nice way for the server to send IQ packets to other XMPP entities and be
     * waked up when a response is received back.
     * <p>
     *
     * Once an IQ result was received, the listener will be invoked and removed
     * from the list of listeners.
     *
     * @param id
     *            the id of the IQ packet being sent from the server to an XMPP
     *            entity.
     * @param listener
     *            the IQResultListener that will be invoked when an answer is
     *            received
     * @param timeoutmillis
     *            The amount of milliseconds after which waiting for a response
     *            should be stopped.
     */
    void addIQResultListener(String id, IQResultListener listener,
            long timeoutmillis) {
        // be generated by the server and simulate like the client sent it. This
        // will let listeners
        // react and be removed from the collection
        resultListeners.put(id, listener);
        resultTimeout.put(id, System.currentTimeMillis() + timeoutmillis);
    }

    /**
     * A TimerTask that keeps connections to the server alive by sending a space
     * character on an interval.
     */
    private class KeepAliveTask extends TimerTask {

        public void run() {
            synchronized (writer) {
                // Send heartbeat if no packet has been sent to the server for a
                // given time
                if (System.currentTimeMillis() - lastActive >= 30000) {
                    try {
                        writer.write(" ");
                        writer.flush();
                    } catch (IOException e) {
                        // Log the exception
                        manager.getLog().error(e);
                        if (!shutdown) {
                            // Connection was lost so try to reconnect
                            connectionLost();
                        }
                    } catch (Exception e) {
                        // Do nothing
                    }
                }
            }
        }
    }

    /**
     * Timer task that will remove Listeners that wait for results to IQ stanzas
     * that have timed out. Time out values can be set to each listener
     * individually by adjusting the timeout value in the third parameter of
     * {@link ExternalComponent#addIQResultListener(String, IQResultListener, long)}
     * .
     *
     * @author Guus der Kinderen, guus@nimbuzz.com
     */
    private class TimeoutTask extends TimerTask {

        /**
         * Iterates over and removes all timed out results.
         * <p>
         *
         * The map that keeps track of timeout values is ordered by timeout
         * date. This way, iteration can be stopped as soon as the first value
         * has been found that didn't timeout yet.
         */
        @Override
        public void run() {
            // Use an Iterator to allow changes to the Map that is backing
            // the Iterator.
            final Iterator<Map.Entry<String, Long>> it = resultTimeout
                    .entrySet().iterator();

            while (it.hasNext()) {
                final Map.Entry<String, Long> pointer = it.next();

                if (System.currentTimeMillis() < pointer.getValue()) {
                    // This entry has not expired yet. Ignore it.
                    continue;
                }

                final String packetId = pointer.getKey();

                // remove this listener from the list
                final IQResultListener listener = resultListeners
                        .remove(packetId);
                if (listener != null) {
                    // notify listener of the timeout.
                    listener.answerTimeout(packetId);
                }

                // remove the packet from the list that's used to track
                // timeouts
                it.remove();
            }
        }
    }
}