it.uniroma2.art.semanticturkey.changetracking.sail.ChangeTracker Maven / Gradle / Ivy
package it.uniroma2.art.semanticturkey.changetracking.sail;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.rdf4j.common.transaction.IsolationLevel;
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.eclipse.rdf4j.model.vocabulary.SESAME;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.RepositoryResolver;
import org.eclipse.rdf4j.repository.base.RepositoryWrapper;
import org.eclipse.rdf4j.repository.http.HTTPRepository;
import org.eclipse.rdf4j.repository.sail.config.RepositoryResolverClient;
import org.eclipse.rdf4j.sail.NotifyingSail;
import org.eclipse.rdf4j.sail.NotifyingSailConnection;
import org.eclipse.rdf4j.sail.SailException;
import org.eclipse.rdf4j.sail.helpers.NotifyingSailWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.MoreObjects;
import it.uniroma2.art.semanticturkey.changetracking.vocabulary.CHANGELOG;
import it.uniroma2.art.semanticturkey.changetracking.vocabulary.CHANGETRACKER;
/**
* A {@link NotifyingSail} keeping track of changes to an underlying {@code Sail}. Each commit containing at
* least one relevant update is recorded into the support repository. The representation of the history
* conforms to the vocabulary {@link it.uniroma2.art.semanticturkey.changetracking.vocabulary.CHANGELOG}.
*
* A change is recorded only if it is an effective update to the underlying data: i.e. either adding a triple
* that wasn't already assert, or removing a previously asserted triples. Self-canceling operations are
* ignored as well.
*
* The client of this Sail
can manage the tracking system, by reading/writing appropriate
* contexts: these operations are intercepted by this Sail, so that they are not executed against the
* underlying data. Currently, only addStatement(..)
, removeStatements(...)
and
* getStatements(..)
are supported. In particular, SPARQL Queries cannot be used to read the
* special-purpose contexts defined by this Sail
.
*
* The contexts {@link CHANGETRACKER#STAGED_ADDITIONS} and {@link CHANGETRACKER#STAGED_REMOVALS} can be used
* to list the triples being staged for addition and removal, respectively.
*
* The context {@link CHANGETRACKER#GRAPH_MANAGEMENT} contains the description of a homonymous resource, which
* is associated with the graphs to include and exclude, via the properties
* {@link CHANGETRACKER#INCLUDE_GRAPH} and {@link CHANGETRACKER#EXCLUDE_GRAPH}, respectively. An update is
* recorded into the history only if its context is a graph such that it satisfies the inclusion criterion and
* it does not satisfy the exclusion criterion. An empty set of included graphs is equivalent to include all
* graphs, while an empty set of excluded graphs is equivalent to not rejecting any graph. The resource
* {@link CHANGELOG#NULL} (formerly {@link SESAME#NIL}) represents the null
context, while
* {@link SESAME#WILDCARD} is another mechanism to specify all graphs.
*
* By default, the history ignores the null
context and includes other contexts. Excluding the
* null
context is a simple mechanism to ignore inferred triples.
*
* It is possible to write additional metadata about a commit through the context
* {@link CHANGETRACKER#COMMIT_METADATA}. Since the client doesn't know which resource will represent the
* commit in the history repository, the client may identify the commit via the IRI
* {@link CHANGETRACKER#COMMIT_METADATA}: when the commit metadata is written into the history, this
* identifier will be replace with the actual identifier of the commit.
*
* When validation is enabled, write operations are first logged inside the validation graph (in the support
* repository) and applied to the staging graphs (in the core repository).
*
* To accept/reject a previous transaction, it is sufficient to write in the context
* {@link CHANGETRACKER#VALIDATION}, as follows:
* {@code conn.add(commitIRI, CHANGETRACKER.VALIDATION, CHANGETRACKER.ACCEPT | CHANGETRACKER.ACCEPT, CHANGETRACKER.VALIDATION)}
*
* @author Manuel Fiorelli
*/
public class ChangeTracker extends NotifyingSailWrapper implements RepositoryResolverClient {
protected static final Logger logger = LoggerFactory.getLogger(ChangeTracker.class);
public static final Optional OPTIONAL_TRUE = Optional.of(true);
public static final Optional OPTIONAL_FALSE = Optional.of(false);
public static final String PROPERTIES = "changetracker.properties";
protected static final Properties props;
protected static final String PROP_VERSION = "version";
protected static final String version;
protected final String supportRepoId;
protected final String serverURL;
protected final String metadataNS;
protected final IRI historyGraph;
protected final IRI validationGraph;
protected final IRI blacklistGraph;
private Repository supportRepo;
protected final Model graphManagement;
protected final boolean historyEnabled;
protected final boolean validationEnabled;
protected final boolean undoEnabled;
protected final boolean blacklistEnabled;
protected final Optional interactiveNotifications;
private Function repositoryResolver;
protected Optional undoStack;
static {
props = new Properties();
try {
try (InputStream is = ChangeTracker.class.getResourceAsStream(PROPERTIES)) {
props.load(is);
}
version = props.getProperty(PROP_VERSION);
if (version == null || version.equals("") || version.contains("$"))
throw new IllegalStateException("Wrong version number detected: " + version);
} catch (IOException e) {
logger.error(
"Exception preventing initialization of class " + ChangeTracker.class.getSimpleName(), e);
throw new IllegalStateException(e);
}
}
public static String getVersion() {
return version;
}
public ChangeTracker(/* @Nullable */ String serverURL, /* @Nullable */ String supportRepoId, String metadataNS,
IRI historyGraph, Set includeGraph, Set excludeGraph, boolean historyEnabled,
boolean validationEnabled, boolean undoEnabled, Optional interactiveNotifications,
/* @Nullable */ IRI validationGraph, boolean blacklistEnabled,
/* @Nullable */ IRI blacklistGraph) {
this.serverURL = serverURL;
this.supportRepoId = supportRepoId;
this.metadataNS = metadataNS;
this.historyGraph = historyGraph;
this.graphManagement = new LinkedHashModel();
this.historyEnabled = historyEnabled;
this.validationEnabled = validationEnabled;
this.validationGraph = validationGraph;
this.blacklistGraph = blacklistGraph;
this.interactiveNotifications = interactiveNotifications;
includeGraph.forEach(
g -> graphManagement.add(CHANGETRACKER.GRAPH_MANAGEMENT, CHANGETRACKER.INCLUDE_GRAPH, g));
excludeGraph.forEach(
g -> graphManagement.add(CHANGETRACKER.GRAPH_MANAGEMENT, CHANGETRACKER.EXCLUDE_GRAPH, g));
if (historyGraph != null) {
graphManagement.add(CHANGETRACKER.GRAPH_MANAGEMENT, CHANGETRACKER.HISTORY_GRAPH, historyGraph);
}
if (validationGraph != null) {
graphManagement.add(CHANGETRACKER.GRAPH_MANAGEMENT, CHANGETRACKER.VALIDATION_GRAPH,
validationGraph);
}
this.undoEnabled = undoEnabled;
if (!historyEnabled && !validationEnabled && this.undoEnabled) {
undoStack = Optional.of(new UndoStack());
} else {
undoStack = Optional.empty();
}
this.blacklistEnabled = blacklistEnabled;
if (blacklistGraph != null) {
graphManagement.add(CHANGETRACKER.GRAPH_MANAGEMENT, CHANGETRACKER.BLACKLIST_GRAPH,
blacklistGraph);
}
}
public void setRepositoryResolver(RepositoryResolver resolver) {
this.repositoryResolver = resolver::getRepository;
}
public void setRepositoryResolver(org.eclipse.rdf4j.repository.sail.config.RepositoryResolver resolver) {
this.repositoryResolver = resolver::getRepository;
}
@Override
public void init() throws SailException {
super.init();
}
@Override
public void shutDown() throws SailException {
try {
releaseSupportRepo();
} finally {
super.shutDown();
}
}
@Override
public ChangeTrackerConnection getConnection() throws SailException {
logger.debug("Obtaining new connection");
NotifyingSailConnection delegate = super.getConnection();
ChangeTrackerConnection connection = new ChangeTrackerConnection(delegate, this);
return connection;
}
@Override
public IsolationLevel getDefaultIsolationLevel() {
IsolationLevel isolationLevel = super.getDefaultIsolationLevel();
if (interactiveNotifications.equals(OPTIONAL_FALSE)) {
return isolationLevel;
} else {
if (isolationLevel.isCompatibleWith(IsolationLevels.SERIALIZABLE)) {
return isolationLevel;
} else {
return MoreObjects.firstNonNull(
IsolationLevels.getCompatibleIsolationLevel(IsolationLevels.SERIALIZABLE,
super.getSupportedIsolationLevels()),
IsolationLevels.SERIALIZABLE);
}
}
}
@Override
public List getSupportedIsolationLevels() {
List supportedByDelegate = super.getSupportedIsolationLevels();
if (interactiveNotifications.equals(OPTIONAL_FALSE)) {
return supportedByDelegate;
} else {
return supportedByDelegate.stream()
.filter(level -> level.isCompatibleWith(IsolationLevels.SERIALIZABLE))
.collect(Collectors.toList());
}
}
synchronized Repository getSupportRepo() {
if (supportRepo == null) {
if (serverURL != null) {
supportRepo = new HTTPRepository(serverURL, supportRepoId);
} else if (supportRepoId != null) {
supportRepo = new RepositoryWrapper(repositoryResolver.apply(supportRepoId)) {
@Override
public void shutDown() throws RepositoryException {
// Ignore shutdown of the referenced repository
}
};
} else {
throw new IllegalStateException("Missing locator for the support repository");
}
}
return supportRepo;
}
synchronized void releaseSupportRepo() {
if (supportRepo != null) {
supportRepo.shutDown();
supportRepo = null;
}
}
}