org.openanzo.client.RealtimeUpdateManager.java Source code

Java tutorial

Introduction

Here is the source code for org.openanzo.client.RealtimeUpdateManager.java

Source

/*******************************************************************************
 * Copyright (c) 2007 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
 *
 * Contributors:
 *     Cambridge Semantics Incorporated
 *******************************************************************************/
package org.openanzo.client;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;

import org.apache.commons.collections15.CollectionUtils;
import org.openanzo.combus.CombusConnection;
import org.openanzo.combus.MessageUtils;
import org.openanzo.exceptions.AnzoException;
import org.openanzo.exceptions.ExceptionConstants;
import org.openanzo.exceptions.LogUtils;
import org.openanzo.exceptions.Messages;
import org.openanzo.rdf.Constants;
import org.openanzo.rdf.Dataset;
import org.openanzo.rdf.IDataset;
import org.openanzo.rdf.IDatasetListener;
import org.openanzo.rdf.IStatementListener;
import org.openanzo.rdf.MemQuadStore;
import org.openanzo.rdf.MemURI;
import org.openanzo.rdf.RDFFormat;
import org.openanzo.rdf.Resource;
import org.openanzo.rdf.Statement;
import org.openanzo.rdf.URI;
import org.openanzo.rdf.Value;
import org.openanzo.rdf.Constants.COMBUS;
import org.openanzo.rdf.utils.ReadWriteUtils;
import org.openanzo.rdf.utils.SerializationConstants;
import org.openanzo.services.INotificationRegistrationService;
import org.openanzo.services.IOperationContext;
import org.openanzo.services.ITracker;
import org.openanzo.services.IUpdateTransaction;
import org.openanzo.services.impl.BaseOperationContext;
import org.openanzo.services.impl.DatasetTracker;
import org.openanzo.services.impl.SelectorTracker;
import org.openanzo.services.impl.UpdateTransaction;
import org.openanzo.services.serialization.CommonSerializationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manager that manages the realtime event trackers
 * 
 * @author Ben Szekely ( <a href="mailto:ben@cambridgesemantics.com">ben@cambridgesemantics.com </a>)
 * 
 */
public class RealtimeUpdateManager implements MessageListener {

    private static final Logger log = LoggerFactory.getLogger(RealtimeUpdateManager.class);

    protected boolean connected = false;

    private final Set<ITransactionListener> transactionListeners = Collections
            .synchronizedSet(new HashSet<ITransactionListener>());

    protected final AnzoClient anzoClient;

    protected final CombusConnection combusConnection;

    protected final Map<Statement, SelectorTracker> patternToTracker;

    protected final Map<URI, DatasetTracker> datasetToTrackers;

    protected final MemQuadStore quadStore;

    private final ReentrantReadWriteLock trackerLock = new ReentrantReadWriteLock();

    private final ReentrantReadWriteLock transactionLock = new ReentrantReadWriteLock();

    protected RealtimeUpdateManager(AnzoClient client) {
        this.anzoClient = client;
        this.combusConnection = client.clientDatasource.getCombusConnection();
        quadStore = new MemQuadStore();
        patternToTracker = new HashMap<Statement, SelectorTracker>();
        datasetToTrackers = new HashMap<URI, DatasetTracker>();

    }

    /**
     * Checks if a tracker is registered for the provided pattern.
     * 
     * @param subject
     *            The subject or null for wildcard.
     * @param predicate
     *            The predicate or null for wildcard.
     * @param object
     *            The object or null for wildcard.
     * @param namedGraphUri
     *            The namedGraphUri or null for wildcard.
     * @return true if there is a tracker that matches the pattern
     */
    boolean containsTracker(Resource subject, URI predicate, Value object, URI namedGraphUri) {
        return patternToTracker.containsKey(
                Constants.valueFactory.createMatchStatement(subject, predicate, object, namedGraphUri));
    }

    /**
     * Add an {@link ITransactionListener} that is notified when ever a transaction event occurs on the server
     * 
     * @param listener
     *            lister to be notified
     * @throws AnzoException
     */
    public void addTransactionListener(ITransactionListener listener) throws AnzoException {
        transactionListeners.add(listener);
        if (transactionListeners.size() == 1 && connected) {
            try {
                combusConnection.registerTopicListener(COMBUS.TRANSACTIONS_TOPIC, this);
            } catch (AnzoException ae) {
                transactionListeners.remove(listener);
            }
        }
    }

