org.trypticon.xmpp.bot.BaseBot.java Source code

Java tutorial

Introduction

Here is the source code for org.trypticon.xmpp.bot.BaseBot.java

Source

/*
 * Copyright 2004-2005 Trypticon
 *
 * 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.trypticon.xmpp.bot;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;

import org.trypticon.xmpp.util.DummyTrustManager;
import org.trypticon.xmpp.util.FixedCallbackHandler;
import org.trypticon.xmpp.util.SrvLookup;

import net.outer_planes.jso.JSO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jabberstudio.jso.JID;
import org.jabberstudio.jso.JSOImplementation;
import org.jabberstudio.jso.NSI;
import org.jabberstudio.jso.PacketError;
import org.jabberstudio.jso.Presence;
import org.jabberstudio.jso.Stream;
import org.jabberstudio.jso.StreamException;
import org.jabberstudio.jso.features.FeaturesetConsumerManager;
import org.jabberstudio.jso.sasl.SASLFeatureConsumer;
import org.jabberstudio.jso.tls.StartTLSSocketFeatureConsumer;
import org.jabberstudio.jso.tls.StartTLSSocketStreamSource;
import org.jabberstudio.jso.util.Monitor;
import org.jabberstudio.jso.util.PacketException;
import org.jabberstudio.jso.util.Utilities;
import org.jabberstudio.jso.x.core.AuthFeatureConsumer;
import org.jabberstudio.jso.x.core.BindFeatureConsumer;
import org.jabberstudio.jso.x.core.SessionFeatureConsumer;
import org.jdom.Element;

/**
 * Base class for Jabber bots.  Tries to provide a layer of convenience over the top of JSO.
 */
public class BaseBot implements Bot {
    /**
     * Logger.
     */
    private static final Log log = LogFactory.getLog(BaseBot.class);

    /**
     * The configuration for the bot.
     */
    private Element config;

    /**
     * The thread running the main processing loop.
     */
    private Thread runnerThread;

    /**
     * The stream source.
     */
    private StartTLSSocketStreamSource streamSource;

    /**
     * The XMPP stream to the server.
     */
    private Stream stream;

    /**
     * The roster.
     */
    private BaseRoster roster;

    /**
     * Constructs the bot.
     *
     * @param config the configuration for the bot.
     */
    public BaseBot(Element config) {
        this.config = config;

        this.roster = new BaseRoster();
    }

    /**
     * Starts the bot.
     */
    public synchronized void start() {
        if (runnerThread == null) {
            runnerThread = new Thread(new BotRunner());
            runnerThread.start();
        }
    }

    /**
     * Stops the bot.
     */
    public synchronized void stop() {
        if (runnerThread != null) {
            runnerThread = null;
        }
    }

    /**
     * Gets a reference to the roster.
     *
     * @return the reference to the roster.
     */
    public Roster getRoster() {
        return roster;
    }

    /**
     * Attach listeners to the bot.  Subclasses should remember to call this method.
     */
    protected void attachListeners() {
        roster.attach(stream);
    }

    /**
     * Connects to the server, opens a stream and logs in.
     *
     * @return <code>true</code> if connection is successful.
     */
    public boolean connect() {
        JSOImplementation jso = JSO.getInstance();

        Element connectionElement = config.getChild("connection");

        JID clientJID = JID.valueOf(connectionElement.getChildTextTrim("jid"));
        JID serverJID = new JID(null, clientJID.getDomain(), null);

        try {
            InetSocketAddress address = SrvLookup.resolveXmppClient(clientJID.getDomain());

            String hostname = connectionElement.getChildTextTrim("hostname");
            if (hostname == null) {
                hostname = address.getHostName();
            }

            boolean tls = "true".equals(connectionElement.getChildTextTrim("tls"));

            String portString = connectionElement.getChildTextTrim("port");
            int port = (portString == null) ? -1 : Integer.parseInt(portString);
            if (port == -1) {
                if (tls) {
                    port = 5223;
                } else {
                    port = address.getPort();
                }
            }

            streamSource = new StartTLSSocketStreamSource(hostname, port);
            streamSource.getTLSContext().init(null, DummyTrustManager.asArray(), null);

            // Old-style TLS requires negotiation before sending any data.
            if (tls) {
                streamSource.negotiateClientTLS();
            }
        } catch (IOException e) {
            log.error("Failure to create stream source", e);
            return false;
        } catch (GeneralSecurityException e) {
            log.error("TLS is not supported by your JRE", e);
            return false;
        }

        // Create the stream...
        stream = jso.createStream(Utilities.CLIENT_NAMESPACE);

        // Attach listeners to the stream.
        attachListeners();

        // Try to connect.
        try {
            stream.connect(streamSource);
            stream.getOutboundContext().setTo(serverJID);
            stream.getOutboundContext().setVersion("1.0");
            stream.open(5000);
        } catch (StreamException e) {
            log.error("Failure to connect to stream", e);
            return false;
        }

        // Consume stream features.
        String password = connectionElement.getChildTextTrim("password");

        try {
            String version = stream.getInboundContext().getVersion();
            if ("1.0".equals(version)) {
                saslLogin(clientJID, password);
            } else if ("".equals(version)) {
                authLogin(clientJID, password);
            } else {
                log.warn("Version '" + version + "' was not an expected version number");
            }
        } catch (PacketException e) {
            log.error("Packet error while authenticating", e);
            return false;
        } catch (StreamException e) {
            log.error("Stream error while authenticating", e);
            return false;
        }

        // Set presence to online, but with negative priority.  Servers compliant with XMPP will therefore
        // not send us any messages which were sent to the bare JID.
        try {
            String priorityString = connectionElement.getChildTextTrim("priority");
            int priority = (priorityString == null) ? -1 : Integer.parseInt(priorityString);

            Presence presence = (Presence) stream.getDataFactory()
                    .createPacketNode(new NSI("presence", Utilities.CLIENT_NAMESPACE));
            presence.setPriority(priority);
            stream.send(presence);
        } catch (StreamException e) {
            log.error("Failure to set presence", e);
            return false;
        }

        return true;
    }

