org.jboss.aerogear.sync.server.gcm.GcmDiffSyncHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.aerogear.sync.server.gcm.GcmDiffSyncHandler.java

Source

/**
 * JBoss, Home of Professional Open Source
 * Copyright Red Hat, Inc., and individual contributors.
 *
 * 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.jboss.aerogear.sync.server.gcm;

import org.jboss.aerogear.sync.Diff;
import org.jboss.aerogear.sync.Document;
import org.jboss.aerogear.sync.Edit;
import org.jboss.aerogear.sync.PatchMessage;
import org.jboss.aerogear.sync.diffmatchpatch.JsonMapper;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
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.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.aerogear.sync.server.MessageType;
import org.jboss.aerogear.sync.server.ServerSyncEngine;
import org.jivesoftware.smack.PacketListener;

public class GcmDiffSyncHandler<T, S extends Edit<? extends Diff>> implements PacketListener {

    private static final Logger logger = Logger.getLogger(GcmDiffSyncHandler.class.getCanonicalName());
    private static final String GCM_ELEMENT_NAME = "gcm";
    private static final String GCM_NAMESPACE = "google:mobile:data";
    private XMPPConnection connection;
    private final ServerSyncEngine<T, S> syncEngine;

    /**
     * Indicates whether the connection is in draining state, which means that
     * it will not accept any new downstream messages.
     */
    protected volatile boolean connectionDraining;

    static {
        ProviderManager.addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE, new PacketExtensionProvider() {
            @Override
            public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
                String json = parser.nextText();
                return new GcmPacketExtension(json);
            }
        });
    }

    public GcmDiffSyncHandler(final ServerSyncEngine<T, S> syncEngine, XMPPConnection connection) {
        this.connection = connection;
        this.syncEngine = syncEngine;
    }

    /**
     * Handles an ACK.
     *
     * <p>
     * Logs a INFO message, but subclasses could override it to properly handle
     * ACKs.
     *
     * @param jsonObject the messageObject
     */
    protected void handleAckReceipt(JsonNode jsonObject) {
        String messageId = jsonObject.get("message_id").asText();
        String from = jsonObject.get("from").asText();
        logger.log(Level.INFO, "handleAckReceipt() from: " + from + ",messageId: " + messageId);
    }

    /**
     * Handles a NACK.
     *
     * <p>
     * Logs a INFO message, but subclasses could override it to properly handle
     * NACKs.
     *
     * @param jsonObject the messageObject
     */
    protected void handleNackReceipt(JsonNode jsonObject) {
        String messageId = jsonObject.get("message_id").asText();
        String from = jsonObject.get("from").asText().replace("\"", "");
        logger.log(Level.INFO, "handleNackReceipt() from: " + from + ",messageId: " + messageId);
    }

    protected void handleControlMessage(JsonNode jsonObject) {
        logger.log(Level.INFO, "handleControlMessage(): " + jsonObject);
        String controlType = jsonObject.get("control_type").asText();
        if ("CONNECTION_DRAINING".equals(controlType)) {
            connectionDraining = true;
        } else {
            logger.log(Level.INFO, "Unrecognized control type: %s. This could happen if new features are "
                    + "added to the CCS protocol.", controlType);
        }
    }

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

    @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 body = gcmPacket.getJson();
        try {
            @SuppressWarnings("unchecked")
            JsonNode jsonObject = JsonMapper.asJsonNode(body);

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

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

                // Send ACK to CCS
                String messageId = jsonObject.get("message_id").asText();
                String clientId = jsonObject.get("from").asText().replace("\"", "");
                String ack = createJsonAck(clientId, messageId);
                send(ack);
            } else if ("ack".equals(messageType.asText())) {
                // Process Ack
                handleAckReceipt(jsonObject);
            } else if ("nack".equals(messageType.asText())) {
                // Process Nack
                handleNackReceipt(jsonObject);
            } else if ("control".equals(messageType.asText())) {
                // Process control message
                handleControlMessage(jsonObject);
            } else {
                logger.log(Level.WARNING, "Unrecognized message type: " + messageType.asText());
            }
        } catch (ParseException e) {
            logger.log(Level.SEVERE, "Error parsing JSON " + body, e);
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Failed to process packet", e);
        }
    }

    protected void messageReceived(JsonNode json) throws Exception {

        JsonNode syncMessage = JsonMapper.asJsonNode(json.get("data").get("message").asText());
        logger.info("Doc:" + json);
        final String googleRegistrationId = json.get("from").asText();
        final String diffsyncClientId = clientIdFromJson(syncMessage);
        switch (MessageType.from(syncMessage.get("msgType").asText())) {
        case ADD:
            final Document<T> doc = syncEngine.documentFromJson(syncMessage);
            final PatchMessage<S> patchMessage = addSubscriber(doc, diffsyncClientId, googleRegistrationId);
            send(GcmMessages.createJsonMessage(googleRegistrationId, "m-" + UUID.randomUUID(),
                    patchMessage.asJson()));
            break;
        case PATCH:
            final PatchMessage<S> clientPatchMessage = syncEngine.patchMessageFromJson(syncMessage.toString());
            logger.info("Client Edits=" + clientPatchMessage);
            checkForReconnect(clientPatchMessage.documentId(), googleRegistrationId, diffsyncClientId);
            patch(clientPatchMessage);
            break;
        case DETACH:
            // detach the client from a specific document.
            break;
        case UNKNOWN:
            //unknownMessageType(ctx, json);
            break;
        }
    }

    private PatchMessage<S> addSubscriber(final Document<T> document, final String clientId,
            final String googleRegistrationId) {
        final GcmSubscriber gcmSubscriber = new GcmSubscriber(clientId, googleRegistrationId, connection);
        return syncEngine.addSubscriber(gcmSubscriber, document);
    }

    private void patch(final PatchMessage<S> patchMessage) {
        syncEngine.notifySubscribers(syncEngine.patch(patchMessage));
    }

    /**
     * Sends a packet with contents provided.
     *
     * @param jsonRequest the request in JSON format
     * @throws SmackException.NotConnectedException if not connected
     */
    protected void send(String jsonRequest) throws SmackException.NotConnectedException {
        Packet request = new GcmPacketExtension(jsonRequest).toPacket();
        connection.sendPacket(request);
    }

    private void checkForReconnect(final String documentId, final String registrationId, final String clientId) {
        logger.info("Reconnected client [" + registrationId + "]. Adding as listener.");
        // the context was used to reconnect so we need to add client as a listener
        final GcmSubscriber gcmSubscriber = new GcmSubscriber(clientId, registrationId, connection);
        syncEngine.connectSubscriber(gcmSubscriber, documentId);
    }

    private static String clientIdFromJson(JsonNode syncMessage) {
        final JsonNode clientId = syncMessage.get("clientId");
        String content = null;
        if (clientId != null && !clientId.isNull()) {
            content = clientId.asText();

        }
        return content;
    }

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

        private final String json;

        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,
                    StringUtils.escapeForXML(json), GCM_ELEMENT_NAME);
        }

        public Packet toPacket() {
            Message message = new Message();
            message.addExtension(this);
            return message;
        }
    }

}