/* * The MIT License * * Copyright (c) 2017 The Things Network * * 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 org.thethingsnetwork.data.mqtt; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringJoiner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttClientPersistence; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.thethingsnetwork.data.common.AbstractClient; import org.thethingsnetwork.data.common.Connection; import org.thethingsnetwork.data.common.Subscribable; import org.thethingsnetwork.data.common.TriConsumer; import org.thethingsnetwork.data.common.events.AbstractEventHandler; import org.thethingsnetwork.data.common.events.ActivationHandler; import org.thethingsnetwork.data.common.events.ConnectHandler; import org.thethingsnetwork.data.common.events.ErrorHandler; import org.thethingsnetwork.data.common.events.EventHandler; import org.thethingsnetwork.data.common.events.UplinkHandler; import org.thethingsnetwork.data.common.messages.ActivationMessage; import org.thethingsnetwork.data.common.messages.DataMessage; import org.thethingsnetwork.data.common.messages.DownlinkMessage; import org.thethingsnetwork.data.common.messages.RawMessage; import org.thethingsnetwork.data.common.messages.UplinkMessage; /** * This is the base class to be used to interact with The Things Network Handler * * @author Romain Cambier */ public class Client extends AbstractClient { /** * Connection settings */ private final String broker; private final String appId; private MqttClientPersistence persistence = new MemoryPersistence(); private final MqttConnectOptions connOpts; /** * Event settings */ private final ExecutorService executor = Executors.newCachedThreadPool(); private final Map<Class, List<EventHandler>> handlers = new HashMap<>(); /** * Runtime vars */ private MqttClient mqttClient; /** * Create a new Client from a custom broker * * @param _broker The broker address, including protocol and port * @param _appId Your appId (or appEUI for staging) * @param _appAccessKey Your appAccessKey * @throws java.net.URISyntaxException if the provided broker address is malformed */ public Client(String _broker, String _appId, String _appAccessKey) throws URISyntaxException { this(_broker, _appId, _appAccessKey, null); } /** * Create a new Client from a custom broker * * @param _broker The broker address, including protocol and port * @param _appId Your appId (or appEUI for staging) * @param _appAccessKey Your appAccessKey * @param _connOpts Connection options to be used * @throws java.net.URISyntaxException if the provided broker address is malformed */ public Client(String _broker, String _appId, String _appAccessKey, MqttConnectOptions _connOpts) throws URISyntaxException { broker = validateBroker(_broker); appId = _appId; if (_connOpts != null) { connOpts = _connOpts; } else { connOpts = new MqttConnectOptions(); } connOpts.setUserName(_appId); connOpts.setPassword(_appAccessKey.toCharArray()); } private String validateBroker(String _source) throws URISyntaxException { URI tempBroker = new URI(_source.contains(".") ? _source : (_source + ".thethings.network")); if ("tcp".equals(tempBroker.getScheme())) { if (tempBroker.getPort() == -1) { return tempBroker.toString() + ":1883"; } else { return tempBroker.toString(); } } else if ("ssl".equals(tempBroker.getScheme())) { if (tempBroker.getPort() == -1) { return tempBroker.toString() + ":8883"; } else { return tempBroker.toString(); } } else if (tempBroker.getPort() != -1) { return "tcp://" + tempBroker.toString(); } else { return "tcp://" + tempBroker.toString() + ":1883"; } } /** * Change the default Mqtt persistence settings * * @param _persistence A custom persistence setting * @return the Client instance */ public Client setMqttPersistence(MqttClientPersistence _persistence) { if (mqttClient != null) { throw new RuntimeException("Can not be called while client is running"); } persistence = _persistence; return this; } @Override public Client start() throws MqttException, Exception { if (mqttClient != null) { throw new RuntimeException("Already connected"); } mqttClient = new MqttClient(broker, MqttClient.generateClientId(), persistence); mqttClient.connect(connOpts); mqttClient.setCallback(new MqttCallback() { @Override public void connectionLost(Throwable cause) { mqttClient = null; if (handlers.containsKey(ErrorHandler.class)) { handlers.get(ErrorHandler.class).stream().forEach((handler) -> { executor.submit(() -> { ((ErrorHandler) handler).safelyHandle(cause); }); }); } } @Override public void messageArrived(String topic, MqttMessage message) throws Exception { String[] tokens = topic.split("\\/"); if (tokens.length < 4) { return; } switch (tokens[3]) { case "up": if (handlers.containsKey(UplinkHandler.class)) { String field; if (tokens.length > 4) { field = concat(4, tokens); } else { field = null; } handlers.get(UplinkHandler.class).stream() .forEach((handler) -> { executor.submit(() -> { try { UplinkHandler uh = (UplinkHandler) handler; if (uh.matches(tokens[2], field)) { if (uh.isField()) { uh.handle(tokens[2], new RawMessage() { String str = new String(message.getPayload()); @Override public String asString() { return str; } }); } else { uh.handle(tokens[2], MAPPER.readValue(message.getPayload(), UplinkMessage.class)); } } } catch (Exception ex) { if (handlers.containsKey(ErrorHandler.class)) { handlers.get(ErrorHandler.class).stream().forEach((handler1) -> { executor.submit(() -> { ((ErrorHandler) handler1).safelyHandle(ex); }); }); } } }); }); } break; case "events": if (tokens.length > 5) { switch (tokens[4]) { case "activations": if (handlers.containsKey(ActivationHandler.class)) { handlers.get(ActivationHandler.class).stream().forEach((handler) -> { executor.submit(() -> { try { ActivationHandler ah = (ActivationHandler) handler; if (ah.matches(tokens[2])) { ah.handle(tokens[2], MAPPER.readValue(message.getPayload(), ActivationMessage.class)); } } catch (Exception ex) { if (handlers.containsKey(ErrorHandler.class)) { handlers.get(ErrorHandler.class).stream().forEach((handler1) -> { executor.submit(() -> { ((ErrorHandler) handler1).safelyHandle(ex); }); }); } } }); }); } break; default: if (handlers.containsKey(AbstractEventHandler.class)) { handlers.get(AbstractEventHandler.class).stream().forEach((handler) -> { executor.submit(() -> { try { AbstractEventHandler aeh = (AbstractEventHandler) handler; String event = concat(4, tokens); if (aeh.matches(tokens[2], event)) { aeh.handle(tokens[2], event, new RawMessage() { String str = new String(message.getPayload()); @Override public String asString() { return str; } }); } } catch (Exception ex) { if (handlers.containsKey(ErrorHandler.class)) { handlers.get(ErrorHandler.class).stream().forEach((handler1) -> { executor.submit(() -> { ((ErrorHandler) handler1).safelyHandle(ex); }); }); } } }); }); } } } break; } } @Override public void deliveryComplete(IMqttDeliveryToken token) { /** * Not supported for now */ } }); for (List<EventHandler> ehl : handlers.values()) { for (EventHandler eh : ehl) { eh.subscribe(new Subscribable() { private static final String WILDCARD_WORD = "+"; private static final String WILDCARD_PATH = "#"; @Override public void subscribe(String[] _key) throws Exception { StringJoiner sj = new StringJoiner("/"); for (String key : _key) { sj.add(key); } mqttClient.subscribe(sj.toString()); } @Override public String getWordWildcard() { return WILDCARD_WORD; } @Override public String getPathWildcard() { return WILDCARD_PATH; } }); } } if (handlers.containsKey(ConnectHandler.class)) { handlers.get(ConnectHandler.class).stream().forEach((handler) -> { executor.submit(() -> { try { ((ConnectHandler) handler).handle(() -> mqttClient); } catch (Exception ex) { if (handlers.containsKey(ErrorHandler.class)) { handlers.get(ErrorHandler.class).stream().forEach((handler1) -> { executor.submit(() -> { ((ErrorHandler) handler1).safelyHandle(ex); }); }); } } }); }); } return this; } private String concat(int _ignore, String[] _tokens) { StringJoiner sj = new StringJoiner("/"); for (int i = _ignore; i < _tokens.length; i++) { sj.add(_tokens[i]); } return sj.toString(); } @Override public Client end() throws MqttException, InterruptedException { if (mqttClient == null) { throw new RuntimeException("Not connected"); } return end(5000); } @Override public Client end(long _timeout) throws MqttException, InterruptedException { if (mqttClient == null) { throw new RuntimeException("Not connected"); } executor.awaitTermination(_timeout, TimeUnit.MILLISECONDS); mqttClient.disconnect(_timeout); if (!mqttClient.isConnected()) { mqttClient = null; } return this; } @Override public Client endNow() throws MqttException { if (mqttClient == null) { throw new RuntimeException("Not connected"); } mqttClient.disconnectForcibly(0, 0); mqttClient = null; return this; } @Override public void send(String _devId, DownlinkMessage _payload) throws Exception { mqttClient.publish(appId + "/devices/" + _devId + "/down", MAPPER.writeValueAsBytes(_payload), 0, false); } @Override public Client onConnected(Consumer<Connection> _handler) { if (mqttClient != null) { throw new RuntimeException("Already connected"); } if (!handlers.containsKey(ConnectHandler.class)) { handlers.put(ConnectHandler.class, new LinkedList<>()); } handlers.get(ConnectHandler.class).add(new ConnectHandler() { @Override public void handle(Connection _client) { _handler.accept(_client); } }); return this; } @Override public Client onError(Consumer<Throwable> _handler) { if (mqttClient != null) { throw new RuntimeException("Already connected"); } if (!handlers.containsKey(ErrorHandler.class)) { handlers.put(ErrorHandler.class, new LinkedList<>()); } handlers.get(ErrorHandler.class).add(new ErrorHandler() { @Override public void handle(Throwable _error) { _handler.accept(_error); } }); return this; } @Override public Client onMessage(String _devId, String _field, BiConsumer<String, DataMessage> _handler) { if (mqttClient != null) { throw new RuntimeException("Already connected"); } if (!handlers.containsKey(UplinkHandler.class)) { handlers.put(UplinkHandler.class, new LinkedList<>()); } handlers.get(UplinkHandler.class).add(new UplinkHandler() { @Override public void handle(String _devId, DataMessage _data) { _handler.accept(_devId, _data); } @Override public String getDevId() { return _devId; } @Override public String getField() { return _field; } }); return this; } @Override public Client onMessage(String _devId, BiConsumer<String, DataMessage> _handler) { return onMessage(_devId, null, _handler); } @Override public Client onMessage(BiConsumer<String, DataMessage> _handler) { return onMessage(null, null, _handler); } @Override public Client onActivation(String _devId, BiConsumer<String, ActivationMessage> _handler) { if (mqttClient != null) { throw new RuntimeException("Already connected"); } if (!handlers.containsKey(ActivationHandler.class)) { handlers.put(ActivationHandler.class, new LinkedList<>()); } handlers.get(ActivationHandler.class).add(new ActivationHandler() { @Override public void handle(String _devId, ActivationMessage _data) { _handler.accept(_devId, _data); } @Override public String getDevId() { return _devId; } }); return this; } @Override public Client onActivation(BiConsumer<String, ActivationMessage> _handler) { return onActivation(null, _handler); } @Override public Client onDevice(String _devId, String _event, TriConsumer<String, String, RawMessage> _handler) { if (mqttClient != null) { throw new RuntimeException("Already connected"); } if (!handlers.containsKey(AbstractEventHandler.class)) { handlers.put(AbstractEventHandler.class, new LinkedList<>()); } handlers.get(AbstractEventHandler.class).add(new AbstractEventHandler() { @Override public void handle(String _devId, String _event, RawMessage _data) { _handler.accept(_devId, _event, _data); } @Override public String getDevId() { return _devId; } @Override public String getEvent() { return _event; } }); return this; } @Override public Client onDevice(String _devId, TriConsumer<String, String, RawMessage> _handler) { return onDevice(_devId, null, _handler); } @Override public Client onDevice(TriConsumer<String, String, RawMessage> _handler) { return onDevice(null, null, _handler); } }