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.integration;
019
020import static java.lang.Integer.parseInt;
021import static java.util.Arrays.asList;
022import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
023import static org.apache.jena.vocabulary.RDF.type;
024import static org.fcrepo.camel.FcrepoHeaders.FCREPO_AGENT;
025import static org.fcrepo.camel.FcrepoHeaders.FCREPO_DATE_TIME;
026import static org.fcrepo.camel.FcrepoHeaders.FCREPO_EVENT_TYPE;
027import static org.fcrepo.camel.FcrepoHeaders.FCREPO_EVENT_ID;
028import static org.fcrepo.camel.FcrepoHeaders.FCREPO_URI;
029import static org.slf4j.LoggerFactory.getLogger;
030
031import java.util.Map;
032import java.util.HashMap;
033import java.io.IOException;
034
035import org.apache.camel.Produce;
036import org.apache.camel.Exchange;
037import org.apache.camel.EndpointInject;
038import org.apache.camel.ProducerTemplate;
039import org.apache.camel.builder.RouteBuilder;
040import org.apache.camel.builder.xml.XPathBuilder;
041import org.apache.camel.builder.xml.Namespaces;
042import org.apache.camel.component.mock.MockEndpoint;
043import org.apache.camel.test.junit4.CamelTestSupport;
044import org.apache.jena.fuseki.embedded.FusekiEmbeddedServer;
045import org.apache.jena.query.Dataset;
046import org.apache.jena.sparql.core.DatasetImpl;
047import org.fcrepo.camel.audit.triplestore.AuditHeaders;
048import org.fcrepo.camel.audit.triplestore.AuditSparqlProcessor;
049import org.junit.After;
050import org.junit.Before;
051import org.junit.Test;
052import org.slf4j.Logger;
053
054/**
055 * Represents an integration test for interacting with an external triplestore.
056 *
057 * @author Aaron Coburn
058 * @since Nov 8, 2014
059 */
060public class AuditSparqlIT extends CamelTestSupport {
061
062    final private Logger logger = getLogger(AuditSparqlIT.class);
063
064    private static final int FUSEKI_PORT = parseInt(System.getProperty(
065            "fuseki.dynamic.test.port", "8080"));
066
067    private static FusekiEmbeddedServer server = null;
068
069    private static final String PREMIS = "http://www.loc.gov/premis/rdf/v1#";
070
071    private static final String USER = "bypassAdmin";
072
073    private static final String USER_AGENT = "curl/7.37.1";
074
075    private static final String EVENT_BASE_URI = "http://example.com/event";
076
077    private static final String EVENT_ID = "ab/cd/ef/gh/abcdefgh12345678";
078
079    private static final String EVENT_URI = EVENT_BASE_URI + "/" + EVENT_ID;
080
081    private static final String AS_NS = "https://www.w3.org/ns/activitystreams#";
082
083    @EndpointInject(uri = "mock:sparql.update")
084    protected MockEndpoint sparqlUpdateEndpoint;
085
086    @EndpointInject(uri = "mock:sparql.query")
087    protected MockEndpoint sparqlQueryEndpoint;
088
089    @Produce(uri = "direct:start")
090    protected ProducerTemplate template;
091
092    @Before
093    public void setup() throws Exception {
094        final Dataset ds = new DatasetImpl(createDefaultModel());
095        server = FusekiEmbeddedServer.create().setPort(FUSEKI_PORT).setContextPath("/fuseki").add("/test", ds).build();
096        logger.info("Starting EmbeddedFusekiServer on port {}", FUSEKI_PORT);
097        server.start();
098    }
099
100    @After
101    public void tearDown() throws Exception {
102        logger.info("Stopping EmbeddedFusekiServer");
103        server.stop();
104    }
105
106    private Map<String, Object> getEventHeaders() {
107        // send an audit event to an external triplestore
108        final Map<String, Object> headers = new HashMap<>();
109        headers.put(FCREPO_URI, "http://localhost/rest/foo");
110        headers.put(FCREPO_EVENT_TYPE, asList(AS_NS + "Create", AS_NS + "Update"));
111        headers.put(FCREPO_DATE_TIME, "2015-04-10T14:30:36Z");
112        headers.put(FCREPO_AGENT, asList(USER, USER_AGENT));
113        headers.put(FCREPO_EVENT_ID, EVENT_ID);
114
115        return headers;
116    }
117
118    @Test
119    public void testAuditEventTypeTriples() throws Exception {
120        sparqlUpdateEndpoint.expectedMessageCount(2);
121        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
122
123        sparqlQueryEndpoint.expectedMessageCount(1);
124        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
125        sparqlQueryEndpoint.expectedBodiesReceivedInAnyOrder(
126                "http://id.loc.gov/vocabulary/preservation/eventType/cre");
127
128        template.sendBody("direct:clear", null);
129        template.sendBodyAndHeaders(null, getEventHeaders());
130
131        template.sendBodyAndHeader("direct:query", null, Exchange.HTTP_QUERY,
132                "query=SELECT ?o WHERE { <" + EVENT_URI + "> <" + PREMIS + "hasEventType> ?o }");
133
134        sparqlQueryEndpoint.assertIsSatisfied();
135        sparqlUpdateEndpoint.assertIsSatisfied();
136    }
137
138    @Test
139    public void testAuditEventRelatedTriples() throws Exception {
140        sparqlUpdateEndpoint.expectedMessageCount(2);
141        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
142
143        sparqlQueryEndpoint.expectedMessageCount(2);
144        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
145        sparqlQueryEndpoint.expectedBodiesReceivedInAnyOrder(
146                "http://localhost/rest/foo"
147            );
148
149        template.sendBody("direct:clear", null);
150        template.sendBodyAndHeaders(null, getEventHeaders());
151
152        template.sendBodyAndHeader("direct:query", null, Exchange.HTTP_QUERY,
153                "query=SELECT ?o WHERE { <" + EVENT_URI + "> <" + PREMIS + "hasEventRelatedObject> ?o }");
154
155        sparqlQueryEndpoint.assertIsSatisfied();
156        sparqlUpdateEndpoint.assertIsSatisfied();
157    }
158
159    @Test
160    public void testAuditEventDateTriples() throws Exception {
161        sparqlUpdateEndpoint.expectedMessageCount(2);
162        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
163
164        sparqlQueryEndpoint.expectedMessageCount(2);
165        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
166        sparqlQueryEndpoint.expectedBodiesReceivedInAnyOrder(
167                "2015-04-10T14:30:36Z"
168            );
169
170        template.sendBody("direct:clear", null);
171        template.sendBodyAndHeaders(null, getEventHeaders());
172
173        template.sendBodyAndHeader("direct:query", null, Exchange.HTTP_QUERY,
174                "query=SELECT ?o WHERE { <" + EVENT_URI + "> <" + PREMIS + "hasEventDateTime> ?o }");
175
176        sparqlQueryEndpoint.assertIsSatisfied();
177        sparqlUpdateEndpoint.assertIsSatisfied();
178    }
179
180    @Test
181    public void testAuditEventAgentTriples() throws Exception {
182        sparqlUpdateEndpoint.expectedMessageCount(2);
183        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
184
185        sparqlQueryEndpoint.expectedMessageCount(2);
186        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
187        sparqlQueryEndpoint.expectedBodiesReceivedInAnyOrder(
188                USER,
189                USER_AGENT
190            );
191
192        template.sendBody("direct:clear", null);
193        template.sendBodyAndHeaders(null, getEventHeaders());
194
195        template.sendBodyAndHeader("direct:query", null, Exchange.HTTP_QUERY,
196                "query=SELECT ?o WHERE { <" + EVENT_URI + "> <" + PREMIS + "hasEventRelatedAgent> ?o }");
197
198        sparqlQueryEndpoint.assertIsSatisfied();
199        sparqlUpdateEndpoint.assertIsSatisfied();
200    }
201
202    @Test
203    public void testAuditEventAllTriples() throws Exception {
204        sparqlUpdateEndpoint.expectedMessageCount(2);
205        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
206
207        sparqlQueryEndpoint.expectedMessageCount(8);
208        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
209
210        template.sendBody("direct:clear", null);
211        template.sendBodyAndHeaders(null, getEventHeaders());
212
213        template.sendBodyAndHeader("direct:query", null, Exchange.HTTP_QUERY,
214                "query=SELECT ?o WHERE { <" + EVENT_URI + "> ?p ?o }");
215
216        sparqlQueryEndpoint.assertIsSatisfied();
217        sparqlUpdateEndpoint.assertIsSatisfied();
218    }
219
220    @Test
221    public void testAuditTypeTriples() throws Exception {
222        sparqlUpdateEndpoint.expectedMessageCount(2);
223        sparqlUpdateEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
224
225        sparqlQueryEndpoint.expectedMessageCount(2);
226        sparqlQueryEndpoint.expectedHeaderReceived(Exchange.HTTP_RESPONSE_CODE, 200);
227        sparqlQueryEndpoint.expectedBodiesReceivedInAnyOrder(
228                "http://www.loc.gov/premis/rdf/v1#Event",
229                "http://fedora.info/definitions/v4/audit#InternalEvent",
230                "http://www.w3.org/ns/prov#InstantaneousEvent"
231            );
232
233        template.sendBody("direct:clear", null);
234        template.sendBodyAndHeaders(null, getEventHeaders());
235
236        template.sendBodyAndHeader("direct:query", null, Exchange.HTTP_QUERY,
237                "query=SELECT ?o WHERE { <" + EVENT_URI + "> <" + type.toString() + "> ?o }");
238
239        sparqlQueryEndpoint.assertIsSatisfied();
240        sparqlUpdateEndpoint.assertIsSatisfied();
241    }
242
243    @Override
244    protected RouteBuilder createRouteBuilder() throws Exception {
245
246        final Namespaces ns = new Namespaces("sparql", "http://www.w3.org/2005/sparql-results#");
247
248        final XPathBuilder xpath = new XPathBuilder(
249                "//sparql:result/sparql:binding[@name='o']");
250        xpath.namespaces(ns);
251
252        final XPathBuilder uriResult = new XPathBuilder(
253                "/sparql:binding/sparql:uri");
254        uriResult.namespaces(ns);
255
256        final XPathBuilder literalResult = new XPathBuilder(
257                "/sparql:binding/sparql:literal");
258        literalResult.namespaces(ns);
259
260        return new RouteBuilder() {
261            public void configure() throws IOException {
262                final String fuseki_url = "http4://localhost:" + Integer.toString(FUSEKI_PORT);
263
264                from("direct:start")
265                    .setHeader(AuditHeaders.EVENT_BASE_URI, constant(EVENT_BASE_URI))
266                    .process(new AuditSparqlProcessor())
267                    .to(fuseki_url + "/fuseki/test/update")
268                    .to("mock:sparql.update");
269
270                from("direct:query")
271                    .to(fuseki_url + "/fuseki/test/query")
272                    .split(xpath)
273                        .choice()
274                            .when().xpath("/sparql:binding/sparql:uri", String.class, ns)
275                                .transform().xpath("/sparql:binding/sparql:uri/text()", String.class, ns)
276                                .to("mock:sparql.query")
277                            .when().xpath("/sparql:binding/sparql:literal", String.class, ns)
278                                .transform().xpath("/sparql:binding/sparql:literal/text()", String.class, ns)
279                                .to("mock:sparql.query");
280
281                from("direct:clear")
282                    .transform().constant("update=DELETE WHERE { ?s ?o ?p }")
283                    .setHeader(Exchange.CONTENT_TYPE).constant("application/x-www-form-urlencoded")
284                    .setHeader(Exchange.HTTP_METHOD).constant("POST")
285                    .to(fuseki_url + "/fuseki/test/update")
286                    .to("mock:sparql.update");
287
288            }
289        };
290    }
291}