    /**
     * Unregister an {@link ITransactionListener}
     * 
     * @param listener
     *            listener to unregister
     * @throws AnzoException
     */
    public void removeTransactionListener(ITransactionListener listener) throws AnzoException {
        transactionListeners.remove(listener);
        if (transactionListeners.size() == 0 && connected) {
            try {
                combusConnection.unregisterTopicListener(COMBUS.TRANSACTIONS_TOPIC);
            } catch (AnzoException ae) {
                transactionListeners.remove(listener);
            }
        }
    }

    void notifyTransactionListeners(IUpdateTransaction update) throws AnzoException {
        IDataset dataset = new Dataset();
        if (update.getTransactionContext() != null) {
            for (Statement stmt : update.getTransactionContext()) {
                URI namedGraphUri = stmt.getNamedGraphUri();
                if (!dataset.containsNamedGraph(namedGraphUri)) {
                    dataset.addNamedGraph(namedGraphUri);
                }
                dataset.add(stmt);
            }
        }
        synchronized (transactionListeners) {
            for (ITransactionListener listener : transactionListeners) {
                listener.transactionComplete(update.getURI(), update.getTransactionTimestamp(),
                        anzoClient.convertUUIDSToNamedGraphURIs(update.getUpdatedNamedGraphRevisions().keySet()),
                        dataset);
            }
        }
    }

    /**
     * Adds a tracker for the provided pattern (subject, predicate, object and namedGraphUri). Registers the provided listener to receive events about quads
     * matching the pattern.
     * 
     * @param subject
     *            The subject of the quad, or null to match all subjects.
     * @param predicate
     *            The predicate of the quad, or null to match all preciates.
     * @param object
     *            The object of the quad, or null to match all objects.
     * @param namedGraphUri
     *            The namedGraphUri of the quad, or null to match all namedGraphUris.
     * @param listener
     *            if not null, this listener is registered to receive events about quads matching the pattern of tracker.
     * @throws AnzoException
     */
    public void addTracker(Resource subject, URI predicate, Value object, URI namedGraphUri,
            IStatementListener<ITracker> listener) throws AnzoException {
        try {
            trackerLock.writeLock().lockInterruptibly();

            Statement statement = Constants.valueFactory.createMatchStatement(subject, predicate, object,
                    namedGraphUri);
            SelectorTracker tracker = patternToTracker.get(statement);
            if (tracker == null) {
                tracker = new SelectorTracker(subject, predicate, object, namedGraphUri);
                patternToTracker.put(statement, tracker);
                setupTrackerNotification(tracker);
                quadStore.add(statement);
            }
            tracker.addListener(listener);
        } catch (InterruptedException e) {
            throw new AnzoException(ExceptionConstants.CLIENT.CLIENT_LOCK_ERROR, e);
        } finally {
            trackerLock.writeLock().unlock();
        }
    }

    /**
     * Removes all trackers matching the provided pattern.
     * 
     * @param subject
     *            The subject of the quad, or null to match all subjects.
     * @param predicate
     *            The predicate of the quad, or null to match all preciates.
     * @param object
     *            The object of the quad, or null to match all objects.
     * @param namedGraphUri
     *            The namedGraphUri of the quad, or null to match all namedGraphUris.
     * @throws AnzoException
     */
    public void removeTracker(Resource subject, URI predicate, Value object, URI namedGraphUri)
            throws AnzoException {
        Statement pattern = Constants.valueFactory.createMatchStatement(subject, predicate, object, namedGraphUri);
        SelectorTracker tracker = patternToTracker.get(pattern);
        if (tracker != null) {
            IOperationContext context = new BaseOperationContext(INotificationRegistrationService.REGISTER_TRACKERS,
                    BaseOperationContext.generateOperationId(), null);
            try {
                boolean regOk = anzoClient.clientDatasource.getNotificationRegistrationService().unregisterTrackers(
                        context, Collections.<SelectorTracker>singleton(tracker),
                        Collections.<DatasetTracker>emptySet(), Collections.<URI>emptySet(), null);
                if (!regOk) {
                    throw new AnzoException(ExceptionConstants.COMBUS.JMS_REGISTER_SELECTOR_ERROR);
                }
            } finally {
                context.clearMDC();
            }
            patternToTracker.remove(pattern);
            quadStore.remove(pattern);
        }
    }

