com.almende.eve.transport.xmpp.XmppService.java Source code

Java tutorial

Introduction

Here is the source code for com.almende.eve.transport.xmpp.XmppService.java

Source

/*
 * Copyright: Almende B.V. (2014), Rotterdam, The Netherlands
 * License: The Apache Software License, Version 2.0
 */
package com.almende.eve.transport.xmpp;

import java.io.IOException;
import java.net.ProtocolException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.apache.commons.codec.binary.Base64;
import org.jivesoftware.smack.SmackConfiguration;

import com.almende.eve.agent.AgentHost;
import com.almende.eve.rpc.annotation.Access;
import com.almende.eve.rpc.annotation.AccessType;
import com.almende.eve.rpc.jsonrpc.jackson.JOM;
import com.almende.eve.state.State;
import com.almende.eve.transport.TransportService;
import com.almende.util.EncryptionUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * The Class XmppService.
 */
public class XmppService implements TransportService {
    private static final String CONNKEY = "_XMPP_Connections";
    private AgentHost agentHost = null;
    private String host = null;
    private Integer port = null;
    private String service = null;
    /** The connections by url, xmpp url as key "xmpp:username@host". */
    private final Map<URI, AgentConnection> connectionsByUrl = new ConcurrentHashMap<URI, AgentConnection>();
    private static List<String> protocols = Arrays.asList("xmpp");
    private static final Logger LOG = Logger.getLogger(XmppService.class.getSimpleName());

    /**
     * Instantiates a new xmpp service.
     */
    protected XmppService() {
    }

    // Needed to force Android loading the ReconnectionManager....
    static {
        try {
            Class.forName("org.jivesoftware.smack.ReconnectionManager");
        } catch (final ClassNotFoundException ex) {
            // problem loading reconnection manager
        }
    }

    /**
     * Construct an XmppService
     * This constructor is called when the TransportService is constructed
     * by the AgentHost.
     * 
     * @param agentHost
     *            the agent host
     * @param params
     *            Available parameters:
     *            {String} host
     *            {Integer} port
     *            {String} serviceName
     *            {String} id
     */
    public XmppService(final AgentHost agentHost, final Map<String, Object> params) {
        this.agentHost = agentHost;

        if (params != null) {
            host = (String) params.get("host");
            port = (Integer) params.get("port");
            service = (String) params.get("service");
        }

        init();
    }

    /**
     * initialize the settings for the xmpp service.
     * 
     * @param agentHost
     *            the agent host
     * @param host
     *            the host
     * @param port
     *            the port
     * @param service
     *            service name
     */
    public XmppService(final AgentHost agentHost, final String host, final Integer port, final String service) {
        this.agentHost = agentHost;
        this.host = host;
        this.port = port;
        this.service = service;

        init();
    }

    /**
     * Gets the conns.
     * 
     * @param agentId
     *            the agent id
     * @return the conns
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    private ArrayNode getConns(final String agentId) throws IOException {
        final State state = agentHost.getStateFactory().get(agentId);

        if (state == null) {
            LOG.warning("getConns(): Couldn't find agent with agentId:" + agentId);
            return null;
        }
        ArrayNode conns = null;
        if (state.containsKey(CONNKEY)) {
            conns = (ArrayNode) JOM.getInstance().readTree(state.get(CONNKEY, String.class));
        }
        return conns;
    }

    /**
     * Get the first XMPP url of an agent from its id.
     * If the agent exists (is not null) retrieve the current 'isConnected'
     * status and return it.
     * 
     * @param agentUrl
     *            The url of the agent
     * @return connectionStatus
     */
    public Boolean isConnected(final String agentUrl) {
        final AgentConnection connection = connectionsByUrl.get(agentUrl);

        if (connection == null) {
            return false;
        }

        LOG.info("Current connection of agent " + agentUrl + " is: " + connection.isConnected());
        return connection.isConnected();
    }

