com.mch.registry.ccs.server.CcsClient.java Source code

Java tutorial

Introduction

Here is the source code for com.mch.registry.ccs.server.CcsClient.java

Source

/*
 *
 * Copyright 2014 Swiss TPH/Isabel Mueller
 *
 * Portions Copyright 2014 Wolfram Rittmeyer.
 *
 * Portions Copyright Google Inc.
 *
 * 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 com.mch.registry.ccs.server;

import com.mch.registry.ccs.server.com.mch.registry.ccs.server.data.MySqlHandler;
import com.mch.registry.ccs.server.com.mch.registry.ccs.server.data.Notification;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketInterceptor;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.DefaultPacketExtension;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.provider.PacketExtensionProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.util.StringUtils;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import org.xmlpull.v1.XmlPullParser;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.SSLSocketFactory;

/**
 * Smack implementation of a client for GCM Cloud Connection Server.
 * Most of it has been taken more or less verbatim from Googles
 * documentation: http://developer.android.com/google/gcm/ccs.html
 * But some additions have been made by Wolfram Rittmeyer. Bigger changes are annotated like that:
 * "/// new Rittmeyer".
 * "/// new Mueller" or /*created by Isa
 */
public class CcsClient {

    public static final Logger logger = Logger.getLogger(CcsClient.class.getName());
    public static final String GCM_SERVER = "gcm.googleapis.com";
    public static final int GCM_PORT = 5235;
    public static final String GCM_ELEMENT_NAME = "gcm";
    public static final String GCM_NAMESPACE = "google:mobile:data";
    static Random random = new Random();
    /// new Rittmeyer: some additional instance and class members
    private static CcsClient sInstance = null;
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    XMPPConnection connection;
    ConnectionConfiguration config;
    private String mApiKey = null;
    private String mProjectId = null;
    private boolean mDebuggable = false;

    private CcsClient(String projectId, String apiKey, boolean debuggable) {
        this();
        mApiKey = apiKey;
        mProjectId = projectId;
        mDebuggable = debuggable;
    }