    /**
     * Add a dataset tracker
     * 
     * @param trackerUri
     *            URI for this tracker
     * @param defaultGraphs
     *            set of default graphs to monitor in this tracker
     * @param namedGraphs
     *            set of named graphs to monitor in this tracker
     * @param namedDatasets
     *            named datasets whose graphs are to be monitored
     * @param listener
     *            Listener that is notified when one of the graphs changes
     * @throws AnzoException
     */
    public void addTracker(URI trackerUri, Set<URI> defaultGraphs, Set<URI> namedGraphs, Set<URI> namedDatasets,
            IDatasetListener listener) throws AnzoException {
        try {
            trackerLock.writeLock().lockInterruptibly();

            DatasetTracker tracker = datasetToTrackers.get(trackerUri);
            if (tracker == null) {
                tracker = new DatasetTracker(trackerUri, defaultGraphs, namedGraphs, namedDatasets);
                datasetToTrackers.put(trackerUri, tracker);
                setupTrackerNotification(tracker);
            }
            tracker.addListener(listener);
        } catch (InterruptedException e) {
            throw new AnzoException(ExceptionConstants.CLIENT.CLIENT_LOCK_ERROR, e);
        } finally {
            trackerLock.writeLock().unlock();
        }
    }

    /**
     * Remove the tracker based on its URI
     * 
     * @param trackerUri
     *            URI of dataset tracker to remove
     * @throws AnzoException
     */
    public void removeTracker(URI trackerUri) throws AnzoException {
        DatasetTracker tracker = datasetToTrackers.remove(trackerUri);
        if (tracker != null) {
            IOperationContext context = new BaseOperationContext(INotificationRegistrationService.REGISTER_TRACKERS,
                    BaseOperationContext.generateOperationId(), null);
            try {
                boolean regOk = anzoClient.clientDatasource.getNotificationRegistrationService().unregisterTrackers(
                        context, Collections.<SelectorTracker>emptySet(),
                        Collections.<DatasetTracker>singleton(tracker), Collections.<URI>emptySet(), null);
                if (!regOk) {
                    throw new AnzoException(ExceptionConstants.COMBUS.JMS_REGISTER_SELECTOR_ERROR);
                }
            } finally {
                context.clearMDC();
            }
        }
    }

    /**
     * Removes the given listener from the tracker matching the given pattern
     * 
     * @param subject
     *            The subject of the quad, or null to match all subjects.
     * @param predicate
     *            The predicate of the quad, or null to match all preciates.
     * @param object
     *            The object of the quad, or null to match all objects.
     * @param namedGraphUri
     *            The namedGraphUri of the quad, or null to match all namedGraphUris.
     * @param listener
     *            Listener to unregister from the given tracker pattern
     * @throws AnzoException
     */
    public void removeTrackerListener(Resource subject, URI predicate, Value object, URI namedGraphUri,
            IStatementListener<ITracker> listener) throws AnzoException {
        SelectorTracker tracker = patternToTracker
                .get(Constants.valueFactory.createMatchStatement(subject, predicate, object, namedGraphUri));
        if (tracker != null) {
            tracker.removeListener(listener);
            if (tracker.getListeners().isEmpty()) {
                removeTracker(subject, predicate, object, namedGraphUri);
            }
        }
    }

    /**
     * Remove a listener for the given dataset tracker
     * 
     * @param trackerUri
     *            URI of the dataset tracker from which to unregister
     * @param listener
     *            listener to unregister
     * @throws AnzoException
     */
    public void removeTrackerListener(URI trackerUri, IDatasetListener listener) throws AnzoException {
        DatasetTracker tracker = datasetToTrackers.get(trackerUri);
        if (tracker != null) {
            tracker.removeListener(listener);
            if (tracker.getListeners().isEmpty()) {
                removeTracker(trackerUri);
            }
        }
    }

    /**
     * Registers with the server to receive notifications of quads matching this tracker's pattern if notification is enabled.
     * 
     * @param tracker
     *            The tracker to register.
     * @throws AnzoException
     */
    private void setupTrackerNotification(SelectorTracker tracker) throws AnzoException {
        if (anzoClient.isConnected()) {
            IOperationContext context = new BaseOperationContext(INotificationRegistrationService.REGISTER_TRACKERS,
                    BaseOperationContext.generateOperationId(), anzoClient.getServicePrincipal());
            try {
                boolean regOk = anzoClient.clientDatasource.getNotificationRegistrationService().registerTrackers(
                        context, Collections.<SelectorTracker>singleton(tracker),
                        Collections.<DatasetTracker>emptySet(), Collections.<URI>emptySet(), null);
                if (!regOk) {
                    throw new AnzoException(ExceptionConstants.COMBUS.JMS_REGISTER_SELECTOR_ERROR);
                }
            } finally {
                context.clearMDC();
            }
        }
    }

