org.thevortex.lighting.jinks.client.PubNubServiceNotifier.java Source code

Java tutorial

Introduction

Here is the source code for org.thevortex.lighting.jinks.client.PubNubServiceNotifier.java

Source

/*
 * Copyright (c) 2015 by the author(s).
 *
 * 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.thevortex.lighting.jinks.client;

import java.io.IOException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.pubnub.api.Callback;
import com.pubnub.api.Pubnub;
import com.pubnub.api.PubnubError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thevortex.lighting.jinks.Subscription;
import org.thevortex.lighting.jinks.WinkObject;

/**
 * Uses the commercial PubNub cloud notification service.
 *
 * @author E. A. Graham Jr.
 */
class PubNubServiceNotifier<W extends WinkObject> {
    private final Logger logger = LoggerFactory.getLogger(PubNubServiceNotifier.class);

    private final String serviceType;

    private Set<WinkService.ServiceChangeListener<W>> listeners = new CopyOnWriteArraySet<>();

    // subscriptions
    private Subscription serviceSubscription;
    private Set<Subscription> itemSubscriptions;

    // the client to subscribe to
    private Pubnub pnClient;

    // parses individual items from data messages
    private final Function<JsonNode, W> itemParser;

    /**
     * @param serviceType for logging and stuff
     * @param parser      since the events come back as Wink objects
     */
    PubNubServiceNotifier(String serviceType, Function<JsonNode, W> parser) {
        this.serviceType = serviceType;
        this.itemParser = parser;
    }

    /**
     * @param listener add this
     */
    void addServiceChangeListener(WinkService.ServiceChangeListener<W> listener) {
        listeners.add(listener);
    }

    /**
     * @param listener remove this
     */
    void removeServiceChangeListener(WinkService.ServiceChangeListener<W> listener) {
        listeners.remove(listener);
    }

    /**
     * Sets up client and subscribes to channels.
     *
     * @param serviceSubscription service level
     * @param itemSubscriptions   all the bits
     */
    synchronized void init(Subscription serviceSubscription, Set<Subscription> itemSubscriptions) {
        // if nothing's changed and already running, ignore this
        if (pnClient != null && Objects.equals(this.serviceSubscription, serviceSubscription)
                && Objects.equals(this.itemSubscriptions, itemSubscriptions)) {
            logger.debug("Client for {} already initialized.", serviceType);
            return;
        }

        close();
        this.serviceSubscription = serviceSubscription;
        this.itemSubscriptions = itemSubscriptions;

        logger.info("Starting notification client for {}", serviceType);
        // subscribe with appropriate call-backs
        if (start()) {
            try {
                // over-all service listening
                if (serviceSubscription == null) {
                    logger.warn("No type subscriptions for {}", serviceType);
                } else {
                    String serviceChannel = serviceSubscription.getPubnub().getChannel();
                    logger.debug("Subscribing to {}", serviceChannel);
                    pnClient.subscribe(serviceChannel, typeCallback);
                }

                // item subscriptions
                if (itemSubscriptions == null || itemSubscriptions.isEmpty()) {
                    logger.warn("No item subscriptions for {}", serviceType);
                } else {
                    Set<String> itemChannels = itemSubscriptions.stream()
                            .map(subscription -> subscription.getPubnub().getChannel()).collect(Collectors.toSet());

                    String[] channels = itemChannels.toArray(new String[itemChannels.size()]);
                    logger.debug("Subscribing to {}", itemChannels);
                    pnClient.subscribe(channels, itemCallback);
                }
            } catch (Exception e) {
                close();
                throw new SubscriptionException("Cannot subscribe to channels", e);
            }
        }
    }

    /**
     * Starts the notification framework implementation.
     */
    private synchronized boolean start() {
        // extract and check all the keys to ensure they're the same
        String subscriptionKey = serviceSubscription == null ? null
                : serviceSubscription.getPubnub().getSubscribeKey();

        if (itemSubscriptions != null) {
            for (Subscription itemSubscription : itemSubscriptions) {
                String itemKey = itemSubscription.getPubnub().getSubscribeKey();
                if (subscriptionKey == null && itemKey != null) {
                    subscriptionKey = itemKey;
                } else if (!Objects.equals(subscriptionKey, itemKey)) {
                    logger.warn("Subscription keys differ for {}: sub = {}, item = {}", serviceType,
                            subscriptionKey, itemKey);
                }
            }
        }

        if (subscriptionKey == null) {
            logger.error("No subscription key for {}: no notifications available.", serviceType);
            return false;
        }

        pnClient = new Pubnub(null, subscriptionKey);
        return true;
    }

