/* * Dog - Addons - Mqtt * * Copyright (c) 2013-2014 Dario Bonino * * 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 it.polito.elite.dog.addons.mqtt.bridge; import it.polito.elite.dog.addons.mqtt.bridge.tasks.MqttNotificationDeliveryTask; import it.polito.elite.dog.addons.mqtt.bridge.tasks.MqttStateDeliveryTask; import it.polito.elite.dog.addons.mqtt.bridge.translators.NotificationTranslator; import it.polito.elite.dog.addons.mqtt.bridge.translators.SimpleNotificationTranslator; import it.polito.elite.dog.addons.mqtt.bridge.translators.SimpleStateTranslator; import it.polito.elite.dog.addons.mqtt.bridge.translators.StateTranslator; import it.polito.elite.dog.addons.mqtt.library.transport.MqttAsyncDispatcher; import it.polito.elite.dog.addons.mqtt.library.transport.MqttQos; import it.polito.elite.dog.core.library.model.DeviceStatus; import it.polito.elite.dog.core.library.model.notification.Notification; import it.polito.elite.dog.core.library.util.LogHelper; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.Vector; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.osgi.service.log.LogService; /** * Implements a bridge between inner monitor (states) and notification events * and externally specified Mqtt brokers. For each connected broker, a specific * translator can be specified allowing for simultaneous delivery of events to * different channels, each having a different format. * * While base translators are provided, specific translator implementations can * easily be provided by registering instances of {@link NotificationTranslator} * or {@link StateTranslator} services in the osgi framework. * * @author <a href="mailto:dario.bonino@polito.it">Dario Bonino</a> * */ public class MqttBridge implements EventHandler, ManagedService { // ------- static key definitions for configuration properties -------- private static final String MQTT_BROKER = "mqtt_broker"; private static final String MQTT_ROOT_TOPIC = "mqtt_root_topic"; private static final String MQTT_NOTIFICATION_ROOT_TOPIC = "mqtt_notification_root_topic"; private static final String MQTT_STATE_ROOT_TOPIC = "mqtt_state_root_topic"; private static final String MQTT_BRIDGE_NOTIFICATIONS_FLAG = "bridge_notifications"; private static final String MQTT_BRIDGE_STATES_FLAG = "bridge_states"; private static final String MQTT_NOTIFICATION_TRANSLATOR = "notification2mqtt"; private static final String MQTT_STATE_TRANSLATOR = "state2mqtt"; private static final String MQTT_NOTIFICATION_TRANSLATOR_SUFFIX = "-notification2mqtt"; private static final String MQTT_STATE_TRANSLATOR_SUFFIX = "-state2mqtt"; private static final String MQTT_QOS = "mqtt_qos"; // the class logger private LogHelper logger; // the context associated to this bundle private BundleContext context; // the MQTT broker addresses, might be more than one... // each single broker address is in the form **host:port** private Set<String> brokerAddresses; // the MQTT qos to use private MqttQos mqttQos; // the MQTT root topic private String mqttRootTopic; // the MQTT notification topic private String mqttNotificationRootTopic; // the MQTT state topic private String mqttStateRootTopic; // the flags indicating what to bridge private boolean bridgeNotifications = false; private boolean bridgeStates = false; // the base event translator classes, typically external services private String baseNotificationTranslatorClass; private String baseStateTranslatorClass; // the default translators to use before failing private NotificationTranslator defaultNotificationTranslator; private StateTranslator defaultStateTranslator; // the map of broker-specific translators private Map<String, String> brokerSpecificNotificationTranslatorClasses; private Map<String, String> brokerSpecificStateTranslatorClasses; // maps a broker-address to the corresponding MQTT client private Map<String, MqttAsyncDispatcher> broker2Client; // the service registration handler private ServiceRegistration<EventHandler> eventHandler; // the message delivery service private ExecutorService messageDeliveryService; /** * The class constructor, called before activation, initializes common * datastructure * * This implementation keeps on translator instance per broker, per type of * event and tries to parallelize as much as possible the event delivery * over MQTT process. This is clearly not efficient in terms of memeory * occupation, in case problems arise, a shared translator approach can be * exploited. */ public MqttBridge() { // build the needed data structures this.brokerAddresses = new HashSet<String>(); this.brokerSpecificNotificationTranslatorClasses = new HashMap<String, String>(); this.brokerSpecificStateTranslatorClasses = new HashMap<String, String>(); this.broker2Client = new HashMap<String, MqttAsyncDispatcher>(); // initialize the default translators this.defaultNotificationTranslator = new SimpleNotificationTranslator(); this.defaultStateTranslator = new SimpleStateTranslator(); // initialize the message dispatching thread pool this.messageDeliveryService = Executors.newFixedThreadPool(4); } /** * Handle the bundle activation */ protected void activate(BundleContext ctx) { // init the logger with a null logger this.logger = new LogHelper(ctx); // log the activation this.logger.log(LogService.LOG_INFO, "Activated Notification to MQTT bridge"); // store the bundle context this.context = ctx; } /** * Handle the bundle de-activation */ protected void deactivate() { // log the de-activation if (this.logger != null) this.logger.log(LogService.LOG_INFO, "Deactivated Notification to MQTT bridge"); // de-register the event handler this.unRegisterService(); } /** * register this service as an EventHandler */ @SuppressWarnings("unchecked") private void registerService() { // register the EventHandler service Hashtable<String, Object> p = new Hashtable<String, Object>(); // Add this bundle as a listener of the MonitorAdmin events and of all // notifications p.put(EventConstants.EVENT_TOPIC, new String[] { "org/osgi/service/monitor/MonitorEvent", "it/polito/elite/dog/core/library/model/notification/*" }); // TODO filter upon ClockNotification, EventNotification, etc. with the // event_filter constant this.eventHandler = (ServiceRegistration<EventHandler>) this.context .registerService(EventHandler.class.getName(), this, p); } /** * remove this service from the framework */ private void unRegisterService() { if (this.eventHandler != null) this.eventHandler.unregister(); } @Override public void handleEvent(Event event) { // handle states only if enabled if ((this.bridgeStates) && (event.getTopic() .equals("org/osgi/service/monitor/MonitorEvent"))) { // handle states only if (event.getProperty("mon.listener.id") == null) { // handle states DeviceStatus currentDeviceState = null; try { // Try the deserialization of the DeviceStatus // (property mon.statusvariable.value) currentDeviceState = DeviceStatus .deserializeFromString((String) event .getProperty("mon.statusvariable.value")); } catch (Exception e) { this.logger.log(LogService.LOG_ERROR, "Device status deserialization error " + e.getClass().getSimpleName()); } // handle this.handleStates(currentDeviceState); } } // handle notifications if ((this.bridgeNotifications) && (event.getTopic() .startsWith("it/polito/elite/dog/core/library/model/notification/"))) { // handle notifications Object eventContent = event.getProperty(EventConstants.EVENT); if (eventContent instanceof Notification) this.handleNotifications((Notification) eventContent); } } private void handleNotifications(Notification eventContent) { // iterate over brokers for (String brokerAddress : this.brokerAddresses) { // ------- prepare dispatching to the given broker // notification name (type) String notificationType = eventContent.getClass().getSimpleName() .toLowerCase(); // device name String deviceUri = eventContent.getDeviceUri(); // topic String topic = this.mqttRootTopic + "/" + this.mqttNotificationRootTopic + "/" + notificationType + "/" + deviceUri; // dispatcher MqttAsyncDispatcher dispatcher = this.broker2Client .get(brokerAddress); // get the translator class String translatorClass = this.brokerSpecificNotificationTranslatorClasses .get(brokerAddress); // use default if null if (translatorClass == null) translatorClass = this.baseNotificationTranslatorClass; // check that all needed information is available if ((dispatcher != null) && (notificationType != null) && (deviceUri != null) && (topic != null) && (translatorClass != null)) { // create the delivery task MqttNotificationDeliveryTask task = new MqttNotificationDeliveryTask( dispatcher, this.mqttQos, topic, translatorClass, this.defaultNotificationTranslator, eventContent, this.context); // submit for delivery this.messageDeliveryService.execute(task); } } } private void handleStates(DeviceStatus currentDeviceState) { // iterate over brokers for (String brokerAddress : this.brokerAddresses) { // ------- prepare dispatching to the given broker // device name String deviceUri = currentDeviceState.getDeviceURI(); // topic String topic = this.mqttRootTopic + "/" + this.mqttStateRootTopic + "/" + deviceUri; // dispatcher MqttAsyncDispatcher dispatcher = this.broker2Client .get(brokerAddress); // get the translator class String translatorClass = this.brokerSpecificStateTranslatorClasses .get(brokerAddress); // use default if null if (translatorClass == null) translatorClass = this.baseStateTranslatorClass; // check that all needed information is available if ((dispatcher != null) && (deviceUri != null) && (topic != null) && (translatorClass != null)) { // create the delivery task MqttStateDeliveryTask task = new MqttStateDeliveryTask( dispatcher, this.mqttQos, topic, translatorClass, this.defaultStateTranslator, currentDeviceState, this.context); // submit for delivery this.messageDeliveryService.execute(task); } } } /** * Handles the bundle configuration updates, used to get the MQTT broker * data */ @Override public void updated(Dictionary<String, ?> properties) throws ConfigurationException { // check not null if ((properties != null) && (!properties.isEmpty())) { // configuration data is supposed to be present... // ----- HANDLE broker addresses ------------- String brokerAddressConfig = (String) properties .get(MqttBridge.MQTT_BROKER); // check not null / empty if ((brokerAddressConfig != null) && (!brokerAddressConfig.isEmpty())) { // prepare the set of addresses if (!this.brokerAddresses.isEmpty()) this.brokerAddresses.clear(); // check if more than one address has been specified if (brokerAddressConfig.contains(",")) { // split over the comma String addresses[] = brokerAddressConfig.split(","); // check the broker address for (int i = 0; i < addresses.length; i++) { // clean the address from leading and trailing spaces String cleanAddress = addresses[i].trim(); // add the address this.brokerAddresses.add(cleanAddress); } } else { // just add a single address this.brokerAddresses.add(brokerAddressConfig.trim()); } } // ----- HANDLE MQTT qos -------------------- String mqttQos = (String) properties.get(MqttBridge.MQTT_QOS); if ((mqttQos != null) && (!mqttQos.isEmpty())) this.mqttQos = MqttQos.valueOf(mqttQos); else this.mqttQos = MqttQos.AT_MOST_ONCE; // ----- HANDLE MQTT topics ------------------ // root topic String mqttRootTopic = (String) properties .get(MqttBridge.MQTT_ROOT_TOPIC); if ((mqttRootTopic != null) && (!mqttRootTopic.isEmpty())) this.mqttRootTopic = mqttRootTopic; // notification root topic String mqttNotificationRootTopic = (String) properties .get(MqttBridge.MQTT_NOTIFICATION_ROOT_TOPIC); if ((mqttNotificationRootTopic != null) && (!mqttNotificationRootTopic.isEmpty())) this.mqttNotificationRootTopic = mqttNotificationRootTopic; // state root topic String mqttStateRootTopic = (String) properties .get(MqttBridge.MQTT_STATE_ROOT_TOPIC); if ((mqttStateRootTopic != null) && (!mqttStateRootTopic.isEmpty())) this.mqttStateRootTopic = mqttStateRootTopic; // ----- HANDLE publish flag // notifications Boolean bridgeNotifications = Boolean.valueOf((String) properties .get(MqttBridge.MQTT_BRIDGE_NOTIFICATIONS_FLAG)); if (bridgeNotifications != null) this.bridgeNotifications = bridgeNotifications.booleanValue(); // states Boolean bridgeStates = Boolean.valueOf((String) properties .get(MqttBridge.MQTT_BRIDGE_STATES_FLAG)); if (bridgeStates != null) this.bridgeStates = bridgeStates.booleanValue(); // ----- HANDLE default translators // default notification translator String defaultNotificationTranslatorClass = (String) properties .get(MqttBridge.MQTT_NOTIFICATION_TRANSLATOR); if ((defaultNotificationTranslatorClass != null) && (!defaultNotificationTranslatorClass.isEmpty())) this.baseNotificationTranslatorClass = defaultNotificationTranslatorClass; // default state translator String defaultStateTranslatorClass = (String) properties .get(MqttBridge.MQTT_STATE_TRANSLATOR); if ((defaultStateTranslatorClass != null) && (!defaultStateTranslatorClass.isEmpty())) this.baseStateTranslatorClass = defaultStateTranslatorClass; // ----- HANDLE broker-specific translators String notificationTranslator = null; String stateTranslator = null; for (String brokerAddress : this.brokerAddresses) { notificationTranslator = (String) properties.get(brokerAddress .replaceAll(":", "-") + MqttBridge.MQTT_NOTIFICATION_TRANSLATOR_SUFFIX); stateTranslator = (String) properties.get(brokerAddress .replaceAll(":", "-") + MqttBridge.MQTT_STATE_TRANSLATOR_SUFFIX); // check not null nor empty if ((notificationTranslator != null) && (!notificationTranslator.isEmpty())) // store the broker-specifc notification translator this.brokerSpecificNotificationTranslatorClasses.put( brokerAddress, notificationTranslator); // check not null or empty if ((stateTranslator != null) && (!stateTranslator.isEmpty())) // store the broker-specific state translator this.brokerSpecificStateTranslatorClasses.put( brokerAddress, stateTranslator); } // once all configurations have been successfully handled, and // mandatory parameters filled, the service can register as an // EventHandler. if ((this.mqttRootTopic != null) && (this.mqttNotificationRootTopic != null) && (this.mqttStateRootTopic != null) && (!this.brokerAddresses.isEmpty())) { // propagate / handle configuration changes this.initClients(); // register the service if not already registered if (this.eventHandler == null) this.registerService(); } } } /** * Initialize and keep up-to-date the set of MQTT clients used by the bridge */ private void initClients() { // differential approach // search for removed brokers List<String> toRemove = new Vector<String>(); for (String brokerAddress : this.broker2Client.keySet()) { if (!this.brokerAddresses.contains(brokerAddress)) toRemove.add(brokerAddress); } // remove brokers for (String brokerAddress : toRemove) { // get the dispatcher associated to the missing broker MqttAsyncDispatcher dispatcher = this.broker2Client .get(brokerAddress); // if connected, disconnect it if (dispatcher.isConnected()) dispatcher.disconnect(); // remove the dispatcher this.broker2Client.remove(dispatcher); } // add new brokers for (String brokerAddress : this.brokerAddresses) { // check if not exists already if (this.broker2Client.get(brokerAddress) == null) { // create the auto-reconnecting client MqttAsyncDispatcher client = new MqttAsyncDispatcher("tcp://" + brokerAddress, UUID.randomUUID().toString(), null, null, true, this.logger); // connect the client client.connect(); // store the client this.broker2Client.put(brokerAddress, client); } } } }