001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.camel.audit.triplestore;
007
008import static java.util.Collections.emptyList;
009import static java.util.Optional.empty;
010import static java.util.Optional.of;
011import static org.apache.jena.riot.RDFDataMgr.write;
012import static org.apache.jena.riot.RDFFormat.NTRIPLES;
013import static org.apache.jena.vocabulary.RDF.type;
014import static org.fcrepo.camel.FcrepoHeaders.FCREPO_AGENT;
015import static org.fcrepo.camel.FcrepoHeaders.FCREPO_DATE_TIME;
016import static org.fcrepo.camel.FcrepoHeaders.FCREPO_EVENT_ID;
017import static org.fcrepo.camel.FcrepoHeaders.FCREPO_EVENT_TYPE;
018import static org.fcrepo.camel.FcrepoHeaders.FCREPO_RESOURCE_TYPE;
019import static org.fcrepo.camel.FcrepoHeaders.FCREPO_URI;
020import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDdateTime;
021import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDstring;
022import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
023import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
024import static org.apache.jena.rdf.model.ResourceFactory.createResource;
025import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral;
026
027import java.io.ByteArrayOutputStream;
028import java.io.IOException;
029import java.util.List;
030import java.util.Optional;
031
032import org.fcrepo.camel.processor.ProcessorUtils;
033
034import org.apache.camel.Exchange;
035import org.apache.camel.Message;
036import org.apache.camel.Processor;
037import org.apache.jena.rdf.model.Model;
038import org.apache.jena.rdf.model.Property;
039import org.apache.jena.rdf.model.Resource;
040
041/**
042 * A processor that converts an audit message into a sparql-update
043 * statement for an external triplestore.
044 *
045 * @author Aaron Coburn
046 * @author escowles
047 * @since 2015-04-09
048 */
049
050public class AuditSparqlProcessor implements Processor {
051
052    static final String AUDIT = "http://fedora.info/definitions/v4/audit#";
053    static final String PREMIS = "http://www.loc.gov/premis/rdf/v1#";
054    static final String PROV = "http://www.w3.org/ns/prov#";
055    static final String XSD = "http://www.w3.org/2001/XMLSchema#";
056    static final String EVENT_TYPE = "http://id.loc.gov/vocabulary/preservation/eventType/";
057    static final String EVENT_NAMESPACE = "http://fedora.info/definitions/v4/event#";
058    static final String AS_NAMESPACE = "https://www.w3.org/ns/activitystreams#";
059    static final String REPOSITORY = "http://fedora.info/definitions/v4/repository#";
060
061    static final String CONTENT_MOD = AUDIT + "contentModification";
062    static final String CONTENT_REM = AUDIT + "contentRemoval";
063    static final String METADATA_MOD = AUDIT + "metadataModification";
064
065    static final String CONTENT_ADD = EVENT_TYPE + "ing";
066    static final String OBJECT_ADD = EVENT_TYPE + "cre";
067    static final String OBJECT_REM = EVENT_TYPE + "del";
068
069    /**
070     * Define how a message should be processed.
071     *
072     * @param exchange the current camel message exchange
073     */
074    public void process(final Exchange exchange) throws Exception {
075        final Message in = exchange.getIn();
076        final String eventURIBase = in.getHeader(AuditHeaders.EVENT_BASE_URI, String.class);
077        final String eventID = in.getHeader(FCREPO_EVENT_ID, String.class);
078        final Resource eventURI = createResource(eventURIBase + "/" + eventID);
079
080        // generate SPARQL Update
081        final StringBuilder query = new StringBuilder("update=");
082        query.append(ProcessorUtils.insertData(serializedGraphForMessage(in, eventURI), ""));
083
084        // update exchange
085        in.setBody(query.toString());
086        in.setHeader(AuditHeaders.EVENT_URI, eventURI.toString());
087        in.setHeader(Exchange.CONTENT_TYPE, "application/x-www-form-urlencoded");
088        in.setHeader(Exchange.HTTP_METHOD, "POST");
089    }
090
091    // namespaces and properties
092    private static final Resource INTERNAL_EVENT = createResource(AUDIT + "InternalEvent");
093    private static final Resource PREMIS_EVENT = createResource(PREMIS + "Event");
094    private static final Resource PROV_EVENT = createResource(PROV + "InstantaneousEvent");
095
096    private static final Property PREMIS_TIME = createProperty(PREMIS + "hasEventDateTime");
097    private static final Property PREMIS_OBJ = createProperty(PREMIS + "hasEventRelatedObject");
098    private static final Property PREMIS_AGENT = createProperty(PREMIS + "hasEventRelatedAgent");
099    private static final Property PREMIS_TYPE = createProperty(PREMIS + "hasEventType");
100
101    private static final String EMPTY_STRING = "";
102
103    /**
104     * Convert a Camel message to audit event description.
105     * @param message Camel message produced by an audit event
106     * @param subject RDF subject of the audit description
107     */
108    private static String serializedGraphForMessage(final Message message, final Resource subject) throws IOException {
109
110        // serialize triples
111        final ByteArrayOutputStream serializedGraph = new ByteArrayOutputStream();
112        final Model model = createDefaultModel();
113
114        // get info from jms message headers
115        @SuppressWarnings("unchecked")
116        final List<String> eventType = message.getHeader(FCREPO_EVENT_TYPE, emptyList(), List.class);
117        final String dateTime = message.getHeader(FCREPO_DATE_TIME, EMPTY_STRING, String.class);
118        @SuppressWarnings("unchecked")
119        final List<String> agents = message.getHeader(FCREPO_AGENT, emptyList(), List.class);
120        @SuppressWarnings("unchecked")
121        final List<String> resourceTypes = message.getHeader(FCREPO_RESOURCE_TYPE, emptyList(), List.class);
122        final String identifier = message.getHeader(FCREPO_URI, EMPTY_STRING, String.class);
123        final Optional<String> premisType = getAuditEventType(eventType, resourceTypes);
124
125        model.add( model.createStatement(subject, type, INTERNAL_EVENT) );
126        model.add( model.createStatement(subject, type, PREMIS_EVENT) );
127        model.add( model.createStatement(subject, type, PROV_EVENT) );
128
129        // basic event info
130        model.add( model.createStatement(subject, PREMIS_TIME, createTypedLiteral(dateTime, XSDdateTime)) );
131        model.add( model.createStatement(subject, PREMIS_OBJ, createResource(identifier)) );
132
133        agents.forEach(agent -> {
134            model.add( model.createStatement(subject, PREMIS_AGENT, createTypedLiteral(agent, XSDstring)) );
135        });
136
137        premisType.ifPresent(rdfType -> {
138            model.add(model.createStatement(subject, PREMIS_TYPE, createResource(rdfType)));
139        });
140
141        write(serializedGraph, model, NTRIPLES);
142        return serializedGraph.toString("UTF-8");
143    }
144
145    /**
146     * Returns the Audit event type based on fedora event type and properties.
147     *
148     * @param eventType from Fedora
149     * @param resourceType associated with the Fedora event
150     * @return Audit event
151     */
152    private static Optional<String> getAuditEventType(final List<String> eventType, final List<String> resourceType) {
153        // mapping event type/properties to audit event type
154        if (eventType.contains(EVENT_NAMESPACE + "ResourceCreation") || eventType.contains(AS_NAMESPACE + "Create")) {
155            if (resourceType.contains(REPOSITORY + "Binary")) {
156                return of(CONTENT_ADD);
157            } else {
158                return of(OBJECT_ADD);
159            }
160        } else if (eventType.contains(EVENT_NAMESPACE + "ResourceDeletion") ||
161                eventType.contains(AS_NAMESPACE + "Delete")) {
162            if (resourceType.contains(REPOSITORY + "Binary")) {
163                return of(CONTENT_REM);
164            } else {
165                return of(OBJECT_REM);
166            }
167        } else if (eventType.contains(EVENT_NAMESPACE + "ResourceModification") ||
168                eventType.contains(AS_NAMESPACE + "Update")) {
169            if (resourceType.contains(REPOSITORY + "Binary")) {
170                return of(CONTENT_MOD);
171            } else {
172                return of(METADATA_MOD);
173            }
174        }
175        return empty();
176    }
177
178
179}