com.mobicage.rogerthat.xmpp.CallBackApiXMPPListener.java Source code

Java tutorial

Introduction

Here is the source code for com.mobicage.rogerthat.xmpp.CallBackApiXMPPListener.java

Source

/*
 * Copyright (c) 2011-2014, MOBICAGE NV
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 * must display the following acknowledgement:
 * This product includes software developed by Mobicage NV.
 * 4. Neither the name of the Mobicage NV nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY MOBICAGE NV ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL MOBICAGE NV BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @@license_version:1.7@@
 */
package com.mobicage.rogerthat.xmpp;

import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.bind.DatatypeConverter;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.provider.ProviderManager;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import com.mobicage.rogerthat.CallbackApiReceiver;
import com.mobicage.rogerthat.CallbackDedup;
import com.mobicage.rogerthat.CallbackProcessor;
import com.mobicage.rogerthat.callbacks.RequestContext;

public class CallBackApiXMPPListener implements CallbackApiReceiver {

    private static final String ROGERTHAT_CALLBACK_BOT = "bot@callback.rogerth.at";
    private static final Logger log = Logger.getLogger(CallBackApiXMPPListener.class.getName());

    static {
        ProviderManager.getInstance().addExtensionProvider(CallbackRequestExtension.CALL,
                CallbackRequestExtension.MOBICAGE_COMM, new CallbackRequestExtensionProvider());
        SmackConfiguration.setKeepAliveInterval(30);
    }

    private volatile String xmppService;
    private volatile String xmppUsername;
    private volatile String xmppPassword;
    private volatile String sik;
    private boolean logTraffic = false;
    private volatile CallbackProcessor processor = new CallbackProcessor();
    private XMPPConnection conn;
    private final LinkedBlockingQueue<Runnable> tasks = new LinkedBlockingQueue<Runnable>();
    private volatile XmppConnectionStatus status = XmppConnectionStatus.Initial;
    private volatile String statusLine = "";
    private volatile Thread connectionThread = null;
    private volatile Runnable onConnected = null;
    private volatile Runnable onConnectionFailed = null;
    private volatile Runnable onAuthenticated = null;
    private volatile Runnable onAuthenticationFailed = null;
    private volatile CallbackDedup callbackDedup;

    /**
     * Set a callback de-duplicator
     * 
     * @param callbackDedup
     */
    @Override
    public void setCallbackDedup(CallbackDedup callbackDedup) {
        this.callbackDedup = callbackDedup;
    }

    /**
     * XMPP host setup to allow federating with rogerth.at XMPP servers. Eg: "jabber.org" if the used XMPP account is
     * john.doe@jabber.org
     * 
     * @param xmppService
     */
    public void setXmppService(String xmppService) {
        this.xmppService = xmppService;
    }

    /**
     * XMPP username. Eg: "john.doe" if the used XMPP account is john.doe@jabber.org
     * 
     * @param xmppUsername
     */
    public void setXmppUsername(String xmppUsername) {
        this.xmppUsername = xmppUsername;
    }

    /**
     * XMPP password.
     * 
     * @param xmppPassword
     */
    public void setXmppPassword(String xmppPassword) {
        this.xmppPassword = xmppPassword;
    }

    /**
     * Rogerthat Service Identifier Key. See Rogerthat service configuration.
     * 
     * @param sik
     */
    @Override
    public void setSikKey(String sik) {
        this.sik = sik;
    }

    /**
     * Sets whether traffic to and from Rogerthat should be logged.
     * 
     * @param logTraffic
     */
    @Override
    public void setLogTraffic(boolean logTraffic) {
        this.logTraffic = logTraffic;
    }

    /**
     * Runnable executed if a connection with the XmppService has been established. Note: connected does not mean
     * authenticated.
     * 
     * @param onConnected
     */
    public void setOnConnected(Runnable onConnected) {
        this.onConnected = onConnected;
    }

    /**
     * Runnable executed if no connection can be made.
     * 
     * @param onConnectionFailed
     */
    public void setOnConnectionFailed(Runnable onConnectionFailed) {
        this.onConnectionFailed = onConnectionFailed;
    }

    /**
     * Runnable executed if the established connection is authenticated.
     * 
     * @param onAuthenticated
     */
    public void setOnAuthenticated(Runnable onAuthenticated) {
        this.onAuthenticated = onAuthenticated;
    }

    /**
     * Runnable executed if the established connection cannot be authenticated. After this, the connection to the
     * XmppService will be dropped.
     * 
     * @param onAuthenticationFailed
     */
    public void setOnAuthenticationFailed(Runnable onAuthenticationFailed) {
        this.onAuthenticationFailed = onAuthenticationFailed;
    }

