From 440d4e83df8b7704d2e2e1c4f90f42b567c8ba5a Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 22 Dec 2023 14:29:39 +0100 Subject: [PATCH] Audit module (Change Tracking) (#390) * merged changeset_user_info * changed default graph uri * audit code * Modified audit controller * Audit controller added user information * Add listener to RDFService configuration models * added some filters for audit history * More filtering options for audit controller * added user id in audit history table * Allow only admins to access audit page * fix for prev commit * Set default end date to be the next day to avoid empty results * Removed not implemented AuditDaoFS implementation, added tests for AuditDatTDB * added example configuration * Fixed some typos * Don't show empty changes on audit history page * fix: release dataset before close * remove empty test file * chore: fixed formatting * Don't delete TDB dataset from temporary directory as it results in test errors on Windows and the directory will be deleted at some point anyway. * refactored and improved sparql queries fixes * style fixes * fixes * one more fix --------- Co-authored-by: Georgy Litvinov --- .../webapp/application/ApplicationImpl.java | 21 + .../audit/AbstractListStatementsMethod.java | 50 ++ .../webapp/audit/AuditChangeListener.java | 63 ++ .../vitro/webapp/audit/AuditChangeSet.java | 240 ++++++++ .../vitro/webapp/audit/AuditModule.java | 11 + .../vitro/webapp/audit/AuditResults.java | 39 ++ .../vitro/webapp/audit/AuditSetup.java | 71 +++ .../audit/ListAddedStatementsMethod.java | 13 + .../audit/ListRemovedStatementsMethod.java | 13 + .../vitro/webapp/audit/TDBAuditModule.java | 52 ++ .../audit/controller/AuditController.java | 331 +++++++++++ .../vitro/webapp/audit/storage/AuditDAO.java | 47 ++ .../webapp/audit/storage/AuditDAOFactory.java | 52 ++ .../webapp/audit/storage/AuditDAOJena.java | 540 ++++++++++++++++++ .../webapp/audit/storage/AuditDAOTDB.java | 63 ++ .../webapp/audit/storage/AuditVocabulary.java | 23 + .../DeleteIndividualController.java | 3 +- .../controller/jena/JenaIngestController.java | 11 +- .../controller/jena/RDFUploadController.java | 40 +- .../edit/n3editing/VTwo/ProcessRdfForm.java | 60 +- .../configuration/IdModelSelector.java | 5 + .../configuration/ModelSelector.java | 2 + .../configuration/StandardModelSelector.java | 9 +- .../controller/ProcessRdfFormController.java | 9 +- .../webapp/i18n/TranslationConverter.java | 8 +- .../vitro/webapp/modules/Application.java | 3 + .../ontology/update/KnowledgeBaseUpdater.java | 6 +- .../vitro/webapp/rdfservice/ChangeSet.java | 49 ++ .../vitro/webapp/rdfservice/ModelChange.java | 10 + .../webapp/rdfservice/impl/ChangeSetImpl.java | 23 +- .../rdfservice/impl/ModelChangeImpl.java | 25 + .../vitro/webapp/reasoner/ABoxRecomputer.java | 10 +- .../vitro/webapp/audit/AuditDaoTDBTest.java | 159 ++++++ .../RDFServiceNotificationTest.java | 134 +++++ .../vitro/webapp/modules/ApplicationStub.java | 7 + .../config/example.applicationSetup.n3 | 11 + .../WEB-INF/resources/startup_listeners.txt | 3 + webapp/src/main/webapp/css/audit/audit.css | 35 ++ .../freemarker/body/audit/auditHistory.ftl | 89 +++ 39 files changed, 2276 insertions(+), 64 deletions(-) create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AbstractListStatementsMethod.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeListener.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeSet.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditModule.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditResults.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditSetup.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListAddedStatementsMethod.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListRemovedStatementsMethod.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/TDBAuditModule.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/controller/AuditController.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAO.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOFactory.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOJena.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOTDB.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditVocabulary.java create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/audit/AuditDaoTDBTest.java create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceNotificationTest.java create mode 100644 webapp/src/main/webapp/css/audit/audit.css create mode 100644 webapp/src/main/webapp/templates/freemarker/body/audit/auditHistory.ftl diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/application/ApplicationImpl.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/application/ApplicationImpl.java index 1838bb3ffe..198c0acf51 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/application/ApplicationImpl.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/application/ApplicationImpl.java @@ -8,6 +8,7 @@ import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import edu.cornell.mannlib.vitro.webapp.audit.AuditModule; import org.apache.jena.ontology.OntDocumentManager; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; @@ -41,6 +42,7 @@ public class ApplicationImpl implements Application { private SearchIndexer searchIndexer; private ImageProcessor imageProcessor; private FileStorage fileStorage; + private AuditModule auditModule; private ContentTripleSource contentTripleSource; private ConfigurationTripleSource configurationTripleSource; private TBoxReasonerModule tboxReasonerModule; @@ -103,6 +105,15 @@ public void setFileStorage(FileStorage fs) { fileStorage = fs; } + @Override + public AuditModule getAuditModule() { + return auditModule; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#hasAuditModule", minOccurs = 0, maxOccurs = 1) + public void setAuditModule(AuditModule am) { + auditModule = am; + } @Override public ContentTripleSource getContentTripleSource() { return contentTripleSource; @@ -165,6 +176,11 @@ public void contextInitialized(ServletContextEvent sce) { fileStorage.startup(app, css); ss.info(this, "Started the FileStorage system: " + fileStorage); + AuditModule auditModule = app.getAuditModule(); + if (auditModule != null) { + auditModule.startup(app, css); + } + ContentTripleSource contentTripleSource = app .getContentTripleSource(); contentTripleSource.startup(app, css); @@ -213,6 +229,11 @@ public void contextDestroyed(ServletContextEvent sce) { app.getFileStorage().shutdown(app); app.getImageProcessor().shutdown(app); app.getSearchEngine().shutdown(app); + + AuditModule auditModule = app.getAuditModule(); + if (auditModule != null) { + auditModule.shutdown(app); + } } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AbstractListStatementsMethod.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AbstractListStatementsMethod.java new file mode 100644 index 0000000000..bb03c1e123 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AbstractListStatementsMethod.java @@ -0,0 +1,50 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +import java.util.List; + +import freemarker.ext.beans.StringModel; +import freemarker.template.SimpleScalar; +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; + +/** + * Base helper method for Freemarker + */ +public abstract class AbstractListStatementsMethod implements TemplateMethodModelEx { + @Override + public Object exec(List arguments) throws TemplateModelException { + // We expect two arguments + // 1 - an AuditChangeSet + // 2 - a graph URI + if (arguments.size() == 2) { + Object arg1 = arguments.get(0); + Object arg2 = arguments.get(1); + + // This looks odd, but the AuditChangeSet is wrapped in a StringModel + if (arg1 instanceof StringModel) { + arg1 = ((StringModel) arg1).getWrappedObject(); + } + + if (arg1 instanceof AuditChangeSet && arg2 instanceof SimpleScalar) { + AuditChangeSet dataset = (AuditChangeSet) arg1; + String graphUri = ((SimpleScalar) arg2).getAsString(); + + // Get the statements from the changeset for the named graph + return getStatements(dataset, graphUri); + } + } + + throw new TemplateModelException("Wrong arguments"); + } + + /** + * Abstract method to be implemented for Added / Removed statements + * + * @param dataset + * @param graphUri + * @return + */ + protected abstract Object getStatements(AuditChangeSet dataset, String graphUri); +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeListener.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeListener.java new file mode 100644 index 0000000000..b5f14f3b9d --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeListener.java @@ -0,0 +1,63 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAOFactory; +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeListener; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ModelChange; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.rdf.listeners.StatementListener; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelChangedListener; + +/** + * Listener for changes in the RDFService + */ +public class AuditChangeListener extends StatementListener implements ModelChangedListener, ChangeListener { + private static final Log log = LogFactory.getLog(AuditChangeListener.class); + + @Override + public void notifyModelChange(ModelChange modelChange) { + + // Convert the serialized statements into a Jena Model + Model changes = + RDFServiceUtils.parseModel(modelChange.getSerializedModel(), modelChange.getSerializationFormat()); + + // Get the changeset for the current request + AuditChangeSet auditChangeset = new AuditChangeSet(); + Model additions = auditChangeset.getAddedModel(modelChange.getGraphURI()); + + String userId = modelChange.getUserId(); + if (StringUtils.isBlank(userId)) { + Exception e = new Exception(); + log.debug("User id is not provided.", e); + userId = AuditVocabulary.RESOURCE_UNKNOWN; + } + auditChangeset.setUserId(userId); + + // Is the change adding or removing statements? + if (modelChange.getOperation() == ModelChange.Operation.REMOVE) { + // If we are removing statements, make sure we don't retain them in the additions + additions.remove(changes); + + // Record all of the changes in the Model of removed statements + Model removed = auditChangeset.getRemovedModel(modelChange.getGraphURI()); + removed.add(changes); + } else { + // Record all of the changes in the Model of added statements + additions.add(changes); + } + if (!auditChangeset.isEmpty()) { + // Write the changes to the audit store + AuditDAOFactory.getAuditDAO().write(auditChangeset); + } + } + + @Override + public void notifyEvent(String graphURI, Object event) { + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeSet.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeSet.java new file mode 100644 index 0000000000..76e8b2c644 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditChangeSet.java @@ -0,0 +1,240 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +import java.io.StringWriter; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; + +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary; +import org.apache.commons.lang3.StringUtils; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.DatasetFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFFormat; + +/** + * The current set of changes for a triple store, as tracked against a single request + */ +public class AuditChangeSet { + // Unique identifier for the changes + private final UUID id; + + // The time the request was made + private final Date requestTime; + + // The statements added to the triple store + private Dataset addedDataset = null; + + // The statements removed from the triple store + private Dataset removedDataset = null; + + // The statements removed from the triple store + private String userId = AuditVocabulary.RESOURCE_UNKNOWN; + + private String userEmail = ""; + + private String userFirstName = ""; + + private String userLastName = ""; + + public String getUserEmail() { + return userEmail; + } + + public void setUserEmail(String userEmail) { + this.userEmail = userEmail; + } + + public String getUserFirstName() { + return userFirstName; + } + + public void setUserFirstName(String userFirstName) { + this.userFirstName = userFirstName; + } + + public String getUserLastName() { + return userLastName; + } + + public void setUserLastName(String userLastName) { + this.userLastName = userLastName; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + /** + * Initialize a change set + */ + public AuditChangeSet() { + this.id = UUID.randomUUID(); + this.requestTime = new Date(); + } + + /** + * Create a change set for a given UUID / request time (e.g. for reading from the audit store) + * + * @param id + * @param requestTime + */ + public AuditChangeSet(UUID id, Date requestTime) { + this.id = id; + this.requestTime = requestTime; + } + + /** + * Get the unique identifier + * + * @return + */ + public UUID getUUID() { + return id; + } + + /** + * Get the request time + * + * @return + */ + public Date getRequestTime() { + return requestTime; + } + + /** + * Get the dataset of additions + * + * @return + */ + public Dataset getAddedDataset() { + return addedDataset; + } + + /** + * Get the dataset of removals + * + * @return + */ + public Dataset getRemovedDataset() { + return removedDataset; + } + + /** + * Get the model of added statements for a named graph + * + * @param graphURI + * @return + */ + public Model getAddedModel(String graphURI) { + if (addedDataset == null) { + addedDataset = DatasetFactory.createGeneral(); + } + + if (StringUtils.isEmpty(graphURI)) { + return addedDataset.getDefaultModel(); + } + + return addedDataset.getNamedModel(graphURI); + } + + /** + * Get the added statements for a named graph + * + * @param graphUri + * @return + */ + public String getAddedStatements(String graphUri) { + return getStatements(getAddedModel(graphUri)); + } + + /** + * Get the model of removed statements for a named graph + * + * @param graphURI + * @return + */ + public Model getRemovedModel(String graphURI) { + if (removedDataset == null) { + removedDataset = DatasetFactory.createGeneral(); + } + + if (StringUtils.isEmpty(graphURI)) { + return removedDataset.getDefaultModel(); + } + + return removedDataset.getNamedModel(graphURI); + } + + /** + * Get the removed statements for a named graph + * + * @param graphUri + * @return + */ + public String getRemovedStatements(String graphUri) { + return getStatements(getRemovedModel(graphUri)); + } + + /** + * Check if the added and removed datasets are empty + * + * @return + */ + public boolean isEmpty() { + if (addedDataset != null && !addedDataset.asDatasetGraph().isEmpty()) { + return false; + } + + if (removedDataset != null && !removedDataset.asDatasetGraph().isEmpty()) { + return false; + } + + return true; + } + + /** + * Get a set of all the named graphs in the added / removed datasets + * + * @return + */ + public Set getGraphUris() { + Set graphUris = new HashSet<>(); + + populateGraphUriSet(graphUris, addedDataset); + populateGraphUriSet(graphUris, removedDataset); + + return graphUris; + } + + private void populateGraphUriSet(Set graphUris, Dataset dataset) { + if (dataset == null) { + return; + } + + Iterator iterator = dataset.listNames(); + while (iterator.hasNext()) { + graphUris.add(iterator.next()); + } + } + + private String getStatements(Model model) { + if (model != null) { + StringWriter sw = new StringWriter(); + RDFDataMgr.write(sw, model, RDFFormat.NTRIPLES); + return sw.toString(); + } + + // No model, so return an empty string + return ""; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditModule.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditModule.java new file mode 100644 index 0000000000..436316d2cc --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditModule.java @@ -0,0 +1,11 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +import edu.cornell.mannlib.vitro.webapp.modules.Application; + +/** + * Module for the Audit engine + */ +public interface AuditModule extends Application.Module { +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditResults.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditResults.java new file mode 100644 index 0000000000..b8493254f8 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditResults.java @@ -0,0 +1,39 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +import java.util.List; + +/** + * Results object for retrieving entries from the audit store + */ +public class AuditResults { + private final long total; + private final long offset; + private final long limit; + + private final List datasets; + + public AuditResults(long total, long offset, long limit, List datasets) { + this.total = total; + this.offset = offset; + this.limit = limit; + this.datasets = datasets; + } + + public long getLimit() { + return limit; + } + + public long getTotal() { + return total; + } + + public long getOffset() { + return offset; + } + + public List getDatasets() { + return datasets; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditSetup.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditSetup.java new file mode 100644 index 0000000000..1a31c5b247 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/AuditSetup.java @@ -0,0 +1,71 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; + +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Listens for requests and manages tracking and writing audit entries + */ +@WebListener +public class AuditSetup implements ServletContextListener { + private static final AuditChangeListener CHANGE_LISTENER = new AuditChangeListener(); + // Collection to track content models per request (thread) + private static final Log log = LogFactory.getLog(AuditSetup.class.getName()); + + @Override + public void contextInitialized(ServletContextEvent sce) { + // Ensure that the audit module has been initialized + if (isAuditEnabled()) { + ServletContext ctx = sce.getServletContext(); + // Register listener with the RDFServices + RDFService contentRdfService = ModelAccess.on(ctx).getRDFService(WhichService.CONTENT); + registerChangeListener(contentRdfService); + RDFService configurationRdfService = ModelAccess.on(ctx).getRDFService(WhichService.CONFIGURATION); + registerChangeListener(configurationRdfService); + } + } + + /** + * Register a change listener with the RDFService + * + * @param ctx + */ + protected synchronized void registerChangeListener(RDFService rdfService) { + try { + rdfService.registerListener(CHANGE_LISTENER); + } catch (RDFServiceException e) { + log.error(e, e); + } + } + + /** + * Check that the audit module has been initialized + * + * @return + */ + private boolean isAuditEnabled() { + try { + // Audit module is enabled if there is one available in the application + return ApplicationUtils.instance().getAuditModule() != null; + } catch (IllegalStateException e) { + log.error(e, e); + } + return false; + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListAddedStatementsMethod.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListAddedStatementsMethod.java new file mode 100644 index 0000000000..d43e70ab41 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListAddedStatementsMethod.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +/** + * Freemarker helper method to retrieve the statements added to a graph + */ +public class ListAddedStatementsMethod extends AbstractListStatementsMethod { + @Override + protected Object getStatements(AuditChangeSet dataset, String graphUri) { + return dataset.getAddedStatements(graphUri); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListRemovedStatementsMethod.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListRemovedStatementsMethod.java new file mode 100644 index 0000000000..8a574f80ca --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/ListRemovedStatementsMethod.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +/** + * Freemarker helper method to retrieve the statements removed from a graph + */ +public class ListRemovedStatementsMethod extends AbstractListStatementsMethod { + @Override + protected Object getStatements(AuditChangeSet dataset, String graphUri) { + return dataset.getRemovedStatements(graphUri); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/TDBAuditModule.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/TDBAuditModule.java new file mode 100644 index 0000000000..b29a35af31 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/TDBAuditModule.java @@ -0,0 +1,52 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit; + +import java.nio.file.Path; + +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAOFactory; +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAOTDB; +import edu.cornell.mannlib.vitro.webapp.modules.Application; +import edu.cornell.mannlib.vitro.webapp.modules.ComponentStartupStatus; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; + +/** + * Implementation of the AuditModule that uses Jena TDB storage + * + * Configure this in applicationSetup.n3 to enable the Audit module + */ +public class TDBAuditModule implements AuditModule { + // The path for the tdb model + String tdbPath; + + /** + * TDB path configuration property set by the bean loader + * + * @param path + */ + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#hasTdbDirectory", minOccurs = 1, maxOccurs = 1) + public void setTdbPath(String path) { + tdbPath = path; + } + + @Override + public void startup(Application application, ComponentStartupStatus css) { + // Get the home directory + Path vitroHome = ApplicationUtils.instance().getHomeDirectory().getPath(); + + // Resolve the auidt tdb store path against the home directory + String resolvedPath = vitroHome.resolve(tdbPath).toString(); + + // Initialize the TDB DAO with the directory + AuditDAOTDB.initialize(resolvedPath); + // Initialize the DAO factory to use TDB + AuditDAOFactory.initialize(AuditDAOFactory.Storage.AUDIT_TDB); + } + + @Override + public void shutdown(Application application) { + // Clean up the Audit DAO TDB + AuditDAOTDB.shutdown(); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/controller/AuditController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/controller/AuditController.java new file mode 100644 index 0000000000..bf22a3ac46 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/controller/AuditController.java @@ -0,0 +1,331 @@ +package edu.cornell.mannlib.vitro.webapp.audit.controller; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.annotation.WebServlet; + +import edu.cornell.mannlib.vedit.beans.LoginStatusBean; +import edu.cornell.mannlib.vitro.webapp.audit.AuditChangeSet; +import edu.cornell.mannlib.vitro.webapp.audit.AuditResults; +import edu.cornell.mannlib.vitro.webapp.audit.ListAddedStatementsMethod; +import edu.cornell.mannlib.vitro.webapp.audit.ListRemovedStatementsMethod; +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAO; +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAOFactory; +import edu.cornell.mannlib.vitro.webapp.auth.permissions.PermissionSets; +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; +import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * UI for browsing audit entries + */ +@WebServlet(name = "AuditViewer", urlPatterns = { "/audit/*" }) +public class AuditController extends FreemarkerHttpServlet { + private static final Log log = LogFactory.getLog(AuditController.class); + + // Template for the UI + private static final String TEMPLATE_DEFAULT = "auditHistory.ftl"; + + private static final String PARAM_OFFSET = "offset"; + private static final String PARAM_LIMIT = "limit"; + private static final String PARAM_GRAPH = "graph"; + private static final String PARAM_ORDER = "order"; + private static final String PARAM_START_DATE = "start_date"; + private static final String PARAM_END_DATE = "end_date"; + private static final String PARAM_USER_URI = "user"; + private static final String DESC_ORDER = "DESC"; + private static final String ASC_ORDER = "ASC"; + private static final String[] limits = { "10", "30", "50", "100", "1000" }; + private static final String[] orders = { ASC_ORDER, DESC_ORDER }; + + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + @Override + protected AuthorizationRequest requiredActions(VitroRequest vreq) { + return SimplePermission.USE_MISCELLANEOUS_ADMIN_PAGES.ACTION; + } + + @Override + protected ResponseValues processRequest(VitroRequest vreq) { + if (log.isDebugEnabled()) { + dumpRequestParameters(vreq); + } + UserAccountsDao uad = ModelAccess.on(vreq).getWebappDaoFactory().getUserAccountsDao(); + Map body = new HashMap<>(); + + // Get the current user + UserAccount acc = LoginStatusBean.getCurrentUser(vreq); + if (acc == null || !isAdmin(acc)) { + return new TemplateResponseValues("error-standard.ftl", body); + } + // Get the offset parameter (or default if unset) + int offset = getOffset(vreq); + body.put(PARAM_OFFSET, offset); + // Get the limit parameter (or default 10 if unset) + int limit = getLimit(vreq); + body.put(PARAM_LIMIT, String.valueOf(limit)); + body.put("limits", limits); + // Get the start_date parameter (or week ago if unset) + Date startDate = getStartDate(vreq); + body.put(PARAM_START_DATE, sdf.format(startDate)); + // Get the end_date parameter (or tomorrow if unset) + Date endDate = getEndDate(vreq); + body.put(PARAM_END_DATE, sdf.format(endDate)); + // Get the user parameter (or empty if unset) + String userUri = getUserUri(vreq); + body.put("userUri", userUri); + // Get the graph_uri parameter (or empty if unset) + String graphUri = getGraph(vreq); + body.put("selectedGraphUri", graphUri); + // Get the order parameter (or default DESC if unset) + String order = getOrder(vreq); + body.put(PARAM_ORDER, order); + body.put("orders", orders); + // Get the Audit DAO + AuditDAO auditDAO = AuditDAOFactory.getAuditDAO(); + // Find a page of audit entries for the current user + AuditResults results = auditDAO.find(offset, limit, dateToTimeStamp(startDate), dateToTimeStamp(endDate), + userUri, graphUri, ASC_ORDER.equals(order)); + List users = auditDAO.getUsers(); + body.put("users", users); + List graphUris = auditDAO.getGraphs(); + body.put("graphs", graphUris); + + setUserData(results.getDatasets(), uad); + // Pass the results to Freemarker + body.put("results", results); + + // Create next / previous links + if (offset > 0) { + body.put("prevPage", getPreviousPageLink(offset, limit, sdf.format(startDate), sdf.format(endDate), order, + userUri, graphUri, vreq.getServletPath())); + } + if (offset < (results.getTotal() - limit)) { + body.put("nextPage", getNextPageLink(offset, limit, sdf.format(startDate), sdf.format(endDate), order, + userUri, graphUri, vreq.getServletPath())); + } + + // Pass the user name to Freemarker + if (StringUtils.isNotEmpty(acc.getFirstName())) { + if (StringUtils.isNoneEmpty(acc.getLastName())) { + body.put("username", acc.getFirstName() + " " + acc.getLastName()); + } else { + body.put("username", acc.getFirstName()); + } + } else if (StringUtils.isNotEmpty(acc.getEmailAddress())) { + body.put("username", acc.getEmailAddress()); + } else { + body.put("username", ""); + } + + // Pass the helper methods to Freemaker + body.put("listAddedStatements", new ListAddedStatementsMethod()); + body.put("listRemovedStatements", new ListRemovedStatementsMethod()); + + // Return the default template and parameters + return new TemplateResponseValues(TEMPLATE_DEFAULT, body); + } + + private boolean isAdmin(UserAccount acc) { + if (acc.isRootUser()) { + return true; + } + Set roles = acc.getPermissionSetUris(); + return (roles.contains(PermissionSets.URI_DBA)); + } + + private void setUserData(List list, UserAccountsDao uad) { + for (AuditChangeSet acs : list) { + UserAccount account = uad.getUserAccountByUri(acs.getUserId()); + if (account == null) { + continue; + } + acs.setUserFirstName(account.getFirstName()); + acs.setUserLastName(account.getLastName()); + acs.setUserEmail(account.getEmailAddress()); + } + } + + /** + * Get the number of entries to show per page, or a default value of 10 + * + * @param vreq + * @return + */ + private int getLimit(VitroRequest vreq) { + int limit = 0; + try { + limit = Integer.parseInt(vreq.getParameter(PARAM_LIMIT)); + } catch (Throwable e) { + log.debug(e, e); + limit = 10; + } + return limit; + } + + /** + * Get the page offset, or a default of 0 + * + * @param vreq + * @return + */ + private int getOffset(VitroRequest vreq) { + int offset = 0; + try { + offset = Integer.parseInt(vreq.getParameter(PARAM_OFFSET)); + } catch (Throwable e) { + log.debug(e, e); + } + return offset; + } + + /** + * Get start date + * + * @param vreq + * @return + */ + private Date getStartDate(VitroRequest vreq) { + String start = vreq.getParameter(PARAM_START_DATE); + try { + if (!StringUtils.isBlank(start)) { + return sdf.parse(start); + } + } catch (Exception e) { + log.error(e, e); + } + return DateUtils.addDays(new Date(), -7); + } + + /** + * Get end date + * + * @param vreq + * @return + */ + private Date getEndDate(VitroRequest vreq) { + String end = vreq.getParameter(PARAM_END_DATE); + try { + if (!StringUtils.isBlank(end)) { + return sdf.parse(end); + } + } catch (Exception e) { + log.error(e, e); + } + return DateUtils.addDays(new Date(), 1); + } + + /** + * Get user + * + * @param vreq + * @return empty string if user wasn't set + */ + private String getUserUri(VitroRequest vreq) { + String user = vreq.getParameter(PARAM_USER_URI); + if (user == null) { + return ""; + } + return user; + } + + /** + * Get graph + * + * @param vreq + * @return empty string if graph wasn't set + */ + private String getGraph(VitroRequest vreq) { + String graph = vreq.getParameter(PARAM_GRAPH); + if (graph == null) { + return ""; + } + return graph; + } + + /* + * Get order + * + * @param vreq + * + * @return empty string if graph wasn't set + */ + private String getOrder(VitroRequest vreq) { + String order = vreq.getParameter(PARAM_ORDER); + if (StringUtils.isBlank(order) || DESC_ORDER.equals(order)) { + return DESC_ORDER; + } + return ASC_ORDER; + } + + /** + * Generate the link to the previous page + * + * @param offset + * @param limit + * @param baseUrl + * @param string + * @param graphUri + * @param userUri + * @param order + * @return + */ + private String getPreviousPageLink(int offset, int limit, String startDate, String endDate, String order, + String userUri, String graphUri, String baseUrl) { + UrlBuilder.ParamMap params = new UrlBuilder.ParamMap(); + params.put(PARAM_OFFSET, String.valueOf(offset - limit)); + params.put(PARAM_LIMIT, String.valueOf(limit)); + params.put(PARAM_START_DATE, startDate); + params.put(PARAM_END_DATE, endDate); + params.put(PARAM_ORDER, order); + params.put(PARAM_USER_URI, userUri); + params.put(PARAM_GRAPH, graphUri); + return UrlBuilder.getUrl(baseUrl, params); + } + + /** + * Generate the link the next page + * + * @param offset + * @param limit + * @param baseUrl + * @param string + * @param graphUri + * @param userUri + * @param order + * @return + */ + private String getNextPageLink(int offset, int limit, String startDate, String endDate, String order, + String userUri, String graphUri, String baseUrl) { + UrlBuilder.ParamMap params = new UrlBuilder.ParamMap(); + params.put(PARAM_OFFSET, String.valueOf(offset + limit)); + params.put(PARAM_LIMIT, String.valueOf(limit)); + params.put(PARAM_START_DATE, String.valueOf(startDate)); + params.put(PARAM_END_DATE, endDate); + params.put(PARAM_ORDER, order); + params.put(PARAM_USER_URI, userUri); + params.put(PARAM_GRAPH, graphUri); + return UrlBuilder.getUrl(baseUrl, params); + } + + private long dateToTimeStamp(Date date) { + return date.getTime() / 1000; + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAO.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAO.java new file mode 100644 index 0000000000..921a6e07ac --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAO.java @@ -0,0 +1,47 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit.storage; + +import java.util.List; + +import edu.cornell.mannlib.vitro.webapp.audit.AuditChangeSet; +import edu.cornell.mannlib.vitro.webapp.audit.AuditResults; + +/** + * Interface for storing and retrieving Audit entries + */ +public interface AuditDAO { + + /** + * Write the dataset to storage + * + * @param dataset + */ + void write(AuditChangeSet dataset); + + /** + * Get list of users + */ + List getUsers(); + + /** + * Get list of graphs + */ + List getGraphs(); + + /** + * Retrieve a set of audit entries for a given user + * + * @param offset + * @param limit + * @param startDate + * @param endDate + * @param userUri + * @param graphUri + * @param order true = ASC, false = DESC + * @return + */ + AuditResults find(long offset, int limit, long startDate, long endDate, String userUri, String graphUri, + boolean order); + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOFactory.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOFactory.java new file mode 100644 index 0000000000..c983aaa0ce --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOFactory.java @@ -0,0 +1,52 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit.storage; + +/** + * Factory for Audit DAOs + */ +public class AuditDAOFactory { + // Available storage engines - Jena TDB + public enum Storage { AUDIT_TDB }; + + // Configured storage engine. + private static Storage storage; + + /** + * Initialise the factory + * + * @param storage + */ + public static void initialize(Storage storage) { + if (AuditDAOFactory.storage != null) { + throw new IllegalStateException("AuditDAOFactory already initialized"); + } + AuditDAOFactory.storage = storage; + } + + /** + * Clean up for the factory + */ + public static void shutdown() { + AuditDAOFactory.storage = null; + } + + /** + * Get an Audit DAO instance + * + * @param req + * @return + */ + public static AuditDAO getAuditDAO() { + if (storage == null) { + throw new IllegalStateException("AuditDAOFactory not initialized"); + } + + switch (storage) { + case AUDIT_TDB: + return new AuditDAOTDB(); + } + + throw new UnsupportedOperationException("Unsupported Audit storage"); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOJena.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOJena.java new file mode 100644 index 0000000000..bfebe25e9b --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOJena.java @@ -0,0 +1,540 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit.storage; + +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.PROP_DATE; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.PROP_GRAPH; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.PROP_GRAPH_ADDED; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.PROP_GRAPH_REMOVED; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.PROP_HASGRAPH; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.PROP_USER; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.PROP_UUID; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.TYPE_CHANGESET; +import static edu.cornell.mannlib.vitro.webapp.audit.storage.AuditVocabulary.TYPE_CHANGESETGRAPH; + +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import edu.cornell.mannlib.vitro.webapp.audit.AuditChangeSet; +import edu.cornell.mannlib.vitro.webapp.audit.AuditResults; +import org.apache.commons.lang3.StringUtils; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.ParameterizedSparqlString; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.query.ResultSet; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.vocabulary.RDF; + +/** + * AuditDAO using Jena triple store as the backend. This is a base class for a either TDB or SDB implementations + */ +public abstract class AuditDAOJena implements AuditDAO { + // The graph that records the metadata for changesets + private final static String auditGraph = "http://vivoweb.org/audit"; + + // The base URI for creating change objects + private final static String changesBaseURI = "http://vivoweb.org/audit/changes/"; + + /** + * Get a dataset from the Jena store that be written to / read from + * + * @return + */ + protected abstract Dataset getDataset(); + + @Override + public void write(AuditChangeSet changes) { + + // Ensure we have something to write + if (changes.getAddedDataset().asDatasetGraph().isEmpty() + && changes.getRemovedDataset().asDatasetGraph().isEmpty()) { + return; + } + + // Get the audit dataset that we will write into + Dataset auditStore = getDataset(); + if (auditStore == null) { + return; + } + // Lock the dataset + auditStore.begin(ReadWrite.WRITE); + try { + // Get the main graph that we will write the metadata into + Model auditModel = auditStore.getNamedModel(auditGraph); + if (auditModel == null) { + return; + } + + // Create a URI for this request + String changeUri = changesBaseURI + changes.getUUID().toString(); + Resource changeResource = auditModel.createResource(changeUri); + + // Define the type of this resource + changeResource.addProperty(RDF.type, auditModel.createResource(TYPE_CHANGESET)); + + // Add the UUID + changeResource.addProperty(auditModel.createProperty(PROP_UUID), + changes.getUUID().toString()); + + // Add the user information + changeResource.addProperty(auditModel.createProperty(PROP_USER), + auditModel.createResource(changes.getUserId())); + + // Add the time of the change + changeResource.addProperty(auditModel.createProperty(PROP_DATE), + Long.toString(changes.getRequestTime().getTime(), 10)); + + // Get the names of modified graphs + Set names = changes.getGraphUris(); + + // Counter for generate graph URIs + int graphCount = 1; + + // Loop through all of the graphs in the changeset + for (String graphName : names) { + // Get any additions / removals in the named graph + Model addedModel = changes.getAddedModel(graphName); + Model removedModel = changes.getRemovedModel(graphName); + + // If we have some changes to write + if (!isEmpty(addedModel) || !isEmpty(removedModel)) { + // Create a URI for the set of changes made to a specific graph + String graphUri = changeUri + "/graph/" + Integer.toString(graphCount, 10); + Resource graphResource = auditModel.createResource(graphUri); + + // Record the URI in the overall metadata + changeResource.addProperty(auditModel.createProperty(PROP_HASGRAPH), graphResource); + + // Define the type of this resource + graphResource.addProperty(RDF.type, auditModel.createResource(TYPE_CHANGESETGRAPH)); + + // If we are recording changes for a named (rather than default) graph, record the graph that the + // changes apply to + if (!StringUtils.isEmpty(graphName)) { + graphResource.addProperty(auditModel.createProperty(PROP_GRAPH), + auditModel.createResource(graphName)); + } + + // Record graph of added statements + if (!isEmpty(addedModel)) { + String addedName = graphUri + "/added"; + Model addedAuditModel = auditStore.getNamedModel(addedName); + if (addedAuditModel != null) { + addedAuditModel.add(addedModel); + graphResource.addProperty(auditModel.createProperty(PROP_GRAPH_ADDED), + auditModel.createResource(addedName)); + } + } + + // Record graph of removed statements + if (!isEmpty((removedModel))) { + String removedName = graphUri + "/removed"; + Model removedAuditModel = auditStore.getNamedModel(removedName); + if (removedAuditModel != null) { + removedAuditModel.add(removedModel); + graphResource.addProperty(auditModel.createProperty(PROP_GRAPH_REMOVED), + auditModel.createResource(removedName)); + } + } + + // Increment to create a new URI for any additional graphs + graphCount++; + } + } + + // Commit the changes to the audit store + auditStore.commit(); + } finally { + auditStore.end(); + } + } + + @Override + public AuditResults find(long offset, int limit, long startDate, long endDate, String userUri, String graphUri, + boolean order) { + long total = 0; + List datasets = new ArrayList(); + + // Get the audit dataset + Dataset auditStore = getDataset(); + if (auditStore == null) { + return null; + } + // Indicate that we are reading from the audit store + auditStore.begin(ReadWrite.READ); + try { + Query query; + QueryExecution qexec; + ParameterizedSparqlString pss; + // SPARQL query to retrieve overall change sets, in reverse chronological order, with pagination + pss = createChangeSetsQuery(offset, limit, startDate, endDate, userUri, graphUri, order); + query = QueryFactory.create(pss.toString()); + qexec = QueryExecutionFactory.create(query, auditStore.getNamedModel(auditGraph)); + + try { + ResultSet rs = qexec.execSelect(); + while (rs.hasNext()) { + QuerySolution qs = rs.next(); + String uri = qs.getResource("dataset").getURI(); + // Read the change set from the audit store + datasets.add(getChangeSet(auditStore, uri)); + } + } finally { + qexec.close(); + query.clone(); + } + // SPARQL Query to obtain a count of all change sets + pss = createChangeSetCountQuery(startDate, endDate, userUri, graphUri); + query = QueryFactory.create(pss.toString()); + qexec = QueryExecutionFactory.create(query, auditStore.getNamedModel(auditGraph)); + try { + ResultSet rs = qexec.execSelect(); + while (rs.hasNext()) { + QuerySolution qs = rs.next(); + total = qs.getLiteral("datasetCount").getLong(); + } + } finally { + qexec.close(); + query.clone(); + } + + } finally { + auditStore.end(); + } + // Create a new results object + return new AuditResults(total, offset, limit, datasets); + } + + private ParameterizedSparqlString createChangeSetCountQuery(long startDate, long endDate, String userUri, + String graphUri) { + ParameterizedSparqlString pss; + pss = new ParameterizedSparqlString(); + pss.append("SELECT (COUNT(?dataset) AS ?datasetCount)\n"); + startWhere(pss); + addType(pss); + addDate(pss); + addUserUri(userUri, pss); + addGraphUri(graphUri, pss); + addStartDate(startDate, pss); + addEndDate(endDate, pss); + endWhere(pss); + return pss; + } + + private ParameterizedSparqlString createChangeSetsQuery(long offset, int limit, long startDate, long endDate, + String userUri, String graphUri, boolean order) { + ParameterizedSparqlString pss = new ParameterizedSparqlString(); + pss.append("SELECT ?dataset\n"); + startWhere(pss); + addType(pss); + addDate(pss); + addUserUri(userUri, pss); + addGraphUri(graphUri, pss); + addStartDate(startDate, pss); + addEndDate(endDate, pss); + endWhere(pss); + addOrder(order, pss); + pss.append("LIMIT " + limit + " OFFSET " + offset); + return pss; + } + + private void addDate(ParameterizedSparqlString pss) { + pss.append(" ?dataset <" + PROP_DATE + "> ?date .\n"); + } + + private void addType(ParameterizedSparqlString pss) { + pss.append(" ?dataset a <" + TYPE_CHANGESET + "> .\n"); + } + + private void startWhere(ParameterizedSparqlString pss) { + pss.append("WHERE {\n"); + } + + private void endWhere(ParameterizedSparqlString pss) { + pss.append(" }"); + } + + private void addOrder(boolean order, ParameterizedSparqlString pss) { + if (order) { + pss.append("ORDER BY ASC(?date)\n"); + } else { + pss.append("ORDER BY DESC(?date)\n"); + } + } + + private void addEndDate(long endDate, ParameterizedSparqlString pss) { + if (endDate > 0) { + pss.append(" FILTER ( ?date <= ?endDate )\n"); + pss.setLiteral("endDate", Long.toString(endDate)); + } + } + + private void addStartDate(long startDate, ParameterizedSparqlString pss) { + if (startDate > 0) { + pss.append(" FILTER ( ?date >= ?startDate )\n"); + pss.setLiteral("startDate", Long.toString(startDate)); + } + } + + private void addGraphUri(String graphUri, ParameterizedSparqlString pss) { + if (!StringUtils.isBlank(graphUri)) { + pss.append(" ?dataset <" + PROP_HASGRAPH + "> ?graph .\n"); + pss.append(" ?graph <" + PROP_GRAPH + "> ?graphUri .\n"); + pss.setIri("graphUri", graphUri); + } + } + + private void addUserUri(String userUri, ParameterizedSparqlString pss) { + if (!StringUtils.isBlank(userUri)) { + pss.append(" ?dataset <" + PROP_USER + "> ?userUri .\n"); + pss.setIri("userUri", userUri); + } + } + + @Override + public List getUsers() { + List users = new LinkedList<>(); + // Get the audit dataset + Dataset auditStore = getDataset(); + if (auditStore == null) { + return users; + } + // Indicate that we are reading from the audit store + auditStore.begin(ReadWrite.READ); + try { + StringBuilder queryString; + Query query; + QueryExecution qexec; + + // SPARQL query to retrieve overall change sets, in reverse chronological order, with pagination + queryString = new StringBuilder(); + queryString.append("SELECT DISTINCT ?userUri "); + queryString.append(" WHERE {"); + // queryString.append(" ?dataset a <").append(TYPE_CHANGESET).append("> . "); + queryString.append(" ?dataset <").append(PROP_USER).append("> ?userUri . "); + queryString.append(" } "); + + query = QueryFactory.create(queryString.toString()); + qexec = QueryExecutionFactory.create(query, auditStore.getNamedModel(auditGraph)); + + try { + ResultSet rs = qexec.execSelect(); + while (rs.hasNext()) { + QuerySolution qs = rs.next(); + RDFNode user = qs.get("userUri"); + if (user.isResource()) { + String uri = user.asResource().getURI(); + if (uri != null) { + users.add(uri); + } + } + } + } finally { + qexec.close(); + } + + } finally { + auditStore.end(); + } + return users; + } + + @Override + public List getGraphs() { + List users = new LinkedList<>(); + // Get the audit dataset + Dataset auditStore = getDataset(); + if (auditStore == null) { + return users; + } + + // Indicate that we are reading from the audit store + auditStore.begin(ReadWrite.READ); + try { + StringBuilder queryString; + Query query; + QueryExecution qexec; + + // SPARQL query to retrieve overall change sets, in reverse chronological order, with pagination + queryString = new StringBuilder(); + queryString.append("SELECT DISTINCT ?graphUri "); + queryString.append(" WHERE {"); + queryString.append(" ?dataset <").append(PROP_GRAPH).append("> ?graphUri . "); + queryString.append(" } "); + + query = QueryFactory.create(queryString.toString()); + qexec = QueryExecutionFactory.create(query, auditStore.getNamedModel(auditGraph)); + + try { + ResultSet rs = qexec.execSelect(); + while (rs.hasNext()) { + QuerySolution qs = rs.next(); + RDFNode user = qs.get("graphUri"); + if (user.isResource()) { + String uri = user.asResource().getURI(); + if (uri != null) { + users.add(uri); + } + } + } + } finally { + qexec.close(); + } + + } finally { + auditStore.end(); + } + return users; + } + + /** + * Retrieve a changeset from the audit store + * + * You should "lock" the data store for read operations before calling this method, and release after. This method + * is provided so that it can be called from other methods that have already done this, such as in the loop for + * reading a set of changes for a given user. + * + * @param auditStore + * @param changesetUri + * @return + */ + private AuditChangeSet getChangeSet(Dataset auditStore, String changesetUri) { + // Must have a changeset URI to retrieve + if (StringUtils.isEmpty(changesetUri)) { + return null; + } + + UUID id = null; + String userId = null; + Date time = null; + List graphUris = new ArrayList<>(); + + // NOTE We do NOT "lock" the data store for read - see the method comment for more information + + // Get the main metadata graph from the audit store + Model auditModel = auditStore.getNamedModel(auditGraph); + if (auditModel == null) { + return null; + } + + // Get the resource that represents metadata about the requested changeset + Resource datasetResource = auditModel.getResource(changesetUri); + if (datasetResource != null) { + // Get the UUID, date, and change graph URIs from the metadata + StmtIterator iter = datasetResource.listProperties(); + try { + while (iter.hasNext()) { + Statement stmt = iter.next(); + switch (stmt.getPredicate().getURI()) { + case PROP_UUID: + id = UUID.fromString(stmt.getObject().asLiteral().toString()); + break; + + case PROP_DATE: + time = new Date(stmt.getObject().asLiteral().getLong()); + break; + + case PROP_HASGRAPH: + graphUris.add(stmt.getObject().asResource().getURI()); + break; + case PROP_USER: + userId = stmt.getObject().asResource().getURI(); + break; + } + stmt.getObject(); + } + } finally { + iter.close(); + } + } + + // Create a changeset object + AuditChangeSet auditChangeSet = new AuditChangeSet(id, time); + auditChangeSet.setUserId(userId); + + // Loop through all of the change graphs + for (String graphUri : graphUris) { + String graphName = null; + String addedGraph = null; + String removedGraph = null; + + Resource graphResource = auditModel.getResource(graphUri); + StmtIterator iter = graphResource.listProperties(); + try { + while (iter.hasNext()) { + Statement stmt = iter.next(); + switch (stmt.getPredicate().getURI()) { + // Get the graph name (e.g. the graph in the content store) that was affected by this change + case PROP_GRAPH: + graphName = stmt.getObject().asResource().getURI(); + break; + + // Get the URI of the graph that contains the statements that were added + case PROP_GRAPH_ADDED: + addedGraph = stmt.getObject().asResource().getURI(); + break; + + // Get the URI of the graph that contains the statements that were removed + case PROP_GRAPH_REMOVED: + removedGraph = stmt.getObject().asResource().getURI(); + break; + } + } + } finally { + iter.close(); + } + + // If there were statements added + if (!StringUtils.isEmpty(addedGraph)) { + // Retrieve the statements from the graph in the audit store + Model added = auditStore.getNamedModel(addedGraph); + + // Get a model from the changeset for the current graphname + Model writeTo = auditChangeSet.getAddedModel(graphName); + + // Copy the statements from the audit store to the (in-memory) changeset + if (writeTo != null && added != null) { + writeTo.add(added); + } + } + + if (!StringUtils.isEmpty(removedGraph)) { + // Retrieve the statements from the graph in the audit store + Model removed = auditStore.getNamedModel(removedGraph); + + // Get a model from the changeset for the current graphname + Model writeTo = auditChangeSet.getRemovedModel(graphName); + + // Copy the statements from the audit store to the (in-memory) changeset + if (writeTo != null && removed != null) { + writeTo.add(removed); + } + } + } + + return auditChangeSet; + } + + /** + * Helper method to determine if a model has not been retrieved, or is empty + * + * @param model + * @return + */ + private boolean isEmpty(Model model) { + return model == null || model.isEmpty(); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOTDB.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOTDB.java new file mode 100644 index 0000000000..ba8cf6d0b5 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditDAOTDB.java @@ -0,0 +1,63 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit.storage; + +import java.io.File; + +import org.apache.jena.query.Dataset; +import org.apache.jena.tdb.TDBFactory; + +/** + * Implementation of Audit store that uses Jena TDB + */ +public class AuditDAOTDB extends AuditDAOJena { + // The TDB instance + private static Dataset dataset = null; + + /** + * Initialize the Jena TFB storage + * + * @param tdbPath + */ + public static void initialize(String tdbPath) { + // If we've already initialized, throw an exception + if (dataset != null) { + throw new IllegalStateException("Already initialised AuditDAOTDB"); + } + + // Create the directories if necessary + File dir = new File(tdbPath); + if (!dir.exists()) { + dir.mkdirs(); + } + + // If the path is pointing to a file rather than a directory, something has gone wrong!! + if (dir.isFile()) { + throw new IllegalStateException("Path for the Audit TDB models must be a directory, not a file"); + } + + // Create the TDB dataset + dataset = TDBFactory.createDataset(tdbPath); + } + + /** + * Shutdown the dataset + */ + public static void shutdown() { + if (dataset != null) { + TDBFactory.release(dataset); + dataset.close(); + dataset = null; + } + } + + /** + * Return the store + * + * @return + */ + protected Dataset getDataset() { + return dataset; + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditVocabulary.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditVocabulary.java new file mode 100644 index 0000000000..51f95fd783 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/audit/storage/AuditVocabulary.java @@ -0,0 +1,23 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.audit.storage; + +/** + * URIs used by the Jena storage engine + */ +public final class AuditVocabulary { + public final static String TYPE_CHANGESET = "http://vivoweb.org/audit/types/ChangeSet"; + public final static String TYPE_CHANGESETGRAPH = "http://vivoweb.org/audit/types/ChangeSetForGraph"; + + public final static String PROP_UUID = "http://vivoweb.org/audit/properties#uuid"; + public final static String PROP_USER = "http://vivoweb.org/audit/properties#user"; + public final static String PROP_DATE = "http://vivoweb.org/audit/properties#date"; + + public final static String PROP_HASGRAPH = "http://vivoweb.org/audit/properties#hasGraph"; + + public final static String PROP_GRAPH = "http://vivoweb.org/audit/properties#graph"; + public final static String PROP_GRAPH_ADDED = "http://vivoweb.org/audit/properties#added"; + public final static String PROP_GRAPH_REMOVED = "http://vivoweb.org/audit/properties#removed"; + + public final static String RESOURCE_UNKNOWN = "http://vivoweb.org/audit/resource/unknown"; +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java index 6c05bc3d89..ec4e03e935 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java @@ -36,6 +36,7 @@ import static edu.cornell.mannlib.vitro.webapp.dao.DisplayVocabulary.HAS_DELETE_QUERY; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent; +import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.N3EditUtils; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; @@ -190,7 +191,7 @@ private void deleteIndividuals(Model model, VitroRequest vreq) { ByteArrayOutputStream out = new ByteArrayOutputStream(); model.write(out, "N3"); InputStream in = new ByteArrayInputStream(out.toByteArray()); - cs.addRemoval(in, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.ABOX_ASSERTIONS); + cs.addRemoval(in, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.ABOX_ASSERTIONS, N3EditUtils.getEditorUri(vreq)); rdfService.changeSetUpdate(cs); } catch (Exception e) { StringWriter sw = new StringWriter(); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/JenaIngestController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/JenaIngestController.java index 4b8566b4a4..49b23bf47f 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/JenaIngestController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/JenaIngestController.java @@ -70,6 +70,7 @@ import edu.cornell.mannlib.vitro.webapp.dao.jena.BlankNodeFilteringModelMaker; import edu.cornell.mannlib.vitro.webapp.dao.jena.RDFServiceGraph; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.EditEvent; +import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.N3EditUtils; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService; import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; @@ -659,7 +660,7 @@ private String processRenameResourceRequest(VitroRequest vreq) { vreq.setAttribute("title","Rename Resource"); return RENAME_RESOURCE; } else { - String result = doRename(oldNamespace, newNamespace); + String result = doRename(oldNamespace, newNamespace, N3EditUtils.getEditorUri(vreq)); vreq.setAttribute("result",result); vreq.setAttribute("title","Rename Resources"); return RENAME_RESULT; @@ -1072,7 +1073,7 @@ private void doExecuteWorkflow(VitroRequest vreq) { vreq)).run(jenaOntModel.getIndividual(workflowStepURI)); } - private String doRename(String oldNamespace,String newNamespace){ + private String doRename(String oldNamespace,String newNamespace, String editorUri){ String uri = null; String result = null; Integer counter = 0; @@ -1142,13 +1143,13 @@ private String doRename(String oldNamespace,String newNamespace){ try { ChangeSet cs = rdfService.manufactureChangeSet(); cs.addAddition(rdfService.sparqlConstructQuery( - addQuery, RDFService.ModelSerializationFormat.N3), + addQuery, RDFService.ModelSerializationFormat.N3), RDFService.ModelSerializationFormat.N3, - ABOX_ASSERTIONS); + ABOX_ASSERTIONS, editorUri); cs.addRemoval(rdfService.sparqlConstructQuery( removeQuery, RDFService.ModelSerializationFormat.N3), RDFService.ModelSerializationFormat.N3, - ABOX_ASSERTIONS); + ABOX_ASSERTIONS, editorUri); rdfService.changeSetUpdate(cs); } catch (RDFServiceException e) { throw new RuntimeException(e); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/RDFUploadController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/RDFUploadController.java index f30bb1a872..1b84f9d0b5 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/RDFUploadController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/jena/RDFUploadController.java @@ -42,6 +42,7 @@ import edu.cornell.mannlib.vitro.webapp.dao.jena.RDFServiceGraph; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.EditEvent; +import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.N3EditUtils; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; @@ -120,6 +121,7 @@ public void doPost(HttpServletRequest req, /* ********************* GET RDF by URL ********************** */ String RDFUrlStr = request.getParameter("rdfUrl"); + String editorUri = N3EditUtils.getEditorUri(request); if (RDFUrlStr != null && RDFUrlStr.length() > 0) { try { uploadModel.enterCriticalSection(Lock.WRITE); @@ -147,7 +149,7 @@ public void doPost(HttpServletRequest req, try { if (directRead) { addUsingRDFService(rdfStream.getInputStream(), languageStr, - request.getRDFService()); + request.getRDFService(), editorUri); } else { uploadModel.enterCriticalSection(Lock.WRITE); try { @@ -200,7 +202,7 @@ public void doPost(HttpServletRequest req, // aggressively seek all statements that are part of the TBox tboxstmtCount = operateOnModel(request.getUnfilteredWebappDaoFactory(), tboxModel, tboxChangeModel, ontModelSelector, - remove, makeClassgroups, loginBean.getUserURI()); + remove, makeClassgroups, loginBean.getUserURI(), editorUri); } if (aboxModel != null) { aboxChangeModel = uploadModel.remove(tboxChangeModel); @@ -210,10 +212,10 @@ public void doPost(HttpServletRequest req, ByteArrayInputStream in = new ByteArrayInputStream(os.toByteArray()); if(!remove) { readIntoModel(in, "N3", request.getRDFService(), - ModelNames.ABOX_ASSERTIONS); + ModelNames.ABOX_ASSERTIONS, editorUri); } else { removeFromModel(in, "N3", request.getRDFService(), - ModelNames.ABOX_ASSERTIONS); + ModelNames.ABOX_ASSERTIONS, editorUri); } // operateOnModel(request.getUnfilteredWebappDaoFactory(), // aboxModel, aboxChangeModel, ontModelSelector, @@ -245,7 +247,7 @@ private ChangeSet makeChangeSet(RDFService rdfService) { } private void addUsingRDFService(InputStream in, String languageStr, - RDFService rdfService) { + RDFService rdfService, String userId) { ChangeSet changeSet = makeChangeSet(rdfService); RDFService.ModelSerializationFormat format = ("RDF/XML".equals(languageStr) @@ -253,7 +255,7 @@ private void addUsingRDFService(InputStream in, String languageStr, ? RDFService.ModelSerializationFormat.RDFXML : RDFService.ModelSerializationFormat.N3; changeSet.addAddition(in, format, - ABOX_ASSERTIONS); + ABOX_ASSERTIONS, userId); try { rdfService.changeSetUpdate(changeSet); } catch (RDFServiceException rdfse) { @@ -280,7 +282,7 @@ public void loadRDF(VitroRequest request, HttpServletResponse response) } else { RDFService rdfService = getRDFService(request, maker, modelName); try { - doLoadRDFData(modelName, docLoc, filePath, languageStr, rdfService, remove); + doLoadRDFData(modelName, docLoc, filePath, languageStr, rdfService, remove, N3EditUtils.getEditorUri(request)); } finally { rdfService.close(); } @@ -315,7 +317,8 @@ private long operateOnModel(WebappDaoFactory webappDaoFactory, OntModelSelector ontModelSelector, boolean remove, boolean makeClassgroups, - String userURI) { + String userURI, + String userId) { EditEvent startEvent = null, endEvent = null; @@ -353,7 +356,7 @@ private long operateOnModel(WebappDaoFactory webappDaoFactory, changesModel.write(out, "N-TRIPLE"); ChangeSet cs = makeChangeSet(rdfService); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - cs.addRemoval(in, RDFService.ModelSerializationFormat.NTRIPLE, null); + cs.addRemoval(in, RDFService.ModelSerializationFormat.NTRIPLE, null, userId); try { rdfService.changeSetUpdate(cs); } catch (RDFServiceException e) { @@ -380,17 +383,18 @@ private void doLoadRDFData(String modelName, String filePath, String language, RDFService rdfService, - boolean remove) { + boolean remove, + String userId) { try { if ( (docLoc != null) && (docLoc.length()>0) ) { URL docLocURL = new URL(docLoc); InputStream in = docLocURL.openStream(); if(!remove) { readIntoModel(in, language, rdfService, - modelName); + modelName, userId); } else { removeFromModel(in, language, rdfService, - modelName); + modelName, userId); } } else if ( (filePath != null) && (filePath.length()>0) ) { File file = new File(filePath); @@ -406,10 +410,10 @@ private void doLoadRDFData(String modelName, try { if(!remove) { readIntoModel(fileStream.getInputStream(), language, rdfService, - modelName); + modelName, userId); } else { removeFromModel(fileStream.getInputStream(), language, rdfService, - modelName); + modelName, userId); } fileStream.delete(); } catch (IOException ioe) { @@ -426,10 +430,10 @@ private void doLoadRDFData(String modelName, } private void readIntoModel(InputStream in, String language, - RDFService rdfService, String modelName) { + RDFService rdfService, String modelName, String userId) { ChangeSet cs = makeChangeSet(rdfService); cs.addAddition(in, RDFServiceUtils.getSerializationFormatFromJenaString( - language), modelName); + language), modelName, userId); try { rdfService.changeSetUpdate(cs); } catch (RDFServiceException e) { @@ -438,10 +442,10 @@ private void readIntoModel(InputStream in, String language, } private void removeFromModel(InputStream in, String language, - RDFService rdfService, String modelName) { + RDFService rdfService, String modelName, String userId) { ChangeSet cs = makeChangeSet(rdfService); cs.addRemoval(in, RDFServiceUtils.getSerializationFormatFromJenaString( - language), modelName); + language), modelName, userId); try { rdfService.changeSetUpdate(cs); } catch (RDFServiceException e) { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/ProcessRdfForm.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/ProcessRdfForm.java index 8e73da167c..2f49529598 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/ProcessRdfForm.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/ProcessRdfForm.java @@ -2,6 +2,9 @@ package edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; @@ -11,10 +14,9 @@ import java.util.Map; import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.jena.datatypes.xsd.XSDDatatype; -import org.apache.jena.ontology.OntModel; import org.apache.jena.rdf.model.Literal; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; @@ -23,16 +25,18 @@ import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.ResourceFactory; import org.apache.jena.rdf.model.Statement; -import org.apache.jena.shared.Lock; import org.apache.jena.vocabulary.RDF; -import org.apache.jena.vocabulary.XSD; -import org.apache.commons.lang3.StringUtils; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.dao.InsertException; import edu.cornell.mannlib.vitro.webapp.dao.jena.DependentResourceDeleteJena; -import edu.cornell.mannlib.vitro.webapp.dao.jena.event.EditEvent; +import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent; import edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.EditConfigurationConstants; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; /** * The goal of this class is to provide processing from @@ -279,26 +283,30 @@ public static AdditionsAndRetractions addDependentDeletes( AdditionsAndRetractio return changes; } - public static void applyChangesToWriteModel( - AdditionsAndRetractions changes, - Model queryModel, Model writeModel, String editorUri) { - //side effect: modify the write model with the changes - Lock lock = null; - try{ - lock = writeModel.getLock(); - lock.enterCriticalSection(Lock.WRITE); - if( writeModel instanceof OntModel){ - ((OntModel)writeModel).getBaseModel().notifyEvent(new EditEvent(editorUri,true)); - } - writeModel.add( changes.getAdditions() ); - writeModel.remove( changes.getRetractions() ); - }catch(Throwable t){ - log.error("error adding edit change n3required model to in memory model \n"+ t.getMessage() ); - }finally{ - if( writeModel instanceof OntModel){ - ((OntModel)writeModel).getBaseModel().notifyEvent(new EditEvent(editorUri,false)); - } - lock.leaveCriticalSection(); + public static void applyChangesToWriteModel(AdditionsAndRetractions changes, + RDFService rdfService, String graphUri, String editorUri) { + ChangeSet cs = rdfService.manufactureChangeSet(); + cs.addPreChangeEvent(new BulkUpdateEvent(null, true)); + cs.addPostChangeEvent(new BulkUpdateEvent(null, false)); + + ByteArrayOutputStream additionsStream = new ByteArrayOutputStream(); + changes.getAdditions().write(additionsStream, "N3"); + InputStream additionsInputStream = new ByteArrayInputStream(additionsStream.toByteArray()); + + ByteArrayOutputStream retractionsStream = new ByteArrayOutputStream(); + changes.getRetractions().write(retractionsStream, "N3"); + InputStream retractionsInputStream = new ByteArrayInputStream( + retractionsStream.toByteArray()); + + cs.addAddition(additionsInputStream, + RDFServiceUtils.getSerializationFormatFromJenaString("N3"), graphUri, editorUri); + cs.addRemoval(retractionsInputStream, + RDFServiceUtils.getSerializationFormatFromJenaString("N3"), graphUri, editorUri); + + try { + rdfService.changeSetUpdate(cs); + } catch (RDFServiceException e) { + log.error(e, e); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/IdModelSelector.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/IdModelSelector.java index 29a6ca332d..ae28fa7865 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/IdModelSelector.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/IdModelSelector.java @@ -30,4 +30,9 @@ public Model getModel(HttpServletRequest request, ServletContext context) { return mSource.getModel(name); } + @Override + public String getDefaultGraphUri() { + return name; + } + } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/ModelSelector.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/ModelSelector.java index 44d771eb5a..647ecb4308 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/ModelSelector.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/ModelSelector.java @@ -18,4 +18,6 @@ */ public interface ModelSelector { public Model getModel(HttpServletRequest request, ServletContext context); + + public String getDefaultGraphUri(); } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/StandardModelSelector.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/StandardModelSelector.java index 0a413a4619..eb6a554950 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/StandardModelSelector.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/StandardModelSelector.java @@ -2,7 +2,7 @@ package edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration; -import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.ABOX_UNION; +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.ABOX_ASSERTIONS; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -19,9 +19,14 @@ public class StandardModelSelector implements ModelSelector { @Override public OntModel getModel(HttpServletRequest request, ServletContext context) { - return ModelAccess.on(context).getOntModel(ABOX_UNION); + return ModelAccess.on(context).getOntModel(ABOX_ASSERTIONS); } public static final ModelSelector selector = new StandardModelSelector(); + @Override + public String getDefaultGraphUri() { + return ABOX_ASSERTIONS; + } + } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/ProcessRdfFormController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/ProcessRdfFormController.java index 1c82b034bd..0d8d1d33e4 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/ProcessRdfFormController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/ProcessRdfFormController.java @@ -38,6 +38,7 @@ import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.N3EditUtils; import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.ProcessRdfForm; import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.RdfLiteralHash; +import edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.ModelSelector; import edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.preprocessors.LimitRemovalsToLanguage; /** @@ -51,8 +52,6 @@ public class ProcessRdfFormController extends FreemarkerHttpServlet{ private Log log = LogFactory.getLog(ProcessRdfFormController.class); - - @Override protected AuthorizationRequest requiredActions(VitroRequest vreq) { return SimplePermission.DO_FRONT_END_EDITING.ACTION; @@ -78,8 +77,8 @@ protected ResponseValues processRequest(VitroRequest vreq) { // get the models to work with in case the write model and query model are not the defaults Model queryModel = configuration.getQueryModelSelector().getModel(vreq, getServletContext()); - Model writeModel = configuration.getWriteModelSelector().getModel(vreq,getServletContext()); - + ModelSelector writeModelSelector = configuration.getWriteModelSelector(); + Model writeModel = writeModelSelector.getModel(vreq,getServletContext()); //If data property check for back button confusion boolean isBackButton = checkForBackButtonConfusion(configuration, vreq, queryModel); if(isBackButton) { @@ -108,7 +107,7 @@ protected ResponseValues processRequest(VitroRequest vreq) { configuration.addModelChangePreprocessor(new LimitRemovalsToLanguage(vreq.getLocale())); N3EditUtils.preprocessModels(changes, configuration, vreq); - ProcessRdfForm.applyChangesToWriteModel(changes, queryModel, writeModel, N3EditUtils.getEditorUri(vreq) ); + ProcessRdfForm.applyChangesToWriteModel(changes, vreq.getRDFService(), writeModelSelector.getDefaultGraphUri(), N3EditUtils.getEditorUri(vreq)); //Here we are trying to get the entity to return to URL, //More involved processing for data property apparently diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java index b66717441f..dd44e06e28 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverter.java @@ -123,7 +123,7 @@ private void cleanTdbModel(OntModel storedModel, RDFService rdfService) { ByteArrayOutputStream removeOS = new ByteArrayOutputStream(); storedModel.write(removeOS, "N3"); InputStream removeIS = new ByteArrayInputStream(removeOS.toByteArray()); - cs.addRemoval(removeIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N); + cs.addRemoval(removeIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N, getClassUri()); try { rdfService.changeSetUpdate(cs); } catch (RDFServiceException e) { @@ -136,7 +136,7 @@ private void updateTDBModel(RDFService rdfService) { ByteArrayOutputStream addOS = new ByteArrayOutputStream(); memModel.write(addOS, "N3"); InputStream addIS = new ByteArrayInputStream(addOS.toByteArray()); - cs.addAddition(addIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N); + cs.addAddition(addIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N, getClassUri()); try { rdfService.changeSetUpdate(cs); } catch (RDFServiceException e) { @@ -329,4 +329,8 @@ private ChangeSet makeChangeSet(RDFService rdfService) { return cs; } + private String getClassUri() { + return "java:" + this.getClass().getCanonicalName(); + } + } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/Application.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/Application.java index c6538fc56d..7abe1cc78f 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/Application.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modules/Application.java @@ -5,6 +5,7 @@ import javax.servlet.ServletContext; import edu.cornell.mannlib.vitro.webapp.application.VitroHomeDirectory; +import edu.cornell.mannlib.vitro.webapp.audit.AuditModule; import edu.cornell.mannlib.vitro.webapp.modules.fileStorage.FileStorage; import edu.cornell.mannlib.vitro.webapp.modules.imageProcessor.ImageProcessor; import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngine; @@ -35,6 +36,8 @@ public interface Application { TBoxReasonerModule getTBoxReasonerModule(); + AuditModule getAuditModule(); + void shutdown(); public interface Component { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java index d2edb4dcb1..c29932514a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java @@ -386,7 +386,7 @@ private void assertSuccess(ServletContext servletContext) throws FileNotFoundExc ChangeSet changeSet = rdfService.manufactureChangeSet(); File successAssertionsFile = new File(settings.getSuccessAssertionsFile()); InputStream inStream = new FileInputStream(successAssertionsFile); - changeSet.addAddition(inStream, RDFService.ModelSerializationFormat.N3, ModelNames.APPLICATION_METADATA); + changeSet.addAddition(inStream, RDFService.ModelSerializationFormat.N3, ModelNames.APPLICATION_METADATA, getClassUri()); rdfService.changeSetUpdate(changeSet); } catch (Exception e) { log.error("unable to make RDF assertions about successful " + @@ -400,6 +400,10 @@ public static boolean isUpdatableABoxGraph(String graphName) { && !graphName.contains("x-arq:UnionGraph")); } + private String getClassUri() { + return "java:" + this.getClass().getCanonicalName(); + } + /** * A class that allows to access two different ontology change lists, * one for class changes and the other for property changes. The diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ChangeSet.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ChangeSet.java index 61547cfd93..2b6a3f0f8c 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ChangeSet.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ChangeSet.java @@ -5,6 +5,9 @@ import java.io.InputStream; import java.util.List; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ModelChange.Operation; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService.ModelSerializationFormat; + /** * Input parameter to changeSetUpdate() method in RDFService. * Represents a precondition query and an ordered list of model changes. @@ -43,6 +46,7 @@ public interface ChangeSet { * @param serializationFormat - format of the serialized RDF model * @param graphURI - URI of the graph to which the RDF model should be added */ + @Deprecated public void addAddition(InputStream model, RDFService.ModelSerializationFormat serializationFormat, String graphURI); @@ -54,10 +58,37 @@ public void addAddition(InputStream model, * @param serializationFormat - format of the serialized RDF model * @param graphURI - URI of the graph from which the RDF model should be removed */ + @Deprecated public void addRemoval(InputStream model, RDFService.ModelSerializationFormat serializationFormat, String graphURI); + /** + * Adds one model change representing an addition to the list of model changes + * + * @param model - a serialized RDF model (collection of triples) + * @param serializationFormat - format of the serialized RDF model + * @param graphURI - URI of the graph to which the RDF model should be added + * @param userId String - identifier of user requested model changes + */ + public void addAddition(InputStream model, + RDFService.ModelSerializationFormat serializationFormat, + String graphURI, + String userId); + + /** + * Adds one model change representing a deletion to the list of model changes + * + * @param model - a serialized RDF model (collection of triples) + * @param serializationFormat - format of the serialized RDF model + * @param graphURI - URI of the graph from which the RDF model should be removed + * @param userId String - identifier of user requested model changes + */ + public void addRemoval(InputStream model, + RDFService.ModelSerializationFormat serializationFormat, + String graphURI, + String userId); + /** * Creates an instance of the ModelChange class * @@ -76,11 +107,27 @@ public void addRemoval(InputStream model, * @return ModelChange - a ModelChange instance initialized with the input * model, model format, operation and graphURI */ + @Deprecated public ModelChange manufactureModelChange(InputStream serializedModel, RDFService.ModelSerializationFormat serializationFormat, ModelChange.Operation operation, String graphURI); + /** + * Creates an instance of the ModelChange class + * + * @param serializedModel - a serialized RDF model (collection of triples) + * @param serializationFormat - format of the serialized RDF model + * @param operation - the type of operation to be performed with the serialized RDF model + * @param graphURI - URI of the graph on which to apply the model change operation + * @param userId - URI of the user requested model changes + * + * @return ModelChange - a ModelChange instance initialized with the input + * model, model format, operation and graphURI + */ + public ModelChange manufactureModelChange(InputStream serializedModel, ModelSerializationFormat serializationFormat, + Operation operation, String graphURI, String userId); + /** * Adds an event that will be be passed to any change listeners in advance of * the change set additions and retractions being performed. The event @@ -112,4 +159,6 @@ public ModelChange manufactureModelChange(InputStream serializedModel, */ public List getPostChangeEvents(); + + } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java index 7bcc37364e..433f469f8b 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java @@ -17,6 +17,16 @@ public enum Operation { ADD, REMOVE } + /** + * @return String - user identifier + */ + public String getUserId(); + + /** + * @param String - user identifier + */ + public void setUserId(String userId); + /** * @return InputStream - the serialized model (collection of RDF triples) representing a change to make */ diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ChangeSetImpl.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ChangeSetImpl.java index 25012e49bc..b5dc31474f 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ChangeSetImpl.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ChangeSetImpl.java @@ -48,21 +48,34 @@ public List getModelChanges() { return modelChanges; } + @Deprecated @Override public void addAddition(InputStream model, RDFService.ModelSerializationFormat format, String graphURI) { modelChanges.add(manufactureModelChange(model,format, ModelChange.Operation.ADD, graphURI)); } + @Override + public void addAddition(InputStream model, RDFService.ModelSerializationFormat format, String graphURI, String userId) { + modelChanges.add(manufactureModelChange(model,format, ModelChange.Operation.ADD, graphURI, userId)); + } + + @Deprecated @Override public void addRemoval(InputStream model, RDFService.ModelSerializationFormat format, String graphURI) { modelChanges.add(manufactureModelChange(model, format, ModelChange.Operation.REMOVE, graphURI)); } + + @Override + public void addRemoval(InputStream model, RDFService.ModelSerializationFormat format, String graphURI, String userId) { + modelChanges.add(manufactureModelChange(model, format, ModelChange.Operation.REMOVE, graphURI, userId)); + } @Override public ModelChange manufactureModelChange() { return new ModelChangeImpl(); } + @Deprecated @Override public ModelChange manufactureModelChange(InputStream serializedModel, RDFService.ModelSerializationFormat serializationFormat, @@ -70,6 +83,15 @@ public ModelChange manufactureModelChange(InputStream serializedModel, String graphURI) { return new ModelChangeImpl(serializedModel, serializationFormat, operation, graphURI); } + + @Override + public ModelChange manufactureModelChange(InputStream serializedModel, + RDFService.ModelSerializationFormat serializationFormat, + Operation operation, + String graphURI, + String userId) { + return new ModelChangeImpl(serializedModel, serializationFormat, operation, graphURI, userId); + } @Override public void addPreChangeEvent(Object o) { @@ -98,5 +120,4 @@ public String toString() { + ", preChangeEvents=" + preChangeEvents + ", postChangeEvents=" + postChangeEvents + "]"; } - } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ModelChangeImpl.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ModelChangeImpl.java index 9e97010b97..a527d0ac20 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ModelChangeImpl.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/ModelChangeImpl.java @@ -17,9 +17,11 @@ public class ModelChangeImpl implements ModelChange { private RDFService.ModelSerializationFormat serializationFormat; private Operation operation; private String graphURI; + private String userId; public ModelChangeImpl() {} + @Deprecated public ModelChangeImpl(InputStream serializedModel, RDFService.ModelSerializationFormat serializationFormat, Operation operation, @@ -30,6 +32,19 @@ public ModelChangeImpl(InputStream serializedModel, this.operation = operation; this.graphURI = graphURI; } + + public ModelChangeImpl(InputStream serializedModel, + RDFService.ModelSerializationFormat serializationFormat, + Operation operation, + String graphURI, + String userId) { + + this.serializedModel = serializedModel; + this.serializationFormat = serializationFormat; + this.operation = operation; + this.graphURI = graphURI; + this.userId = userId; + } @Override public InputStream getSerializedModel() { @@ -92,4 +107,14 @@ private String streamToString(InputStream stream) { return "Failed to read input stream: " + e; } } + + @Override + public String getUserId() { + return userId; + } + + @Override + public void setUserId(String userId) { + this.userId = userId; + } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java index 8082a0bac5..031c63bb24 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java @@ -200,7 +200,7 @@ protected void recomputeIndividuals(Queue individuals, TypeCaches caches log.debug("Writing additional inferences generated by reasoner plugins."); ChangeSet change = rdfService.manufactureChangeSet(); change.addAddition(makeN3InputStream(additionalInferences), - RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_INFERENCES); + RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_INFERENCES, getClassUri()); try { rdfService.changeSetUpdate(change); } catch (RDFServiceException e) { @@ -531,11 +531,11 @@ protected void updateInferenceModel(Model rebuildModel, ChangeSet change = rdfService.manufactureChangeSet(); if (retractions.size() > 0) { change.addRemoval(makeN3InputStream(retractions), - RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_INFERENCES); + RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_INFERENCES, getClassUri()); } if (additions.size() > 0) { change.addAddition(makeN3InputStream(additions), - RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_INFERENCES); + RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_INFERENCES, getClassUri()); } rdfService.changeSetUpdate(change); log.debug((System.currentTimeMillis() - start) + @@ -544,6 +544,10 @@ protected void updateInferenceModel(Model rebuildModel, } } + private String getClassUri() { + return "java:" + this.getClass().getCanonicalName(); + } + private InputStream makeN3InputStream(Model m) { ByteArrayOutputStream out = new ByteArrayOutputStream(); m.write(out, "N3"); diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/audit/AuditDaoTDBTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/audit/AuditDaoTDBTest.java new file mode 100644 index 0000000000..b11d6aaf82 --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/audit/AuditDaoTDBTest.java @@ -0,0 +1,159 @@ +package edu.cornell.mannlib.vitro.webapp.audit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAO; +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAOFactory; +import edu.cornell.mannlib.vitro.webapp.audit.storage.AuditDAOTDB; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class AuditDaoTDBTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + private RDFService rdfService; + + @Before + public void initializeModule() throws IOException { + AuditDAOTDB.initialize(folder.getRoot().getAbsolutePath()); + AuditDAOFactory.initialize(AuditDAOFactory.Storage.AUDIT_TDB); + Model model = ModelFactory.createDefaultModel(); + rdfService = new RDFServiceModel(model); + AuditSetup auditSetup = new AuditSetup(); + auditSetup.registerChangeListener(rdfService); + } + + @After + public void closeModule() throws IOException { + AuditDAOTDB.shutdown(); + AuditDAOFactory.shutdown(); + } + + @Test + public void testInitialization() { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + assertNotNull(dao); + } + + @Test + public void testGetGraphs() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + List graphList = dao.getGraphs(); + assertTrue(graphList.isEmpty()); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + graphList = dao.getGraphs(); + assertEquals(2, graphList.size()); + assertTrue(graphList.contains(ModelNames.ABOX_ASSERTIONS)); + assertTrue(graphList.contains(ModelNames.TBOX_ASSERTIONS)); + } + + @Test + public void testGetUsers() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + List userList = dao.getUsers(); + assertTrue(userList.isEmpty()); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + userList = dao.getUsers(); + assertEquals(2, userList.size()); + assertTrue(userList.contains("test:bob")); + assertTrue(userList.contains("test:alice")); + } + + @Test + public void testFindWrongDate() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + AuditResults results = dao.find(0, 10, 0, 1, "", "", false); + List datasets = results.getDatasets(); + assertEquals(0, datasets.size()); + } + + @Test + public void testFindSpecificEditor() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + AuditResults results = dao.find(0, 10, 0, System.currentTimeMillis(), "test:bob", "", false); + List datasets = results.getDatasets(); + assertEquals(1, datasets.size()); + assertEquals("test:bob", datasets.get(0).getUserId()); + } + + @Test + public void testFindLimit() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + AuditResults results = dao.find(0, 1, 0, System.currentTimeMillis(), "", "", false); + List datasets = results.getDatasets(); + assertEquals(1, datasets.size()); + } + + @Test + public void testFindDescOrder() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + AuditResults results = dao.find(0, 10, 0, System.currentTimeMillis(), "", "", false); + List datasets = results.getDatasets(); + assertEquals(2, datasets.size()); + assertEquals("test:alice", datasets.get(0).getUserId()); + } + + @Test + public void testFindAscOrder() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + AuditResults results = dao.find(0, 10, 0, System.currentTimeMillis(), "", "", true); + List datasets = results.getDatasets(); + assertEquals(2, datasets.size()); + assertEquals("test:bob", datasets.get(0).getUserId()); + } + + @Test + public void testFindSpecificGraph() throws Exception { + AuditDAO dao = AuditDAOFactory.getAuditDAO(); + addData("42", "test:bob", ModelNames.ABOX_ASSERTIONS, false); + addData("77", "test:alice", ModelNames.TBOX_ASSERTIONS, true); + AuditResults results = dao.find(0, 10, 0, System.currentTimeMillis(), "", ModelNames.TBOX_ASSERTIONS, false); + List datasets = results.getDatasets(); + assertEquals(1, datasets.size()); + assertEquals(1, datasets.get(0).getGraphUris().size()); + assertEquals(ModelNames.TBOX_ASSERTIONS, datasets.get(0).getGraphUris().iterator().next()); + } + + public void addData(String propValue, String editorUri, String graphUri, boolean isAddition) throws Exception { + ChangeSet cs = rdfService.manufactureChangeSet(); + String inputString = " \"" + propValue + "\" . "; + try (InputStream stream = new ByteArrayInputStream(inputString.getBytes(StandardCharsets.UTF_8))) { + if (isAddition) { + cs.addAddition(stream, RDFService.ModelSerializationFormat.N3, graphUri, editorUri); + } else { + cs.addRemoval(stream, RDFService.ModelSerializationFormat.N3, graphUri, editorUri); + } + rdfService.changeSetUpdate(cs); + } + } +} diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceNotificationTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceNotificationTest.java new file mode 100644 index 0000000000..247cfe2e7a --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceNotificationTest.java @@ -0,0 +1,134 @@ +package edu.cornell.mannlib.vitro.webapp.rdfservice; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; +import org.apache.commons.io.IOUtils; +import org.apache.jena.ontology.OntModel; +import org.apache.jena.ontology.OntModelSpec; +import org.apache.jena.rdf.listeners.StatementListener; +import org.apache.jena.rdf.model.ModelChangedListener; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.Before; +import org.junit.Test; + +public class RDFServiceNotificationTest { + + private String TEST_TRIPLE = " \"test value\" ."; + private List modelChanges = null; + + @Before + public void reset() { + modelChanges = new ArrayList<>(); + } + + @Test + public void testModelChangeUserIdNotificationAdd() throws RDFServiceException { + OntModel model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + RDFServiceModel rdfServiceModel = new RDFServiceModel(model); + String editorUri = "test:user-id"; + rdfServiceModel.registerListener(new TestListener()); + ChangeSet cs = createChangeSet(rdfServiceModel, editorUri, TEST_TRIPLE, null); + rdfServiceModel.changeSetUpdate(cs); + assertTrue(modelChanges.size() > 0); + assertTrue(editorUri.equals(modelChanges.get(0).getUserId())); + } + + @Test + public void testModelChangeUserIdNotificationRetract() throws RDFServiceException { + OntModel model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + RDFServiceModel rdfServiceModel = new RDFServiceModel(model); + String editorUri = "test:user-id"; + rdfServiceModel.registerListener(new TestListener()); + ChangeSet cs = createChangeSet(rdfServiceModel, editorUri, null, TEST_TRIPLE); + rdfServiceModel.changeSetUpdate(cs); + assertTrue(modelChanges.size() > 0); + assertTrue(editorUri.equals(modelChanges.get(0).getUserId())); + } + + @Test + public void testMultipleNotifications() throws RDFServiceException { + OntModel model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + RDFServiceModel rdfServiceModel = new RDFServiceModel(model); + String editorUri = "test:user-id"; + int n = 5; + for (int i = 0; i < n; i++) { + rdfServiceModel.registerListener(new TestListener()); + } + ChangeSet cs = createChangeSet(rdfServiceModel, editorUri, null, TEST_TRIPLE); + rdfServiceModel.changeSetUpdate(cs); + assertTrue(modelChanges.size() == n); + } + + @Test + public void testMultipleNotificationsReceived() throws RDFServiceException { + OntModel model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + RDFServiceModel rdfServiceModel = new RDFServiceModel(model); + String editorUri = "test:user-id"; + int n = 5; + for (int i = 0; i < n; i++) { + rdfServiceModel.registerListener(new TestListener(TEST_TRIPLE)); + } + ChangeSet cs = createChangeSet(rdfServiceModel, editorUri, null, TEST_TRIPLE); + rdfServiceModel.changeSetUpdate(cs); + } + + private ChangeSet createChangeSet(RDFServiceModel rdfServiceModel, String editorUri, String additions, + String retractions) { + ChangeSet cs = rdfServiceModel.manufactureChangeSet(); + cs.addPreChangeEvent(new BulkUpdateEvent(null, true)); + cs.addPostChangeEvent(new BulkUpdateEvent(null, false)); + if (additions != null) { + InputStream additionsInputStream = new ByteArrayInputStream(additions.getBytes(StandardCharsets.UTF_8)); + cs.addAddition(additionsInputStream, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), null, + editorUri); + } + if (retractions != null) { + InputStream retractionsInputStream = new ByteArrayInputStream(retractions.getBytes(StandardCharsets.UTF_8)); + cs.addRemoval(retractionsInputStream, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), null, + editorUri); + } + return cs; + } + + private class TestListener extends StatementListener implements ModelChangedListener, ChangeListener { + + private String serializedChange; + + public TestListener() { + } + + public TestListener(String serializedChange) { + this.serializedChange = serializedChange; + } + + @Override + public void notifyModelChange(ModelChange modelChange) { + if (serializedChange != null) { + String receivedTriple = null; + try { + receivedTriple = IOUtils.toString(modelChange.getSerializedModel(), StandardCharsets.UTF_8); + } catch (IOException e) { + e.printStackTrace(); + } + assertEquals(serializedChange, receivedTriple); + } else { + modelChanges.add(modelChange); + } + } + + @Override + public void notifyEvent(String graphURI, Object event) { + } + } +} diff --git a/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/modules/ApplicationStub.java b/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/modules/ApplicationStub.java index 01f78981f9..0afa77d588 100644 --- a/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/modules/ApplicationStub.java +++ b/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/modules/ApplicationStub.java @@ -6,6 +6,7 @@ import javax.servlet.ServletContext; +import edu.cornell.mannlib.vitro.webapp.audit.AuditModule; import stubs.edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexerStub; import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; import edu.cornell.mannlib.vitro.webapp.application.VitroHomeDirectory; @@ -95,6 +96,12 @@ public FileStorage getFileStorage() { "ApplicationStub.getFileStorage() not implemented."); } + @Override + public AuditModule getAuditModule() { + throw new RuntimeException( + "ApplicationStub.getAuditModule() not implemented."); + } + @Override public void shutdown() { throw new RuntimeException( diff --git a/home/src/main/resources/config/example.applicationSetup.n3 b/home/src/main/resources/config/example.applicationSetup.n3 index 0df7bdb6e4..d5beb3c9ca 100644 --- a/home/src/main/resources/config/example.applicationSetup.n3 +++ b/home/src/main/resources/config/example.applicationSetup.n3 @@ -26,10 +26,21 @@ :hasSearchIndexer :basicSearchIndexer ; :hasImageProcessor :iioImageProcessor ; :hasFileStorage :ptiFileStorage ; +# :hasAuditModule :tdbAuditModule ; :hasContentTripleSource :tdbContentTripleSource ; :hasConfigurationTripleSource :tdbConfigurationTripleSource ; :hasTBoxReasonerModule :jfactTBoxReasonerModule . +# ---------------------------- +# +# Audit module: +# + +#:tdbAuditModule +# a , +# ; +# :hasTdbDirectory "tdbAuditModels" . + # ---------------------------- # # Image processor module: diff --git a/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt b/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt index 3938adcb43..3f6d0ececa 100644 --- a/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt +++ b/webapp/src/main/webapp/WEB-INF/resources/startup_listeners.txt @@ -66,3 +66,6 @@ edu.cornell.mannlib.vitro.webapp.dao.jena.VClassGroupCache$Setup # This should be near the end, because it will issue a warning if the connection to Solr or ElasticSearch times out. edu.cornell.mannlib.vitro.webapp.servlet.setup.SearchEngineSmokeTest + +edu.cornell.mannlib.vitro.webapp.audit.AuditSetup + diff --git a/webapp/src/main/webapp/css/audit/audit.css b/webapp/src/main/webapp/css/audit/audit.css new file mode 100644 index 0000000000..d5b6394018 --- /dev/null +++ b/webapp/src/main/webapp/css/audit/audit.css @@ -0,0 +1,35 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +/* Styles for the search index controller. */ + +section#audit table.history { + font-size: smaller; + border: 1px solid gray; + width: 100%; +} +tbody tr td:nth-child(1) { + min-width: 2em; +} + +tbody tr td:nth-child(2) { + max-width: 10em; + word-break: break-word ; +} + +tbody tr td:nth-child(3) { + max-width: 7em; + word-break: break-word ; +} + +section#audit table.history th { + font-weight: bolder; +} + +section#audit table.history td { + padding: 2px 5px 2px 5px; +} + +section#audit table.history pre { + line-height: normal; + white-space: break-spaces; +} diff --git a/webapp/src/main/webapp/templates/freemarker/body/audit/auditHistory.ftl b/webapp/src/main/webapp/templates/freemarker/body/audit/auditHistory.ftl new file mode 100644 index 0000000000..7fcf40b955 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/audit/auditHistory.ftl @@ -0,0 +1,89 @@ +<#-- $This file is distributed under the terms of the license in LICENSE$ --> + +
+

Audit history

+
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + <#assign pos = results.offset> + <#list results.datasets as dataset> + <#assign pos = pos + 1> + + + + + + + + +
PosUserDateChanges
${pos}${dataset.userLastName!} ${dataset.userFirstName!} ${dataset.userEmail!} (${dataset.userId!})${dataset.requestTime?datetime} + <#list dataset.graphUris as graphUri> + <#assign added = listAddedStatements(dataset, graphUri)> + <#assign removed = listRemovedStatements(dataset, graphUri)> + + Graph: ${graphUri}
+ <#if added?has_content> +
Added:
+
${added?html}
+ + <#if removed?has_content> +
Removed:
+
${removed?html}
+ + +
+ <#if prevPage??> + <#if nextPage??> +
+ +<#macro printSelectOption currentOption selectedOption > + <#assign selected = ""> + <#if currentOption = selectedOption> + <#assign selected = "selected=\"selected\""> + + + + + +${stylesheets.add('')} +