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