    /**
     * Performs login using SASL.
     *
     * @throws StreamException if a stream-level exception occurred.
     * @throws PacketException if a packet-level exception occurred.
     */
    private void saslLogin(JID client, String password) throws StreamException, PacketException {
        FeaturesetConsumerManager manager = new FeaturesetConsumerManager();

        StartTLSSocketFeatureConsumer tls = new StartTLSSocketFeatureConsumer(streamSource);
        manager.registerFeatureConsumer(tls);

        SASLFeatureConsumer sasl = new SASLFeatureConsumer();
        sasl.getClientInfo().setServer(client.getDomain());
        sasl.getClientInfo().setCallbackHandler(new FixedCallbackHandler(client.getNode(), password));
        manager.registerFeatureConsumer(sasl);

        BindFeatureConsumer bind = new BindFeatureConsumer(client.getResource());
        manager.registerFeatureConsumer(bind);

        SessionFeatureConsumer session = new SessionFeatureConsumer(false);
        manager.registerFeatureConsumer(session);

        try {
            // Attach and run the consumers.
            manager.attach(stream);
            manager.run();

            // Check to see if an error occurred.
            Throwable failure = manager.getFailure();
            if (failure != null) {
                log.error("Authentication failed", failure);

                if (failure instanceof StreamException) {
                    throw (StreamException) failure;
                } else if (failure instanceof PacketException) {
                    throw (PacketException) failure;
                } else {
                    throw new PacketException(failure.getLocalizedMessage(), PacketError.CANCEL,
                            PacketError.UNDEFINED_CONDITION);
                }
            }

            // If the SASL consumer didn't complete, try an older-style login.
            if (!manager.isFeatureConsumerCompleted(sasl)) {
                authLogin(client, password);
            } else {
                // Make sure the resource was bound.
                if (!manager.isFeatureConsumerCompleted(bind)) {
                    throw new PacketException(PacketError.AUTH, PacketError.RESOURCE_CONSTRAINT_CONDITION);
                }

                // Make sure the session is active.
                if (!manager.isFeatureConsumerCompleted(session)) {
                    throw new PacketException(PacketError.CANCEL, PacketError.SERVICE_UNAVAILABLE_CONDITION);
                }
            }
        } finally {
            manager.detach();
        }
    }

    /**
     * Performs login using the older-style authentication.
     *
     * @throws StreamException if a stream-level exception occurred.
     * @throws PacketException if a packet-level exception occurred.
     */
    private void authLogin(JID client, String password) throws StreamException, PacketException {
        AuthFeatureConsumer auth = new AuthFeatureConsumer(JID.valueOf(client.getDomain()), client.getNode(),
                password, client.getResource());
        auth.authenticate(stream, stream);
    }

    /**
     * Closes the stream and disconnects from the server.
     */
    private void disconnect() {
        try {
            if (stream != null) {
                if (stream.getCurrentStatus() == Stream.OPENED) {
                    stream.close();
                }

                if (stream.getCurrentStatus() == Stream.CONNECTED) {
                    stream.disconnect();
                }
            }
        } catch (StreamException e) {
            log.warn("Failed to close and disconnect, stream is probably already closed", e);
        }
    }

    /**
     * Tests if the bot is connected.
     * @return <code>true</code> if the bot is connected, <code>false</code> otherwise.
     */
    public boolean isConnected() {
        return (stream != null && stream.getCurrentStatus() == Stream.OPENED);
    }

    /**
     * Gets a reference to the XMPP Stream.
     *
     * @return the XMPP stream reference.
     */
    protected Stream getStream() {
        return stream;
    }

    /**
     * <code>Runnable</code> which runs the bot.
     */
    private class BotRunner implements Runnable {
        public void run() {
            Thread currentThread = Thread.currentThread();
            while (runnerThread == currentThread) {
                try {
                    if (!connect()) {
                        try {
                            log.warn("Failed to connect.  Sleeping for a while before trying again.");
                            Thread.sleep(20000);
                            continue;
                        } catch (InterruptedException e) {
                            break;
                        }
                    }

                    // Main processing loop.
                    while (runnerThread == currentThread) {
                        // Process a single packet.
                        try {
                            stream.process();
                        } catch (StreamException e) {
                            log.error("Error processing stream", e);
                            break;
                        }

                        // Surely there is a better way to do this by now... can't we _wait_ for data?
                        try {
                            Thread.sleep(Monitor.DEFAULT_PROC_INTERVAL);
                        } catch (InterruptedException e) {
                            log.warn("Thread interrupted", e);
                            break;
                        }
                    }
                } catch (Throwable t) {
                    log.error("Unexpected error from reconnection loop, reconnecting.", t);
                } finally {
                    disconnect();
                }
            }
        }
    }
}