    /**
     * Status of the XMPP connection.
     * 
     * @return the status of the XMPP connection.
     */
    public XmppConnectionStatus getStatus() {
        return status;
    }

    /**
     * Error description of the current status.
     * 
     * @return the error description of the current status.
     */
    public String getStatusLine() {
        return statusLine;
    }

    /**
     * Disconnect the XMPP connection and stop listening for Rogerthat API callbacks.
     * 
     * @throws InterruptedException
     */
    public void stopListening() throws InterruptedException {
        if (connectionThread == null)
            return;
        tasks.add(new Runnable() {
            @Override
            public void run() {
                throw new StopListeningException();
            }
        });
    }

    /**
     * Establish an XMPP connection to XmppService and listen for Rogerthat API callbacks.
     */
    public void startListening() {

        if (connectionThread != null) {
            throw new RuntimeException("Previous connection has not yet been closed!");
        }

        if (xmppUsername == null || xmppService == null || xmppPassword == null || sik == null)
            throw new RuntimeException("Not enough information present to setup an xmpp connection");

        final ConnectionListener connectionListener = new ConnectionListener() {
            @Override
            public void reconnectionSuccessful() {
                log.info("Reconnection to jabber server succeeded.");
                status = XmppConnectionStatus.Connected;
            }

            @Override
            public void reconnectionFailed(Exception e) {
                log.info("Reconnection to jabber server failed.");
            }

            @Override
            public void reconnectingIn(int seconds) {
                log.info("Reconnecting to jabber in " + seconds + " seconds ...");
                status = XmppConnectionStatus.Reconnecting;
            }

            @Override
            public void connectionClosedOnError(Exception e) {
                log.info("Connection closed to jabber due to " + e.toString());
            }

            @Override
            public void connectionClosed() {
                log.info("Connection to jabber closed.");
            }
        };

        tasks.clear();

        connectionThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        Runnable task = tasks.take();
                        task.run();
                    }
                } catch (StopListeningException e) {
                    disconnect(connectionListener);
                    status = XmppConnectionStatus.Closed;
                    statusLine = "";
                } catch (Throwable e) {
                    disconnect(connectionListener);
                    status = XmppConnectionStatus.Closed;
                    statusLine = "Connection interrupted.";
                } finally {
                    connectionThread = null;
                }
            }
        });
        connectionThread.setName("Rogerthat callback listener");
        connectionThread.setDaemon(true);
        connectionThread.start();

        tasks.add(new Runnable() {
            @Override
            public void run() {
                ConnectionConfiguration conf = new ConnectionConfiguration(xmppService);

                status = XmppConnectionStatus.Connecting;

                log.info("Connecting to jabber server ...");
                conn = new XMPPConnection(conf);
                try {
                    conn.connect();
                } catch (XMPPException e) {
                    status = XmppConnectionStatus.ConnectionFailed;
                    statusLine = "Failed to reach Rogerthat servers.\n" + e.getMessage();
                    conn = null;
                    connectionThread = null;
                    if (onConnectionFailed != null)
                        try {
                            onConnectionFailed.run();
                        } catch (Throwable t) {
                            log.log(Level.WARNING, "Failure in onConnectionFailed handler.", t);
                        }
                    throw new RuntimeException(e); // Stop thread.
                }

                if (onConnected != null)
                    try {
                        onConnected.run();
                    } catch (Throwable t) {
                        log.log(Level.WARNING, "Failure in onConnected handler.", t);
                    }

                conn.addConnectionListener(connectionListener);

                SASLAuthentication.supportSASLMechanism("PLAIN", 0);

                PacketFilter filter = new PacketFilter() {
                    @Override
                    public boolean accept(Packet packet) {
                        boolean accept = packet instanceof Message
                                && ROGERTHAT_CALLBACK_BOT.equals(packet.getFrom());
                        if (!accept)
                            log.info("Dropping packet:\n" + packet.toXML());
                        return accept;
                    }
                };

                conn.addPacketListener(new PacketListener() {
                    @Override
                    public void processPacket(Packet packet) {
                        log.info("Processing packet:\n" + packet.toXML());
                        if (!(packet instanceof Message)) {
                            log.info("Ignoring non message packet.");
                            return;
                        }
                        Message message = (Message) packet;
                        PacketExtension extension = packet.getExtension("call", "mobicage:comm");
                        if (extension == null || !(extension instanceof CallbackRequestExtension)) {
                            log.info("Ignoring incomplete packet.");
                            return;
                        }
                        CallbackRequestExtension call = (CallbackRequestExtension) extension;
                        if (!sik.equals(call.getSik())) {
                            log.info("Ignoring packet with incorrect sik.");
                            return;
                        }
                        String json;
                        try {
                            json = new String(DatatypeConverter.parseBase64Binary(call.getBase64Body()), "UTF-8");
                        } catch (UnsupportedEncodingException e) {
                            log.log(Level.WARNING, "Could not decode base64 packet.", e);
                            return;
                        }
                        final JSONObject request = (JSONObject) JSONValue.parse(json);

                        if (logTraffic)
                            log.info(String.format("Incoming Rogerthat API Callback.\nSIK: %s\n\n%s", sik, json));

                        final String id = (String) request.get("id");

                        if (callbackDedup != null) {
                            byte[] response = callbackDedup.getResponse(id);
                            if (response != null) {
                                Message resultMessage = new Message(message.getFrom());
                                resultMessage.setFrom(message.getTo());
                                resultMessage.addExtension(new CallbackResponseExtension(sik,
                                        DatatypeConverter.printBase64Binary(response)));
                                log.info("Sending message:\n" + resultMessage.toXML());
                                conn.sendPacket(resultMessage);
                                return;
                            }
                        }

                        final JSONObject result = new JSONObject();
                        final RequestContext requestContext = new RequestContext(id, sik);
                        try {
                            processor.process(request, result, requestContext);
                        } finally {
                            try {
                                StringWriter writer = new StringWriter();
                                try {
                                    result.writeJSONString(writer);
                                    writer.flush();
                                    json = writer.toString();

                                    if (logTraffic)
                                        log.info("Returning result:\n" + json);

                                } finally {
                                    writer.close();
                                }
                            } catch (IOException e) {
                                log.log(Level.SEVERE, "Could not write json object to string", e);
                                return;
                            }
                            Message resultMessage = new Message(message.getFrom());
                            resultMessage.setFrom(message.getTo());
                            try {
                                byte[] response = json.getBytes("UTF-8");
                                resultMessage.addExtension(new CallbackResponseExtension(sik,
                                        DatatypeConverter.printBase64Binary(response)));
                                if (callbackDedup != null) {
                                    callbackDedup.storeResponse(id, response);
                                }
                            } catch (UnsupportedEncodingException e) {
                                log.log(Level.SEVERE, "Could not add result to message packet", e);
                                return;
                            }
                            log.info("Sending message:\n" + resultMessage.toXML());
                            conn.sendPacket(resultMessage);
                        }

                    }
                }, filter);

                try {
                    conn.login(xmppUsername, xmppPassword);
                } catch (XMPPException e1) {
                    status = XmppConnectionStatus.ConnectionFailed;
                    statusLine = "Failed to authenticate jabber connection. Verify your configuration.\n"
                            + e1.getMessage();
                    conn = null;
                    connectionThread = null;
                    if (onAuthenticationFailed != null)
                        try {
                            onAuthenticationFailed.run();
                        } catch (Throwable t) {
                            log.log(Level.WARNING, "Failure in onAuthenticationFailed handler.", t);
                        }
                    throw new RuntimeException(); // Stop thread.
                }

                status = XmppConnectionStatus.Connected;

                if (onAuthenticated != null)
                    try {
                        onAuthenticated.run();
                    } catch (Throwable t) {
                        log.log(Level.WARNING, "Failure in onAuthenticated handler.", t);
                    }
            }
        });

    }

    /**
     * Disconnects xmpp connection. Should only be called from connectionThread.
     */
    private void disconnect(ConnectionListener connectionListener) {
        if (conn == null)
            return;
        conn.removeConnectionListener(connectionListener);
        conn.disconnect();
        conn = null;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.test.TestHandler handler) {
        processor.testTestHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.messaging.UpdateHandler handler) {
        processor.messagingUpdateHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.messaging.PokeHandler handler) {
        processor.messagingPokeHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.messaging.FormUpdateHandler handler) {
        processor.messagingFormUpdateHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.messaging.FlowMemberResultHandler handler) {
        processor.messagingFlowMemberResultHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.friend.InviteResultHandler handler) {
        processor.friendInviteResultHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.friend.InvitedHandler handler) {
        processor.friendInvitedHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.friend.BrokeUpHandler handler) {
        processor.friendBrokeUpHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.friend.InReachHandler handler) {
        processor.friendInReachHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.friend.OutOfReachHandler handler) {
        processor.friendOutOfReachHandler = handler;
    }

    @Override
    public void subscribe(com.mobicage.rogerthat.callbacks.system.ApiCallHandler handler) {
        processor.systemApiCallHandler = handler;
    }
}