/*
* Copyright (c) 2009-2012 Clark & Parsia, LLC. <http://www.clarkparsia.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.clarkparsia.empire.ds.impl;
import com.complexible.common.openrdf.model.Graphs;
import org.openrdf.model.Graph;
import org.openrdf.model.Statement;
import com.clarkparsia.empire.ds.DataSource;
import com.clarkparsia.empire.ds.DataSourceException;
import com.clarkparsia.empire.ds.QueryException;
import com.clarkparsia.empire.ds.MutableDataSource;
import com.clarkparsia.empire.ds.ResultSet;
import com.clarkparsia.empire.ds.TripleSource;
import com.clarkparsia.empire.QueryFactory;
import com.clarkparsia.empire.ds.SupportsTransactions;
import java.net.ConnectException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
/**
* <p><b>Very</b> simple transactional support to put on top of a database that does not already support it.
* We do the operations live on the database, but keep a track of what was added or deleted so on rollback we can
* try and undo the edits. If the rollback fails, it very well could have failed for part of the rollback
* and you are left with an inconsistent database. For real transactional support, use a database that supports it.</p>
*
* @author Michael Grove
* @since 0.1
* @version 0.7
*/
public class TransactionalDataSource implements DataSource, MutableDataSource, SupportsTransactions {
/**
* The DataSource the operations will be applied to
*/
private MutableDataSource mDataSource;
/**
* If the underlying DataSource (mDataSource) is a TripleSource, this is
* identical to mDataSource. Otherwise, this is a TripleSourceAdapter to mDataSource
*/
private TripleSource mTripleSource;
private List<TransactionOp> mTransactionOps;
/**
* Whether or not a transaction is currently active
*/
private boolean mIsInTransaction;
/**
* @inheritDoc
*/
public TransactionalDataSource(final MutableDataSource theDataSource) {
mDataSource = theDataSource;
if (mDataSource instanceof TripleSource) {
mTripleSource = (TripleSource) mDataSource;
}
else {
mTripleSource = new TripleSourceAdapter(mDataSource);
}
mTransactionOps = new LinkedList<TransactionOp>();
}
/**
* @inheritDoc
*/
public void begin() throws DataSourceException {
assertNotInTransaction();
mIsInTransaction = true;
mTransactionOps.clear();
}
/**
* @inheritDoc
*/
public void commit() throws DataSourceException {
assertInTransaction();
mIsInTransaction = false;
mTransactionOps.clear();
}
/**
* @inheritDoc
*/
public void rollback() throws DataSourceException {
assertInTransaction();
try {
// revert all operations starting from the last one and go backwards until the first one
for (ListIterator<TransactionOp> it = mTransactionOps.listIterator(mTransactionOps.size());
it.hasPrevious(); ) {
TransactionOp op = it.previous();
if (op.isAdded()) {
mDataSource.remove(op.getData());
}
else {
mDataSource.add(op.getData());
}
}
}
catch (DataSourceException e) {
throw new DataSourceException("Rollback failed, database is likely to be in an inconsistent state.", e);
}
finally {
mIsInTransaction = false;
mTransactionOps.clear();
}
}
/**
* @inheritDoc
*/
public void add(final Graph theGraph) throws DataSourceException {
if (isInTransaction()) {
mTransactionOps.add(new TransactionOp(nonExistingTriples(theGraph), true));
}
mDataSource.add(theGraph);
}
/**
* @inheritDoc
*/
public void remove(final Graph theGraph) throws DataSourceException {
if (isInTransaction()) {
mTransactionOps.add(new TransactionOp(existingTriples(theGraph), false));
}
mDataSource.remove(theGraph);
}
/**
* @inheritDoc
*/
public boolean isConnected() {
return mDataSource.isConnected();
}
/**
* @inheritDoc
*/
public void connect() throws ConnectException {
mDataSource.connect();
}
/**
* @inheritDoc
*/
public void disconnect() {
mDataSource.disconnect();
}
/**
* @inheritDoc
*/
public ResultSet selectQuery(final String theQuery) throws QueryException {
return mDataSource.selectQuery(theQuery);
}
/**
* @inheritDoc
*/
public Graph graphQuery(final String theQuery) throws QueryException {
return mDataSource.graphQuery(theQuery);
}
/**
* @inheritDoc
*/
public Graph describe(final String theQuery) throws QueryException {
return mDataSource.describe(theQuery);
}
/**
* @inheritDoc
*/
public boolean ask(final String theQuery) throws QueryException {
return mDataSource.ask(theQuery);
}
/**
* @inheritDoc
*/
public QueryFactory getQueryFactory() {
return mDataSource.getQueryFactory();
}
/**
* Return whether or not this data source is in a transaction
* @return true if it is in a transaction, false otherwise
*/
public boolean isInTransaction() {
return mIsInTransaction;
}
/**
* Asserts that this DataSource should not be in a transaction
* @throws com.clarkparsia.empire.ds.DataSourceException thrown if the data source is in a transaction
*/
private void assertNotInTransaction() throws DataSourceException {
if (isInTransaction()) {
throw new DataSourceException("Cannot complete action, currently in a transaction");
}
}
/**
* Asserts that this DataSource should be in a transaction
* @throws DataSourceException thrown if the data source is not in a transaction
*/
private void assertInTransaction() throws DataSourceException {
if (!isInTransaction()) {
throw new DataSourceException("Cannot complete action, not in a transaction");
}
}
/**
* Filters all triples from the specified graph that already exist in the underlying data source
*
* @param theData the data to be filtered
* @return a graph that contains only triples that do not exist in the data source
* @throws DataSourceException if querying the data source causes an error
*/
private Graph nonExistingTriples(Graph theData) throws DataSourceException {
Graph aResult = Graphs.newGraph();
// TODO: is there a more efficient way to check that than triple-by-triple?
// (for remote data sources this will cause one request for triple ...)
for (Iterator<Statement> it = theData.iterator(); it.hasNext(); ) {
Statement statement = it.next();
if (!existsInDataSource(statement)) {
aResult.add(statement);
}
}
return aResult;
}
/**
* Filters all triples from the specified graph that do not exist in the underlying data source
*
* @param theData the data to be filtered
* @return a graph that contains only triples that already exist in the data source
* @throws DataSourceException if querying the data source causes an error
*/
private Graph existingTriples(Graph theData) throws DataSourceException {
Graph aResult = Graphs.newGraph();
// TODO: is there a more efficient way to check that than triple-by-triple?
// (for remote data sources this will cause one request for triple ...)
for (Iterator<Statement> it = theData.iterator(); it.hasNext(); ) {
Statement statement = it.next();
if (existsInDataSource(statement)) {
aResult.add(statement);
}
}
return aResult;
}
/**
* Checks whether the given statement exists in the data source.
*
* @param s the statement to be checked
* @return true, if the statement exists, false otherwise
* @throws DataSourceException
*/
private boolean existsInDataSource(Statement s) throws DataSourceException {
return mTripleSource.getStatements(s.getSubject(), s.getPredicate(), s.getObject(), s.getContext()).iterator().hasNext();
}
/**
* Holds information about an add/remove operation within transaction
*
* @author Blazej Bulka <blazej@clarkparsia.com>
*/
private static class TransactionOp {
/**
* The data that was actually added/removed.
*
* By "actually added" means triples that did not exist in the triple store before and
* were added.
*
* By "actually removed" means triples that existed in the triple store and were removed
*
* The terms above are introduced because it is possible that the user attempts to add triples that were
* already there before -- a rollback must not remove such triples. Similarly, a user can request removal
* of triples that did not exist in the triple store -- a rollback must not add such triples.
*/
private Graph mData;
/**
* Information whether triples were added (true) or removed (false).
*/
private boolean mAdded;
TransactionOp(Graph theData, boolean theAdded) {
this.mData = theData;
this.mAdded = theAdded;
}
/**
* Gets the data involved in the operation
*
* @return graph containing the data that was actually added/deleted
*/
public Graph getData() {
return mData;
}
/**
* Gets the flag whether the data was added/deleted
*
* @return true if the data was added, false if it was deleted
*/
public boolean isAdded() {
return mAdded;
}
}
}