    private void setupTrackerNotification(DatasetTracker tracker) throws AnzoException {
        if (anzoClient.isConnected()) {
            IOperationContext context = new BaseOperationContext(INotificationRegistrationService.REGISTER_TRACKERS,
                    BaseOperationContext.generateOperationId(), null);
            try {
                boolean regOk = anzoClient.clientDatasource.getNotificationRegistrationService().registerTrackers(
                        context, Collections.<SelectorTracker>emptySet(),
                        Collections.<DatasetTracker>singleton(tracker), Collections.<URI>emptySet(), null);
                if (!regOk) {
                    throw new AnzoException(ExceptionConstants.COMBUS.JMS_REGISTER_SELECTOR_ERROR);
                }
            } finally {
                context.clearMDC();
            }
        }
    }

    /**
     * Notifies the trackers of statement changes.
     * 
     * @param addition
     *            If true then the event is for addition of a new statement, if false then deletion of a statement.
     * @param statement
     *            The statement that was added or removed.
     */
    private void notifyTrackers(boolean addition, Statement statement) {
        Collection<Statement> result = this.findMatchingPatterns(statement);

        for (Statement stmt : result) {
            SelectorTracker tracker = patternToTracker.get(stmt);
            if (tracker != null) {
                tracker.notifyListeners(addition, statement);
            }
        }
    }

    private void notifyTrackers(URI namedGraphUri, Collection<URI> datasetUris) {
        for (URI datasetURI : datasetUris) {
            DatasetTracker tracker = datasetToTrackers.get(datasetURI);
            if (tracker != null) {
                tracker.notifyListeners(namedGraphUri);
            }
        }
    }

    /**
     * Finds all the tracker patterns that match the provided statements.
     * 
     * Implementation:
     * 
     * Trackers are stored as quads in a quad store, allowing the find operation to be use to find the matching trackers.
     * 
     * Nodes in a tracker pattern may be the special "ANY_URI", indicating the node in the quad is a wildcard.
     * 
     * The matching trackers are found as follows:
     * 
     * <pre>
     * (INTERSECTION
     *  (UNION trackers-with-matching-subject trackers-with-wildcard-subject)
     *  (UNION trackers-with-matching-predicate trackers-with-wildcard-predicate)
     *  (UNION trackers-with-matching-object trackers-with-wildcard-object)
     *  (UNION trackers-with-matching-namedGraphUri trackers-with-wildcard-namedGraphUri))
     * </pre>
     * 
     * @param statement
     *            The statement find matching tracker patterns for.
     * @return The tracker patterns matching the provided statement.
     */
    protected Collection<Statement> findMatchingPatterns(Statement statement) {

        Collection<Statement> subjectExactMatches = quadStore.find(statement.getSubject(), null, null,
                (URI[]) null);
        Collection<Statement> subjectWildcardMatches = quadStore.find(Constants.ANY_URI, null, null, (URI[]) null);

        // UNION exact and wildcard matches of the statements subject
        Collection<Statement> subjectMatches = CollectionUtils.union(subjectExactMatches, subjectWildcardMatches);

        Collection<Statement> predicateExactMatches = quadStore.find(null, statement.getPredicate(), null,
                (URI[]) null);
        Collection<Statement> predicateWildcardMatches = quadStore.find(null, Constants.ANY_URI, null,
                (URI[]) null);

        // UNION exact and wildcard matches of the statements predicate
        Collection<Statement> predicateMatches = CollectionUtils.union(predicateExactMatches,
                predicateWildcardMatches);

        Collection<Statement> objectExactMatches = quadStore.find(null, null, statement.getObject(), (URI[]) null);
        Collection<Statement> objectWildcardMatches = quadStore.find(null, null, Constants.ANY_URI, (URI[]) null);

        // UNION exact and wildcard matches of the statements object
        Collection<Statement> objectMatches = CollectionUtils.union(objectExactMatches, objectWildcardMatches);

        Collection<Statement> namedGraphExactMatches = quadStore.find(null, null, null,
                statement.getNamedGraphUri());
        Collection<Statement> namedGraphWildcardMatches = quadStore.find(null, null, null, Constants.ANY_URI);

        // UNION exact and wildcard matches of the statements namedGraphUri
        Collection<Statement> namedGraphMatches = CollectionUtils.union(namedGraphExactMatches,
                namedGraphWildcardMatches);

        // INTERSECTION of the unions
        Collection<Statement> intersection = CollectionUtils.intersection(CollectionUtils.intersection(
                CollectionUtils.intersection(subjectMatches, predicateMatches), objectMatches), namedGraphMatches);

        return intersection;
    }

