com.wealdtech.gcm.GCMClient.java Source code

Java tutorial

Introduction

Here is the source code for com.wealdtech.gcm.GCMClient.java

Source

/*
 * Copyright 2012 - 2014 Weald Technology Trading Limited
 *
 * 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.wealdtech.gcm;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.wealdtech.ServerError;
import com.wealdtech.jackson.WealdMapper;
import org.jivesoftware.smack.*;
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.tcp.XMPPTCPConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;

import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * A CCS client for Google Cloud Messaging, with the ability to send and receive messages.
 */
public class GCMClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(GCMClient.class);

    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();
    XMPPConnection connection;
    ConnectionConfiguration config;

    /**
     * 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);
        }

        @SuppressWarnings("unused")
        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();
                //        }
            };
        }
    }

    public GCMClient() {
        // Add GcmPacketExtension
        ProviderManager.addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE, new PacketExtensionProvider() {

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

    /**
     * Returns a random message id to uniquely identify a message.
     * <p/>
     * <p>Note: This is generated by a pseudo random number generator for illustration purpose, and is not guaranteed to be unique.
     */
    public String getRandomMessageId() {
        return "m-" + Long.toHexString(random.nextLong());
    }

    /**
     * Sends a downstream GCM message.
     */
    public void send(String jsonRequest) {
        final Packet request = new GcmPacketExtension(jsonRequest).toPacket();
        try {
            connection.sendPacket(request);
        } catch (final SmackException.NotConnectedException e) {
            LOGGER.warn("Attempt to send a packet when not connected");
            // TODO what to do with the packet?
            throw new ServerError("Attempt to send a packet when not connected");
        }
    }

    /**
     * Handles an upstream data message from a device application.
     * <p/>
     * <p>This sample echo server sends an echo message back to the device. Subclasses should override this method to process an
     * upstream message.
     */
    public void handleIncomingDataMessage(final Map<String, Object> jsonObject) {
        final String from = jsonObject.get("from").toString();

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

        // Use the packageName as the collapseKey in the echo packet
        final String collapseKey = "echo:CollapseKey";
        @SuppressWarnings("unchecked")
        final Map<String, String> payload = (Map<String, String>) jsonObject.get("data");
        payload.put("ECHO", "Application: " + category);

        // Send an ECHO response back
        final String echo = createJsonMessage(from, getRandomMessageId(), payload, collapseKey, null, false);
        send(echo);
    }

    /**
     * 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) {
        final String messageId = jsonObject.get("message_id").toString();
        final String from = jsonObject.get("from").toString();
        LOGGER.info("handleAckReceipt() from: {}, messageId: {}", from, 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) {
        final String messageId = jsonObject.get("message_id").toString();
        final String from = jsonObject.get("from").toString();
        LOGGER.info("handleNackReceipt() from: {}, messageId: {}", from, messageId);
    }

    /**
     * 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(final String to, final String messageId,
            final Map<String, String> payload, final String collapseKey, final Long timeToLive,
            final Boolean delayWhileIdle) {
        final Map<String, Object> message = new HashMap<>();
        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);
        }
        message.put("message_id", messageId);
        message.put("data", payload);
        return encodeMessageAsJson(message);
    }

    /**
     * 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) {
        final Map<String, Object> message = new HashMap<>();
        message.put("message_type", "ack");
        message.put("to", to);
        message.put("message_id", messageId);
        return encodeMessageAsJson(message);
    }

    /**
     * Encode a message as JSON, capturing errors
     * @param message the message
     * @return a JSON string
     */
    private static String encodeMessageAsJson(final Map<String, Object> message) {
        try {
            return WealdMapper.getMapper().writeValueAsString(message);
        } catch (JsonProcessingException e) {
            throw new ServerError(
                    "Failed to create JSON message from \"" + message.toString() + "\": " + e.getMessage());
        }
    }

    /**
     * Connects to GCM Cloud Connection Server using the supplied credentials.
     *
     * @param username GCM_SENDER_ID@gcm.googleapis.com
     * @param password API Key
     *
     * @throws XMPPException
     */
    public void connect(String username, String password) throws XMPPException {
        config = new ConnectionConfiguration(GCM_SERVER, GCM_PORT);
        config.setSecurityMode(ConnectionConfiguration.SecurityMode.enabled);
        config.setReconnectionAllowed(true);
        config.setRosterLoadedAtLogin(false);
        config.setSendPresence(false);
        config.setSocketFactory(SSLSocketFactory.getDefault());
        config.setDebuggerEnabled(true);

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

        connection = new XMPPTCPConnection(config);
        try {
            connection.connect();
        } catch (final SmackException e) {
            throw new ServerError("Smack exception", e);
        } catch (final IOException e) {
            throw new ServerError("IO exception", e);
        }

        connection.addConnectionListener(new ConnectionListener() {

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

            @Override
            public void reconnectionFailed(Exception e) {
                LOGGER.info("Reconnection failed.. ", e);
            }

            @Override
            public void reconnectingIn(int seconds) {
                LOGGER.info("Reconnecting in {} secs", seconds);
            }

            @Override
            public void connectionClosedOnError(Exception e) {
                LOGGER.info("Connection closed on error.");
            }

            @Override
            public void connected(final XMPPConnection connection) {
                LOGGER.info("Connected.");
            }

            @Override
            public void authenticated(final XMPPConnection connection) {
                LOGGER.info("Authenticated.");
            }

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

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

            @Override
            public void processPacket(Packet packet) {
                LOGGER.info("Received: {}", packet.toXML());
                final Message incomingMessage = (Message) packet;
                final GcmPacketExtension gcmPacket = (GcmPacketExtension) incomingMessage
                        .getExtension(GCM_NAMESPACE);
                final String json = gcmPacket.getJson();
                try {
                    final Map<String, Object> jsonObject = WealdMapper.getMapper().readValue(json,
                            new TypeReference<Map<String, Object>>() {
                            });

                    // present for "ack"/"nack", null otherwise
                    final Object messageType = jsonObject.get("message_type");

                    if (messageType == null) {
                        // Normal upstream data message
                        handleIncomingDataMessage(jsonObject);

                        // Send ACK to CCS
                        String messageId = jsonObject.get("message_id").toString();
                        String from = jsonObject.get("from").toString();
                        String ack = createJsonAck(from, messageId);
                        send(ack);
                    } else if ("ack".equals(messageType.toString())) {
                        // Process Ack
                        handleAckReceipt(jsonObject);
                    } else if ("nack".equals(messageType.toString())) {
                        // Process Nack
                        handleNackReceipt(jsonObject);
                    } else {
                        LOGGER.warn("Unrecognized message type {}", messageType.toString());
                    }
                } catch (JsonMappingException e) {
                    LOGGER.error("Error mapping JSON {}: {}", json, e.getMessage());
                } catch (JsonParseException e) {
                    LOGGER.error("Error parsing JSON {}: {}", json, e.getMessage());
                } catch (IOException e) {
                    LOGGER.error("General IO error", e);
                }
            }
        }, new PacketTypeFilter(Message.class));

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

        try {
            connection.login(username, password);
        } catch (SmackException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        final String userName = "your_sender_id" + "@gcm.googleapis.com";
        final String password = "your_api_key";

        final GCMClient ccsClient = new GCMClient();

        try {
            ccsClient.connect(userName, password);
        } catch (XMPPException e) {
            e.printStackTrace();
        }

        // Send a sample hello downstream message to a device.
        final String toRegId = "RegistrationIdOfTheTargetDevice";
        final String messageId = ccsClient.getRandomMessageId();
        final Map<String, String> payload = new HashMap<>();
        payload.put("Hello", "World");
        payload.put("CCS", "Dummy Message");
        payload.put("EmbeddedMessageId", messageId);
        final String collapseKey = "sample";
        final Long timeToLive = 10000L;
        final Boolean delayWhileIdle = true;
        ccsClient.send(createJsonMessage(toRegId, messageId, payload, collapseKey, timeToLive, delayWhileIdle));
    }
}