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 org.fcrepo.camel.RdfNamespaces.RDF;
021import static org.fcrepo.camel.RdfNamespaces.REPOSITORY;
022import static com.hp.hpl.jena.datatypes.xsd.XSDDatatype.XSDdateTime;
023import static com.hp.hpl.jena.datatypes.xsd.XSDDatatype.XSDstring;
024import static com.hp.hpl.jena.rdf.model.ModelFactory.createDefaultModel;
025import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty;
026import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
027import static com.hp.hpl.jena.rdf.model.ResourceFactory.createTypedLiteral;
028
029import java.io.ByteArrayOutputStream;
030import java.io.IOException;
031import java.text.DateFormat;
032import java.text.SimpleDateFormat;
033import java.util.Date;
034import java.util.TimeZone;
035
036import org.fcrepo.camel.JmsHeaders;
037import org.fcrepo.camel.processor.ProcessorUtils;
038
039import org.apache.camel.Exchange;
040import org.apache.camel.Message;
041import org.apache.camel.Processor;
042import com.hp.hpl.jena.rdf.model.Model;
043import com.hp.hpl.jena.rdf.model.Property;
044import com.hp.hpl.jena.rdf.model.Resource;
045
046/**
047 * A processor that converts an audit message into a sparql-update
048 * statement for an external triplestore.
049 *
050 * @author Aaron Coburn
051 * @author escowles
052 * @since 2015-04-09
053 */
054
055public class AuditSparqlProcessor implements Processor {
056
057    static final String AUDIT = "http://fedora.info/definitions/v4/audit#";
058    static final String PREMIS = "http://www.loc.gov/premis/rdf/v1#";
059    static final String PROV = "http://www.w3.org/ns/prov#";
060    static final String XSD = "http://www.w3.org/2001/XMLSchema#";
061    static final String EVENT_TYPE = "http://id.loc.gov/vocabulary/preservation/eventType/";
062    static final String EVENT_NAMESPACE = "http://fedora.info/definitions/v4/event#";
063
064    static final String CONTENT_MOD = AUDIT + "contentModification";
065    static final String CONTENT_REM = AUDIT + "contentRemoval";
066    static final String METADATA_MOD = AUDIT + "metadataModification";
067
068    static final String CONTENT_ADD = EVENT_TYPE + "ing";
069    static final String OBJECT_ADD = EVENT_TYPE + "cre";
070    static final String OBJECT_REM = EVENT_TYPE + "del";
071
072    static final String HAS_CONTENT = REPOSITORY + "hasContent";
073
074    static final String NODE_ADDED = REPOSITORY + "NODE_ADDED";
075    static final String NODE_REMOVED = REPOSITORY + "NODE_REMOVED";
076    static final String PROPERTY_CHANGED = REPOSITORY + "PROPERTY_CHANGED";
077
078    /**
079     * Define how a message should be processed.
080     *
081     * @param exchange the current camel message exchange
082     */
083    public void process(final Exchange exchange) throws Exception {
084        final Message in = exchange.getIn();
085        final String eventURIBase = in.getHeader(AuditHeaders.EVENT_BASE_URI, String.class);
086        final String eventID = in.getHeader(JmsHeaders.EVENT_ID, String.class);
087        final Resource eventURI = createResource(eventURIBase + "/" + eventID);
088
089        // generate SPARQL Update
090        final StringBuilder query = new StringBuilder("update=");
091        query.append(ProcessorUtils.insertData(serializedGraphForMessage(in, eventURI), null));
092
093        // update exchange
094        in.setBody(query.toString());
095        in.setHeader(AuditHeaders.EVENT_URI, eventURI.toString());
096        in.setHeader(Exchange.CONTENT_TYPE, "application/x-www-form-urlencoded");
097        in.setHeader(Exchange.HTTP_METHOD, "POST");
098    }
099
100    // namespaces and properties
101    private static final Resource INTERNAL_EVENT = createResource(AUDIT + "InternalEvent");
102    private static final Resource PREMIS_EVENT = createResource(PREMIS + "Event");
103    private static final Resource PROV_EVENT = createResource(PROV + "InstantaneousEvent");
104
105    private static final Property PREMIS_TIME = createProperty(PREMIS + "hasEventDateTime");
106    private static final Property PREMIS_OBJ = createProperty(PREMIS + "hasEventRelatedObject");
107    private static final Property PREMIS_AGENT = createProperty(PREMIS + "hasEventRelatedAgent");
108    private static final Property PREMIS_TYPE = createProperty(PREMIS + "hasEventType");
109    private static final Property RDF_TYPE = createProperty(RDF + "type");
110
111    private static final String EMPTY_STRING = "";
112    private static final String RESOURCE_TYPES = "org.fcrepo.jms.resourceType";
113
114    /**
115     * Convert a Camel message to audit event description.
116     * @param message Camel message produced by an audit event
117     * @param subject RDF subject of the audit description
118     */
119    private static String serializedGraphForMessage(final Message message, final Resource subject) throws IOException {
120
121        // serialize triples
122        final ByteArrayOutputStream serializedGraph = new ByteArrayOutputStream();
123        final Model model = createDefaultModel();
124
125        // get info from jms message headers
126        final String eventType = message.getHeader(JmsHeaders.EVENT_TYPE, EMPTY_STRING, String.class);
127        final Long timestamp =  message.getHeader(JmsHeaders.TIMESTAMP, 0, Long.class);
128        final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
129        df.setTimeZone(TimeZone.getTimeZone("UTC"));
130        final String date = df.format(new Date(timestamp));
131        final String user = message.getHeader(JmsHeaders.USER, EMPTY_STRING, String.class);
132        final String agent = message.getHeader(JmsHeaders.USER_AGENT, EMPTY_STRING, String.class);
133        final String properties = message.getHeader(JmsHeaders.PROPERTIES, EMPTY_STRING, String.class);
134        final String resourceTypes = message.getHeader(RESOURCE_TYPES, EMPTY_STRING, String.class);
135        final String identifier = ProcessorUtils.getSubjectUri(message);
136        final String premisType = getAuditEventType(eventType, properties, resourceTypes);
137
138        model.add( model.createStatement(subject, RDF_TYPE, INTERNAL_EVENT) );
139        model.add( model.createStatement(subject, RDF_TYPE, PREMIS_EVENT) );
140        model.add( model.createStatement(subject, RDF_TYPE, PROV_EVENT) );
141
142        // basic event info
143        model.add( model.createStatement(subject, PREMIS_TIME, createTypedLiteral(date, XSDdateTime)) );
144        model.add( model.createStatement(subject, PREMIS_OBJ, createResource(identifier)) );
145        model.add( model.createStatement(subject, PREMIS_AGENT, createTypedLiteral(user, XSDstring)) );
146        model.add( model.createStatement(subject, PREMIS_AGENT, createTypedLiteral(agent, XSDstring)) );
147        if (premisType != null) {
148            model.add(model.createStatement(subject, PREMIS_TYPE, createResource(premisType)));
149        }
150
151        model.write(serializedGraph, "N-TRIPLE");
152        return serializedGraph.toString("UTF-8");
153    }
154
155    /**
156     * Returns the Audit event type based on fedora event type and properties.
157     *
158     * @param eventType from Fedora
159     * @param properties associated with the Fedora event
160     * @return Audit event
161     */
162    private static String getAuditEventType(final String eventType, final String properties,
163            final String resourceType) {
164        // mapping event type/properties to audit event type
165        if (eventType.contains(NODE_ADDED) || eventType.contains(EVENT_NAMESPACE + "ResourceCreation")) {
166            if (properties != null && properties.contains(HAS_CONTENT)) {
167                return CONTENT_ADD;
168            } else if (resourceType != null && resourceType.contains(REPOSITORY + "Binary")) {
169                return CONTENT_ADD;
170            } else {
171                return OBJECT_ADD;
172            }
173        } else if (eventType.contains(NODE_REMOVED) || eventType.contains(EVENT_NAMESPACE + "ResourceDeletion")) {
174            if (properties != null && properties.contains(HAS_CONTENT)) {
175                return CONTENT_REM;
176            } else if (resourceType != null && resourceType.contains(REPOSITORY + "Binary")) {
177                return CONTENT_REM;
178            } else {
179                return OBJECT_REM;
180            }
181        } else if (eventType.contains(PROPERTY_CHANGED) ||
182                eventType.contains(EVENT_NAMESPACE + "ResourceModification")) {
183            if (properties != null && properties.contains(HAS_CONTENT)) {
184                return CONTENT_MOD;
185            } else if (resourceType != null && resourceType.contains(REPOSITORY + "Binary")) {
186                return CONTENT_MOD;
187            } else {
188                return METADATA_MOD;
189            }
190        }
191        return null;
192    }
193
194
195}