com.pubkit.platform.messaging.protocol.pkmp.PKMPInBoundEventHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.pubkit.platform.messaging.protocol.pkmp.PKMPInBoundEventHandler.java

Source

/* Copyright (c) 2015 32skills Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.pubkit.platform.messaging.protocol.pkmp;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.WebSocketSession;

import com.lmax.disruptor.EventHandler;
import com.pubkit.platform.commons.RoquitoKeyGenerator;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPBasePayload;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPConnAck;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPConnect;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPDisconnect;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPPayload;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPPubAck;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPPubMessage;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPPublish;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPSubsAck;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPSubscribe;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPUnSubsAck;
import com.pubkit.platform.messaging.protocol.pkmp.proto.PKMPUnSubscribe;
import com.pubkit.platform.model.Application;
import com.pubkit.platform.model.DataConstants;
import com.pubkit.platform.model.DeviceInfo;
import com.pubkit.platform.notification.SimpleGcmPushNotification;
import com.pubkit.platform.service.ApplicationService;
import com.pubkit.platform.service.DeviceInfoService;
import com.pubkit.platform.service.MessageStoreService;
import com.pubkit.platform.service.QueueService;
import com.pubkit.platform.service.StatsService;

public class PKMPInBoundEventHandler implements EventHandler<PKMPEvent> {

    private static Logger LOG = LoggerFactory.getLogger(PKMPInBoundEventHandler.class);

    private final RoquitoKeyGenerator keyGenerator = new RoquitoKeyGenerator();

    private ApplicationService applicationService;
    private MessageStoreService messageStoreService;
    private QueueService queueService;
    private DeviceInfoService deviceInfoService;
    private StatsService statsService;

    public ApplicationService getApplicationService() {
        return applicationService;
    }

    public void setApplicationService(ApplicationService applicationService) {
        this.applicationService = applicationService;
    }

    public MessageStoreService getMessagingService() {
        return messageStoreService;
    }

    public void setMessagingService(MessageStoreService messageStoreService) {
        this.messageStoreService = messageStoreService;
    }

    public QueueService getQueueService() {
        return queueService;
    }

    public void setQueueService(QueueService queueService) {
        this.queueService = queueService;
    }

    public void setDeviceInfoService(DeviceInfoService deviceInfoService) {
        this.deviceInfoService = deviceInfoService;
    }

    public void setStatsService(StatsService statsService) {
        this.statsService = statsService;
    }

    @Override
    public void onEvent(PKMPEvent event, long sequence, boolean endOfBatch) throws Exception {
        LOG.debug("Input event received with sequence:" + sequence);
        PKMPPayload pKMPPayload = event.getPayload();
        switch (pKMPPayload.getType()) {
        case PKMPBasePayload.CONNECT:
            handleConnect(new PKMPConnect(pKMPPayload), event.getSession());
            break;
        case PKMPBasePayload.SUBSCRIBE:
            handleSubscribe(new PKMPSubscribe(pKMPPayload), event.getSession());
            break;
        case PKMPBasePayload.UNSUBSCRIBE:
            handleUnSubscribe(new PKMPUnSubscribe(pKMPPayload), event.getSession());
            break;
        case PKMPBasePayload.PUBLISH:
            handlePublish(new PKMPPublish(pKMPPayload), event.getSession());
            break;
        case PKMPBasePayload.DISCONNECT:
            sendDisconnect(pKMPPayload.getClientId(), event.getSession());
            break;
        default:
            break;
        }
    }

    private void handleConnect(PKMPConnect connect, WebSocketSession session) {
        boolean valid = validateApplication(connect, session);
        if (!valid) {
            sendPayload(new PKMPConnAck(connect.getClientId(), PKMPConnAck.APPLICATION_INVALID), session);
            return;
        }
        LOG.debug("Connecting client {" + connect.getClientId() + "}");
        PKMPConnection existingConnection = messageStoreService.getConnection(connect.getClientId());
        if (existingConnection != null) {
            existingConnection.setSessionId(session.getId());
            messageStoreService.addConnection(connect.getClientId(), existingConnection);
        } else {
            PKMPConnection newConnection = new PKMPConnection(connect.getClientId(), connect.getSourceUserId(),
                    session.getId(), connect.getApplicationId(), connect.getApiVersion());
            // add new connection
            messageStoreService.addConnection(connect.getClientId(), newConnection);
        }
        String accessToken = keyGenerator.getSecureSessionId();
        boolean success = messageStoreService.saveAccessToken(session.getId(), accessToken);
        if (success) {
            // set active session
            messageStoreService.addSession(session);
            LOG.debug("Connected client {" + connect.getClientId() + "}");
            sendPayload(new PKMPConnAck(connect.getClientId(), session.getId(), accessToken), session);
        } else {
            LOG.debug("PKMPConnection failed for client {" + connect.getClientId() + "}");
            sendPayload(new PKMPConnAck(connect.getClientId(), PKMPConnAck.CONNECTION_FAILED), session);
        }
    }

    private void handleSubscribe(PKMPSubscribe subscribe, WebSocketSession session) {
        String topic = subscribe.getTopic();
        if (topic == null || StringUtils.isEmpty(topic)) {
            LOG.error("Client {" + subscribe.getClientId() + "} failed to subscribe topic {" + subscribe.getTopic()
                    + "}. Invalid topic");

            PKMPSubsAck errorAck = new PKMPSubsAck(subscribe.getClientId(), topic, PKMPSubsAck.INVALID_TOPIC);
            sendPayload(errorAck, session);

            return;
        }
        boolean tokenValid = validateAccessToken(subscribe.getClientId(), subscribe.getSessionToken(), session);
        if (!tokenValid) {
            LOG.error("Client {" + subscribe.getClientId() + "} failed to subscribe topic {" + topic
                    + "}. Invalid token");
            PKMPSubsAck errorAck = new PKMPSubsAck(subscribe.getClientId(), topic, PKMPSubsAck.INVALID_TOKEN);
            sendPayload(errorAck, session);

            return;
        }

        PKMPConnection pKMPConnection = messageStoreService.getConnection(subscribe.getClientId());
        if (pKMPConnection != null && pKMPConnection.getSessionId().equals(session.getId())) {
            messageStoreService.subscribeTopic(topic, pKMPConnection);
            LOG.debug("Client id {" + subscribe.getClientId() + "} subscribed to topic {" + topic + "}");

            PKMPSubsAck pKMPSubsAck = new PKMPSubsAck(subscribe.getClientId(), topic);
            sendPayload(pKMPSubsAck, session);
        } else {
            LOG.error("Client {" + subscribe.getClientId() + "} failed to subscribe topic {" + topic
                    + "}. No connection found so closing connection");

            PKMPSubsAck errorAck = new PKMPSubsAck(subscribe.getClientId(), topic, PKMPSubsAck.CONNECTION_ERROR);
            sendPayload(errorAck, session);
        }
    }

    private void handleUnSubscribe(PKMPUnSubscribe unsubscribe, WebSocketSession session) {
        String topic = unsubscribe.getTopic();
        if (topic == null || StringUtils.isEmpty(topic)) {
            LOG.error("Client {" + unsubscribe.getClientId() + "} failed to unsubscribe topic {"
                    + unsubscribe.getTopic() + "}. Invalid topic");

            PKMPUnSubsAck errorAck = new PKMPUnSubsAck(unsubscribe.getClientId(), topic, PKMPSubsAck.INVALID_TOPIC);
            sendPayload(errorAck, session);

            return;
        }
        boolean tokenValid = validateAccessToken(unsubscribe.getClientId(), unsubscribe.getSessionToken(), session);
        if (!tokenValid) {
            LOG.error("Client {" + unsubscribe.getClientId() + "} failed to unsubscribe topic {" + topic
                    + "}. Invalid token");
            PKMPUnSubsAck errorAck = new PKMPUnSubsAck(unsubscribe.getClientId(), topic, PKMPSubsAck.INVALID_TOKEN);
            sendPayload(errorAck, session);

            return;
        }

        PKMPConnection pKMPConnection = messageStoreService.getConnection(unsubscribe.getClientId());
        if (pKMPConnection != null && pKMPConnection.getSessionId().equals(session.getId())) {
            messageStoreService.subscribeTopic(topic, pKMPConnection);
            LOG.debug("Client id {" + unsubscribe.getClientId() + "} unsubscribed to topic {" + topic + "}");

            PKMPUnSubsAck pKMPUnSubsAck = new PKMPUnSubsAck(unsubscribe.getClientId(), topic);
            sendPayload(pKMPUnSubsAck, session);
        } else {
            LOG.error("Client {" + unsubscribe.getClientId() + "} failed to unsubscribe topic {" + topic
                    + "}. No connection found so closing connection");

            PKMPUnSubsAck errorAck = new PKMPUnSubsAck(unsubscribe.getClientId(), topic,
                    PKMPSubsAck.CONNECTION_ERROR);
            sendPayload(errorAck, session);
        }
    }

    private void handlePublish(PKMPPublish publish, WebSocketSession session) {
        String topic = publish.getTopic();
        if (topic == null || "".equals(topic)) {
            LOG.debug("PKMPPublish failed. Null or empty topic name.");

            PKMPPubAck errorAck = new PKMPPubAck(publish.getClientId(), topic, PKMPPubAck.INVALID_TOPIC);
            sendPayload(errorAck, session);

            return;
        }
        boolean tokenValid = validateAccessToken(publish.getClientId(), publish.getSessionToken(), session);
        if (!tokenValid) {
            LOG.error("Client {" + publish.getClientId() + "} failed to publish on topic {" + topic
                    + "}. Invalid token");
            PKMPPubAck errorAck = new PKMPPubAck(publish.getClientId(), topic, PKMPSubsAck.INVALID_TOKEN);
            sendPayload(errorAck, session);

            return;
        }

        List<PKMPConnection> subscribers = messageStoreService.getAllSubscribers(topic);
        for (PKMPConnection subscriber : subscribers) {
            if (subscriber.getClientId().equals(publish.getClientId())) {
                continue;
            }
            WebSocketSession subscriberSession = messageStoreService.getSession(subscriber.getSessionId());
            if (subscriberSession != null && subscriberSession.isOpen()) {
                PKMPPubMessage pKMPPubMessage = new PKMPPubMessage(subscriber.getClientId(), publish.getClientId());
                pKMPPubMessage.setTopic(publish.getTopic());
                pKMPPubMessage.setMessageId(publish.getMessageId());
                pKMPPubMessage.setData(publish.getData());

                sendPayload(pKMPPubMessage, subscriberSession);
            } else {
                LOG.debug("Session not found for client with client id {" + subscriber.getClientId() + "} "
                        + "and session id {" + subscriber.getSessionId() + "}");

                // send push notification if possible for inactive subscriber
                handleInactiveSubscriber(subscriber, publish.getMessageId());

                if (subscriberSession != null) {
                    sendDisconnect(subscriber.getClientId(), subscriberSession);
                }
            }
            statsService.incrMessageCount();
        }
        sendPayload(new PKMPPubAck(publish.getClientId(), topic), session);
    }

    private void sendDisconnect(String clientId, WebSocketSession session) {
        LOG.info("Closing and invalidating client session for {" + clientId + "}");

        messageStoreService.removeConnection(clientId);
        messageStoreService.removeSession(session);
        messageStoreService.invalidateSessionToken(clientId);

        PKMPDisconnect pKMPDisconnect = new PKMPDisconnect(clientId);

        sendPayload(pKMPDisconnect, session);
    }

    private boolean validateAccessToken(String clientId, String accessToken, WebSocketSession session) {
        boolean tokenValid = messageStoreService.isAccessTokenValid(accessToken);
        if (!tokenValid) {
            PKMPDisconnect pKMPDisconnect = new PKMPDisconnect(clientId);
            LOG.debug("Invalid access token. Closing the client connection {" + clientId + "}");
            pKMPDisconnect.setData("Invalid access token. Closing the client connection {" + clientId + "}");
            sendPayload(pKMPDisconnect, session);

            return false;
        }
        return true;
    }

    private boolean validateApplication(PKMPConnect pKMPConnect, WebSocketSession session) {
        String applicationId = pKMPConnect.getApplicationId();

        String closeMessage = null;
        boolean valid = true;
        if (applicationId == null) {
            LOG.debug("Null application id received, closing connection");
            closeMessage = "Null application id received, closing connection";
            valid = false;
        }
        //TODO: Make sure this is not blocking this thread for too long!
        Application savedApplication = applicationService.findByApplicationId(applicationId);
        if (savedApplication == null) {
            LOG.debug("Application not found, closing connection");
            closeMessage = "Application not found, closing connection";
            valid = false;
        }
        if (!savedApplication.getApplicationKey().equals(pKMPConnect.getApiKey())) {
            LOG.debug("Application key does not match, closing connection");
            closeMessage = "Application key does not match, closing connection";
            valid = false;
        }
        if (!valid) {
            PKMPDisconnect pKMPDisconnect = new PKMPDisconnect(pKMPConnect.getClientId());
            pKMPDisconnect.setData(closeMessage);
            sendPayload(pKMPDisconnect, session);
        }
        return valid;
    }

    private void sendPayload(PKMPBasePayload pKMPBasePayload, WebSocketSession session) {
        this.queueService.publishOutputMessageEvent(pKMPBasePayload, session);
    }

    /*
     * TODO: See if this is not blocking message processor too often!!
     */
    private void handleInactiveSubscriber(PKMPConnection subscriber, String data) {
        long startTime = System.currentTimeMillis() / 1000;

        List<DeviceInfo> devices = deviceInfoService.getDeviceInfoForUserId(subscriber.getAppId(),
                subscriber.getSourceUserId(), null);
        if (devices != null && !devices.isEmpty()) {
            List<String> registrationIds = new ArrayList<String>();
            for (DeviceInfo device : devices) {
                if (DataConstants.DEVICE_TYPE_ANDROID.equals(device.getDeviceType())) {
                    registrationIds.add(device.getRegistrationId());
                } else if (DataConstants.DEVICE_TYPE_IOS.equals(device.getDeviceType())) {
                } else {
                    //ignore!
                }
            }
            if (!registrationIds.isEmpty()) {
                SimpleGcmPushNotification gcm = new SimpleGcmPushNotification();

                gcm.setApplicationId(subscriber.getAppId());
                gcm.setApplicationVersion(subscriber.getApiVersion());
                gcm.setRegistrationIds(registrationIds);

                Map<String, String> dataMap = new HashMap<>();
                dataMap.put("data", data);
                gcm.setData(dataMap);

                gcm.setMulticast(registrationIds.size() > 1);

                queueService.sendGcmPushNotification(gcm);
            }
        }
        long endTime = System.currentTimeMillis() / 1000;
        LOG.info("Time spent on sending push notification to inactive subscriber in seconds:"
                + (endTime - startTime));
    }
}