package eu.play_project.dcep.distributedetalis;
import static eu.play_project.dcep.constants.DcepConstants.LOG_DCEP;
import static eu.play_project.dcep.constants.DcepConstants.LOG_DCEP_EXIT;
import static eu.play_project.dcep.constants.DcepConstants.LOG_DCEP_FAILED_EXIT;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.xml.namespace.QName;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RDFFormat;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.moxy.json.MoxyJsonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.petalslink.dsb.commons.service.api.Service;
import org.petalslink.dsb.notification.service.NotificationConsumerService;
import org.petalslink.dsb.soap.CXFExposer;
import org.petalslink.dsb.soap.api.Exposer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hp.hpl.jena.sparql.core.DatasetGraph;
import com.hp.hpl.jena.sparql.core.DatasetGraphFactory;
import eu.play_project.dcep.constants.DcepConstants;
import eu.play_project.dcep.distributedetalis.api.EcConnectionManager;
import eu.play_project.dcep.distributedetalis.api.EcConnectionmanagerException;
import eu.play_project.dcep.distributedetalis.join.SelectResults;
import eu.play_project.dcep.distributedetalis.listeners.EcConnectionListenerRest;
import eu.play_project.dcep.distributedetalis.listeners.EcConnectionListenerWsn;
import eu.play_project.dcep.distributedetalis.persistence.Persistence;
import eu.play_project.dcep.distributedetalis.persistence.PersistenceException;
import eu.play_project.dcep.distributedetalis.persistence.Sqlite;
import eu.play_project.dcep.distributedetalis.persistence.Sqlite.SubscriptionPerCloud;
import eu.play_project.dcep.distributedetalis.utils.EventCloudHelpers;
import eu.play_project.play_commons.constants.Event;
import eu.play_project.play_commons.constants.Stream;
import eu.play_project.play_eventadapter.AbstractReceiverRest;
import eu.play_project.play_eventadapter.AbstractSenderRest;
import eu.play_project.play_platformservices.api.BdplQuery;
import fr.inria.eventcloud.api.CompoundEvent;
import fr.inria.eventcloud.api.PublishSubscribeConstants;
import fr.inria.eventcloud.api.Quadruple;
/**
* An abstract connection manager implementing common methods needed to receive
* events via WS-Notification.
*
* @author Roland Stühmer
*/
public abstract class EcConnectionManagerWsn implements EcConnectionManager {
private final Map<String, SubscriptionUsage> subscriptions = new HashMap<String, SubscriptionUsage>();
private final Logger logger = LoggerFactory.getLogger(EcConnectionManagerWsn.class);
protected boolean init = false;
private AbstractReceiverRest rdfReceiver;
private AbstractSenderRest rdfSender;
private final DistributedEtalis dEtalis;
private EcConnectionListenerWsn dsbListener;
private EcConnectionListenerRest dsbRestListener;
static final Properties constants = DcepConstants.getProperties();
public static final String SOAP_URI = constants.getProperty("dcep.notify.endpoint");
public static final String REST_URI = constants.getProperty("dcep.notify.rest.local");
private Service notifyReceiverSoap;
private Server notifyReceiverRest;
private Persistence persistence;
public EcConnectionManagerWsn(DistributedEtalis dEtalis) {
this.dEtalis = dEtalis;
}
public void init() throws EcConnectionmanagerException {
if (init) {
throw new IllegalStateException(this.getClass().getSimpleName() + " has ALREADY been initialized.");
}
logger.info("Initialising {}.", this.getClass().getSimpleName());
this.rdfReceiver = new AbstractReceiverRest() {};
// Use an arbitrary topic as default:
this.rdfSender = new AbstractSenderRest(Stream.FacebookCepResults.getTopicQName()) {};
/*
* Expose the SOAP service to receive notifications
*/
try {
this.dsbListener = new EcConnectionListenerWsn(this.rdfReceiver);
this.dsbListener.setDetalis(this.dEtalis);
QName interfaceName = new QName("http://docs.oasis-open.org/wsn/bw-2",
"NotificationConsumer");
QName serviceName = new QName("http://docs.oasis-open.org/wsn/bw-2",
"NotificationConsumerService");
QName endpointName = new QName("http://docs.oasis-open.org/wsn/bw-2",
"NotificationConsumerPort");
final String notificationReceiverEndpointLocal = constants.getProperty("dcep.notify.endpoint.local");
logger.info("Exposing notification endpoint at: {} which should be reachable at {}.", notificationReceiverEndpointLocal, SOAP_URI);
NotificationConsumerService service = new NotificationConsumerService(interfaceName,
serviceName, endpointName, "NotificationConsumerService.wsdl", notificationReceiverEndpointLocal,
this.dsbListener);
Exposer exposer = new CXFExposer();
notifyReceiverSoap = exposer.expose(service);
notifyReceiverSoap.start();
} catch (Exception e) {
throw new EcConnectionmanagerException("Error while starting DSB listener (SOAP service).", e);
}
/*
* Expose the REST service to receive notifications
*/
try {
this.dsbRestListener = new EcConnectionListenerRest(this.rdfReceiver);
this.dsbRestListener.setDetalis(this.dEtalis);
final ResourceConfig rc = new ResourceConfig()
.register(MoxyJsonFeature.class)
.register(dsbRestListener);
notifyReceiverRest = new Server(URI.create(REST_URI).getPort());
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder h = new ServletHolder(new ServletContainer(rc));
context.addServlet(h, "/");
notifyReceiverRest.setHandler(context);
notifyReceiverRest.start();
} catch (Exception e) {
throw new EcConnectionmanagerException("Error while starting DSB listener (REST service).", e);
}
final List<String> topics = this.rdfReceiver.getTopics();
if (topics.isEmpty()) {
logger.warn("No topics were found in DSB, possible misconfiguration of event adapters.");
} else {
for (String topic : topics) {
logger.info("Topic on the DSB: " + topic);
}
}
try {
persistence = new Sqlite();
for (SubscriptionPerCloud sub : persistence.getSubscriptions()) {
logger.info("Cleaning stale subscription from cloud {}: {}", sub.cloudId,
sub.subscriptionId);
try {
rdfReceiver.unsubscribe(sub.subscriptionId);
} catch (Exception e) {
logger.debug(e.getMessage());
}
}
} catch (PersistenceException e) {
throw new EcConnectionmanagerException(e.getMessage(), e);
}
init = true;
}
@Override
public void destroy() {
logger.info("Terminating {}.", this.getClass().getSimpleName());
logger.info("Unsubscribe from Topics");
// Unsubscribe
this.rdfReceiver.unsubscribeAll();
subscriptions.clear();
persistence.deleteAllSubscriptions();
if (this.notifyReceiverSoap != null) {
this.notifyReceiverSoap.stop();
}
if (this.notifyReceiverRest != null) {
try {
this.notifyReceiverRest.stop();
} catch (Exception e) {
logger.error("Exception while stoppping REST server. Nothing we can do now. " + e.getMessage());
}
this.notifyReceiverRest.destroy();
}
init = false;
}
/**
* Persist data in historic storage.
*
* @param event event containing quadruples
* @param cloudId the cloud ID to allow partitioning of storage
*/
@Override
public abstract void putDataInCloud(CompoundEvent event, String cloudId);
/**
* Retreive data from historic storage using a SPARQL SELECT query. SPARQL 1.1
* enhancements like the VALUES clause are allowed.
*/
@Override
public abstract SelectResults getDataFromCloud(String query, String cloudId)
throws EcConnectionmanagerException;
@Override
public void publish(CompoundEvent event) {
if (!init) {
throw new IllegalStateException(this.getClass().getSimpleName() + " has not been initialized.");
}
String cloudId = EventCloudHelpers.getCloudId(event);
if (!cloudId.isEmpty()) {
// Send event to DSB:
ByteArrayOutputStream out = new ByteArrayOutputStream();
RDFDataMgr.write(out, quadruplesToDatasetGraph(event), RDFFormat.TRIG_BLOCKS);
// Do not remove this line, needed for logs. :stuehmer
logger.info(LOG_DCEP_EXIT + event.getGraph() + " " + EventCloudHelpers.getMembers(event));
if (logger.isDebugEnabled()) {
logger.debug(LOG_DCEP + "Complex Event:\n{}", event.toString());
}
this.rdfSender.notify(new String(out.toByteArray()), cloudId);
// Store event in Triple Store:
this.putDataInCloud(event, cloudId);
}
else {
logger.warn(LOG_DCEP_FAILED_EXIT + "Got empty cloud ID from event '{}', don't know which cloud to publish to. Discarding complex event.", event.getGraph() + Event.EVENT_ID_SUFFIX);
}
}
@Override
public void registerEventPattern(BdplQuery bdplQuery) throws EcConnectionmanagerException {
if (!init) {
throw new IllegalStateException(this.getClass().getSimpleName() + " has not been initialized.");
}
for (String cloudId : bdplQuery.getDetails().getInputStreams()) {
subscribe(cloudId);
}
// Nothing to do for output streams, they are stateless
}
@Override
public void unregisterEventPattern(BdplQuery bdplQuery) {
for (String cloudId : bdplQuery.getDetails().getInputStreams()) {
SubscriptionUsage sub = this.subscriptions.get(cloudId);
if (sub != null) {
unsubscribe(cloudId, sub.sub);
}
}
}
/**
* Subscribe to a given topic on the DSB. Duplicate subscriptions are handled using counters.
*/
private void subscribe(String cloudId) throws EcConnectionmanagerException {
if (!init) {
throw new IllegalStateException(this.getClass().getSimpleName() + " has not been initialized.");
}
if (this.subscriptions.containsKey(cloudId)) {
logger.info("Still subscribed to topic {}.", cloudId);
this.subscriptions.get(cloudId).usage++;
}
else {
logger.info("Subscribing to topic {}.", cloudId);
String subId = this.rdfReceiver.subscribe(cloudId, SOAP_URI);
this.subscriptions.put(cloudId, new SubscriptionUsage(subId));
this.persistence.storeSubscription(cloudId, subId);
}
}
/**
* Unsubscribe from a given topic on the DSB. Duplicate subscriptions are handled using counters.
*/
private void unsubscribe(String cloudId, String subId) {
if (!init) {
throw new IllegalStateException(this.getClass().getSimpleName() + " has not been initialized.");
}
if (this.subscriptions.containsKey(cloudId)) {
this.subscriptions.get(cloudId).usage--;
if (this.subscriptions.get(cloudId).usage == 0) {
logger.info("Unsubscribing from topic {}.", cloudId);
rdfReceiver.unsubscribe(subId);
this.subscriptions.remove(cloudId);
}
else {
logger.info("Still subscribed to topic {}.", cloudId);
}
}
}
/**
* Usage counter for a subscription.
*/
private class SubscriptionUsage implements Serializable {
private static final long serialVersionUID = 100L;
public SubscriptionUsage(String sub) {
this.sub = sub;
this.usage = 1;
}
public String sub;
public int usage;
}
/**
* A private method to convert a collection of quadruples into the
* corresponding data set graph to be used in the event format writers
*
* @author ialshaba
*
* @param quads
* the collection of the quadruples
* @return the corresponding data set graph
*/
private static DatasetGraph quadruplesToDatasetGraph(CompoundEvent quads) {
DatasetGraph dsg = DatasetGraphFactory.createMem();
for (Quadruple q : quads) {
if (q.getPredicate() != PublishSubscribeConstants.EVENT_NB_QUADRUPLES_NODE) {
dsg.add(
q.getGraph(), q.getSubject(), q.getPredicate(),
q.getObject());
}
}
return dsg;
}
}