/*******************************************************************************
* Copyright (c) 2011, 2016 Red Hat Inc and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat Inc - initial API and implementation
*******************************************************************************/
package org.eclipse.kura.camel.internal.camelcloud;
import static java.lang.String.format;
import static org.apache.camel.ServiceStatus.Started;
import static org.eclipse.kura.KuraErrorCode.CONFIGURATION_ERROR;
import static org.eclipse.kura.KuraErrorCode.OPERATION_NOT_SUPPORTED;
import static org.eclipse.kura.camel.camelcloud.KuraCloudClientConstants.CAMEL_KURA_CLOUD_CONTROL;
import static org.eclipse.kura.camel.camelcloud.KuraCloudClientConstants.CAMEL_KURA_CLOUD_DEVICEID;
import static org.eclipse.kura.camel.camelcloud.KuraCloudClientConstants.CAMEL_KURA_CLOUD_MESSAGEID;
import static org.eclipse.kura.camel.camelcloud.KuraCloudClientConstants.CAMEL_KURA_CLOUD_PRIORITY;
import static org.eclipse.kura.camel.camelcloud.KuraCloudClientConstants.CAMEL_KURA_CLOUD_QOS;
import static org.eclipse.kura.camel.camelcloud.KuraCloudClientConstants.CAMEL_KURA_CLOUD_RETAIN;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.StartupListener;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultShutdownStrategy;
import org.apache.camel.spi.ShutdownStrategy;
import org.eclipse.kura.KuraErrorCode;
import org.eclipse.kura.KuraException;
import org.eclipse.kura.camel.camelcloud.CamelCloudService;
import org.eclipse.kura.cloud.CloudClient;
import org.eclipse.kura.cloud.CloudClientListener;
import org.eclipse.kura.message.KuraPayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CamelCloudClient implements CloudClient {
private final static Logger logger = LoggerFactory.getLogger(CamelCloudClient.class);
private final CamelCloudService cloudService;
private final CamelContext camelContext;
private final ProducerTemplate producerTemplate;
private final List<CloudClientListener> cloudClientListeners = new CopyOnWriteArrayList<>();
private final String applicationId;
private final String baseEndpoint;
private final ExecutorService executorService;
public CamelCloudClient(CamelCloudService cloudService, CamelContext camelContext, String applicationId,
String baseEndpoint) {
this.cloudService = cloudService;
this.camelContext = camelContext;
this.producerTemplate = camelContext.createProducerTemplate();
this.applicationId = applicationId;
this.baseEndpoint = baseEndpoint;
this.executorService = camelContext.getExecutorServiceManager().newThreadPool(this,
"CamelCloudClient/" + applicationId, 0, 1);
}
public CamelCloudClient(CamelCloudService cloudService, CamelContext camelContext, String applicationId) {
this(cloudService, camelContext, applicationId, "vm:%s");
}
// Cloud client API
@Override
public String getApplicationId() {
return this.applicationId;
}
@Override
public void release() {
this.cloudService.release(this.applicationId);
this.camelContext.getExecutorServiceManager().shutdown(this.executorService);
}
@Override
public boolean isConnected() {
return this.camelContext.getStatus() == Started;
}
@Override
public int publish(String topic, KuraPayload kuraPayload, int qos, boolean retain) throws KuraException {
return publish(topic, kuraPayload, qos, retain, 5);
}
@Override
public int publish(String topic, KuraPayload kuraPayload, int qos, boolean retain, int priority)
throws KuraException {
return doPublish(false, null, topic, kuraPayload, qos, retain, priority);
}
@Override
public int publish(String s, byte[] bytes, int i, boolean b, int i1) throws KuraException {
KuraPayload kuraPayload = new KuraPayload();
kuraPayload.setBody(bytes);
return publish(s, kuraPayload, i, b);
}
@Override
public int controlPublish(String topic, KuraPayload payload, int qos, boolean retain, int priority)
throws KuraException {
return doPublish(true, null, topic, payload, qos, retain, priority);
}
@Override
public int controlPublish(String deviceId, String topic, KuraPayload kuraPayload, int qos, boolean retain,
int priority) throws KuraException {
return doPublish(true, deviceId, topic, kuraPayload, qos, retain, priority);
}
@Override
public int controlPublish(String deviceId, String topic, byte[] payload, int qos, boolean b, int priority)
throws KuraException {
KuraPayload kuraPayload = new KuraPayload();
kuraPayload.setBody(payload);
return doPublish(true, deviceId, topic, kuraPayload, qos, b, priority);
}
@Override
public void subscribe(String topic, int qos) throws KuraException {
forkSubscribe(false, topic, qos);
}
@Override
public void controlSubscribe(String topic, int qos) throws KuraException {
forkSubscribe(true, topic, qos);
}
@Override
public void unsubscribe(String topic) throws KuraException {
final String internalQueue = this.applicationId + ":" + topic;
try {
ShutdownStrategy strategy = this.camelContext.getShutdownStrategy();
if (strategy instanceof DefaultShutdownStrategy) {
if (((DefaultShutdownStrategy) strategy).getCurrentShutdownTaskFuture() != null) {
logger.info("Skipping cleanup of '{}' since the camel context is being shut down", internalQueue);
// we are "in shutdown" and would deadlock
return;
}
}
// perform shutdown
this.camelContext.stopRoute(internalQueue);
this.camelContext.removeRoute(internalQueue);
} catch (Exception e) {
throw new KuraException(KuraErrorCode.INTERNAL_ERROR, e);
}
}
@Override
public void controlUnsubscribe(String topic) throws KuraException {
unsubscribe(topic);
}
@Override
public void addCloudClientListener(CloudClientListener cloudClientListener) {
this.cloudClientListeners.add(cloudClientListener);
}
@Override
public void removeCloudClientListener(CloudClientListener cloudClientListener) {
this.cloudClientListeners.remove(cloudClientListener);
}
@Override
public List<Integer> getUnpublishedMessageIds() throws KuraException {
throw new KuraException(OPERATION_NOT_SUPPORTED);
}
@Override
public List<Integer> getInFlightMessageIds() throws KuraException {
throw new KuraException(OPERATION_NOT_SUPPORTED);
}
@Override
public List<Integer> getDroppedInFlightMessageIds() throws KuraException {
throw new KuraException(OPERATION_NOT_SUPPORTED);
}
// Helpers
private int doPublish(boolean isControl, String deviceId, String topic, KuraPayload kuraPayload, int qos,
boolean retain, int priority) throws KuraException {
final String target = target(this.applicationId + ":" + topic);
final int kuraMessageId = Math.abs(new Random().nextInt());
Map<String, Object> headers = new HashMap<>();
headers.put(CAMEL_KURA_CLOUD_CONTROL, isControl);
headers.put(CAMEL_KURA_CLOUD_MESSAGEID, kuraMessageId);
headers.put(CAMEL_KURA_CLOUD_DEVICEID, deviceId);
headers.put(CAMEL_KURA_CLOUD_QOS, qos);
headers.put(CAMEL_KURA_CLOUD_RETAIN, retain);
headers.put(CAMEL_KURA_CLOUD_PRIORITY, priority);
logger.trace("Publishing: {} -> {} / {}", new Object[] { target, kuraPayload, this.camelContext });
this.producerTemplate.sendBodyAndHeaders(target, kuraPayload, headers);
return kuraMessageId;
}
private void forkSubscribe(final boolean isControl, final String topic, final int qos) throws KuraException {
/*
* This construct is needed due to CAMEL-10206
*
* It does fork off the subscription process, which actually creates a
* new camel route, into the background since we currently may be in the
* process of starting the camel context. If that is the case then the
* newly added route won't be started since the camel context is in the
* "starting" mode. Events won't get processed.
*
* So we do fork off the subscription process after the camel context
* has been started. The executor is needed since, according to the
* camel javadoc on StartupListener, the camel context may still be in
* "starting" mode when the "onCamelContextStarted" method is called.
*/
try {
this.camelContext.addStartupListener(new StartupListener() {
@Override
public void onCamelContextStarted(CamelContext context, boolean alreadyStarted) throws Exception {
CamelCloudClient.this.executorService.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
doSubscribe(isControl, topic, qos);
return null;
}
});
}
});
} catch (Exception e) {
throw new KuraException(KuraErrorCode.INTERNAL_ERROR, e);
}
}
private void doSubscribe(final boolean isControl, final String topic, final int qos) throws KuraException {
logger.debug("About to subscribe to topic {} with QOS {}.", topic, qos);
final String internalQueue = this.applicationId + ":" + topic;
logger.debug("\tInternal target: {} / {}", target(internalQueue), this.camelContext);
try {
this.camelContext.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from(target(internalQueue)).routeId(internalQueue).process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
logger.debug("Processing: {}", exchange);
for (CloudClientListener listener : CamelCloudClient.this.cloudClientListeners) {
logger.debug("\t{}", listener);
Object body = exchange.getIn().getBody();
KuraPayload payload;
if (body instanceof KuraPayload) {
payload = (KuraPayload) body;
} else {
payload = new KuraPayload();
payload.setBody(getContext().getTypeConverter().convertTo(byte[].class, body));
}
String deviceId = exchange.getIn().getHeader(CAMEL_KURA_CLOUD_DEVICEID, String.class);
int qos = exchange.getIn().getHeader(CAMEL_KURA_CLOUD_QOS, 0, int.class);
listener.onMessageArrived(deviceId, "camel", payload, qos, true);
}
}
});
}
});
} catch (Exception e) {
logger.warn("Error while adding subscription route. Rethrowing root cause.");
throw new KuraException(CONFIGURATION_ERROR, e);
}
}
private String target(String topic) {
if (this.baseEndpoint.contains("%s")) {
return format(this.baseEndpoint, topic);
}
return this.baseEndpoint + topic;
}
}