    private CcsClient() {
        // Add GcmPacketExtension
        ProviderManager.getInstance().addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE,
                new PacketExtensionProvider() {

                    @Override
                    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
                        String json = parser.nextText();
                        GcmPacketExtension packet = new GcmPacketExtension(json);
                        return packet;
                    }
                });
    }

    public static CcsClient getInstance() {
        if (sInstance == null) {
            throw new IllegalStateException("You have to prepare the client first");
        }
        return sInstance;
    }

    public static CcsClient prepareClient(String projectId, String apiKey, boolean debuggable) {
        synchronized (CcsClient.class) {
            if (sInstance == null) {
                sInstance = new CcsClient(projectId, apiKey, debuggable);
            }
        }
        return sInstance;
    }

    /**
     * Creates a JSON encoded ACK message for an upstream message received from
     * an application.
     *
     * @param to        RegistrationId of the device who sent the upstream message.
     * @param messageId messageId of the upstream message to be acknowledged to
     *                  CCS.
     * @return JSON encoded ack.
     */
    public static String createJsonAck(String to, String messageId) {
        Map<String, Object> message = new HashMap<String, Object>();
        message.put("message_type", "ack");
        message.put("to", to);
        message.put("message_id", messageId);
        return JSONValue.toJSONString(message);
    }

    /// new Rittmeyer: customized version of the standard handleIncomingDateMessage method
    /**
     * Creates a JSON encoded NACK message for an upstream message received from
     * an application.
     *
     * @param to        RegistrationId of the device who sent the upstream message.
     * @param messageId messageId of the upstream message to be acknowledged to
     *                  CCS.
     * @return JSON encoded ack.
     */
    public static String createJsonNack(String to, String messageId) {
        Map<String, Object> message = new HashMap<String, Object>();
        message.put("message_type", "ack");
        message.put("to", to);
        message.put("message_id", messageId);
        return JSONValue.toJSONString(message);
    }

    /// new Rittmeyer: was part of the previous method
    /**
     * Returns a random message id to identify a message, but it is not guaranteed to be unique
     */
    public static String getRandomMessageId() {
        return "m-" + Long.toString(random.nextLong());
    }

    /**
     * Creates a JSON encoded GCM message.
     *
     * @param to             RegistrationId of the target device (Required).
     * @param messageId      Unique messageId for which CCS will send an "ack/nack"
     *                       (Required).
     * @param payload        Message content intended for the application. (Optional).
     * @param collapseKey    GCM collapse_key parameter (Optional).
     * @param timeToLive     GCM time_to_live parameter (Optional).
     * @param delayWhileIdle GCM delay_while_idle parameter (Optional).
     * @return JSON encoded GCM message.
     */
    public static String createJsonMessage(String to, String messageId, Map<String, String> payload,
            String collapseKey, Long timeToLive, Boolean delayWhileIdle) {
        return createJsonMessage(
                createAttributeMap(to, messageId, payload, collapseKey, timeToLive, delayWhileIdle));
    }

    public static String createJsonMessage(Map map) {
        return JSONValue.toJSONString(map);
    }

    public static Map createAttributeMap(String to, String messageId, Map<String, String> payload,
            String collapseKey, Long timeToLive, Boolean delayWhileIdle) {
        Map<String, Object> message = new HashMap<String, Object>();
        if (to != null) {
            message.put("to", to);
        }
        if (collapseKey != null) {
            message.put("collapse_key", collapseKey);
        }
        if (timeToLive != null) {
            message.put("time_to_live", timeToLive);
        }
        if (delayWhileIdle != null && delayWhileIdle) {
            message.put("delay_while_idle", true);
        }
        if (messageId != null) {
            message.put("message_id", messageId);
        }
        message.put("data", payload);
        return message;
    }

    /**
     * Handles an upstream data message from a device application
     */
    public void handleIncomingDataMessage(CcsMessage msg) {
        if (msg.getPayload().get("action") != null) {
            PayloadProcessor processor = ProcessorFactory.getProcessor(msg.getPayload().get("action"));
            processor.handleMessage(msg);
        }
    }

    private CcsMessage getMessage(Map<String, Object> jsonObject) {
        String from = jsonObject.get("from").toString();

        // PackageName of the application that sent this message.
        String category = jsonObject.get("category").toString();

        // unique id of this message
        String messageId = jsonObject.get("message_id").toString();

        @SuppressWarnings("unchecked")
        Map<String, String> payload = (Map<String, String>) jsonObject.get("data");

        CcsMessage msg = new CcsMessage(from, category, messageId, payload);

        return msg;
    }

    /**
     * Handles an ACK.
     * <p/>
     * <p/>
     * By default, it only logs a INFO message, but subclasses could override it
     * to properly handle ACKS.
     */
    public void handleAckReceipt(Map<String, Object> jsonObject) {
        String messageId = jsonObject.get("message_id").toString();
        String from = jsonObject.get("from").toString();
        logger.log(Level.INFO, "handleAckReceipt() from: " + from + ", messageId: " + messageId);
    }

    /**
     * Handles a NACK.
     * <p/>
     * <p/>
     * By default, it only logs a INFO message, but subclasses could override it
     * to properly handle NACKS.
     */
    public void handleNackReceipt(Map<String, Object> jsonObject) {
        String messageId = jsonObject.get("message_id").toString();
        String from = jsonObject.get("from").toString();
        logger.log(Level.INFO, "handleNackReceipt() from: " + from + ", messageId: " + messageId);
    }

    /**
     * Connects to GCM Cloud Connection Server using the supplied credentials.
     *
     * @throws XMPPException
     */
    public void connect() throws XMPPException {
        config = new ConnectionConfiguration(GCM_SERVER, GCM_PORT);
        config.setSecurityMode(SecurityMode.enabled);
        config.setReconnectionAllowed(true);
        config.setRosterLoadedAtLogin(false);
        config.setSendPresence(false);
        config.setSocketFactory(SSLSocketFactory.getDefault());

        // NOTE: Set to true to launch a window with information about packets sent and received
        config.setDebuggerEnabled(mDebuggable);

        // -Dsmack.debugEnabled=true
        XMPPConnection.DEBUG_ENABLED = true;

        connection = new XMPPConnection(config);
        connection.connect();

        connection.addConnectionListener(new ConnectionListener() {

            @Override
            public void reconnectionSuccessful() {
                logger.info("Reconnecting..");
            }

            @Override
            public void reconnectionFailed(Exception e) {
                logger.log(Level.INFO, "Reconnection failed.. ", e);
            }

            @Override
            public void reconnectingIn(int seconds) {
                logger.log(Level.INFO, "Reconnecting in %d secs", seconds);
            }

            @Override
            public void connectionClosedOnError(Exception e) {
                logger.log(Level.INFO, "Connection closed on error.");
            }

            @Override
            public void connectionClosed() {
                logger.info("Connection closed.");
            }
        });

        // Handle incoming packets
        connection.addPacketListener(new PacketListener() {

            @Override
            public void processPacket(Packet packet) {
                logger.log(Level.INFO, "Received: " + packet.toXML());
                Message incomingMessage = (Message) packet;
                GcmPacketExtension gcmPacket = (GcmPacketExtension) incomingMessage.getExtension(GCM_NAMESPACE);
                String json = gcmPacket.getJson();
                try {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> jsonMap = (Map<String, Object>) JSONValue.parseWithException(json);
                    handleMessage(jsonMap);
                } catch (ParseException e) {
                    logger.log(Level.SEVERE, "Error parsing JSON " + json, e);
                } catch (Exception e) {
                    logger.log(Level.SEVERE, "Couldn't send echo.", e);
                }
            }
        }, new PacketTypeFilter(Message.class));

        // Log all outgoing packets
        connection.addPacketInterceptor(new PacketInterceptor() {
            @Override
            public void interceptPacket(Packet packet) {
                logger.log(Level.INFO, "Sent: {0}", packet.toXML());
            }
        }, new PacketTypeFilter(Message.class));

        connection.login(mProjectId + "@gcm.googleapis.com", mApiKey);

        logger.log(Level.INFO, "logged in: " + mProjectId);
    }

    private void handleMessage(Map<String, Object> jsonMap) {
        // present for "ack"/"nack", null otherwise
        Object messageType = jsonMap.get("message_type");

        if (messageType == null) {
            CcsMessage msg = getMessage(jsonMap);
            // Normal upstream data message
            try {
                handleIncomingDataMessage(msg);
                // Send ACK to CCS
                String ack = createJsonAck(msg.getFrom(), msg.getMessageId());
                send(ack);
            } catch (Exception e) {
                // Send NACK to CCS
                String nack = createJsonNack(msg.getFrom(), msg.getMessageId());
                send(nack);
            }
        } else if ("ack".equals(messageType.toString())) {
            // Process Ack
            handleAckReceipt(jsonMap);
        } else if ("nack".equals(messageType.toString())) {
            // Process Nack
            handleNackReceipt(jsonMap);
        } else {
            logger.log(Level.WARNING, "Unrecognized message type (%s)", messageType.toString());
        }
    }

    /**
     * Sends a downstream GCM message.
     */
    public void send(String jsonRequest) {
        Packet request = new GcmPacketExtension(jsonRequest).toPacket();
        connection.sendPacket(request);
    }

    /**
     * XMPP Packet Extension for GCM Cloud Connection Server.
     */
    class GcmPacketExtension extends DefaultPacketExtension {

        String json;

        public GcmPacketExtension(String json) {
            super(GCM_ELEMENT_NAME, GCM_NAMESPACE);
            this.json = json;
        }

        public String getJson() {
            return json;
        }

        @Override
        public String toXML() {
            return String.format("<%s xmlns=\"%s\">%s</%s>", GCM_ELEMENT_NAME, GCM_NAMESPACE, json,
                    GCM_ELEMENT_NAME);
        }

        public Packet toPacket() {
            return new Message() {
                // Must override toXML() because it includes a <body>
                @Override
                public String toXML() {

                    StringBuilder buf = new StringBuilder();
                    buf.append("<message");
                    if (getXmlns() != null) {
                        buf.append(" xmlns=\"").append(getXmlns()).append("\"");
                    }
                    if (getLanguage() != null) {
                        buf.append(" xml:lang=\"").append(getLanguage()).append("\"");
                    }
                    if (getPacketID() != null) {
                        buf.append(" id=\"").append(getPacketID()).append("\"");
                    }
                    if (getTo() != null) {
                        buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\"");
                    }
                    if (getFrom() != null) {
                        buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\"");
                    }
                    buf.append(">");
                    buf.append(GcmPacketExtension.this.toXML());
                    buf.append("</message>");
                    return buf.toString();
                }
            };
        }
    }

    /// new: Mueller
    /**
     * Checks if time lies off-hours (not between 6:00 and 22:29)
     */
    private static boolean isOffHours() {

        Calendar cal = Calendar.getInstance();
        cal.getTime();
        Integer hour = cal.get(Calendar.HOUR_OF_DAY);
        Integer minute = cal.get(Calendar.MINUTE);

        if ((hour > 22 && minute > 29) && hour < 6) {
            return true;
        } else {
            return false;
        }
    }

    /// new Mueller: Send messages from queue
    /**
     * Sends messages to registered devices
     */
    public static void main(String[] args) {

        Config config = new Config();
        final String projectId = config.getProjectId();
        final String key = config.getKey();

        final CcsClient ccsClient = CcsClient.prepareClient(projectId, key, true);

        try {
            ccsClient.connect();
        } catch (XMPPException e) {
            logger.log(Level.WARNING, "XMPP Exception ", e);
        }

        final Runnable sendNotifications = new Runnable() {
            public void run() {
                try {
                    logger.log(Level.INFO, "Working Q!");
                    if (!isOffHours()) {
                        //Prepare downstream message
                        String toRegId = "";
                        String message = "";
                        String messageId = "";
                        Map<String, String> payload = new HashMap<String, String>();
                        String collapseKey = null;
                        Long timeToLive = 10000L;
                        Boolean delayWhileIdle = true;
                        String messagePrefix = "";
                        int notificationQueueID = 0;
                        boolean sucessfullySent = false;

                        //Read from mysql database
                        MySqlHandler mysql = new MySqlHandler();
                        ArrayList<Notification> queue = new ArrayList<Notification>();

                        for (int i = 1; i < 3; i++) {
                            queue = mysql.getNotificationQueue(i);
                            if (queue.size() > 0) {

                                switch (i) {
                                case 1:
                                    messagePrefix = "_V: ";
                                    break;
                                case 2:
                                    messagePrefix = "_R: ";
                                    break;
                                default:
                                    messagePrefix = "";
                                    logger.log(Level.WARNING, "Unknown message type!");
                                }

                                Notification notification = new Notification();
                                Iterator<Notification> iterator = queue.iterator();

                                while (iterator.hasNext()) {
                                    notification = iterator.next();

                                    toRegId = notification.getGcmRegID();
                                    message = notification.getNotificationText();
                                    notificationQueueID = notification.getNotificationQueueID();
                                    messageId = "m-" + Long.toString(random.nextLong());

                                    payload = new HashMap<String, String>();
                                    payload.put("message", messagePrefix + message);

                                    try {
                                        // Send the downstream message to a device.
                                        ccsClient.send(createJsonMessage(toRegId, messageId, payload, collapseKey,
                                                timeToLive, delayWhileIdle));
                                        sucessfullySent = true;
                                        logger.log(Level.INFO, "Message sent. ID: " + notificationQueueID
                                                + ", RegID: " + toRegId + ", Text: " + message);
                                    } catch (Exception e) {
                                        mysql.prepareNotificationForTheNextDay(notificationQueueID);
                                        sucessfullySent = false;
                                        logger.log(Level.WARNING,
                                                "Message could not be sent! ID: " + notificationQueueID
                                                        + ", RegID: " + toRegId + ", Text: " + message);
                                    }

                                    if (sucessfullySent) {
                                        mysql.moveNotificationToHistory(notificationQueueID);
                                    }
                                }
                            } else {
                                logger.log(Level.INFO, "No notifications to send. Type: " + Integer.toString(i));
                            }
                        }
                    }
                } catch (Exception e) {
                    logger.log(Level.WARNING, "Exception ", e);
                }
            }
        };

        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        //Start when server starts and every 30 minutes after
        ScheduledFuture task = executor.scheduleAtFixedRate(sendNotifications, 0, 30, TimeUnit.MINUTES);
        try {
            task.get();
        } catch (ExecutionException e) {
            logger.log(Level.SEVERE, "Exception ", e);
        } catch (InterruptedException e) {
            logger.log(Level.SEVERE, "Exception ", e);
        }
        task.cancel(false);

        try {
            executor.shutdown();
            executor.awaitTermination(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            logger.log(Level.SEVERE, "Exception ", e);
        }

    }

}