    /**
     * Full shutdown of the notification client.
     */
    synchronized void close() {
        if (pnClient != null) {
            logger.info("Shutdown notifications for {}", serviceType);
            pnClient.shutdown();
            pnClient = null;
        }
    }

    /**
     * Parse and fire off events to listeners for items.
     *
     * @param node the data as we know it
     * @return {@code true} if the item was parsable and notified
     */
    private boolean parseAndNotify(ObjectNode node) {
        W item = itemParser.apply(node);
        // if the parser was not able to determine what this is, we're screwed
        if (item == null || item.getId() == null) {
            logger.warn("Listeners not notified: no item for {}", node);
            return false;
        }

        // if there's a "desired" bit, this is the change-request message
        boolean isRequest = node.has("desired_state") && node.path("desired_state").size() > 0;

        // TODO check groups/scenes?

        notifyItemChange(item, isRequest);
        return true;
    }

    /**
     * Handle changes to the structure/sequence of {@link W}.
     */
    private final LoggingCallback typeCallback = new LoggingCallback() {
        @Override
        public void successCallback(String channel, Object message) {
            super.successCallback(channel, message);
            notifyServiceChange(message);
        }
    };

    /**
     * Handling single {@link W}.
     */
    private final LoggingCallback itemCallback = new LoggingCallback() {
        @Override
        public void successCallback(String channel, Object message) {
            super.successCallback(channel, message);
            String stringMessage = message.toString();

            try {
                ObjectNode node = (ObjectNode) WinkClient.MAPPER.readTree(stringMessage);
                // different package
                if (node.has("data")) {
                    node = (ObjectNode) node.get("data");
                }
                if (!parseAndNotify(node)) {
                    logger.error("Parser cannot understand message:\n{}", stringMessage);
                }
            } catch (IOException e) {
                logger.error("Unable to parse message:\n{}\n", stringMessage, e);
            }
        }
    };

    /**
     * Fire off events to listeners for items.
     *
     * @param item      the item
     * @param isRequest whether or not it's a change request or an actual change
     */
    private void notifyItemChange(W item, boolean isRequest) {
        for (WinkService.ServiceChangeListener<W> listener : listeners) {
            // rule 1: always isolate your listeners because they may be crap
            try {
                if (isRequest) {
                    listener.itemChangeRequested(item);
                } else {
                    listener.itemChanged(item);
                }
            } catch (Exception e) {
                logger.error("Unable to notify listener {}", listener, e);
            }
        }
    }

    /**
     * Fires off the message to listeners for service change.
     *
     * @param message the message
     */
    private void notifyServiceChange(Object message) {
        for (WinkService.ServiceChangeListener listener : listeners) {
            // rule 1: always isolate your listeners because they may be crap
            try {
                listener.serviceChanged(message.toString());
            } catch (Exception e) {
                logger.error("Unable to notify listener {}", listener, e);
            }
        }
    }

    /**
     * Logs everything.
     */
    private abstract class LoggingCallback extends Callback {
        @Override
        public void successCallback(String channel, Object message) {
            logger.debug("Success {} channel {}: {}", serviceType, channel, message);
        }

        @Override
        public void errorCallback(String channel, PubnubError error) {
            logger.error("Error {} channel {}: {}", serviceType, channel, error);
        }

        @Override
        public void connectCallback(String channel, Object message) {
            logger.debug("Connect {} channel {}: {}", serviceType, channel, message);
        }

        @Override
        public void reconnectCallback(String channel, Object message) {
            logger.warn("Reconnect {} channel {}: {}", serviceType, channel, message);
        }

        @Override
        public void disconnectCallback(String channel, Object message) {
            logger.warn("Disconnect {} channel {}: {}", serviceType, channel, message);
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        close();
    }
}