    protected void connect() throws AnzoException {
        IOperationContext context = null;
        try {
            context = new BaseOperationContext(INotificationRegistrationService.REGISTER_SUBSCRIBER,
                    BaseOperationContext.generateOperationId(), anzoClient.getServicePrincipal());
            boolean regOk = anzoClient.clientDatasource.getNotificationRegistrationService()
                    .registerSubscriber(context, null);
            if (!regOk) {
                throw new AnzoException(ExceptionConstants.COMBUS.SERVER_CONNECT_EXCEPTION);
            }
            this.combusConnection.registerMessageListener(this);
            for (Statement pattern : patternToTracker.keySet()) {
                SelectorTracker tracker = patternToTracker.get(pattern);
                setupTrackerNotification(tracker);
            }
            if (transactionListeners.size() > 0) {
                combusConnection.registerTopicListener(COMBUS.TRANSACTIONS_TOPIC, this);
            }
            connected = true;
        } finally {
            if (context != null) {
                context.clearMDC();
            }
        }
    }

    protected void disconnect(boolean enableReconnect, boolean clean) throws AnzoException {
        IOperationContext context = null;
        try {
            connected = false;
            if (clean && combusConnection.isConnected()) {
                context = new BaseOperationContext(INotificationRegistrationService.UNREGISTER_SUBSCRIBER,
                        BaseOperationContext.generateOperationId(), null);
                anzoClient.clientDatasource.getNotificationRegistrationService().unregisterSubscriber(context,
                        null);
                if (transactionListeners.size() > 0) {
                    combusConnection.unregisterTopicListener(COMBUS.TRANSACTIONS_TOPIC);
                }
                this.combusConnection.unregisterMessageListener(this);
            }

            if (!enableReconnect) {
                quadStore.clear();
                patternToTracker.clear();
            }
        } finally {
            if (context != null) {
                context.clearMDC();
            }
        }
    }

    public void onMessage(final Message message) {
        if (message == null) {
            log.error(LogUtils.COMBUS_MARKER,
                    Messages.formatString(ExceptionConstants.COMBUS.ERROR_PROCESSING_MESSGE, "null message"),
                    new AnzoException(ExceptionConstants.COMBUS.JMS_MESSAGE_PARSING));
            return;
        }
        try {
            if (log.isTraceEnabled()) {
                log.trace(LogUtils.COMBUS_MARKER, MessageUtils.prettyPrint(message, "Notification Recieved: "));
            }
            String operation = message.getStringProperty(SerializationConstants.operation);
            if (operation != null) {
                if (operation.equals(SerializationConstants.transactionComplete)) {
                    handleTransactionMessage(message);
                } else if (SerializationConstants.updateResults.equals(operation)) {
                    handleUpdateMessage(message);
                } else if (SerializationConstants.datasetUpdate.equals(operation)) {
                    handleDatasetUpdateMessage(message);
                }
            }

        } catch (JMSException jmsex) {
            log.error(LogUtils.INTERNAL_MARKER, Messages.formatString(
                    ExceptionConstants.COMBUS.ERROR_PROCESSING_MESSGE, "realtime update message"), jmsex);
        } catch (AnzoException e) {
            log.error(LogUtils.INTERNAL_MARKER, Messages
                    .formatString(ExceptionConstants.COMBUS.ERROR_PROCESSING_MESSGE, "realtime update message"), e);
        }

    }

