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.integration.http.api;
019
020import com.github.benmanes.caffeine.cache.Cache;
021import com.github.benmanes.caffeine.cache.Caffeine;
022import com.google.common.base.Strings;
023
024import org.apache.http.Header;
025import org.apache.http.HttpHost;
026import org.apache.http.HttpResponse;
027import org.apache.http.NoHttpResponseException;
028import org.apache.http.auth.AuthScope;
029import org.apache.http.auth.UsernamePasswordCredentials;
030import org.apache.http.client.AuthCache;
031import org.apache.http.client.CredentialsProvider;
032import org.apache.http.client.methods.CloseableHttpResponse;
033import org.apache.http.client.methods.HttpDelete;
034import org.apache.http.client.methods.HttpGet;
035import org.apache.http.client.methods.HttpHead;
036import org.apache.http.client.methods.HttpPatch;
037import org.apache.http.client.methods.HttpPost;
038import org.apache.http.client.methods.HttpPut;
039import org.apache.http.client.methods.HttpRequestBase;
040import org.apache.http.client.methods.HttpUriRequest;
041import org.apache.http.client.protocol.HttpClientContext;
042import org.apache.http.entity.StringEntity;
043import org.apache.http.impl.auth.BasicScheme;
044import org.apache.http.impl.client.BasicAuthCache;
045import org.apache.http.impl.client.BasicCredentialsProvider;
046import org.apache.http.impl.client.CloseableHttpClient;
047import org.apache.http.impl.client.HttpClientBuilder;
048import org.apache.http.impl.client.HttpClients;
049import org.apache.http.util.EntityUtils;
050import org.apache.jena.graph.Node;
051import org.apache.jena.rdf.model.Model;
052import org.apache.jena.riot.RDFDataMgr;
053import org.apache.jena.riot.RDFFormat;
054
055import org.fcrepo.config.AuthPropsConfig;
056import org.fcrepo.config.FedoraPropsConfig;
057import org.fcrepo.http.commons.test.util.CloseableDataset;
058import org.fcrepo.http.commons.test.util.ContainerWrapper;
059import org.fcrepo.kernel.api.auth.ACLHandle;
060
061import org.junit.Before;
062import org.junit.runner.RunWith;
063import org.slf4j.Logger;
064import org.springframework.context.annotation.Bean;
065import org.springframework.context.annotation.Configuration;
066import org.springframework.test.context.ContextConfiguration;
067import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
068
069import javax.inject.Inject;
070import javax.ws.rs.core.HttpHeaders;
071import javax.ws.rs.core.Link;
072import javax.ws.rs.core.Response.Status;
073import javax.xml.bind.DatatypeConverter;
074import java.io.ByteArrayInputStream;
075import java.io.ByteArrayOutputStream;
076import java.io.IOException;
077import java.io.InputStream;
078import java.io.UnsupportedEncodingException;
079import java.net.URI;
080import java.net.URLEncoder;
081import java.nio.charset.StandardCharsets;
082import java.util.Arrays;
083import java.util.Calendar;
084import java.util.Collection;
085import java.util.List;
086import java.util.Map;
087import java.util.Objects;
088import java.util.Optional;
089import java.util.concurrent.TimeUnit;
090import java.util.stream.Collectors;
091
092import static java.lang.Integer.MAX_VALUE;
093import static java.lang.Integer.parseInt;
094import static java.util.Arrays.stream;
095import static java.util.UUID.randomUUID;
096import static java.util.stream.Collectors.toList;
097import static javax.ws.rs.core.HttpHeaders.ACCEPT;
098import static javax.ws.rs.core.HttpHeaders.CONTENT_LOCATION;
099import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
100import static javax.ws.rs.core.HttpHeaders.LINK;
101import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
102import static javax.ws.rs.core.Response.Status.CREATED;
103import static javax.ws.rs.core.Response.Status.GONE;
104import static javax.ws.rs.core.Response.Status.NOT_FOUND;
105import static javax.ws.rs.core.Response.Status.NO_CONTENT;
106import static javax.ws.rs.core.Response.Status.OK;
107import static org.apache.commons.lang3.StringUtils.isNotEmpty;
108import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
109import static org.apache.jena.vocabulary.DC_11.title;
110import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER;
111import static org.fcrepo.http.commons.test.util.TestHelpers.parseTriples;
112import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
113import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
114import static org.fcrepo.kernel.api.RdfLexicon.CONSTRAINED_BY;
115import static org.fcrepo.kernel.api.RdfLexicon.CREATED_BY;
116import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE;
117import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
118import static org.fcrepo.kernel.api.RdfLexicon.EXTERNAL_CONTENT;
119import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_BY;
120import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE;
121import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
122import static org.fcrepo.kernel.api.RdfLexicon.PREFER_SERVER_MANAGED;
123import static org.hamcrest.CoreMatchers.is;
124import static org.hamcrest.MatcherAssert.assertThat;
125import static org.junit.Assert.assertArrayEquals;
126import static org.junit.Assert.assertEquals;
127import static org.junit.Assert.assertTrue;
128import static org.slf4j.LoggerFactory.getLogger;
129
130/**
131 * <p>Abstract AbstractResourceIT class.</p>
132 *
133 * @author awoods
134 * @author ajs6f
135 */
136@RunWith(SpringJUnit4ClassRunner.class)
137@ContextConfiguration("/spring-test/test-container.xml")
138public abstract class AbstractResourceIT {
139
140    protected static Logger logger;
141
142    protected static final String NON_RDF_SOURCE_LINK_HEADER = "<" + NON_RDF_SOURCE.getURI() + ">;rel=\"type\"";
143    protected static final String BASIC_CONTAINER_LINK_HEADER = "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\"";
144    protected static final String DIRECT_CONTAINER_LINK_HEADER = "<" + DIRECT_CONTAINER.getURI() + ">; rel=\"type\"";
145
146    private static final String OMIT_SERVER_PREFER_HEADER = "return=representation; omit=\"" + PREFER_SERVER_MANAGED +
147            "\"";
148
149    protected static final Node DCTITLE = title.asNode();
150
151    @Inject
152    protected ContainerWrapper containerWrapper;
153
154    protected FedoraPropsConfig propsConfig;
155
156    protected AuthPropsConfig authPropsConfig;
157
158    protected void restartContainer() throws Exception {
159        this.containerWrapper.stop();
160        this.containerWrapper.start();
161    }
162    /**
163     * Decode the Digest header
164     * @param digestHeader the digest header value.
165     * @return Map with keys of algorithms and values of hashes.
166     */
167    protected static Map<String, String> decodeDigestHeader(final String digestHeader) {
168        return stream(digestHeader.split(",")).map(h -> h.split("=", 2))
169                .collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1] : ""));
170    }
171
172    @Before
173    public void setLogger() {
174        logger = getLogger(this.getClass());
175        propsConfig = getBean(FedoraPropsConfig.class);
176        authPropsConfig = getBean(AuthPropsConfig.class);
177    }
178
179    private static final int SERVER_PORT = parseInt(Objects.requireNonNullElse(
180            Strings.emptyToNull(System.getProperty("fcrepo.dynamic.test.port")), "8080"));
181
182    private static final String HOSTNAME = "localhost";
183
184    private static final String PROTOCOL = "http";
185
186    protected static final String serverAddress = PROTOCOL + "://" + HOSTNAME + ":" + SERVER_PORT + "/";
187
188    protected <T> T getBean(final Class<T> type) {
189        return containerWrapper.getSpringAppContext().getBean(type);
190    }
191
192    protected  <T> T getBean(final String name, final Class<T> type) {
193        return containerWrapper.getSpringAppContext().getBean(name, type);
194    }
195
196    protected static final CloseableHttpClient client = createClient();
197
198    protected static CloseableHttpClient createClient() {
199        return createClient(false);
200    }
201
202    protected static CloseableHttpClient createClient(final boolean disableRedirects) {
203        final HttpClientBuilder client =
204            HttpClientBuilder.create().setMaxConnPerRoute(MAX_VALUE).setMaxConnTotal(MAX_VALUE);
205        if (disableRedirects) {
206            client.disableRedirectHandling();
207        }
208        return client.build();
209    }
210
211    protected static HttpPost postObjMethod() {
212        return postObjMethod("/");
213    }
214
215    protected static HttpPost postObjMethod(final String id) {
216        return new HttpPost(serverAddress + id);
217    }
218
219    protected static HttpPut putObjMethod(final String id) {
220        return new HttpPut(serverAddress + id);
221    }
222
223    protected static HttpGet getObjMethod(final String id) {
224        return new HttpGet(serverAddress + id);
225    }
226
227    protected static HttpHead headObjMethod(final String id) {
228        return new HttpHead(serverAddress + id);
229    }
230
231    protected static HttpDelete deleteObjMethod(final String id) {
232        return new HttpDelete(serverAddress + id);
233    }
234
235    protected static HttpPatch patchObjMethod(final String id) {
236        return new HttpPatch(serverAddress + id);
237    }
238
239    protected static HttpPut putDSMethod(final String pid, final String ds, final String content)
240            throws UnsupportedEncodingException {
241        return putDSMethod(pid + "/" + ds, content);
242    }
243
244    protected static HttpPut putDSMethod(final String id, final String content) throws
245            UnsupportedEncodingException {
246        final HttpPut put = new HttpPut(serverAddress + id);
247        put.setEntity(new StringEntity(content == null ? "" : content));
248        put.setHeader(CONTENT_TYPE, TEXT_PLAIN);
249        put.setHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
250        return put;
251    }
252
253    protected static HttpPut putObjMethod(final String pid, final String contentType, final String content)
254            throws UnsupportedEncodingException {
255        final HttpPut put = new HttpPut(serverAddress + pid);
256        put.setEntity(new StringEntity(content));
257        put.setHeader(CONTENT_TYPE, contentType);
258        return put;
259    }
260
261    protected static HttpGet getDSMethod(final String pid, final String ds) {
262        return new HttpGet(serverAddress + pid + "/" + ds);
263    }
264
265    protected static HttpGet getDSDescMethod(final String pid, final String ds) {
266        return new HttpGet(serverAddress + pid + "/" + ds + "/" + FCR_METADATA);
267    }
268
269    /**
270     * Execute an HTTP request and return the open response.
271     *
272     * @param req Request to execute
273     * @return the open response
274     * @throws IOException in case of an IOException
275     */
276    protected static CloseableHttpResponse execute(final HttpUriRequest req) throws IOException {
277        logger.debug("Executing: " + req.getMethod() + " to " + req.getURI());
278        try {
279            return client.execute(req);
280        } catch (final NoHttpResponseException e) {
281            // sometimes the server is slow starting up -- retry once
282            try {
283                TimeUnit.SECONDS.sleep(2);
284                return client.execute(req);
285            } catch (final InterruptedException e2) {
286                throw new RuntimeException(e2);
287            }
288        }
289    }
290
291    /**
292     * Execute an HTTP request and close the response.
293     *
294     * @param req the request to execute
295     */
296    protected static void executeAndClose(final HttpUriRequest req) {
297        logger.debug("Executing: " + req.getMethod() + " to " + req.getURI());
298        try {
299            execute(req).close();
300        } catch (final IOException e) {
301            throw new RuntimeException(e);
302        }
303    }
304
305
306    /**
307     * Execute an HTTP request with preemptive basic authentication.
308     *
309     * @param request the request to execute
310     * @param username usename to use
311     * @param password password to use
312     * @return the open responses
313     * @throws IOException in case of IOException
314     */
315    @SuppressWarnings("resource")
316    protected CloseableHttpResponse executeWithBasicAuth(final HttpUriRequest request, final String username,
317            final String password) throws IOException {
318        final HttpHost target = new HttpHost(HOSTNAME, SERVER_PORT, PROTOCOL);
319        final CredentialsProvider credsProvider = new BasicCredentialsProvider();
320        credsProvider.setCredentials(
321                new AuthScope(target.getHostName(), target.getPort()),
322                new UsernamePasswordCredentials(username, password));
323        final CloseableHttpClient httpclient =
324                HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
325        final AuthCache authCache = new BasicAuthCache();
326        final BasicScheme basicAuth = new BasicScheme();
327        authCache.put(target, basicAuth);
328
329        final HttpClientContext localContext = HttpClientContext.create();
330        localContext.setAuthCache(authCache);
331        return httpclient.execute(request, localContext);
332    }
333
334    /**
335     * Retrieve the HTTP status code from an open response.
336     *
337     * @param response the open response
338     * @return the HTTP status code of the response
339     */
340    protected static int getStatus(final HttpResponse response) {
341        return response.getStatusLine().getStatusCode();
342    }
343
344    /**
345     * Executes an HTTP request and returns the status code of the response, closing the response.
346     *
347     * @param req the request to execute
348     * @return the HTTP status code of the response
349     */
350    protected static int getStatus(final HttpUriRequest req) {
351        try (final CloseableHttpResponse response = execute(req)) {
352            final int result = getStatus(response);
353            if (!(result > 199) || !(result < 400)) {
354                logger.warn("Got status {}", result);
355                if (response.getEntity() != null) {
356                    logger.warn(EntityUtils.toString(response.getEntity()));
357                }
358            }
359            EntityUtils.consume(response.getEntity());
360            return result;
361        } catch (final IOException e) {
362            throw new RuntimeException(e);
363        }
364    }
365
366    /**
367     * Executes an HTTP request and returns the first Location header in the response, then closes the response.
368     *
369     * @param req the request to execute
370     * @return the value of the first Location header in the response
371     * @throws IOException in case of IOException
372     */
373    protected static String getLocation(final HttpUriRequest req) throws IOException {
374        try (final CloseableHttpResponse response = execute(req)) {
375            EntityUtils.consume(response.getEntity());
376            return getLocation(response);
377        }
378    }
379
380    /**
381     * Retrieve the value of the first Location header from an open HTTP response.
382     *
383     * @param response the open response
384     * @return the value of the first Location header in the response
385     */
386    protected static String getLocation(final HttpResponse response) {
387        return response.getFirstHeader("Location").getValue();
388    }
389
390     /**
391     * Retrieve the value of the first Content-Location header from an open HTTP response.
392     *
393     * @param response the open response
394     * @return the value of the first Content-Location header in the response
395     */
396    protected static String getContentLocation(final HttpResponse response) {
397        return response.getFirstHeader(CONTENT_LOCATION).getValue();
398    }
399
400    protected String getContentType(final HttpUriRequest method) throws IOException {
401        return getContentType(method, OK);
402    }
403
404    protected String getContentType(final HttpUriRequest method, final Status httpStatus) throws IOException {
405        try (final CloseableHttpResponse response = execute(method)) {
406            final int result = getStatus(response);
407            assertEquals(httpStatus.getStatusCode(), result);
408            EntityUtils.consume(response.getEntity());
409            return response.getFirstHeader(CONTENT_TYPE).getValue();
410        }
411    }
412
413    /**
414     * Get the etag of the given URI, as returned from a HEAD request
415     *
416     * @param uri uri of the resource
417     * @return etag
418     * @throws IOException if the uri is invalid
419     */
420    protected String getEtag(final String uri) throws IOException {
421        return getEtag(new HttpHead(uri));
422    }
423
424    /**
425     * Get the etag returned when executing the provided method
426     * @param method method to execute
427     * @return etag
428     * @throws IOException in case of a problem or the connection was aborted
429     */
430    protected String getEtag(final HttpUriRequest method) throws IOException {
431        try (final CloseableHttpResponse response = execute(method)) {
432            return getEtag(response);
433        }
434    }
435
436    /**
437     * Get the etag header value present in the provided response
438     * @param response response
439     * @return etag header value or null
440     */
441    protected String getEtag(final HttpResponse response) {
442        final var etag = response.getFirstHeader(HttpHeaders.ETAG);
443        return etag == null ? null : etag.getValue();
444    }
445
446    protected static Collection<String> getLinkHeaders(final HttpResponse response) {
447        return getHeader(response, LINK);
448    }
449
450    protected Collection<String> getLinkHeaders(final HttpUriRequest method) throws IOException {
451        try (final CloseableHttpResponse response = execute(method)) {
452            return getLinkHeaders(response);
453        }
454    }
455
456    protected static List<String> headerValues(final HttpResponse response, final String headerName) {
457        return stream(response.getHeaders(headerName)).map(Header::getValue).map(s -> s.split(",")).flatMap(
458                Arrays::stream).map(String::trim).collect(toList());
459    }
460
461    protected static Collection<String> getHeader(final HttpResponse response, final String header) {
462        return stream(response.getHeaders(header)).map(Header::getValue).collect(toList());
463    }
464
465    /**
466     * Executes an HTTP request and parses the RDF found in the response, returning it in a
467     * {@link CloseableDataset}, then closes the response.
468     *
469     * @param client the client to use
470     * @param req the request to execute
471     * @return the graph retrieved
472     * @throws IOException in case of IOException
473     */
474    private CloseableDataset getDataset(final CloseableHttpClient client, final HttpUriRequest req)
475            throws IOException {
476        if (!req.containsHeader(ACCEPT)) {
477            req.addHeader(ACCEPT, "application/n-triples");
478        }
479        logger.debug("Retrieving RDF using mimeType: {}", req.getFirstHeader(ACCEPT));
480
481        try (final CloseableHttpResponse response = client.execute(req)) {
482            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
483            final CloseableDataset result = parseTriples(response.getEntity());
484            logger.trace("Retrieved RDF: {}", result);
485            return result;
486        }
487
488    }
489
490    /**
491     * Parses the RDF found in and HTTP response, returning it in a {@link CloseableDataset}.
492     *
493     * @param response the response to parse
494     * @return the graph retrieved
495     * @throws IOException in case of IOException
496     */
497    protected CloseableDataset getDataset(final HttpResponse response) throws IOException {
498        assertEquals(OK.getStatusCode(), getStatus(response));
499        final CloseableDataset result = parseTriples(response.getEntity());
500        logger.trace("Retrieved RDF: {}", result);
501        return result;
502    }
503
504    /**
505     * Executes an HTTP request and parses the RDF found in the response, returning it in a
506     * {@link CloseableDataset}, then closes the response.
507     *
508     * @param req the request to execute
509     * @return the constructed graph
510     * @throws IOException in case of IOException
511     */
512    protected CloseableDataset getDataset(final HttpUriRequest req) throws IOException {
513        return getDataset(client, req);
514    }
515
516    protected Model getModel(final String pid) throws Exception {
517        return getModel(null, pid, false);
518    }
519
520    protected Model getModel(final String pid, final boolean omitSMTs) throws IOException {
521        return getModel(null, pid, omitSMTs);
522    }
523
524    protected Model getModel(final String txUri, final String pid) throws Exception {
525        return getModel(txUri, pid, false);
526    }
527
528    /**
529     * Get a model of the triples for the resource.
530     * @param txUri id of the transaction
531     * @param pid id of the resource
532     * @param omitSMTs whether to omit server managed triples from the response.
533     * @return the model of the resource triples.
534     * @throws IOException on problems getting response.
535     */
536    protected Model getModel(final String txUri, final String pid, final boolean omitSMTs) throws IOException {
537        final Model model = createDefaultModel();
538        final HttpGet get = getObjMethod(pid);
539        if (txUri != null) {
540            get.addHeader(ATOMIC_ID_HEADER, txUri);
541        }
542        if (omitSMTs) {
543            get.addHeader("Prefer", OMIT_SERVER_PREFER_HEADER);
544        }
545        try (final CloseableHttpResponse response = execute(get)) {
546            model.read(response.getEntity().getContent(), serverAddress + pid, "TURTLE");
547        }
548        return model;
549    }
550
551    protected static InputStream streamModel(final Model model, final RDFFormat format) throws IOException {
552        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
553            RDFDataMgr.write(bos, model, format);
554            return new ByteArrayInputStream(bos.toByteArray());
555        }
556    }
557
558    protected CloseableHttpResponse createObject() {
559        return createObject("");
560    }
561
562    protected CloseableHttpResponse createObject(final String pid) {
563        return createObjectWithLinkHeader(pid, null);
564    }
565
566    private CloseableHttpResponse createObjectWithLinkHeader(final String pid, final String... linkHeaders) {
567        final HttpPost httpPost = postObjMethod("/");
568        if (isNotEmpty(pid)) {
569            httpPost.addHeader("Slug", URLEncoder.encode(pid, StandardCharsets.UTF_8));
570        }
571
572        if (linkHeaders != null && linkHeaders.length > 0) {
573            for (final String linkHeader : linkHeaders) {
574                httpPost.addHeader(LINK, linkHeader);
575            }
576        }
577        try {
578            final CloseableHttpResponse response = execute(httpPost);
579            assertEquals(CREATED.getStatusCode(), getStatus(response));
580            return response;
581        } catch (final IOException e) {
582            throw new RuntimeException(e);
583        }
584    }
585
586    protected void createObjectAndClose(final String pid) {
587        try {
588            createObject(pid).close();
589        } catch (final IOException e) {
590            throw new RuntimeException(e);
591        }
592    }
593
594    protected void createObjectAndClose(final String pid, final String... linkHeaders) {
595        try {
596
597            createObjectWithLinkHeader(pid, linkHeaders).close();
598        } catch (final IOException e) {
599            throw new RuntimeException(e);
600        }
601    }
602
603    protected String createDatastream(final String id, final String content) throws IOException {
604        try (final var response = execute(putDSMethod(id, content))) {
605            assertEquals(CREATED.getStatusCode(), getStatus(response));
606            return getLocation(response);
607        }
608    }
609
610    protected String createDatastream(final String pid, final String dsid, final String content) throws IOException {
611        logger.trace("Attempting to create datastream for object: {} at datastream ID: {}", pid, dsid);
612        try (final var response = execute(putDSMethod(pid, dsid, content))) {
613            assertEquals(CREATED.getStatusCode(), getStatus(response));
614            return getLocation(response);
615        }
616    }
617
618    protected CloseableHttpResponse setProperty(final String pid, final String propertyUri, final String value)
619            throws IOException {
620        return setProperty(pid, null, propertyUri, "\"" + value + "\"");
621    }
622
623    protected CloseableHttpResponse setProperty(final String pid, final String propertyUri, final URI value)
624            throws IOException {
625        return setProperty(pid, null, propertyUri, "<" + value.toString() + ">");
626    }
627
628    private CloseableHttpResponse setProperty(final String id, final String txId, final String propertyUri,
629                                              final String value) throws IOException {
630        final HttpPatch postProp = new HttpPatch(serverAddress + id);
631        if (txId != null) {
632            addTxTo(postProp, txId);
633        }
634        postProp.setHeader(CONTENT_TYPE, "application/sparql-update");
635        final String updateString =
636                "INSERT { <" + serverAddress + id.replace("/" + FCR_METADATA, "") +
637                        "> <" + propertyUri + "> " + value + " } WHERE { }";
638        postProp.setEntity(new StringEntity(updateString));
639        final CloseableHttpResponse dcResp = execute(postProp);
640        assertEquals(dcResp.getStatusLine().toString(), NO_CONTENT.getStatusCode(), getStatus(dcResp));
641        postProp.releaseConnection();
642        return dcResp;
643    }
644
645    protected CloseableHttpResponse setDescriptionProperty(final String id, final String txId,
646            final String propertyUri, final String value) throws IOException {
647        final HttpPatch postProp = new HttpPatch(serverAddress + (txId != null ? txId + "/" : "") + id +
648                "/fcr:metadata");
649        postProp.setHeader(CONTENT_TYPE, "application/sparql-update");
650        final String updateString =
651                "INSERT { <" + serverAddress + id + "> <" + propertyUri + "> \"" + value + "\" } WHERE { }";
652        postProp.setEntity(new StringEntity(updateString));
653        final CloseableHttpResponse dcResp = execute(postProp);
654        assertEquals(dcResp.getStatusLine().toString(), NO_CONTENT.getStatusCode(), getStatus(dcResp));
655        postProp.releaseConnection();
656        return dcResp;
657    }
658
659    /**
660     * Creates a transaction, asserts that it's successful and returns the transaction location.
661     *
662     * @return string containing transaction location
663     * @throws IOException exception thrown during the function
664     */
665    protected String createTransaction() throws IOException {
666        final HttpPost createTx = new HttpPost(serverAddress + "fcr:tx");
667        try (final CloseableHttpResponse response = execute(createTx)) {
668            assertEquals(CREATED.getStatusCode(), getStatus(response));
669            return getLocation(response);
670        }
671    }
672
673    /**
674     * Add a transaction id to a http request.
675     *
676     * @param req a http request object.
677     * @param txId the transaction id.
678     * @return the http request object with the transaction id added as a header.
679     */
680    protected <T extends HttpRequestBase> T addTxTo(final T req, final String txId) {
681        req.addHeader(ATOMIC_ID_HEADER, txId);
682        return req;
683    }
684
685    /**
686     * Gets a random (but valid) id for use in testing. This id is guaranteed to be unique within runs of this
687     * application.
688     *
689     * @return string containing new id
690     */
691    protected static String getRandomUniqueId() {
692        return randomUUID().toString();
693    }
694
695    protected static void assertDeleted(final String id) {
696        final String location = serverAddress + id;
697        assertThat("Expected object to be deleted", getStatus(new HttpHead(location)), is(GONE.getStatusCode()));
698        assertThat("Expected object to be deleted", getStatus(new HttpGet(location)), is(GONE.getStatusCode()));
699    }
700
701    protected static void assertNotFound(final String id) {
702        final String location = serverAddress + id;
703        assertThat("Expected object to return 404", getStatus(new HttpHead(location)), is(NOT_FOUND.getStatusCode()));
704        assertThat("Expected object to return 404", getStatus(new HttpGet(location)), is(NOT_FOUND.getStatusCode()));
705    }
706
707    protected static void assertNotDeleted(final String id) {
708        final String location = serverAddress + id;
709        assertThat("Expected object not to be deleted", getStatus(new HttpHead(location)), is(OK.getStatusCode()));
710        assertThat("Expected object not to be deleted", getStatus(new HttpGet(location)), is(OK.getStatusCode()));
711    }
712
713    protected static String getTTLThatUpdatesServerManagedTriples(final String createdBy, final Calendar created,
714                                                                  final String modifiedBy, final Calendar modified) {
715        final StringBuilder ttl = new StringBuilder();
716        if (createdBy != null) {
717            addClause(ttl, CREATED_BY.getURI(), "\"" + createdBy + "\"");
718        }
719        if (created != null) {
720            addClause(ttl, CREATED_DATE.getURI(),
721                    "\"" + DatatypeConverter.printDateTime(created)
722                    + "\"^^<http://www.w3.org/2001/XMLSchema#dateTime>");
723        }
724        if (modifiedBy != null) {
725            addClause(ttl, LAST_MODIFIED_BY.getURI(), "\"" + modifiedBy + "\"");
726        }
727        if (modified != null) {
728            addClause(ttl, LAST_MODIFIED_DATE.getURI(),
729                    "\"" + DatatypeConverter.printDateTime(modified)
730                    + "\"^^<http://www.w3.org/2001/XMLSchema#dateTime>");
731        }
732        ttl.append(" .\n");
733        return ttl.toString();
734
735    }
736
737    private static void addClause(final StringBuilder ttl, final String predicateUri, final String literal) {
738        if (ttl.length() == 0) {
739            ttl.append("<>");
740        } else {
741            ttl.append(" ;\n");
742        }
743        ttl.append(" <" + predicateUri + "> ");
744        ttl.append(literal);
745    }
746
747    /**
748     * Test a response for the absence of a specific LINK header
749     *
750     * @param response the HTTP response
751     * @param uri the URI not to exist in the LINK header
752     * @param rel the rel argument to check for
753     */
754    protected static void assertNoLinkHeader(final HttpResponse response, final String uri, final String rel) {
755        assertEquals(0, countLinkHeader(response, uri, rel));
756    }
757
758    /**
759     * Test a response for a specific LINK header
760     *
761     * @param response the HTTP response
762     * @param uri the URI expected in the LINK header
763     * @param rel the rel argument to check for
764     */
765    protected static void checkForLinkHeader(final HttpResponse response, final String uri, final String rel) {
766        assertEquals(1, countLinkHeader(response, uri, rel));
767    }
768
769    /**
770     * Utility for counting LINK headers
771     *
772     * @param response the HTTP response
773     * @param uri the URI expected in the LINK header
774     * @param rel the rel argument to check for
775     * @return the count of LINK headers.
776     */
777    private static int countLinkHeader(final HttpResponse response, final String uri, final String rel) {
778        final Link linkA = Link.valueOf("<" + uri + ">; rel=" + rel);
779        return (int) Arrays.stream(response.getHeaders(LINK)).filter(x -> {
780            final Link linkB = Link.valueOf(x.getValue());
781            return linkB.equals(linkA);
782        }).count();
783    }
784
785    protected static String getOriginalResourceUri(final CloseableHttpResponse response) {
786        return getLinkHeaders(response).stream()
787            .map(x -> Link.valueOf(x))
788            .filter(x -> x.getRel().equals("original"))
789            .findFirst().get().getUri().toString();
790    }
791
792    protected String getExternalContentLinkHeader(final String url, final String handling, final String mimeType) {
793        // leave lots of room to leave things out of the link to test variations.
794        String link = "";
795        if (url != null && !url.isEmpty()) {
796            link += "<" + url + ">";
797        }
798
799        link += "; rel=\"" + EXTERNAL_CONTENT + "\"";
800
801        if (handling != null && !handling.isEmpty()) {
802            link += "; handling=\"" + handling + "\"";
803        }
804
805        if (mimeType != null && !mimeType.isEmpty()) {
806            link += "; type=\"" + mimeType + "\"";
807        }
808        return link;
809    }
810
811    protected static void assertConstrainedByPresent(final CloseableHttpResponse response) {
812        final Collection<String> linkHeaders = getLinkHeaders(response);
813        assertTrue("Constrained by link header not present",
814                linkHeaders.stream().map(Link::valueOf)
815                        .anyMatch(l -> l.getRel().equals(CONSTRAINED_BY.getURI())));
816    }
817
818
819    /**
820     * Create a Prefer header
821     * @param includes String of include URIs or null if none
822     * @param omits String of omit URIs or null if none
823     * @return The Prefer header.
824     */
825    protected static String preferLink(final String includes, final String omits) {
826        if (includes != null || omits != null) {
827            String link = "return=representation; ";
828            if (includes != null) {
829                link += "include=\"" + includes + "\"";
830            }
831            if (includes != null && omits != null) {
832                link += "; ";
833            }
834            if (omits != null) {
835                link += "omit=\"" + omits + "\"";
836            }
837            return link;
838        }
839        return "";
840    }
841
842    /**
843     * Compare two N-Triple response bodies to determine if they are identical
844     * @param responseBodyA the first n-triple body
845     * @param responseBodyB the second n-triple body
846     */
847    protected static void confirmResponseBodyNTriplesAreEqual(final String responseBodyA, final String responseBodyB) {
848        final String[] aTriples = responseBodyA.split(".(\\r\\n|\\r|\\n)");
849        final String[] bTriples = responseBodyB.split(".(\\r\\n|\\r|\\n)");
850        Arrays.stream(aTriples).map(String::trim).sorted().toArray(unused -> aTriples);
851        Arrays.stream(bTriples).map(String::trim).sorted().toArray(unused -> bTriples);
852        assertArrayEquals(aTriples, bTriples);
853    }
854
855    /**
856     * Instantiation of the authentication handle cache for integration tests.
857     */
858    @Configuration
859    static class TestConfig {
860        @Bean
861        public Cache<String, Optional<ACLHandle>> authHandleCache() {
862            return Caffeine.newBuilder().weakValues().expireAfterAccess(10, TimeUnit.SECONDS)
863                    .maximumSize(10).build();
864        }
865    }
866}