package eu.fbk.knowledgestore.triplestore;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterators;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.impl.ContextStatementImpl;
import org.openrdf.query.Binding;
import org.openrdf.query.BindingSet;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.query.QueryEvaluationException;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.TupleQuery;
import org.openrdf.repository.Repository;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.sail.SailRepository;
import org.openrdf.sail.Sail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import info.aduna.iteration.CloseableIteration;
import info.aduna.iteration.CloseableIteratorIteration;
import info.aduna.iteration.IterationWrapper;
import eu.fbk.knowledgestore.data.Handler;
import eu.fbk.knowledgestore.internal.Util;
import eu.fbk.knowledgestore.runtime.DataCorruptedException;
public final class RepositoryTripleStore implements TripleStore {
private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryTripleStore.class);
private final Repository repository;
public RepositoryTripleStore(final Sail sail) {
this(new SailRepository(sail));
}
public RepositoryTripleStore(final Repository repository) {
this.repository = Preconditions.checkNotNull(repository);
LOGGER.info("RepositoryTripleStore configured, backend={}", repository.getClass()
.getSimpleName());
}
@Override
public void init() throws IOException {
try {
this.repository.initialize();
} catch (final Throwable ex) {
throw new IOException("Could not initialize Sesame repository");
}
}
@Override
public TripleTransaction begin(final boolean readOnly) throws IOException {
return new RepositoryTripleTransaction(readOnly);
}
@Override
public void reset() throws IOException {
RepositoryConnection connection = null;
try {
connection = this.repository.getConnection();
connection.clear();
connection.clearNamespaces();
LOGGER.info("Sesame repository successfully resetted");
} catch (final RepositoryException ex) {
throw new IOException("Could not reset Sesame repository", ex);
} finally {
Util.closeQuietly(connection);
}
}
@Override
public void close() {
try {
this.repository.shutDown();
} catch (final RepositoryException ex) {
LOGGER.error("Failed to shutdown Sesame repository", ex);
}
}
@Override
public String toString() {
return getClass().getSimpleName();
}
private class RepositoryTripleTransaction implements TripleTransaction {
private final RepositoryConnection connection;
private final boolean readOnly;
private final long ts;
private boolean dirty;
RepositoryTripleTransaction(final boolean readOnly) throws IOException {
final long ts = System.currentTimeMillis();
final RepositoryConnection connection;
try {
connection = RepositoryTripleStore.this.repository.getConnection();
} catch (final RepositoryException ex) {
throw new IOException("Could not connect to Sesame repository", ex);
}
this.connection = connection;
this.readOnly = readOnly;
this.ts = ts;
this.dirty = false;
try {
connection.begin();
} catch (final Throwable ex) {
Util.closeQuietly(connection);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(this + " started in " + (readOnly ? "read-only" : "read-write")
+ " mode, " + (System.currentTimeMillis() - ts) + " ms");
}
}
private void checkWritable() {
if (this.readOnly) {
throw new IllegalStateException(
"Write operation not allowed on read-only transaction");
}
}
@Nullable
private <T, E extends Exception> CloseableIteration<T, E> logClose(
@Nullable final CloseableIteration<T, E> iteration) {
if (iteration == null || !LOGGER.isDebugEnabled()) {
return iteration;
}
final long ts = System.currentTimeMillis();
return new IterationWrapper<T, E>(iteration) {
@Override
protected void handleClose() throws E {
try {
super.handleClose();
} finally {
LOGGER.debug("Repository iteration closed after {} ms",
System.currentTimeMillis() - ts);
}
}
};
}
@Override
public CloseableIteration<? extends Statement, ? extends Exception> get(
final Resource subject, final URI predicate, final Value object,
final Resource context) throws IOException, IllegalStateException {
try {
final long ts = System.currentTimeMillis();
final CloseableIteration<? extends Statement, ? extends Exception> result;
if (subject == null || predicate == null || object == null || context == null) {
result = logClose(this.connection.getStatements(subject, predicate, object,
false, context));
LOGGER.debug("getStatements() iteration obtained in {} ms",
System.currentTimeMillis() - ts);
} else {
Iterator<Statement> iterator;
if (this.connection.hasStatement(subject, predicate, object, true, context)) {
iterator = Collections.emptyIterator();
} else {
iterator = Iterators
.<Statement>singletonIterator(new ContextStatementImpl(subject,
predicate, object, context));
}
result = new CloseableIteratorIteration<Statement, RuntimeException>(iterator);
LOGGER.debug("hasStatement() evaluated in {} ms", System.currentTimeMillis()
- ts);
}
return result;
} catch (final RepositoryException ex) {
throw new IOException("Error while retrieving matching statements", ex);
}
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> query(
final SelectQuery query, final BindingSet bindings, @Nullable final Long timeout)
throws IOException, UnsupportedOperationException, IllegalStateException {
LOGGER.debug("Evaluating query:\n{}", query.getString());
final TupleQuery tupleQuery;
try {
tupleQuery = this.connection.prepareTupleQuery(QueryLanguage.SPARQL,
query.getString());
} catch (final RepositoryException ex) {
throw new IOException("Failed to prepare SPARQL tuple query:\n" + query, ex);
} catch (final MalformedQueryException ex) {
// should not happen, as SelectQuery can only be created with valid queries
throw new UnsupportedOperationException(
"SPARQL query rejected as malformed by Sesame repository:\n" + query, ex);
}
if (bindings != null) {
for (final Binding binding : bindings) {
tupleQuery.setBinding(binding.getName(), binding.getValue());
}
}
if (timeout != null) {
// Note: we pass the value in ms, although the spec says seconds. However, at
// least for Virtuoso it seems that the value passed is interpreted as a ms value
tupleQuery.setMaxQueryTime(timeout.intValue());
}
final long ts = System.currentTimeMillis();
try {
// execute the query
final CloseableIteration<BindingSet, QueryEvaluationException> result;
result = logClose(tupleQuery.evaluate());
LOGGER.debug("Query result iteration obtained in {} ms",
System.currentTimeMillis() - ts);
return result;
} catch (final QueryEvaluationException ex) {
// return all the information available, so to help debugging
final StringBuilder builder = new StringBuilder();
boolean emitQuery = false;
builder.append("Query evaluation failed after ")
.append(System.currentTimeMillis() - ts).append(" ms");
if (ex.getMessage() != null) {
builder.append("\n").append(ex.getMessage());
emitQuery = !ex.getMessage().contains(query.getString());
}
if (emitQuery) {
builder.append("\nFailed query:\n\n").append(query);
}
throw new IOException(builder.toString(), ex);
}
}
@Override
public void infer(final Handler<? super Statement> handler) throws IOException,
IllegalStateException {
checkWritable();
// No inference done at this level (to be implemented in a decorator).
if (handler != null) {
try {
handler.handle(null);
} catch (final Throwable ex) {
Throwables.propagateIfPossible(ex, IOException.class);
throw new RuntimeException(ex);
}
}
}
@Override
public void add(final Iterable<? extends Statement> statements) throws IOException,
IllegalStateException {
Preconditions.checkNotNull(statements);
checkWritable();
try {
this.dirty = true;
this.connection.add(statements);
} catch (final RepositoryException ex) {
throw new DataCorruptedException("Error while adding statements", ex);
}
}
@Override
public void remove(final Iterable<? extends Statement> statements) throws IOException,
IllegalStateException {
Preconditions.checkNotNull(statements);
checkWritable();
try {
this.dirty = true;
this.connection.remove(statements);
} catch (final RepositoryException ex) {
throw new DataCorruptedException("Error while removing statements", ex);
}
}
@Override
public void end(final boolean commit) throws DataCorruptedException, IOException,
IllegalStateException {
final long ts = System.currentTimeMillis();
boolean committed = false;
try {
if (this.dirty) {
if (commit) {
try {
this.connection.commit();
committed = true;
} catch (final Throwable ex) {
try {
this.connection.rollback();
LOGGER.debug("{} rolled back after commit failure", this);
} catch (final RepositoryException ex2) {
throw new DataCorruptedException(
"Failed to rollback transaction after commit failure", ex);
}
throw new IOException(
"Failed to commit transaction (rollback forced)", ex);
}
} else {
try {
this.connection.rollback();
} catch (final Throwable ex) {
throw new DataCorruptedException("Failed to rollback transaction", ex);
}
}
}
} finally {
try {
this.connection.close();
} catch (final RepositoryException ex) {
LOGGER.error("Failed to close connection", ex);
} finally {
if (LOGGER.isDebugEnabled()) {
final long now = System.currentTimeMillis();
LOGGER.debug("{} {} and closed in {} ms, tx duration {} ms", this,
committed ? "committed" : "rolled back", now - ts, now - this.ts);
}
}
}
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}
}