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