    /**
     * Get the first XMPP url of an agent from its id.
     * If no agent with given id is connected via XMPP, null is returned.
     * 
     * @param agentId
     *            The id of the agent
     * @return agentUrl
     */
    @Override
    public URI getAgentUrl(final String agentId) {
        try {
            final ArrayNode conns = getConns(agentId);
            if (conns != null) {
                for (final JsonNode conn : conns) {
                    final ObjectNode params = (ObjectNode) conn;

                    final String encryptedUsername = params.has("username") ? params.get("username").textValue()
                            : null;
                    final String encryptedResource = params.has("resource") ? params.get("resource").textValue()
                            : null;
                    if (encryptedUsername != null) {
                        final String username = EncryptionUtil.decrypt(encryptedUsername);
                        String resource = null;
                        if (encryptedResource != null) {
                            resource = EncryptionUtil.decrypt(encryptedResource);
                        }
                        return generateUrl(username, host, resource);
                    }
                }
            }
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        return null;
    }

    /**
     * Get the id of an agent from its url.
     * If no agent with given id is connected via XMPP, null is returned.
     * 
     * @param agentUrl
     *            the agent url
     * @return agentId
     */
    @Override
    public String getAgentId(final URI agentUrl) {
        final AgentConnection connection = connectionsByUrl.get(agentUrl.toString());
        if (connection != null) {
            return connection.getAgentId();
        }
        return null;
    }

    /**
     * initialize the transport service.
     */
    private void init() {
        SmackConfiguration.setPacketReplyTimeout(15000);
    }

    /**
     * Get the protocols supported by the XMPPService.
     * Will return an array with one value, "xmpp"
     * 
     * @return protocols
     */
    @Override
    public List<String> getProtocols() {
        return protocols;
    }

    /**
     * Connect to the configured messaging service (such as XMPP). The service
     * must be configured in the Eve configuration
     * 
     * @param agentId
     *            the agent id
     * @param username
     *            the username
     * @param password
     *            the password
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    @Access(AccessType.UNAVAILABLE)
    public final void connect(final String agentId, final String username, final String password)
            throws IOException {
        final String resource = null;
        connect(agentId, username, password, resource);
    }

    /**
     * Connect to the configured messaging service (such as XMPP). The service
     * must be configured in the Eve configuration
     * 
     * @param agentId
     *            the agent id
     * @param username
     *            the username
     * @param password
     *            the password
     * @param resource
     *            (optional)
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    @Access(AccessType.UNAVAILABLE)
    public final void connect(final String agentId, final String username, final String password,
            final String resource) throws IOException {
        // First store the connection info for later reconnection.
        try {
            storeConnection(agentId, username, password, resource);
        } catch (final Exception e) {
            LOG.log(Level.SEVERE, "Failed to store XMPP Connection.", e);
        }

        final URI agentUrl = generateUrl(username, host, resource);
        AgentConnection connection;
        if (connectionsByUrl.containsKey(agentUrl)) {
            LOG.warning("Warning, agent was already connected, reconnecting.");
            connection = connectionsByUrl.get(agentUrl);
        } else {
            // instantiate open the connection
            connection = new AgentConnection(agentHost);
        }

        if (username.indexOf('@') > 0) {
            LOG.warning("Warning: Username should not contain a domain! " + username);
        }
        connection.connect(agentId, host, port, service, username, password, resource);
        connectionsByUrl.put(agentUrl, connection);
    }

    /**
     * Store connection.
     * 
     * @param agentId
     *            the agent id
     * @param username
     *            the username
     * @param password
     *            the password
     * @param resource
     *            the resource
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     * @throws InvalidKeyException
     *             the invalid key exception
     * @throws InvalidAlgorithmParameterException
     *             the invalid algorithm parameter exception
     * @throws NoSuchAlgorithmException
     *             the no such algorithm exception
     * @throws InvalidKeySpecException
     *             the invalid key spec exception
     * @throws NoSuchPaddingException
     *             the no such padding exception
     * @throws IllegalBlockSizeException
     *             the illegal block size exception
     * @throws BadPaddingException
     *             the bad padding exception
     */
    private void storeConnection(final String agentId, final String username, final String password,
            final String resource)
            throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {

        final State state = agentHost.getStateFactory().get(agentId);

        final String conns = state.get(CONNKEY, String.class);
        ArrayNode newConns;
        if (conns != null) {
            newConns = (ArrayNode) JOM.getInstance().readTree(conns);
        } else {
            newConns = JOM.createArrayNode();
        }

        final ObjectNode params = JOM.createObjectNode();
        params.put("username", EncryptionUtil.encrypt(username));
        params.put("password", EncryptionUtil.encrypt(password));
        if (resource != null && !resource.equals("")) {
            params.put("resource", EncryptionUtil.encrypt(resource));
        }
        for (final JsonNode item : newConns) {
            if (item.get("username").equals(params.get("username"))) {
                return;
            }
        }
        newConns.add(params);
        if (!state.putIfUnchanged(CONNKEY, JOM.getInstance().writeValueAsString(newConns), conns)) {
            // recursive retry
            storeConnection(agentId, username, password, resource);
        }
    }

    /**
     * Del connections.
     * 
     * @param agentId
     *            the agent id
     */
    private void delConnections(final String agentId) {
        final State state = agentHost.getStateFactory().get(agentId);
        if (state != null) {
            state.remove(CONNKEY);
        }
    }

    /**
     * Disconnect the agent from the connected messaging service(s) (if any).
     * 
     * @param agentId
     *            the agent id
     * @throws InvalidKeyException
     *             the invalid key exception
     * @throws InvalidAlgorithmParameterException
     *             the invalid algorithm parameter exception
     * @throws NoSuchAlgorithmException
     *             the no such algorithm exception
     * @throws InvalidKeySpecException
     *             the invalid key spec exception
     * @throws NoSuchPaddingException
     *             the no such padding exception
     * @throws IllegalBlockSizeException
     *             the illegal block size exception
     * @throws BadPaddingException
     *             the bad padding exception
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    @Access(AccessType.UNAVAILABLE)
    public final void disconnect(final String agentId) throws InvalidKeyException,
            InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeySpecException,
            NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, IOException {

        final ArrayNode conns = getConns(agentId);
        if (conns != null) {
            for (final JsonNode conn : conns) {
                final ObjectNode params = (ObjectNode) conn;

                final String encryptedUsername = params.has("username") ? params.get("username").textValue() : null;
                final String encryptedResource = params.has("resource") ? params.get("resource").textValue() : null;
                if (encryptedUsername != null) {
                    final String username = EncryptionUtil.decrypt(encryptedUsername);
                    String resource = null;
                    if (encryptedResource != null) {
                        resource = EncryptionUtil.decrypt(encryptedResource);
                    }

                    final URI url = generateUrl(username, host, resource);
                    final AgentConnection connection = connectionsByUrl.get(url);
                    if (connection != null) {
                        connection.disconnect();
                        connectionsByUrl.remove(url);
                    }
                }
            }
        }
        delConnections(agentId);
    }

    /* (non-Javadoc)
     * @see com.almende.eve.transport.TransportService#sendAsync(java.net.URI, java.net.URI, byte[], java.lang.String)
     */
    @Override
    public void sendAsync(URI senderUri, URI receiverUri, byte[] message, String tag) throws IOException {
        sendAsync(senderUri, receiverUri, Base64.encodeBase64String(message), tag);
    }

    /* (non-Javadoc)
     * @see com.almende.eve.transport.TransportService#sendAsync(java.net.URI, java.net.URI, java.lang.String, java.lang.String)
     */
    @Override
    public void sendAsync(final URI senderUrl, final URI receiverUrl, final String message, final String tag)
            throws IOException {

        AgentConnection connection = null;

        if (senderUrl != null) {
            connection = connectionsByUrl.get(senderUrl);
        }
        if (connection != null) {
            // remove the protocol from the receiver url
            String receiver = receiverUrl.toString();
            final String protocol = "xmpp:";
            if (!receiver.startsWith(protocol)) {
                throw new ProtocolException(
                        "Receiver url must start with '" + protocol + "' (receiver='" + receiver + "')");
            }
            // username@domain
            final String fullUsername = receiver.substring(protocol.length());
            connection.send(fullUsername, message);
        } else {
            // TODO: use an anonymous xmpp connection when the sender agent has
            // no xmpp connection.
            throw new IOException("Cannot send an xmpp request, " + "agent has no xmpp connection.");
        }
    }

    /**
     * Get the url of an xmpp connection "xmpp:username@host".
     * 
     * @param username
     *            the username
     * @param host
     *            the host
     * @param resource
     *            optional
     * @return url
     * @throws URISyntaxException
     */
    private static URI generateUrl(final String username, final String host, final String resource) {
        String url = "xmpp:" + username + "@" + host;
        if (resource != null && !resource.equals("")) {
            url += "/" + resource;
        }
        try {
            return new URI(url);
        } catch (URISyntaxException e) {
            LOG.warning("Strange, couldn't generate URI.");
            return null;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        final Map<String, Object> data = new HashMap<String, Object>();
        data.put("class", this.getClass().getName());
        data.put("host", host);
        data.put("port", port);
        data.put("service", service);
        data.put("protocols", protocols);

        return data.toString();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.almende.eve.transport.TransportService#reconnect(java.lang.String)
     */
    @Override
    public void reconnect(final String agentId) throws IOException {
        final ArrayNode conns = getConns(agentId);
        if (conns != null) {
            for (final JsonNode conn : conns) {
                final ObjectNode params = (ObjectNode) conn;
                LOG.info("Initializing connection:" + agentId + " --> " + params);
                try {
                    final String encryptedUsername = params.has("username") ? params.get("username").textValue()
                            : null;
                    final String encryptedPassword = params.has("password") ? params.get("password").textValue()
                            : null;
                    final String encryptedResource = params.has("resource") ? params.get("resource").textValue()
                            : null;
                    if (encryptedUsername != null && encryptedPassword != null) {
                        final String username = EncryptionUtil.decrypt(encryptedUsername);
                        final String password = EncryptionUtil.decrypt(encryptedPassword);
                        String resource = null;
                        if (encryptedResource != null) {
                            resource = EncryptionUtil.decrypt(encryptedResource);
                        }
                        connect(agentId, username, password, resource);
                    }
                } catch (final Exception e) {
                    throw new IOException("Failed to connect XMPP.", e);
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.transport.TransportService#getKey()
     */
    @Override
    public String getKey() {
        return "xmpp://" + host + ":" + port + "/" + service;
    }

    /**
     * Ping.
     * 
     * @param senderUrl
     *            the sender url
     * @param receiver
     *            the receiver
     * @return true, if successful
     */
    public boolean ping(final String senderUrl, final String receiver) {
        final AgentConnection connection = connectionsByUrl.get(senderUrl);
        return connection.isAvailable(receiver);
    }

}