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