/*******************************************************************************
* Copyright (c) 2004, 2007 IBM Corporation and Cambridge Semantics Incorporated.
* 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
*
* File: $Source: /cvsroot/slrp/boca/com.ibm.adtech.boca.model/src/com/ibm/adtech/boca/publisher/EventPublisher.java,v $
* Created by: Christopher R. Vincent
* Created on: 3/22/2006
* Revision: $Id: EventPublisher.java 180 2007-07-31 14:24:13Z mroy $
*
* Contributors:
* IBM Corporation - initial API and implementation
* Cambridge Semantics Incorporated - Fork to Anzo
*******************************************************************************/
package org.openanzo.combus.publisher;
import java.io.StringWriter;
import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.advisory.DestinationEvent;
import org.apache.activemq.advisory.DestinationListener;
import org.apache.activemq.advisory.DestinationSource;
import org.apache.activemq.broker.BrokerStoppedException;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.transport.TransportDisposedIOException;
import org.openanzo.combus.CombusDictionary;
import org.openanzo.combus.IJmsProvider;
import org.openanzo.combus.MessageUtils;
import org.openanzo.combus.realtime.RealtimeUpdatePublisher;
import org.openanzo.exceptions.AnzoException;
import org.openanzo.exceptions.ExceptionConstants;
import org.openanzo.exceptions.LogUtils;
import org.openanzo.rdf.Constants;
import org.openanzo.rdf.RDFFormat;
import org.openanzo.rdf.Statement;
import org.openanzo.rdf.URI;
import org.openanzo.rdf.Constants.COMBUS;
import org.openanzo.rdf.Constants.NAMESPACES;
import org.openanzo.rdf.utils.ReadWriteUtils;
import org.openanzo.rdf.utils.SerializationConstants;
import org.openanzo.rdf.utils.UriGenerator;
import org.openanzo.services.INamedGraphUpdate;
import org.openanzo.services.IOperationContext;
import org.openanzo.services.IUpdateResultListener;
import org.openanzo.services.IUpdateTransaction;
import org.openanzo.services.IUpdates;
import org.openanzo.services.ServicesDictionary;
import org.openanzo.services.serialization.CommonSerializationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Event Publisher publishes transaction update messages from the UpdateService onto the combus.
*
* @author Christopher R. Vincent
*/
class EventPublisher implements IUpdateResultListener, ExceptionListener, EventPublisherMBean {
/** Log factory for logging events */
private static final Logger log = LoggerFactory.getLogger(EventPublisher.class);
/** The default number of events to queue for publication before discarding old messages. */
private static final int QUEUE_SIZE_DEFAULT = 5000;
/** JMS ConnectionFactory */
private ActiveMQConnectionFactory factory = null;
/** JMS broker configuration. */
private ActiveMQConnection connection = null;
private DestinationSource ds = null;
/** JMS session */
private Session session = null;
/** Producer to send update messages */
private MessageProducer producer = null;
/** Producer to send update messages */
private MessageProducer topicProducer = null;
/** Max allowable size of queue */
private int queueSize = QUEUE_SIZE_DEFAULT;
/** Queue of publisher messages */
private final LinkedList<IUpdates> queue = new LinkedList<IUpdates>();
/** Thread that publisher runs */
private PublisherThread thread = null;
/** Map of ids and topics */
private Hashtable<String, Destination> topics = new Hashtable<String, Destination>();
final Lock lock = new ReentrantLock();
final Condition notEmpty = lock.newCondition();
private final WeakHashMap<String, SoftReference<Destination>> destinations = new WeakHashMap<String, SoftReference<Destination>>();
private final String jmsUpdateQueueName;
private final Dictionary<? extends Object, ? extends Object> configProperties;
private HashSet<String> ngTopics = new HashSet<String>();
private RealtimeUpdatePublisher realtimePublisher;
/**
* Create a new EventPublisher
*/
protected EventPublisher(Dictionary<? extends Object, ? extends Object> configProperties, RealtimeUpdatePublisher realtimePublisher) {
this.jmsUpdateQueueName = CombusDictionary.getUpdatesQueue(configProperties, null);
this.configProperties = configProperties;
this.realtimePublisher = realtimePublisher;
}
protected void start(IJmsProvider jmsProvider) throws AnzoException {
this.factory = (ActiveMQConnectionFactory) jmsProvider.createConnectionFactory(configProperties);
this.factory.setUseAsyncSend(true);
thread = new PublisherThread();
thread.start();
}
protected void stop(boolean stopping) throws AnzoException {
if (thread != null) {
thread.interrupt();
}
lock.lock();
try {
queue.clear();
notEmpty.signal();
} finally {
lock.unlock();
}
topics.clear();
disconnect(stopping);
}
public void reset() throws AnzoException {
lock.lock();
try {
queue.clear();
if (connection != null && session != null) {
try {
TextMessage bytesMessage = session.createTextMessage();
bytesMessage.setStringProperty(SerializationConstants.operation, SerializationConstants.reset);
publishTransactionUpdateMessage(bytesMessage);
} catch (JMSException jmsex) {
log.error(LogUtils.COMBUS_MARKER, "Error sending reset message", jmsex);
throw new AnzoException(ExceptionConstants.COMBUS.SEND_MESSAGE_FAILED, jmsex);
}
}
for (SoftReference<Destination> ref : destinations.values()) {
Destination dest = ref.get();
if (dest != null) {
try {
connection.destroyDestination(((ActiveMQDestination) dest));
} catch (JMSException be) {
log.warn(LogUtils.COMBUS_MARKER, "Error destroying destinations", be);
}
}
}
ngTopics.clear();
Set<ActiveMQTopic> topics = ds.getTopics();
for (ActiveMQTopic topic : topics) {
if (topic.getPhysicalName().startsWith(Constants.NAMESPACES.NAMEDGRAPH_TOPIC_PREFIX)) {
ngTopics.add(topic.getPhysicalName());
}
}
} finally {
lock.unlock();
destinations.clear();
}
}
protected void connect() throws AnzoException {
// Create broker connection.
try {
String user = ServicesDictionary.getUser(configProperties, null);
String password = ServicesDictionary.getPassword(configProperties, null);
connection = (ActiveMQConnection) factory.createConnection(user, password);
ds = new DestinationSource(connection);
ds.start();
Set<ActiveMQTopic> topics = ds.getTopics();
for (ActiveMQTopic topic : topics) {
if (topic.getPhysicalName().startsWith(Constants.NAMESPACES.NAMEDGRAPH_TOPIC_PREFIX)) {
ngTopics.add(topic.getPhysicalName());
}
}
ds.setDestinationListener(new DestinationListener() {
public void onDestinationEvent(DestinationEvent event) {
if (event.getDestination().getPhysicalName().startsWith(Constants.NAMESPACES.NAMEDGRAPH_TOPIC_PREFIX)) {
if (event.isAddOperation()) {
ngTopics.add(event.getDestination().getPhysicalName());
} else if (event.isRemoveOperation()) {
ngTopics.remove(event.getDestination().getPhysicalName());
}
}
}
});
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue updateQueue = session.createQueue(jmsUpdateQueueName);
if (updateQueue != null) {
producer = session.createProducer(updateQueue);
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
}
topicProducer = session.createProducer(null);
topicProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
} catch (JMSException e) {
log.error(LogUtils.COMBUS_MARKER, "Error connecting combus update publisher", e);
disconnect(true);
throw new AnzoException(ExceptionConstants.COMBUS.JMS_CONNECT_FAILED, e);
}
}
/**
* Disconnect the event publisher from the JMS server
*
*/
private void disconnect(boolean stopping) {
try {
if (stopping) {
try {
if (producer != null)
producer.close();
} catch (JMSException e) {
if (stopping) {
log.error(LogUtils.COMBUS_MARKER, "Error closing combus update publisher producer", e);
}
}
try {
if (session != null)
session.close();
} catch (JMSException e) {
if (stopping) {
log.error(LogUtils.COMBUS_MARKER, "Error closing combus update publisher session", e);
}
}
try {
if (ds != null) {
ds.stop();
}
} catch (JMSException e) {
if (stopping) {
log.error(LogUtils.COMBUS_MARKER, "Error closing combus update publisher destination source", e);
}
}
}
} finally {
try {
if (connection != null)
connection.close();
} catch (JMSException e) {
if (stopping) {
log.error(LogUtils.COMBUS_MARKER, "Error closing combus update publisher connection", e);
}
}
}
ds = null;
producer = null;
session = null;
connection = null;
ngTopics.clear();
}
/**
* Get the current size of the event publication queue.
*
* @return Size of event publication queue
*/
public int getMaxQueueSize() {
return queueSize;
}
public void onException(JMSException exception) {
if (exception.getCause() instanceof BrokerStoppedException || exception.getCause() instanceof TransportDisposedIOException) {
disconnect(false);
} else {
log.error(LogUtils.COMBUS_MARKER, "Exception on the combus update publisher connection", exception);
disconnect(true);
}
}
/**
* Publish the specified properties as a new event.
*
* @param properties
* map of {@link java.lang.String}property name to one of the supported JMS property value types.
* @param data
* data for message body
*/
private void publish(String destination, Map<String, Object> properties, String data) {
if (connection != null && session != null) {
try {
TextMessage bytesMessage = session.createTextMessage();
bytesMessage.setIntProperty(SerializationConstants.protocolVersion, Constants.VERSION);
setMessageProperties(bytesMessage, properties);
if (data != null)
bytesMessage.setText(data);
if (destination == null) {
publishTransactionUpdateMessage(bytesMessage);
} else {
publishNamedGraphUpdateMessage(destination, bytesMessage);
}
} catch (JMSException e) {
onException(e);
}
}
}
// Message should be discarded if EventPublisherException is thrown.
private void publishTransactionUpdateMessage(TextMessage message) throws JMSException {
lock.lock();
try {
if (log.isTraceEnabled()) {
log.trace(LogUtils.COMBUS_MARKER, MessageUtils.prettyPrint(message, "Publishing Message to " + producer.getDestination()));
} else if (log.isDebugEnabled()) {
log.debug(LogUtils.COMBUS_MARKER, "Publishing Message to " + producer.getDestination());
}
producer.send(message);
} finally {
lock.unlock();
}
}
// Message should be discarded if EventPublisherException is thrown.
private void publishNamedGraphUpdateMessage(String topic, TextMessage message) throws JMSException {
lock.lock();
try {
if (log.isTraceEnabled()) {
log.trace(LogUtils.COMBUS_MARKER, MessageUtils.prettyPrint(message, "Publishing Message to " + topic));
} else if (log.isDebugEnabled()) {
log.debug(LogUtils.COMBUS_MARKER, "Publishing Message to " + topic);
}
SoftReference<Destination> ref = destinations.get(topic);
Destination dest = (ref != null) ? ref.get() : null;
if (dest == null) {
dest = session.createTopic(topic);
destinations.put(topic, new SoftReference<Destination>(dest));
}
topicProducer.send(dest, message);
} finally {
lock.unlock();
}
}
private void setMessageProperties(Message message, Map<String, Object> properties) throws JMSException {
// How can we do this more efficiently?
for (Map.Entry<String, Object> entry : properties.entrySet()) {
String name = entry.getKey();
Object value = entry.getValue();
if (value instanceof String) {
message.setStringProperty(name, (String) value);
} else if (value instanceof Integer) {
message.setIntProperty(name, ((Integer) value).intValue());
} else if (value instanceof Long) {
message.setLongProperty(name, ((Long) value).longValue());
} else if (value instanceof Float) {
message.setFloatProperty(name, ((Float) value).floatValue());
} else if (value instanceof Double) {
message.setDoubleProperty(name, ((Double) value).doubleValue());
} else if (value instanceof Short) {
message.setShortProperty(name, ((Short) value).shortValue());
} else if (value instanceof Byte) {
message.setByteProperty(name, ((Byte) value).byteValue());
} else if (value instanceof Boolean) {
message.setBooleanProperty(name, ((Boolean) value).booleanValue());
}
}
}
/* Thread that reconnects to notification server and publishes any messages in the queue */
protected class PublisherThread extends Thread {
PublisherThread() {
super("EventPublisherThread");
setDaemon(true);
}
@Override
public void run() {
while (true) {
try {
lock.lock();
try {
while (queue.size() == 0)
notEmpty.await(5, TimeUnit.SECONDS);
} catch (InterruptedException ie) {
return;
} finally {
lock.unlock();
}
// Send message to broker, retry on failure.
boolean success = false;
IUpdates message = null;
int retries = 0;
while (!success) {
if (retries > 0) {
sleep(5000);
}
// Get latest message in case of overflow.
lock.lock();
try {
try {
message = queue.getFirst();
} catch (NoSuchElementException nsee) {
break;
}
} finally {
lock.unlock();
}
if (message != null) {
// Ensure connection.
try {
if (connection == null) {
connect();
}
} catch (AnzoException e) {
log.error(LogUtils.COMBUS_MARKER, "Error connecting combus update publisher connection", e);
retries++;
continue;
}
// Attempt to publish.
processUpdateMessage(message);
success = true;
}
}
// Remove from front of queue, unless already gone due to overflow.
lock.lock();
try {
try {
if (message == queue.getFirst()) {
queue.removeFirst();
}
} catch (NoSuchElementException e) {
log.debug(LogUtils.COMBUS_MARKER, "Error removing message from queue", e);
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
return;
}
}
}
}
public void updateComplete(IOperationContext context, IUpdates results) throws AnzoException {
lock.lock();
try {
queue.addLast(results);
if ((queueSize > 0) && (queue.size() > queueSize)) {
queue.removeFirst();
}
notEmpty.signal();
} finally {
lock.unlock();
}
}
private void processUpdateMessage(IUpdates message) {
try {
//StringWriter output = new StringWriter();
//CommonSerializationUtils.writeUpdates(true, message.updates, output, RDFFormat.JSON.getDefaultMIMEType());
//publish(null, message.properties, output.toString());
for (IUpdateTransaction transaction : message.getTransactions()) {
if (transaction.getErrors().size() == 0) {
String operationId = message.getOperationId();
publishNamedGraphUpdates(transaction, operationId);
publishTransactionEvent(transaction, operationId);
}
}
} catch (AnzoException ae) {
log.error(LogUtils.COMBUS_MARKER, "Error processing update message", ae);
}
}
private void publishNamedGraphUpdates(IUpdateTransaction transaction, String operationId) throws AnzoException {
Map<String, Object> message = new HashMap<String, Object>();
message.put(SerializationConstants.type, SerializationConstants.namedGraphUpdate);
message.put(SerializationConstants.operation, SerializationConstants.namedGraphUpdate);
message.put(SerializationConstants.transactionTimestamp, transaction.getTransactionTimestamp());
if (transaction.getURI() != null)
message.put(SerializationConstants.transactionURI, transaction.getURI().toString());
if (transaction.getTransactionContext() != null) {
StringWriter tc = new StringWriter();
ReadWriteUtils.writeStatements(transaction.getTransactionContext(), tc, RDFFormat.JSON, null, false);
message.put(SerializationConstants.transactionContext, tc.toString());
}
message.put(Constants.COMBUS.JMS_CORRELATION_ID, operationId);
String ngRevs = CommonSerializationUtils.writeNamedGraphRevisions(transaction.getUpdatedNamedGraphRevisions());
if (ngRevs != null) {
message.put(SerializationConstants.namedGraphUpdates, ngRevs);
}
for (Map.Entry<URI, URI> key : transaction.getUpdatedNamedGraphs().entrySet()) {
URI ngUri = key.getKey();
URI UUID = key.getValue();
INamedGraphUpdate update = transaction.getNamedGraphUpdate(ngUri);
String topic = UriGenerator.generateEncapsulatedString(NAMESPACES.NAMEDGRAPH_TOPIC_PREFIX, UUID.toString());
Map<String, Object> messageClone = new HashMap<String, Object>(message);
messageClone.put(SerializationConstants.namedGraphUri, ngUri.toString());
messageClone.put(SerializationConstants.namedGraphUUID, UUID.toString());
if (update != null) {
Collection<Statement> additions = update.getAdditions();
if (additions != null) {
StringWriter adds = new StringWriter();
ReadWriteUtils.writeStatements(additions, adds, RDFFormat.JSON);
messageClone.put(SerializationConstants.additions, adds.toString());
}
Collection<Statement> removals = update.getRemovals();
if (removals != null) {
StringWriter removes = new StringWriter();
ReadWriteUtils.writeStatements(removals, removes, RDFFormat.JSON);
messageClone.put(SerializationConstants.removals, removes.toString());
}
Collection<Statement> metaAdditions = update.getMetaAdditions();
if (metaAdditions != null) {
StringWriter adds = new StringWriter();
ReadWriteUtils.writeStatements(metaAdditions, adds, RDFFormat.JSON);
messageClone.put(SerializationConstants.metaAdditions, adds.toString());
}
Collection<Statement> metaRemovals = update.getMetaRemovals();
if (metaRemovals != null) {
StringWriter removes = new StringWriter();
ReadWriteUtils.writeStatements(metaRemovals, removes, RDFFormat.JSON);
messageClone.put(SerializationConstants.metaRemovals, removes.toString());
}
if (additions != null || removals != null || metaAdditions != null || metaRemovals != null) {
if (ngTopics.contains(topic)) {
publish(topic, messageClone, null);
}
Map<String, Object> messageClone2 = new HashMap<String, Object>(messageClone);
//messageClone2.remove(SerializationConstants.namedGraphUpdates);
//messageClone2.remove(SerializationConstants.transactionContext);
if (realtimePublisher == null) {
publish(null, messageClone2, null);
} else {
realtimePublisher.handleNamedgraphUpdateMessage(operationId, update, messageClone2);
}
}
}
}
}
private void publishTransactionEvent(IUpdateTransaction transaction, String operationId) throws AnzoException {
Map<String, Object> message = new HashMap<String, Object>();
message.put(SerializationConstants.type, SerializationConstants.transactionComplete);
message.put(SerializationConstants.operation, SerializationConstants.transactionComplete);
message.put(SerializationConstants.transactionTimestamp, transaction.getTransactionTimestamp());
String ngRevs = CommonSerializationUtils.writeNamedGraphRevisions(transaction.getUpdatedNamedGraphRevisions());
if (ngRevs != null)
message.put(SerializationConstants.namedGraphUpdates, ngRevs);
if (transaction.getURI() != null)
message.put(SerializationConstants.transactionURI, transaction.getURI().toString());
if (transaction.getTransactionContext() != null) {
StringWriter tc = new StringWriter();
ReadWriteUtils.writeStatements(transaction.getTransactionContext(), tc, RDFFormat.JSON);
message.put(SerializationConstants.transactionContext, tc.toString());
}
message.put(Constants.COMBUS.JMS_CORRELATION_ID, operationId);
publish(COMBUS.TRANSACTIONS_TOPIC, message, null);
}
public void flushQueue() {
lock.lock();
try {
queue.clear();
} finally {
lock.unlock();
}
}
public int getQueueSize() {
return queue.size();
}
public void setMaxQueueSize(int queueSize) {
this.queueSize = queueSize;
}
}