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 static java.nio.charset.StandardCharsets.UTF_8; 021import static java.time.format.DateTimeFormatter.ISO_INSTANT; 022import static java.util.Calendar.getInstance; 023import static java.util.Spliterator.IMMUTABLE; 024import static java.util.Spliterators.spliteratorUnknownSize; 025import static java.util.TimeZone.getTimeZone; 026import static java.util.stream.StreamSupport.stream; 027import static javax.ws.rs.core.HttpHeaders.ALLOW; 028import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 029import static javax.ws.rs.core.HttpHeaders.LINK; 030import static javax.ws.rs.core.MediaType.TEXT_PLAIN; 031import static javax.ws.rs.core.Response.Status.BAD_REQUEST; 032import static javax.ws.rs.core.Response.Status.CONFLICT; 033import static javax.ws.rs.core.Response.Status.CREATED; 034import static javax.ws.rs.core.Response.Status.NO_CONTENT; 035import static javax.ws.rs.core.Response.Status.OK; 036import static org.apache.jena.graph.Node.ANY; 037import static org.apache.jena.graph.NodeFactory.createLiteral; 038import static org.apache.jena.graph.NodeFactory.createLiteralByValue; 039import static org.apache.jena.graph.NodeFactory.createURI; 040import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 041import static org.apache.jena.rdf.model.ModelFactory.createModelForGraph; 042import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 043import static org.fcrepo.http.commons.test.util.TestHelpers.parseTriples; 044import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 045import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER; 046import static org.fcrepo.kernel.api.RdfLexicon.CREATED_BY; 047import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE; 048import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER; 049import static org.fcrepo.kernel.api.RdfLexicon.HAS_MESSAGE_DIGEST; 050import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_BY; 051import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE; 052import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; 053import static org.fcrepo.kernel.api.RdfLexicon.PREFER_SERVER_MANAGED; 054import static org.junit.Assert.assertEquals; 055import static org.junit.Assert.assertFalse; 056import static org.junit.Assert.assertTrue; 057import static org.junit.Assert.fail; 058 059import java.io.ByteArrayInputStream; 060import java.io.ByteArrayOutputStream; 061import java.io.IOException; 062import java.time.Instant; 063import java.time.temporal.ChronoUnit; 064import java.util.Arrays; 065import java.util.Calendar; 066import java.util.HashSet; 067import java.util.Iterator; 068import java.util.Set; 069import java.util.stream.Stream; 070 071import javax.ws.rs.core.Link; 072import javax.xml.bind.DatatypeConverter; 073 074import org.fcrepo.config.ServerManagedPropsMode; 075import org.fcrepo.http.commons.test.util.CloseableDataset; 076import org.fcrepo.kernel.api.utils.GraphDifferencer; 077 078import org.apache.commons.io.IOUtils; 079import org.apache.http.client.methods.CloseableHttpResponse; 080import org.apache.http.client.methods.HttpDelete; 081import org.apache.http.client.methods.HttpGet; 082import org.apache.http.client.methods.HttpPatch; 083import org.apache.http.client.methods.HttpPost; 084import org.apache.http.client.methods.HttpPut; 085import org.apache.http.entity.ByteArrayEntity; 086import org.apache.http.entity.StringEntity; 087import org.apache.http.util.EntityUtils; 088import org.apache.jena.datatypes.xsd.XSDDatatype; 089import org.apache.jena.datatypes.xsd.XSDDateTime; 090import org.apache.jena.datatypes.xsd.impl.XSDDateTimeType; 091import org.apache.jena.graph.Node; 092import org.apache.jena.graph.Triple; 093import org.apache.jena.query.Dataset; 094import org.apache.jena.rdf.model.Model; 095import org.apache.jena.rdf.model.Property; 096import org.apache.jena.rdf.model.Statement; 097import org.apache.jena.rdf.model.StmtIterator; 098import org.apache.jena.riot.RDFDataMgr; 099import org.apache.jena.riot.RDFFormat; 100import org.apache.jena.sparql.core.DatasetGraph; 101import org.apache.jena.sparql.core.Quad; 102import org.apache.jena.vocabulary.RDF; 103import org.junit.After; 104import org.junit.Before; 105import org.junit.Test; 106import org.junit.runner.RunWith; 107import org.springframework.test.context.TestExecutionListeners; 108import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 109 110/** 111 * @author Mike Durbin 112 */ 113@RunWith(SpringJUnit4ClassRunner.class) 114@TestExecutionListeners( 115 listeners = { TestIsolationExecutionListener.class }, 116 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) 117public class FedoraRelaxedLdpIT extends AbstractResourceIT { 118 119 private final String providedUsername = "provided-username"; 120 private Calendar providedDate; 121 122 public FedoraRelaxedLdpIT() { 123 providedDate = nowUTC(); 124 providedDate.add(Calendar.YEAR, -20); 125 } 126 127 @Before 128 public void switchToRelaxedMode() { 129 propsConfig.setServerManagedPropsMode(ServerManagedPropsMode.RELAXED); 130 } 131 132 133 @Test 134 public void testBasicPutRoundtrip() throws IOException { 135 final String subjectURI; 136 try (final CloseableHttpResponse response = execute(postObjMethod())) { 137 assertEquals(CREATED.getStatusCode(), getStatus(response)); 138 subjectURI = getLocation(response); 139 } 140 141 final String body; 142 final HttpGet get = new HttpGet(subjectURI); 143 try (final CloseableHttpResponse response = execute(get)) { 144 assertEquals(OK.getStatusCode(), getStatus(response)); 145 body = EntityUtils.toString(response.getEntity()); 146 } 147 148 final HttpPut put = new HttpPut(subjectURI); 149 put.setEntity(new StringEntity(body)); 150 put.setHeader(CONTENT_TYPE, "text/turtle"); 151 put.addHeader("Prefer", "handling=lenient"); 152 assertEquals(NO_CONTENT.getStatusCode(), getStatus(put)); 153 } 154 155 @Test 156 public void testCreateResourceWithSpecificCreationInformationIsAllowed() throws IOException { 157 assertEquals(ServerManagedPropsMode.RELAXED, propsConfig.getServerManagedPropsMode()); // sanity check 158 final String subjectURI; 159 try (final CloseableHttpResponse response = postResourceWithTTL( 160 getTTLThatUpdatesServerManagedTriples(providedUsername, providedDate, null, null))) { 161 assertEquals(CREATED.getStatusCode(), getStatus(response)); 162 subjectURI = getLocation(response); 163 } 164 165 try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) { 166 triples(subjectURI, dataset) 167 .mustHave(CREATED_BY.asNode(), createLiteral(providedUsername)) 168 .mustHave(CREATED_DATE.asNode(), createDateTime(providedDate)); 169 } 170 } 171 172 @Test 173 public void testUpdateNonRdfResourceWithSpecificInformationIsAllowed() throws IOException { 174 assertEquals(ServerManagedPropsMode.RELAXED, propsConfig.getServerManagedPropsMode()); // sanity check 175 176 final String subjectURI; 177 try (final CloseableHttpResponse response = postBinaryResource(serverAddress, "this is the binary")) { 178 assertEquals(CREATED.getStatusCode(), getStatus(response)); 179 subjectURI = getLocation(response); 180 } 181 182 final String describedByURI = subjectURI + "/fcr:metadata"; 183 184 try (final CloseableHttpResponse response = putResourceWithTTL(describedByURI, 185 getTTLThatUpdatesServerManagedTriples(providedUsername, providedDate, 186 providedUsername, providedDate))) { 187 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 188 } 189 190 try (final CloseableDataset dataset = getDataset(new HttpGet(describedByURI))) { 191 triples(subjectURI, dataset) 192 .mustHave(CREATED_BY.asNode(), createLiteral(providedUsername)) 193 .mustHave(CREATED_DATE.asNode(), createDateTime(providedDate)) 194 .mustHave(LAST_MODIFIED_BY.asNode(), createLiteral(providedUsername)) 195 .mustHave(LAST_MODIFIED_DATE.asNode(), createDateTime(providedDate)); 196 } 197 } 198 199 @Test 200 public void testValidSparqlUpdate() throws IOException { 201 assertEquals(ServerManagedPropsMode.RELAXED, propsConfig.getServerManagedPropsMode()); // sanity check 202 203 final String subjectURI; 204 try (final CloseableHttpResponse response = postResourceWithTTL( 205 getTTLThatUpdatesServerManagedTriples(providedUsername, providedDate, null, null))) { 206 assertEquals(CREATED.getStatusCode(), getStatus(response)); 207 subjectURI = getLocation(response); 208 } 209 210 final Calendar updatedDate = Calendar.getInstance(getTimeZone("UTC")); 211 final String sparqlUpdate = "PREFIX fedora: <http://fedora.info/definitions/v4/repository#>\n" + 212 "\n" + 213 "DELETE { <> fedora:created \"" + DatatypeConverter.printDateTime(providedDate) 214 + "\"^^<http://www.w3.org/2001/XMLSchema#dateTime> }\n" + 215 "INSERT { <> fedora:created \"" + DatatypeConverter.printDateTime(updatedDate) 216 + "\"^^<http://www.w3.org/2001/XMLSchema#dateTime> }\n" + 217 "WHERE { }"; 218 try (final CloseableHttpResponse response = patchResource(subjectURI, sparqlUpdate)) { 219 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 220 } 221 222 try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) { 223 triples(subjectURI, dataset) 224 .mustHave(CREATED_BY.asNode(), createLiteral(providedUsername)) 225 .mustHave(CREATED_DATE.asNode(), createDateTime(updatedDate)); 226 } 227 } 228 229 @Test 230 public void testInvalidSparqlUpdate() throws IOException { 231 assertEquals(ServerManagedPropsMode.RELAXED, propsConfig.getServerManagedPropsMode()); // sanity check 232 233 final String subjectURI; 234 try (final CloseableHttpResponse response = execute(postObjMethod())) { 235 assertEquals(CREATED.getStatusCode(), getStatus(response)); 236 subjectURI = getLocation(response); 237 } 238 239 final Calendar updatedDate = Calendar.getInstance(getTimeZone("UTC")); 240 final String sparqlUpdate = "PREFIX fedora: <http://fedora.info/definitions/v4/repository#>\n" + 241 "\n" + 242 "INSERT { <> fedora:created \"" + DatatypeConverter.printDateTime(updatedDate) + 243 "\"^^<http://www.w3.org/2001/XMLSchema#dateTime> .\n" + 244 " <> fedora:created \"" + DatatypeConverter.printDateTime(providedDate) + 245 "\"^^<http://www.w3.org/2001/XMLSchema#dateTime> }\n" + 246 "WHERE { }"; 247 try (final CloseableHttpResponse response = patchResource(subjectURI, sparqlUpdate)) { 248 assertEquals(BAD_REQUEST.getStatusCode(), getStatus(response)); 249 } 250 251 try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) { 252 triples(subjectURI, dataset) 253 .mustNotHave(CREATED_BY.asNode(), createLiteral(providedUsername)) 254 .mustNotHave(CREATED_DATE.asNode(), createDateTime(updatedDate)); 255 } 256 } 257 258 @Test 259 public void testUpdateResourceWithSpecificModificationInformationIsAllowed() throws IOException { 260 assertEquals(ServerManagedPropsMode.RELAXED, propsConfig.getServerManagedPropsMode()); // sanity check 261 262 final String subjectURI; 263 try (final CloseableHttpResponse response = createObject()) { 264 assertEquals(CREATED.getStatusCode(), getStatus(response)); 265 subjectURI = getLocation(response); 266 } 267 268 providedDate = nowUTC(); 269 providedDate.add(Calendar.MONTH, 1); 270 271 try (final CloseableHttpResponse response = putResourceWithTTL(subjectURI, 272 getTTLThatUpdatesServerManagedTriples(null, null, providedUsername, providedDate))) { 273 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 274 } 275 276 try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) { 277 triples(subjectURI, dataset) 278 .mustHave(LAST_MODIFIED_BY.asNode(), createLiteral(providedUsername)) 279 .mustHave(LAST_MODIFIED_DATE.asNode(), createDateTime(providedDate)); 280 } 281 } 282 283 /** 284 * Tests a lossless roundtrip of a resource. 285 * @throws IOException if an error occurs while reading or writing to repository over HTTP 286 */ 287 @Test 288 public void testRoundtripping() throws IOException { 289 assertEquals(ServerManagedPropsMode.RELAXED, propsConfig.getServerManagedPropsMode()); // sanity check 290 291 // POST a resource with one user-managed triple 292 final String containerURI; 293 try (final CloseableHttpResponse response 294 = postResourceWithTTL("<> <http://purl.org/dc/elements/1.1/title> 'title' .")) { 295 assertEquals(CREATED.getStatusCode(), getStatus(response)); 296 containerURI = getLocation(response); 297 } 298 299 // POST a non-rdf child resource 300 final String containedBinaryURI; 301 final String containedBinaryDescriptionURI; 302 try (final CloseableHttpResponse response = postBinaryResource(containerURI, "content")) { 303 assertEquals(CREATED.getStatusCode(), getStatus(response)); 304 containedBinaryURI = getLocation(response); 305 containedBinaryDescriptionURI = containedBinaryURI + "/fcr:metadata"; 306 } 307 308 // export the RDF of the container 309 final String containerBody; 310 try (final CloseableHttpResponse response = execute(new HttpGet(containerURI))) { 311 assertEquals(OK.getStatusCode(), getStatus(response)); 312 containerBody = EntityUtils.toString(response.getEntity()); 313 } 314 315 // export the RDF of the child 316 final String containedBinaryDescriptionBody; 317 try (final CloseableHttpResponse response = execute(new HttpGet(containedBinaryDescriptionURI))) { 318 assertEquals(OK.getStatusCode(), getStatus(response)); 319 containedBinaryDescriptionBody = EntityUtils.toString(response.getEntity()); 320 } 321 322 323 // delete the container and its tombstone 324 try (final CloseableHttpResponse response = execute(new HttpDelete(containerURI))) { 325 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 326 try (final CloseableHttpResponse r2 = execute(new HttpGet(containerURI))) { 327 final Link tombstone = Link.valueOf(r2.getFirstHeader(LINK).getValue()); 328 assertEquals("hasTombstone", tombstone.getRel()); 329 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(tombstone.getUri()))); 330 } 331 } 332 333 334 // post the container from the export 335 final String containerRdf = filterRdf(containerBody, CREATED_BY, CREATED_DATE, LAST_MODIFIED_BY, 336 LAST_MODIFIED_DATE, createProperty("http://purl.org/dc/elements/1.1/title")); 337 try (final CloseableHttpResponse response = postResourceWithTTL(containerURI, containerRdf)) { 338 assertEquals(CREATED.getStatusCode(), getStatus(response)); 339 } 340 341 // post the binary then patch its metadata to match the export 342 try (final CloseableHttpResponse response = postBinaryResource(containerURI, 343 containedBinaryURI.substring(containerURI.length() + 1), "content")) { 344 assertEquals(CREATED.getStatusCode(), getStatus(response)); 345 } 346 final String sparqlUpdate; 347 try (final CloseableDataset d = getDataset(new HttpGet(containedBinaryDescriptionURI))) { 348 final Model model = createDefaultModel(); 349 model.read(new ByteArrayInputStream(containedBinaryDescriptionBody.getBytes()), "", "N3"); 350 final Stream<Statement> statements = 351 stream(spliteratorUnknownSize(d.getDefaultModel().listStatements(), IMMUTABLE), false); 352 final GraphDifferencer diff = new GraphDifferencer(model, statements.map(Statement::asTriple)); 353 sparqlUpdate = buildSparqlUpdate(diff.difference(), diff.notCommon(), CREATED_BY, CREATED_DATE, 354 LAST_MODIFIED_BY, LAST_MODIFIED_DATE); 355 } 356 357 try (final CloseableHttpResponse response = patchResource(containedBinaryDescriptionURI, sparqlUpdate)) { 358 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 359 } 360 361 // Because the creation of the contained resource resulted in an implicit modification to 362 // the triples on the parent, we need to overwrite them again... This is done doing a PUT 363 // but could be achieved using a sparql update. 364 assertEquals(NO_CONTENT.getStatusCode(), getStatus(putResourceWithTTL(containerURI, containerRdf))); 365 366 assertIdentical(containerURI, containerBody); 367 assertIdentical(containedBinaryDescriptionURI, containedBinaryDescriptionBody); 368 } 369 370 @Test 371 public void testChangeInteractionModelPutRdf() throws Exception { 372 final var postBinary = postObjMethod(); 373 postBinary.setHeader(CONTENT_TYPE, "text/plain"); 374 postBinary.setEntity(new StringEntity("some test data")); 375 final String binaryUri; 376 // Create a binary 377 try (final var response = execute(postBinary)) { 378 assertEquals(CREATED.getStatusCode(), getStatus(response)); 379 binaryUri = getLocation(response); 380 } 381 final Node binaryNode = createURI(binaryUri); 382 final String oldDate; 383 // Get the binary description 384 try (final var dataset = getDataset(new HttpGet(binaryUri + "/" + FCR_METADATA))) { 385 final var graph = dataset.asDatasetGraph(); 386 // Assert it has a NonRDFSource type 387 assertTrue(graph.contains(ANY, binaryNode, RDF.type.asNode(), NON_RDF_SOURCE.asNode())); 388 final var iter = graph.find(ANY, binaryNode, CREATED_DATE.asNode(), ANY); 389 assertTrue(iter.hasNext()); 390 final var stmt = iter.next(); 391 // We'll change the createdDate 392 oldDate = stmt.getObject().getLiteralValue().toString(); 393 } 394 // Now GET without server managed properties 395 final ByteArrayOutputStream newModel = new ByteArrayOutputStream(); 396 final var newDate = "2000-01-01T00:00:00Z"; 397 // Instant one day after the day we are trying to set. 398 final var newDatePlusOneInstant = Instant.from(ISO_INSTANT.parse(newDate)) 399 .plus(1, ChronoUnit.DAYS); 400 final var getDesc = new HttpGet(binaryUri + "/" + FCR_METADATA); 401 getDesc.addHeader("Prefer", preferLink(null, PREFER_SERVER_MANAGED.getURI())); 402 try (final var dataset = getDataset(getDesc)) { 403 final var graph = dataset.asDatasetGraph(); 404 // Now add some fields to the new model 405 final var newGraph = graph.getDefaultGraph(); 406 newGraph.add(Triple.create(binaryNode, RDF.type.asNode(), DIRECT_CONTAINER.asNode())); 407 newGraph.add(Triple.create(binaryNode, CREATED_DATE.asNode(), 408 createLiteral(newDate, XSDDateTimeType.XSDdateTime))); 409 RDFDataMgr.write(newModel, newGraph, RDFFormat.NTRIPLES_UTF8); 410 } 411 // Now PUT this new graph back 412 final var putBinaryDescription = new HttpPut(binaryUri + "/" + FCR_METADATA); 413 putBinaryDescription.setHeader(CONTENT_TYPE, "application/n-triples"); 414 putBinaryDescription.setEntity(new ByteArrayEntity(newModel.toByteArray())); 415 assertEquals(CONFLICT.getStatusCode(), getStatus(putBinaryDescription)); 416 // Check that the date didn't change either. 417 try (final var dataset = getDataset(new HttpGet(binaryUri + "/" + FCR_METADATA))) { 418 final var graph = dataset.asDatasetGraph(); 419 // Assert it has a NonRDFSource type 420 assertTrue(graph.contains(ANY, binaryNode, RDF.type.asNode(), NON_RDF_SOURCE.asNode())); 421 final var dateIter = graph.find(ANY, binaryNode, CREATED_DATE.asNode(), ANY); 422 // Assert at least one date 423 assertTrue(dateIter.hasNext()); 424 final var graphDate = dateIter.next(); 425 // Assert only one date 426 assertFalse(dateIter.hasNext()); 427 final var graphDateInstant = 428 Instant.from(ISO_INSTANT.parse(graphDate.getObject().getLiteralValue().toString())); 429 // Assert the date is after the date we tried to set 430 assertTrue(graphDateInstant.isAfter(newDatePlusOneInstant)); 431 assertTrue(graph.contains(ANY, binaryNode, CREATED_DATE.asNode(), 432 createLiteral(oldDate, XSDDateTimeType.XSDdateTime))); 433 assertFalse(graph.contains(ANY, binaryNode, RDF.type.asNode(), DIRECT_CONTAINER.asNode())); 434 assertFalse(graph.contains(ANY, binaryNode, CREATED_DATE.asNode(), 435 createLiteral(newDate, XSDDateTimeType.XSDdateTime))); 436 } 437 } 438 439 @Test 440 public void testChangeInteractionModelPatchRdf() throws Exception { 441 final var postBinary = postObjMethod(); 442 postBinary.setHeader(CONTENT_TYPE, "text/plain"); 443 postBinary.setEntity(new StringEntity("some test data")); 444 final String binaryUri; 445 // Create a binary 446 try (final var response = execute(postBinary)) { 447 assertEquals(CREATED.getStatusCode(), getStatus(response)); 448 binaryUri = getLocation(response); 449 } 450 final Node binaryNode = createURI(binaryUri); 451 // Get the binary description 452 try (final var dataset = getDataset(new HttpGet(binaryUri + "/" + FCR_METADATA))) { 453 final var graph = dataset.asDatasetGraph(); 454 // Assert it has a NonRDFSource type 455 assertTrue(graph.contains(ANY, binaryNode, RDF.type.asNode(), NON_RDF_SOURCE.asNode())); 456 assertTrue(graph.contains(ANY, binaryNode, CREATED_DATE.asNode(), ANY)); 457 } 458 // Now PUT this new graph back 459 final var newDate = "2000-01-01T00:00:00Z"; 460 // Instant one day after the day we are trying to set. 461 final var newDatePlusOneInstant = Instant.from(ISO_INSTANT.parse(newDate)) 462 .plus(1, ChronoUnit.DAYS); 463 final var patchBinaryDescription = new HttpPatch(binaryUri + "/" + FCR_METADATA); 464 patchBinaryDescription.setHeader(CONTENT_TYPE, "application/sparql-update"); 465 patchBinaryDescription.setEntity(new StringEntity("DELETE { <> <" + RDF.type + "> ?type ; " + 466 "<" + CREATED_DATE + "> ?date . } INSERT { <> <" + RDF.type + "> <" + DIRECT_CONTAINER + "> ; " + 467 "<" + CREATED_DATE + "> \"" + newDate + "\"^^<" + 468 XSDDateTimeType.XSDdateTime.getURI() + "> . } WHERE { <> <" + RDF.type + "> ?type ; " + 469 "<" + CREATED_DATE + "> ?date . } ")); 470 assertEquals(CONFLICT.getStatusCode(), getStatus(patchBinaryDescription)); 471 // Check that the date didn't change either. 472 try (final var dataset = getDataset(new HttpGet(binaryUri + "/" + FCR_METADATA))) { 473 final var graph = dataset.asDatasetGraph(); 474 // Assert it has a NonRDFSource type 475 assertTrue(graph.contains(ANY, binaryNode, RDF.type.asNode(), NON_RDF_SOURCE.asNode())); 476 assertFalse(graph.contains(ANY, binaryNode, RDF.type.asNode(), DIRECT_CONTAINER.asNode())); 477 final var dateIter = graph.find(ANY, binaryNode, CREATED_DATE.asNode(), ANY); 478 // Assert at least one date 479 assertTrue(dateIter.hasNext()); 480 final var graphDate = dateIter.next(); 481 // Assert only one date 482 assertFalse(dateIter.hasNext()); 483 final var graphDateInstant = 484 Instant.from(ISO_INSTANT.parse(graphDate.getObject().getLiteralValue().toString())); 485 // Assert the date is after the date we tried to set 486 assertTrue(graphDateInstant.isAfter(newDatePlusOneInstant)); 487 } 488 } 489 490 @Test 491 public void testPutNonRelaxablePredicates() throws Exception { 492 final var postBinary = postObjMethod(); 493 postBinary.setHeader(CONTENT_TYPE, "text/plain"); 494 postBinary.setEntity(new StringEntity("some test data")); 495 final String binaryUri; 496 // Create a binary 497 try (final var response = execute(postBinary)) { 498 assertEquals(CREATED.getStatusCode(), getStatus(response)); 499 binaryUri = getLocation(response); 500 } 501 final String binaryDescription = binaryUri + "/" + FCR_METADATA; 502 // Get the original body. 503 final var getObj = new HttpGet(binaryDescription); 504 getObj.addHeader(ALLOW, "application/n-triples"); 505 final String originalBody; 506 try (final var response = execute(getObj)) { 507 assertEquals(OK.getStatusCode(), getStatus(response)); 508 originalBody = IOUtils.toString(response.getEntity().getContent(), UTF_8); 509 } 510 // Can't set a non-relaxable property, even in relaxed mode. 511 final var put = new HttpPut(binaryDescription); 512 put.setHeader(CONTENT_TYPE, "application/n-triples"); 513 put.setEntity(new StringEntity("<" + binaryUri + "> <" + HAS_MESSAGE_DIGEST + "> \"not a real digest\" .\n" + 514 "<" + binaryUri + "> <" + CREATED_BY + "> \"some-test-user\" .")); 515 assertEquals(CONFLICT.getStatusCode(), getStatus(put)); 516 517 // Get the body again. 518 final String newBody; 519 try (final var response = execute(getObj)) { 520 assertEquals(OK.getStatusCode(), getStatus(response)); 521 newBody = IOUtils.toString(response.getEntity().getContent(), UTF_8); 522 } 523 confirmResponseBodyNTriplesAreEqual(originalBody, newBody); 524 } 525 526 @Test 527 public void testPatchNonRelaxablePredicates() throws Exception { 528 final var postBinary = postObjMethod(); 529 postBinary.setHeader(CONTENT_TYPE, "text/plain"); 530 postBinary.setEntity(new StringEntity("some test data")); 531 final String binaryUri; 532 // Create a binary 533 try (final var response = execute(postBinary)) { 534 assertEquals(CREATED.getStatusCode(), getStatus(response)); 535 binaryUri = getLocation(response); 536 } 537 final String binaryDescription = binaryUri + "/" + FCR_METADATA; 538 // Get the original body. 539 final var getObj = new HttpGet(binaryDescription); 540 getObj.addHeader(ALLOW, "application/n-triples"); 541 final String originalBody; 542 try (final var response = execute(getObj)) { 543 assertEquals(OK.getStatusCode(), getStatus(response)); 544 originalBody = IOUtils.toString(response.getEntity().getContent(), UTF_8); 545 } 546 547 // Can't patch a non-relaxable property, even in relaxed mode. 548 final var patch = new HttpPatch(binaryDescription); 549 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 550 patch.setEntity(new StringEntity("DELETE { <> <" + HAS_MESSAGE_DIGEST + "> ?digest } INSERT { " + 551 "<> <" + HAS_MESSAGE_DIGEST + "> \"fake-digest\". } WHERE { <> <" + HAS_MESSAGE_DIGEST + 552 "> ?digest }")); 553 assertEquals(CONFLICT.getStatusCode(), getStatus(patch)); 554 555 // Get the body again. 556 final String newBody; 557 try (final var response = execute(getObj)) { 558 assertEquals(OK.getStatusCode(), getStatus(response)); 559 newBody = IOUtils.toString(response.getEntity().getContent(), UTF_8); 560 } 561 confirmResponseBodyNTriplesAreEqual(originalBody, newBody); 562 } 563 564 @After 565 public void switchToStrictMode() { 566 propsConfig.setServerManagedPropsMode(ServerManagedPropsMode.STRICT); 567 } 568 569 private void assertIdentical(final String uri, final String originalRdf) throws IOException { 570 try (final CloseableDataset roundtripped = getDataset(new HttpGet(uri))) { 571 try (final CloseableDataset original = parseTriples(new ByteArrayInputStream(originalRdf.getBytes()))) { 572 final DatasetGraph originalGraph = original.asDatasetGraph(); 573 final DatasetGraph roundtrippedGraph = roundtripped.asDatasetGraph(); 574 final Iterator<Quad> originalQuadIt = originalGraph.find(); 575 while (originalQuadIt.hasNext()) { 576 final Quad q = originalQuadIt.next(); 577 if (!assertQuadsAreSimilar(q, roundtrippedGraph)) { 578 fail(q + " should be preserved through a roundtrip! \nOriginal RDF: " + originalRdf 579 + "\nRoundtripped Graph:\n" + roundtrippedGraph); 580 } 581 roundtrippedGraph.delete(q); 582 } 583 assertTrue("Roundtripped graph had extra quads! " + roundtrippedGraph, roundtrippedGraph.isEmpty()); 584 } 585 } 586 } 587 588 /** 589 * In roundtripping XSDDateTimes get truncated to 3 digits of microseconds. This checks that if the quad doesn't 590 * match and is a XSDDateTime that it is within .001 of a second. 591 * 592 * @param testQuad the quad to check 593 * @param checkGraph the graph to verify against 594 * @return true if the quad is exact or very close, false if not. 595 */ 596 private boolean assertQuadsAreSimilar(final Quad testQuad, final DatasetGraph checkGraph) { 597 if (checkGraph.contains(testQuad)) { 598 return true; 599 } else if (testQuad.getObject().isLiteral() && 600 testQuad.getObject().getLiteral().getDatatype() instanceof XSDDateTimeType) { 601 final var testLiteral = (XSDDateTime)testQuad.getObject().getLiteralValue(); 602 if (checkGraph.contains(ANY, testQuad.getSubject(), testQuad.getPredicate(), ANY)) { 603 final var roundTripIter = 604 checkGraph.find(ANY, testQuad.getSubject(), testQuad.getPredicate(), ANY); 605 while (roundTripIter.hasNext()) { 606 final var dateStmt = roundTripIter.next(); 607 final var comparisonVal = (XSDDateTime) dateStmt.getObject().getLiteralValue(); 608 if (testLiteral.getYears() == comparisonVal.getYears() && 609 testLiteral.getMonths() == comparisonVal.getMonths() && 610 testLiteral.getDays() == comparisonVal.getDays() && 611 testLiteral.getHours() == comparisonVal.getHours() && 612 testLiteral.getMinutes() == comparisonVal.getMinutes() && 613 testLiteral.getFullSeconds() == comparisonVal.getFullSeconds() && 614 Math.abs(testLiteral.getSeconds() - comparisonVal.getSeconds()) < .001) { 615 // Delete here as it won't actually match the quad 616 checkGraph.delete(dateStmt); 617 return true; 618 } 619 } 620 } 621 } 622 return false; 623 } 624 625 private String buildSparqlUpdate(final Stream<Triple> toRemove, final Stream<Triple> toAdd, 626 final Property ... predicates) { 627 final Set<Property> allowedPredicates = new HashSet<>(Arrays.asList(predicates)); 628 final StringBuilder r = new StringBuilder(); 629 r.append("DELETE { \n"); 630 toRemove.filter(t -> allowedPredicates.contains(createProperty(t.getPredicate().getURI()))) 631 .forEach(triple -> r.append(" <> <" + triple.getPredicate().toString() + "> " 632 + wrapLiteral(triple.getObject()) + " .\n")); 633 r.append("}\nINSERT { "); 634 toAdd.filter(t -> allowedPredicates.contains(createProperty(t.getPredicate().getURI()))) 635 .forEach(triple -> r.append(" <> <" + triple.getPredicate().toString() + "> " 636 + wrapLiteral(triple.getObject()) + " .\n")); 637 r.append("} WHERE {}"); 638 return r.toString(); 639 } 640 641 private String wrapLiteral(final Node node) { 642 return "\"" + node.getLiteralLexicalForm() + "\"^^<" + node.getLiteralDatatype().getURI() + ">"; 643 } 644 645 /** 646 * Parses the provided triples and filters it to just include statements with the 647 * specified predicates. 648 * @param triples to filter 649 * @param predicates to filter on 650 * @return filtered triples 651 */ 652 private String filterRdf(final String triples, final Property ... predicates) { 653 try (final CloseableDataset original = parseTriples(new ByteArrayInputStream(triples.getBytes()))) { 654 final Model m = original.getDefaultModel(); 655 final StmtIterator it = m.listStatements(); 656 while (it.hasNext()) { 657 final Statement stmt = it.nextStatement(); 658 boolean keep = false; 659 for (final Property p : predicates) { 660 if (stmt.getPredicate().equals(p)) { 661 keep = true; 662 break; 663 } 664 } 665 if (!keep) { 666 it.remove(); 667 } 668 } 669 final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 670 m.write(baos, "N3"); 671 return baos.toString(); 672 } 673 } 674 675 private CloseableHttpResponse patchResource(final String uri, final String sparqlUpdate) throws IOException { 676 final HttpPatch patch = new HttpPatch(uri); 677 patch.setHeader("Content-type", "application/sparql-update"); 678 patch.setEntity(new StringEntity(sparqlUpdate)); 679 return execute(patch); 680 } 681 682 private CloseableHttpResponse postBinaryResource(final String parentURI, final String content) throws IOException { 683 return postBinaryResource(parentURI, null, content); 684 } 685 686 private CloseableHttpResponse postBinaryResource(final String parentURI, final String slug, 687 final String content) throws IOException { 688 final HttpPost post = new HttpPost(parentURI); 689 if (slug != null) { 690 post.setHeader("Slug", slug); 691 } 692 post.setEntity(new StringEntity(content == null ? "" : content)); 693 post.setHeader(CONTENT_TYPE, TEXT_PLAIN); 694 post.setHeader(LINK, "<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\""); 695 return execute(post); 696 } 697 698 private CloseableHttpResponse postResourceWithTTL(final String ttl) throws IOException { 699 return this.postResourceWithTTL(null, ttl); 700 } 701 702 private CloseableHttpResponse postResourceWithTTL(final String uri, final String ttl) throws IOException { 703 final HttpPost httpPost = new HttpPost(serverAddress); 704 httpPost.addHeader("Slug", uri == null ? getRandomUniqueId() : uri.substring(serverAddress.length())); 705 httpPost.addHeader(CONTENT_TYPE, "text/turtle"); 706 httpPost.addHeader(LINK, "<" + BASIC_CONTAINER.toString() + ">; rel=\"type\""); 707 httpPost.setEntity(new StringEntity(ttl)); 708 return execute(httpPost); 709 } 710 711 private CloseableHttpResponse putResourceWithTTL(final String uri, final String ttl) 712 throws IOException { 713 final HttpPut httpPut = new HttpPut(uri); 714 httpPut.addHeader(CONTENT_TYPE, "text/turtle"); 715 httpPut.setEntity(new StringEntity(ttl)); 716 return execute(httpPut); 717 } 718 719 private ResultGraphWrapper triples(final String uri, final Dataset dataset) { 720 return new ResultGraphWrapper(uri, dataset.asDatasetGraph()); 721 } 722 723 private static Calendar nowUTC() { 724 return getInstance(getTimeZone("UTC")); 725 } 726 727 private class ResultGraphWrapper { 728 729 private final DatasetGraph graph; 730 private final Node nodeUri; 731 private final String statements; 732 733 public ResultGraphWrapper(final String subjectURI, final DatasetGraph graph) { 734 this.graph = graph; 735 final Model model = createModelForGraph(graph.getDefaultGraph()); 736 nodeUri = createURI(subjectURI); 737 738 final StringBuilder statements = new StringBuilder(); 739 final StmtIterator it = model.listStatements(); 740 while (it.hasNext()) { 741 statements.append(it.next() + "\n"); 742 } 743 this.statements = statements.toString(); 744 } 745 746 public ResultGraphWrapper mustHave(final Node predicate, final Node object) { 747 assertTrue(predicate.getLocalName() + " should have been set to " + object.getLiteral().toString() 748 + "!\nStatements:\n" + statements, graph.contains(ANY, nodeUri, predicate, object)); 749 return this; 750 } 751 752 public ResultGraphWrapper mustNotHave(final Node predicate, final Node object) { 753 assertFalse(predicate.getLocalName() + " should not have been set to " + object.getLiteral().toString() 754 + "!", graph.contains(ANY, nodeUri, predicate, object)); 755 return this; 756 } 757 758 } 759 760 private Node createDateTime(final Calendar cal) { 761 return createLiteralByValue(DatatypeConverter.printDateTime(cal), XSDDatatype.XSDdateTime); 762 } 763 764}