com.telekom.m2m.cot.restsdk.smartrest.SmartCepConnector Maven / Gradle / Ivy
package com.telekom.m2m.cot.restsdk.smartrest;
import com.telekom.m2m.cot.restsdk.CloudOfThingsRestClient;
import com.telekom.m2m.cot.restsdk.util.CotSdkException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import static com.telekom.m2m.cot.restsdk.smartrest.SmartRestApi.LINE_BREAK_PATTERN;
import static com.telekom.m2m.cot.restsdk.smartrest.SmartRestApi.MSG_REALTIME_ADVICE;
import static com.telekom.m2m.cot.restsdk.smartrest.SmartRestApi.MSG_REALTIME_HANDSHAKE;
import static com.telekom.m2m.cot.restsdk.smartrest.SmartRestApi.MSG_REALTIME_SUBSCRIBE;
import static com.telekom.m2m.cot.restsdk.smartrest.SmartRestApi.MSG_REALTIME_UNSUBSCRIBE;
import static com.telekom.m2m.cot.restsdk.smartrest.SmartRestApi.MSG_REALTIME_CONNECT;
import static com.telekom.m2m.cot.restsdk.smartrest.SmartRestApi.MSG_REALTIME_XID;
/**
* The SmartCepConnector handles subscriptions and holds the connection to the CEP notification service.
*
* It passes on notifications to {@link SmartListener}s.
*
* Basic usage is like this:
*
* - store at least one {@link SmartResponseTemplate}, that can extract data from the notifications that you are interested in.
* - call {@link SmartCepConnector#subscribe(String, Set)} to subscribe to a channel.
* - extend {@link SmartListener} and do something with {@link SmartNotification}s passed to {@link SmartListener#onNotification(SmartNotification)}.
* - add the {@link SmartListener} via {@link SmartCepConnector#addListener(SmartListener)}.
* - call {@link SmartCepConnector#connect()} to establish the connection and start the background polling thread.
*
*
*
* See SmartCepConnectorTest and SmartRestRealTimeIT for examples.
*
*/
public class SmartCepConnector implements Runnable {
private static final List KNOWN_ERROR_MESSAGES = Arrays.asList(40, 41, 42, 43, 45, 50);
private static final int DEFAULT_READ_TIMEOUT_MILLIS = 60000;
private static final int DEFAULT_RECONNECT_INTERVAL_MILLIS = 100;
private static final int THREAD_JOIN_GRACE_MILLIS = 1000;
private CloudOfThingsRestClient cloudOfThingsRestClient;
private String xId;
private String clientId;
// Read timeout in milliseconds for the connect request:
private int timeout = DEFAULT_READ_TIMEOUT_MILLIS;
// Interval in milliseconds between connect requests:
private int interval = DEFAULT_RECONNECT_INTERVAL_MILLIS;
//
private boolean connected = false;
private volatile boolean shallDisconnect = false; // Volatile to be synced with the polling thread.
private Thread pollingThread;
// Have to be thread safe because they will be used from the polling thread too:
private Map> subscriptions = new ConcurrentHashMap<>(); // mapping channel names to sets of X-Ids.
private Set listeners = new CopyOnWriteArraySet<>();
/**
* Construct a new SmartCepConnector with a default X-Id. That Id determines which response templates will be used.
*
* Additional X-Ids can be specified for each subscription.
*
* @param cloudOfThingsRestClient the client to use for connection to the cloud
* @param xId default X-Id for all requests. Additional X-Ids are possible per subscription.
*/
public SmartCepConnector(CloudOfThingsRestClient cloudOfThingsRestClient, String xId) {
this.cloudOfThingsRestClient = cloudOfThingsRestClient;
this.xId = xId;
}
/**
* Subscribe to a notification channel. Will take effect immediately, even for currently running connect requests.
*
* Subscribing to the same channel twice will just add any additional X-Ids.
*
* @param channel the name of the channel. No wildcards are allowed for SmartREST.
* @param additionalXIds additionalXIds that are to be registered for this channel.
*/
public void subscribe(String channel, Set additionalXIds) {
// Wildcards in channel names are not supported by SmartREST:
if (channel == null || channel.contains("*")) {
throw new CotSdkException("Invalid channel name '"+channel+"'");
}
Set xIds = subscriptions.get(channel);
if (xIds == null) {
xIds = new CopyOnWriteArraySet<>();
subscriptions.put(channel, xIds);
}
if (additionalXIds != null) {
xIds.addAll(additionalXIds);
}
// If we already have a clientId we should immediately send the subscribe request:
if (clientId != null) {
SmartRequest smartRequest = new SmartRequest(
xId,
MSG_REALTIME_SUBSCRIBE +
"," + clientId +
"," + channel +
(xIds.isEmpty() ? "" : "," + String.join(",", xIds)));
cloudOfThingsRestClient.doSmartRealTimeRequest(smartRequest);
}
}
/**
* Unsubscribe from a channel. Will take effect immediately, even for currently running connect requests.
*
* @param channel the name of the channel.
*/
public void unsubscribe(String channel) {
subscriptions.remove(channel);
// If we already have a clientId we should immediately send the unsubscribe request:
if (clientId != null) {
SmartRequest smartRequest = new SmartRequest(
xId,
MSG_REALTIME_UNSUBSCRIBE + "," + clientId + "," + channel);
cloudOfThingsRestClient.doSmartRealTimeRequest(smartRequest);
}
}
/**
* Add a {@link SmartListener} that will from then on be called for all the notifications that this connector
* receives.
*
* Adding the same listener multiple times has no additional effect.
*
* @param listener the SmartListener that shall receive notifications from this connector
*/
public void addListener(SmartListener listener) {
listeners.add(listener);
}
/**
* Remove a previously registered {@link SmartListener}.
*
* Trying to remove a listener that doesn't exist is ok.
*
* @param listener the SmartListener to remove
*/
public void removeListener(SmartListener listener) {
listeners.remove(listener);
}
/**
* Connect to the cloud server and start the asynchronous polling loop.
*
* To stop it call {@link #disconnect()}.
*
* The connector doesn't stop on most errors. There should probably be a {@link SmartListener} which can handle
* errors and trigger a disconnect, if necessary.
*
*
* This convenience method should be sufficient for most simple cases. If not, then you can extend the this
* class and build your own connection handling using the protected do*-methods.
*
*/
public void connect() {
shallDisconnect = false;
if (connected || (clientId != null)) {
throw new CotSdkException("Already connected. Please disconnect first.");
}
// If there's no connection possible at all we want to fail fast, synchronously:
clientId = doHandshake();
if (clientId == null) {
throw new CotSdkException("Handshake failed, could not get clientId.");
}
pollingThread = new Thread(this);
pollingThread.setName("SmartCepConnector.pollingThread");
pollingThread.start();
}
/**
* Break the polling loop and disconnect from the cloud server.
* Will try to interrupt the polling thread too.
*/
public void disconnect() {
shallDisconnect = true;
if (pollingThread != null) {
pollingThread.interrupt();
try {
pollingThread.join(THREAD_JOIN_GRACE_MILLIS); // One second should be more than enough to end the loop.
} catch (InterruptedException ex) {
throw new CotSdkException("Real time polling thread didn't finish properly when asked to disconnect.", ex);
}
}
}
/**
* Get the clientId that was assigned by the server during handshake.
*
* @return the clientId or null, if we are not currently connected.
*/
public String getClientId() {
return clientId;
}
/**
* Whether there is currently a polling thread connected to the server.
*
* @return true = yes; false = no
*/
public boolean isConnected() {
return connected;
}
/**
* The current read timeout.
*
* @return the timeout in milliseconds
*/
public int getTimeout() {
return timeout;
}
/**
* Set the read timeout for the polling connect request.
*
* Can also be overwritten by advice messages sent from the server while the connector is connected.
*
* Default is {@value DEFAULT_READ_TIMEOUT_MILLIS}.
*
* @param timeout the timeout in milliseconds
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* The current interval, which is the time that the polling thread waits before it reconnects, after receiving a response.
*
* @return the waiting interval in milliseconds
*/
public int getInterval() {
return interval;
}
/**
* Set the time that the polling thread waits before it reconnects, after receiving a response.
*
* Can also be overwritten by advice messages sent from the server while the connector is connected.
*
* Default is {@value DEFAULT_RECONNECT_INTERVAL_MILLIS}.
*
* @param interval the waiting interval in milliseconds
*/
public void setInterval(int interval) {
this.interval = interval;
}
/**
* Don't call this method. It will be called by the system when the polling thread is started.
* Use {@link #connect()} instead.
*/
@Override
public void run() {
connected = true;
try {
postInitialSubscriptions();
do {
String response[] = doConnect();
String activeXId = xId;
int alternativeXIdCounter = 0;
for (String line : response) {
String messageId = line.split(",", 2)[0];
// Handle new alternative X-Id:
if (MSG_REALTIME_XID.equals(messageId)) {
String[] messageParts = line.split(",");
alternativeXIdCounter = Integer.parseInt(messageParts[1]);
activeXId = messageParts[2];
}
SmartNotification notification = new SmartNotification(line, activeXId);
// System-messages (<100) are not passed on to any listeners:
if (notification.getMessageId() >= 100) {
for (SmartListener listener : listeners) {
listener.onNotification(notification);
}
// When we have processed all the announced alternative X-Id lines we fall back to the default:
if (--alternativeXIdCounter == 0) {
activeXId = xId;
}
}
// Defined error messages in the response are passed to our listeners as exceptions:
if (KNOWN_ERROR_MESSAGES.contains(notification.getMessageId())) {
CotSdkException exception = new CotSdkException("Smart real time response contained an error: "+line);
for (SmartListener listener : listeners) {
listener.onError(exception);
}
}
if (MSG_REALTIME_ADVICE.equals(notification.getMessageId()+"")) {
handleAdvice(notification);
}
// TODO: check for errors that should cause us to abort the loop or connection.
}
try {
if (!shallDisconnect) {
Thread.sleep(interval);
}
} catch (InterruptedException e) {
shallDisconnect = true;
}
} while (!shallDisconnect);
} finally {
connected = false;
clientId = null;
}
}
protected String[] doConnect() {
SmartRequest smartRequest = new SmartRequest(
xId,
MSG_REALTIME_CONNECT + "," + clientId);
SmartResponse response = cloudOfThingsRestClient.doSmartRealTimePollingRequest(smartRequest, timeout);
if (response == null) {
return new String[0];
} else {
String[] responseLines = response.getLines();
if (responseLines.length > 0) {
// The first line can contain leading spaces, periodically sent by the server as a keep-alive signal.
responseLines[0] = responseLines[0].trim();
}
return responseLines;
}
}
protected String doHandshake() {
SmartRequest smartRequest = new SmartRequest(xId, MSG_REALTIME_HANDSHAKE);
SmartResponse response = cloudOfThingsRestClient.doSmartRealTimeRequest(smartRequest);
String[] responseLines = response.getLines();
switch (responseLines.length) {
case 1:
// TODO: 43,1,Invalid Message Identifier?!?
return responseLines[0].trim();
case 0:
throw new CotSdkException("SmartREST notification handshake failed: empty response => no clientId.");
default:
throw new CotSdkException("SmartREST notification handshake failed: ambiguous multi line response: " + Arrays.toString(responseLines));
}
}
/**
* Post all the subscriptions that we have to the server. Necessary for reconnects and when there already were
* subscriptions before we were connected.
*/
protected void postInitialSubscriptions() {
if (clientId == null) {
throw new CotSdkException("Cannot subscribe to SmartREST notification because we don't have a clientId yet.");
}
// Unfortunately we need one request for each channel.
for (Map.Entry> entry: subscriptions.entrySet()) {
String xIds = String.join(",", entry.getValue());
SmartRequest smartRequest = new SmartRequest(
xId,
MSG_REALTIME_SUBSCRIBE + ","
+ clientId + ","
+ entry.getKey()
+ ((xIds.length() == 0) ? "" : "," + xIds));
cloudOfThingsRestClient.doSmartRealTimeRequest(smartRequest);
}
}
/**
* Override this method if You don't want the server advice to automatically change the read timeout for the
* connect request.
* @param timeout the timeout, that the server recommended
*/
protected void setTimeoutByAdvice(int timeout) {
setTimeout(timeout);
}
/**
* Override this method if You don't want the server advice to automatically change the interval between
* response and reconnect.
* @param interval the interval, that the server recommended
*/
protected void setIntervalByAdvice(int interval) {
setInterval(interval);
}
protected void handleAdvice(SmartNotification notification) {
String[] parts = notification.getData().split(",");
// For unknown reasons the advice line seems to have an additional undocumented first field too:
// e.g. "86,,,,"
// instead of "86,,,"
if (!parts[1].isEmpty()) {
setTimeoutByAdvice(Integer.parseInt(parts[1]));
}
if (!parts[2].isEmpty()) {
setIntervalByAdvice(Integer.parseInt(parts[2]));
}
String reconnectPolicy = parts[3];
switch (reconnectPolicy) {
case "none" :
shallDisconnect = true;
break;
case "handshake" :
clientId = doHandshake();
postInitialSubscriptions();
break;
case "retry" :
default:
// Nothing to do, just continue with the next iteration.
}
}
}