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}