    // Normal update message. Iterate through those who have permission.
    protected void handleUpdateMessage(Message message) throws AnzoException {
        try {
            if (!message.propertyExists(SerializationConstants.method)) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            if (!message.propertyExists(SerializationConstants.subject)) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            if (!message.propertyExists(SerializationConstants.predicate)) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            if (!message.propertyExists(SerializationConstants.namedGraphUri)) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            if (!message.propertyExists(SerializationConstants.object)) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            boolean method = message.getBooleanProperty(SerializationConstants.method);
            // Extract statement info from message.
            String predicateUri = message.getStringProperty(SerializationConstants.predicate);
            String namedGraph = message.getStringProperty(SerializationConstants.namedGraphUri);

            // Construct  statement.
            Value object = CommonSerializationUtils.getObjectFromMessage(message);
            if (object == null) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            Resource subject = CommonSerializationUtils.getSubjectFromMessage(message);
            if (subject == null) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            URI predicate = Constants.valueFactory.createURI(predicateUri);
            URI namedGraphUri = Constants.valueFactory.createURI(namedGraph);
            URI graphURI = namedGraphUri;
            Statement stmt = Constants.valueFactory.createStatement(subject, predicate, object, graphURI);
            notifyTrackers(method, stmt);
        } catch (JMSException jmsex) {
            throw new AnzoException(ExceptionConstants.COMBUS.JMS_MESSAGE_PARSING, jmsex);
        }
    }

    // Normal update message. Iterate through those who have permission.
    protected void handleDatasetUpdateMessage(Message message) throws AnzoException {
        try {
            if (!message.propertyExists(SerializationConstants.datasetUri)) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            if (!message.propertyExists(SerializationConstants.namedGraphUri)) {
                throw new AnzoException(ExceptionConstants.COMBUS.JMS_MISSING_MESSAGE_PARAMETER);
            }
            String namedGraph = message.getStringProperty(SerializationConstants.namedGraphUri);
            String datasetUris = message.getStringProperty(SerializationConstants.datasetUri);
            Collection<URI> uris = new ArrayList<URI>();
            StringTokenizer st = new StringTokenizer(datasetUris, ",");
            while (st.hasMoreTokens()) {
                uris.add(Constants.valueFactory.createURI(st.nextToken()));
            }
            notifyTrackers(Constants.valueFactory.createURI(namedGraph), uris);
        } catch (JMSException jmsex) {
            throw new AnzoException(ExceptionConstants.COMBUS.JMS_MESSAGE_PARSING, jmsex);
        }
    }

    // End of transaction.
    protected void handleTransactionMessage(Message message) throws AnzoException {
        // Extract method and transactionId.
        String transactionContextString = null;
        try {
            transactionContextString = message.propertyExists(SerializationConstants.transactionContext)
                    ? message.getStringProperty(SerializationConstants.transactionContext)
                    : null;
        } catch (JMSException e) {
            throw new AnzoException(ExceptionConstants.COMBUS.JMS_MESSAGE_PARSING, e);
        }
        String updatedNamedGraphs = null;
        try {
            updatedNamedGraphs = message.propertyExists(SerializationConstants.namedGraphUpdates)
                    ? message.getStringProperty(SerializationConstants.namedGraphUpdates)
                    : null;
        } catch (JMSException e) {
            throw new AnzoException(ExceptionConstants.COMBUS.JMS_MESSAGE_PARSING, e);
        }
        long timestamp = -1;
        try {
            timestamp = Long.valueOf(message.getLongProperty(SerializationConstants.transactionTimestamp));
        } catch (JMSException e) {
            throw new AnzoException(ExceptionConstants.COMBUS.JMS_MESSAGE_PARSING, e);
        }
        String transactionUri = null;
        try {
            transactionUri = message.propertyExists(SerializationConstants.transactionURI)
                    ? message.getStringProperty(SerializationConstants.transactionURI)
                    : null;
        } catch (JMSException e) {
            throw new AnzoException(ExceptionConstants.COMBUS.JMS_MESSAGE_PARSING, e);
        }
        URI transactionURI = (transactionUri != null) ? MemURI.create(transactionUri) : null;
        transactionLock.writeLock().lock();
        try {
            Collection<Statement> context = null;
            if (transactionContextString != null) {
                context = ReadWriteUtils.readStatements(transactionContextString, RDFFormat.JSON);
            }
            Map<URI, Long> updatedGraphs = null;
            if (updatedNamedGraphs != null) {
                updatedGraphs = CommonSerializationUtils.readNamedGraphRevisions(updatedNamedGraphs);
            }
            UpdateTransaction transaction = new UpdateTransaction(transactionURI, timestamp, context,
                    updatedGraphs);
            notifyTransactionListeners(transaction);
        } finally {
            transactionLock.writeLock().unlock();
        }
    }
}