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 javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 021import static javax.ws.rs.core.HttpHeaders.LINK; 022import static javax.ws.rs.core.HttpHeaders.LOCATION; 023import static javax.ws.rs.core.Response.Status.BAD_REQUEST; 024import static javax.ws.rs.core.Response.Status.CONFLICT; 025import static javax.ws.rs.core.Response.Status.CREATED; 026import static javax.ws.rs.core.Response.Status.GONE; 027import static javax.ws.rs.core.Response.Status.NO_CONTENT; 028import static javax.ws.rs.core.Response.Status.OK; 029import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 030import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 031import static org.apache.jena.rdf.model.ResourceFactory.createResource; 032import static org.awaitility.Awaitility.await; 033import static org.fcrepo.http.commons.domain.RDFMediaType.APPLICATION_LINK_FORMAT; 034import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER; 035import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; 036import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER; 037import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION; 038import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER; 039import static org.fcrepo.kernel.api.RdfLexicon.INSERTED_CONTENT_RELATION; 040import static org.fcrepo.kernel.api.RdfLexicon.LDP_MEMBER; 041import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE; 042import static org.fcrepo.kernel.api.RdfLexicon.MEMBER_SUBJECT; 043import static org.fcrepo.kernel.api.RdfLexicon.PREFER_MEMBERSHIP; 044import static org.junit.Assert.assertEquals; 045import static org.junit.Assert.assertFalse; 046import static org.junit.Assert.assertNotEquals; 047import static org.junit.Assert.assertNotNull; 048import static org.junit.Assert.assertTrue; 049 050import java.io.IOException; 051import java.io.StringWriter; 052import java.util.Arrays; 053import java.util.List; 054import java.util.concurrent.TimeUnit; 055import java.util.stream.Collectors; 056 057import javax.ws.rs.core.HttpHeaders; 058import javax.ws.rs.core.Link; 059import javax.ws.rs.core.Response.Status; 060 061import org.fcrepo.config.OcflPropsConfig; 062import org.fcrepo.kernel.api.RdfLexicon; 063import org.fcrepo.storage.ocfl.CommitType; 064import org.fcrepo.storage.ocfl.DefaultOcflObjectSessionFactory; 065 066import org.apache.commons.lang.StringUtils; 067import org.apache.http.client.methods.CloseableHttpResponse; 068import org.apache.http.client.methods.HttpDelete; 069import org.apache.http.client.methods.HttpGet; 070import org.apache.http.client.methods.HttpHead; 071import org.apache.http.client.methods.HttpPatch; 072import org.apache.http.client.methods.HttpPost; 073import org.apache.http.client.methods.HttpPut; 074import org.apache.http.entity.StringEntity; 075import org.apache.http.util.EntityUtils; 076import org.apache.jena.rdf.model.Model; 077import org.apache.jena.rdf.model.ModelFactory; 078import org.apache.jena.rdf.model.Property; 079import org.apache.jena.rdf.model.Resource; 080import org.apache.jena.vocabulary.DC; 081import org.apache.jena.vocabulary.RDF; 082import org.junit.After; 083import org.junit.Before; 084import org.junit.Test; 085import org.springframework.test.context.TestExecutionListeners; 086 087/** 088 * @author bbpennel 089 */ 090@TestExecutionListeners( 091 listeners = { TestIsolationExecutionListener.class }, 092 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) 093public class LDPContainerIT extends AbstractResourceIT { 094 095 private final String PCDM_HAS_MEMBER = "http://pcdm.org/models#hasMember"; 096 097 private final Property PCDM_HAS_MEMBER_PROP = createProperty(PCDM_HAS_MEMBER); 098 099 private final Property EX_IS_MEMBER_PROP = createProperty("http://example.com/isMember"); 100 101 public static final Property PROXY_FOR = createProperty("http://example.com/proxyFor"); 102 103 private static final String DIRECT_CONTAINER_LINK_HEADER = "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\""; 104 105 private static final String INDIRECT_CONTAINER_LINK_HEADER = "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\""; 106 107 private DefaultOcflObjectSessionFactory objectSessionFactory; 108 private OcflPropsConfig ocflPropsConfig; 109 110 @Before 111 public void init() { 112 objectSessionFactory = getBean(DefaultOcflObjectSessionFactory.class); 113 ocflPropsConfig = getBean(OcflPropsConfig.class); 114 } 115 116 @After 117 public void after() { 118 objectSessionFactory.setDefaultCommitType(CommitType.NEW_VERSION); 119 ocflPropsConfig.setAutoVersioningEnabled(true); 120 } 121 122 @Test 123 public void testIndirectContainerDefaults() throws Exception { 124 final String id = getRandomUniqueId(); 125 final String subjectURI = serverAddress + id; 126 final HttpPut put = putObjMethod(id); 127 put.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER); 128 executeAndClose(put); 129 130 final Model model = getModel(id); 131 132 final Resource resc = model.getResource(subjectURI); 133 assertTrue("Must have container type", resc.hasProperty(RDF.type, INDIRECT_CONTAINER)); 134 135 assertTrue("Default ldp:membershipResource must be set", 136 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 137 138 assertTrue("Default ldp:hasMemberRelation must be set", 139 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 140 141 assertTrue("Default ldp:insertedContentRelation must be set", 142 resc.hasProperty(INSERTED_CONTENT_RELATION, MEMBER_SUBJECT)); 143 } 144 145 @Test 146 public void testIndirectContainerOverrides() throws Exception { 147 final String parentId = createBasicContainer(); 148 final String parentURI = serverAddress + parentId; 149 150 final String indirectId = createIndirectContainer(parentURI); 151 final String indirectURI = serverAddress + indirectId; 152 153 final Model model = getModel(indirectId); 154 final Resource resc = model.getResource(indirectURI); 155 assertTrue("Must have container type", resc.hasProperty(RDF.type, INDIRECT_CONTAINER)); 156 157 assertTrue("Provided ldp:membershipResource must be present", 158 resc.hasProperty(MEMBERSHIP_RESOURCE, createResource(parentURI))); 159 assertFalse("Default ldp:membershipResource must not be present", 160 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 161 162 assertTrue("Provided ldp:hasMemberRelation must be set", 163 resc.hasProperty(HAS_MEMBER_RELATION, PCDM_HAS_MEMBER_PROP)); 164 assertFalse("Default ldp:hasMemberRelation must not be present", 165 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 166 167 assertTrue("Provided ldp:insertedContentRelation must be set", 168 resc.hasProperty(INSERTED_CONTENT_RELATION, PROXY_FOR)); 169 assertFalse("Default ldp:insertedContentRelation must not be present", 170 resc.hasProperty(INSERTED_CONTENT_RELATION, MEMBER_SUBJECT)); 171 } 172 173 @Test 174 public void testIndirectContainerDefaultsAfterPUT() throws Exception { 175 final String parentId = createBasicContainer(); 176 final String parentURI = serverAddress + parentId; 177 178 final String indirectId = createIndirectContainer(parentURI); 179 final String indirectURI = serverAddress + indirectId; 180 181 final Model replaceModel = getModel(indirectId); 182 replaceModel.removeAll(null, MEMBERSHIP_RESOURCE, null); 183 replaceModel.removeAll(null, HAS_MEMBER_RELATION, null); 184 replaceModel.removeAll(null, INSERTED_CONTENT_RELATION, null); 185 186 replacePropertiesWithPUT(indirectURI, replaceModel); 187 188 final Model model = getModel(indirectId); 189 final Resource resc = model.getResource(indirectURI); 190 assertTrue("Must have container type", resc.hasProperty(RDF.type, INDIRECT_CONTAINER)); 191 192 assertFalse("Provided ldp:membershipResource must be removed", 193 resc.hasProperty(MEMBERSHIP_RESOURCE, createResource(parentURI))); 194 assertTrue("Default ldp:membershipResource must be set", 195 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 196 197 assertFalse("Provided ldp:hasMemberRelation must be removed", 198 resc.hasProperty(HAS_MEMBER_RELATION, createResource(parentURI))); 199 assertTrue("Default ldp:hasMemberRelation must be set", 200 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 201 202 assertFalse("Provided ldp:insertedContentRelation must be removed", 203 resc.hasProperty(INSERTED_CONTENT_RELATION, PROXY_FOR)); 204 assertTrue("Default ldp:insertedContentRelation must be present", 205 resc.hasProperty(INSERTED_CONTENT_RELATION, MEMBER_SUBJECT)); 206 } 207 208 @Test 209 public void testIndirectContainerDefaultsAfterPatch() throws Exception { 210 final String parentId = createBasicContainer(); 211 final String parentURI = serverAddress + parentId; 212 213 final String indirectId = createIndirectContainer(parentURI); 214 final String indirectURI = serverAddress + indirectId; 215 216 final HttpPatch patch = new HttpPatch(indirectURI); 217 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 218 patch.setEntity(new StringEntity( 219 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 220 "DELETE { <> ldp:membershipResource <" + parentURI + "> ;\n" + 221 "ldp:hasMemberRelation <" + PCDM_HAS_MEMBER + "> ;\n" + 222 "ldp:insertedContentRelation <" + PROXY_FOR.getURI() + "> . } WHERE {}")); 223 try (final CloseableHttpResponse response = execute(patch)) { 224 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 225 } 226 227 final Model model = getModel(indirectId); 228 final Resource resc = model.getResource(indirectURI); 229 assertTrue("Must have container type", resc.hasProperty(RDF.type, INDIRECT_CONTAINER)); 230 231 assertFalse("Provided ldp:membershipResource must be removed", 232 resc.hasProperty(MEMBERSHIP_RESOURCE, createResource(parentURI))); 233 assertTrue("Default ldp:membershipResource must be set", 234 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 235 236 assertFalse("Provided ldp:hasMemberRelation must be removed", 237 resc.hasProperty(HAS_MEMBER_RELATION, createResource(parentURI))); 238 assertTrue("Default ldp:hasMemberRelation must be set", 239 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 240 241 assertFalse("Provided ldp:insertedContentRelation must be removed", 242 resc.hasProperty(INSERTED_CONTENT_RELATION, PROXY_FOR)); 243 assertTrue("Default ldp:insertedContentRelation must be present", 244 resc.hasProperty(INSERTED_CONTENT_RELATION, MEMBER_SUBJECT)); 245 } 246 247 @Test 248 public void testIndirectContainerPatchWithLdpContainsHasMember() throws IOException { 249 final String id = getRandomUniqueId(); 250 final HttpPut put = putObjMethod(id); 251 put.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER); 252 executeAndClose(put); 253 254 final HttpPatch patch = patchObjMethod(id); 255 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 256 patch.setEntity(new StringEntity( 257 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 258 "DELETE { <> ldp:hasMemberRelation ?memRel . }\n" + 259 "INSERT { <> ldp:hasMemberRelation <" + RdfLexicon.CONTAINS + "> }\n" + 260 " WHERE { <> ldp:hasMemberRelation ?memRel}")); 261 assertEquals("Patch with sparql update allowed ldp:contains in indirect container!", 262 CONFLICT.getStatusCode(), getStatus(patch)); 263 } 264 265 @Test 266 public void testIndirectContainerPatchWithLdpContainsIsMember() throws IOException { 267 final String id = getRandomUniqueId(); 268 final HttpPut put = putObjMethod(id); 269 put.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER); 270 executeAndClose(put); 271 272 final HttpPatch patch = patchObjMethod(id); 273 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 274 patch.setEntity(new StringEntity( 275 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 276 "DELETE { <> ldp:hasMemberRelation ?memRel . }\n" + 277 "INSERT { <> ldp:isMemberOfRelation <" + RdfLexicon.CONTAINS + "> }\n" + 278 " WHERE { <> ldp:hasMemberRelation ?memRel}")); 279 assertEquals("Patch with sparql update allowed ldp:contains in indirect container!", 280 CONFLICT.getStatusCode(), getStatus(patch)); 281 } 282 283 @Test 284 public void testIndirectContainerPatchWithoutLdpContains() throws IOException { 285 final String id = getRandomUniqueId(); 286 final HttpPut put = putObjMethod(id); 287 put.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER); 288 executeAndClose(put); 289 290 final HttpPatch patch = patchObjMethod(id); 291 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 292 patch.setEntity(new StringEntity( 293 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 294 "DELETE { <> ldp:hasMemberRelation ?memRel . }\n" + 295 "INSERT { <> ldp:hasMemberRelation <info:some/relation> }\n" + 296 " WHERE { <> ldp:hasMemberRelation ?memRel}")); 297 assertEquals("Patch with sparql update did not allow non SMT relation in indirect container!", 298 NO_CONTENT.getStatusCode(), getStatus(patch)); 299 } 300 301 @Test 302 public void testETagOnDeletedLdpIndirectContainerChild() throws Exception { 303 final String membershipId = createBasicContainer(); 304 final String membershipUri = serverAddress + membershipId; 305 306 // Create the IndirectContainer 307 final String indirectId = createIndirectContainer(membershipUri); 308 309 final String proxyId = indirectId + "/proxy1"; 310 final String proxyUri = serverAddress + proxyId; 311 312 final String etag0 = getEtag(membershipUri); 313 314 // Create member 315 final String memberId = createBasicContainer(); 316 final String memberUri = serverAddress + memberId; 317 318 // Create proxy to the member 319 createProxy(proxyId, memberUri); 320 321 assertHasMembers(membershipId, PCDM_HAS_MEMBER_PROP, memberId); 322 323 final String etag1 = getEtag(membershipUri); 324 assertNotEquals("ETag didn't change!", etag0, etag1); 325 326 // Wait a second so that the creation and deletion of the child are not simultaneous 327 TimeUnit.SECONDS.sleep(1); 328 329 assertEquals("Child resource not deleted!", NO_CONTENT.getStatusCode(), 330 getStatus(new HttpDelete(proxyUri))); 331 332 assertHasNoMembership(membershipId, PCDM_HAS_MEMBER_PROP); 333 334 final String etag2 = getEtag(membershipUri); 335 336 assertNotEquals("ETag didn't change!", etag1, etag2); 337 } 338 339 // Ensure that membership is generated/manager when an IndirectContainer is also a proxy 340 @Test 341 public void indirectContainerWithIndirectContainerChild() throws Exception { 342 final String membershipId = createBasicContainer(); 343 final String membershipUri = serverAddress + membershipId; 344 345 // Create the parent IndirectContainer 346 final String parentIndirectId = createIndirectContainer(membershipUri); 347 348 // Create members 349 final String member1Id = createBasicContainer(); 350 final String member1Uri = serverAddress + member1Id; 351 final String member2Id = createBasicContainer(); 352 final String member2Uri = serverAddress + member2Id; 353 354 // Create the nested child IndirectContainer, 355 final String childIndirectId = parentIndirectId + "/childIndirect"; 356 createIndirectContainer(childIndirectId, membershipUri, PCDM_HAS_MEMBER_PROP, PROXY_FOR.getURI(), false); 357 358 // Add a proxy into the child container 359 final String proxyId = childIndirectId + "/proxy1"; 360 createProxy(proxyId, member1Uri); 361 362 assertHasMembers(membershipId, PCDM_HAS_MEMBER_PROP, member1Id); 363 364 // Make the nested child into a proxy itself by adding the insertedContentRelation 365 final HttpPatch patch = patchObjMethod(childIndirectId); 366 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 367 patch.setEntity(new StringEntity( 368 "PREFIX ldp: <http://www.w3.org/ns/ldp#>" + 369 " INSERT { <> <" + PROXY_FOR + "> <" + member2Uri + "> }" + 370 " WHERE {}")); 371 assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch)); 372 373 assertHasMembers(membershipId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 374 375 // Change the hasMemberRelation of the child container 376 final HttpPatch patch2 = patchObjMethod(childIndirectId); 377 patch2.addHeader(CONTENT_TYPE, "application/sparql-update"); 378 patch2.setEntity(new StringEntity( 379 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 380 "DELETE { <> ldp:hasMemberRelation <" + PCDM_HAS_MEMBER + "> . }" + 381 "INSERT { <> ldp:hasMemberRelation ldp:member } WHERE {}\n")); 382 try (final CloseableHttpResponse response = execute(patch2)) { 383 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 384 } 385 386 assertHasMembers(membershipId, PCDM_HAS_MEMBER_PROP, member2Id); 387 assertHasMembers(membershipId, RdfLexicon.LDP_MEMBER, member1Id); 388 } 389 390 @Test 391 public void testETagLdpIndirectContainerChildIsMemberOf() throws Exception { 392 final String membershipId = createBasicContainer(); 393 final String membershipUri = serverAddress + membershipId; 394 395 // Create the IndirectContainer 396 final String indirectId = createIndirectContainerIsMemberOf(membershipUri); 397 398 final String proxyId = indirectId + "/proxy1"; 399 final String proxyUri = serverAddress + proxyId; 400 401 // Create member 402 final String memberId = createBasicContainer(); 403 final String memberUri = serverAddress + memberId; 404 405 final String etag0 = getEtag(memberUri); 406 407 // Create proxy to the member 408 createProxy(proxyId, memberUri); 409 410 assertIsMemberOf(memberId, EX_IS_MEMBER_PROP, membershipId); 411 412 final String etag1 = getEtag(memberUri); 413 assertNotEquals("ETag didn't change!", etag0, etag1); 414 415 // Wait a second so that the creation and deletion of the child are not simultaneous 416 TimeUnit.SECONDS.sleep(1); 417 418 assertEquals("Child resource not deleted!", NO_CONTENT.getStatusCode(), 419 getStatus(new HttpDelete(proxyUri))); 420 421 assertHasNoMembership(memberId, EX_IS_MEMBER_PROP); 422 423 final String etag2 = getEtag(memberUri); 424 425 assertNotEquals("ETag didn't change!", etag1, etag2); 426 } 427 428 @Test 429 public void testIndirectContainerInsertedContentRelationMemberSubject() throws Exception { 430 final String membershipId = createBasicContainer(); 431 final String membershipUri = serverAddress + membershipId; 432 433 // Create the IndirectContainer 434 final String indirectId = createIndirectContainer(membershipUri, PCDM_HAS_MEMBER_PROP, 435 MEMBER_SUBJECT.getURI(), false); 436 // Create the member resource, not as a proxy 437 final String memberId = createBasicContainer(indirectId, "member1"); 438 439 assertHasMembers(membershipId, PCDM_HAS_MEMBER_PROP, memberId); 440 } 441 442 @Test 443 public void testIndirectContainerChildWithoutInsertedContentRelation() throws Exception { 444 final String membershipId = createBasicContainer(); 445 final String membershipUri = serverAddress + membershipId; 446 447 // Create the IndirectContainer 448 final String indirectId = createIndirectContainer(membershipUri); 449 // Create child resource in indirect container, without the expected insertedContentRelation 450 final String memberId = indirectId + "/member1"; 451 createObjectAndClose(memberId); 452 453 assertHasNoMembership(membershipId, PCDM_HAS_MEMBER_PROP); 454 } 455 456 @Test 457 public void testIndirectContainerChildInsertedContentRelationLiteral() throws Exception { 458 final String membershipId = createBasicContainer(); 459 final String membershipUri = serverAddress + membershipId; 460 461 // Create the IndirectContainer 462 final String indirectId = createIndirectContainer(membershipUri); 463 // Create child with insertedContentRelation specifying a literal as the object 464 final String proxyId = indirectId + "/proxy1"; 465 final HttpPut putProxy = putObjMethod(proxyId); 466 putProxy.addHeader(CONTENT_TYPE, "text/turtle"); 467 final String proxyRDF = "<> <" + PCDM_HAS_MEMBER + "> \"helloworld\" ."; 468 putProxy.setEntity(new StringEntity(proxyRDF)); 469 assertEquals(Status.CREATED.getStatusCode(), getStatus(putProxy)); 470 471 assertHasNoMembership(membershipId, PCDM_HAS_MEMBER_PROP); 472 } 473 474 @Test 475 public void indirectContainerPopulatesHasMember() throws Exception { 476 final var membershipRescId = createBasicContainer(); 477 final var membershipRescURI = serverAddress + membershipRescId; 478 479 final var indirectId = createIndirectContainer(membershipRescURI); 480 481 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 482 483 // Create the members 484 final String member1Id = createBasicContainer(); 485 final String member1Uri = serverAddress + member1Id; 486 final String member2Id = createBasicContainer(); 487 final String member2Uri = serverAddress + member2Id; 488 489 // Create proxies to members 490 final String proxy1Id = indirectId + "/proxy1"; 491 createProxy(proxy1Id, member1Uri); 492 final String proxy2Id = indirectId + "/proxy2"; 493 createProxy(proxy2Id, member2Uri); 494 495 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 496 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 497 498 // Deleting a member does not impact membership 499 executeAndClose(deleteObjMethod(member1Id)); 500 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 501 502 // Deleting a proxy impacts membership 503 executeAndClose(deleteObjMethod(proxy1Id)); 504 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 505 506 // Deleting the indirect container should remove all membership from it 507 executeAndClose(deleteObjMethod(indirectId)); 508 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 509 } 510 511 @Test 512 public void indirectContainerPopulatesIsMemberOf() throws Exception { 513 final var membershipRescId = createBasicContainer(); 514 final var membershipRescURI = serverAddress + membershipRescId; 515 516 final var indirectId = createIndirectContainerIsMemberOf(membershipRescURI); 517 518 // Create the members 519 final String member1Id = createBasicContainer(); 520 final String member1Uri = serverAddress + member1Id; 521 final String member2Id = createBasicContainer(); 522 final String member2Uri = serverAddress + member2Id; 523 524 // Create proxies to members 525 final String proxy1Id = indirectId + "/proxy1"; 526 createProxy(proxy1Id, member1Uri); 527 final String proxy2Id = indirectId + "/proxy2"; 528 createProxy(proxy2Id, member2Uri); 529 530 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 531 assertHasNoMembership(membershipRescId, EX_IS_MEMBER_PROP); 532 533 assertIsMemberOf(member1Id, EX_IS_MEMBER_PROP, membershipRescId); 534 assertHasNoMembershipWhenOmitted(member1Id, EX_IS_MEMBER_PROP); 535 536 assertIsMemberOf(member2Id, EX_IS_MEMBER_PROP, membershipRescId); 537 assertHasNoMembershipWhenOmitted(member2Id, EX_IS_MEMBER_PROP); 538 539 // Deleting a proxy impacts membership 540 executeAndClose(deleteObjMethod(proxy1Id)); 541 assertHasNoMembership(member1Id, EX_IS_MEMBER_PROP); 542 543 assertIsMemberOf(member2Id, EX_IS_MEMBER_PROP, membershipRescId); 544 545 // Deleting the indirect container should remove all membership from it 546 executeAndClose(deleteObjMethod(indirectId)); 547 assertHasNoMembership(member2Id, EX_IS_MEMBER_PROP); 548 } 549 550 // Verify that committing delete transactions works after modifying a proxy 551 // without actually changing the produced membership triples. 552 @Test 553 public void indirectContainerDeleteProxyAfterModifying() throws Exception { 554 final var membershipRescId = createBasicContainer(); 555 final var membershipRescURI = serverAddress + membershipRescId; 556 557 final var indirect1Id = createIndirectContainer(membershipRescURI); 558 559 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 560 561 // Create the members 562 final String member1Id = createBasicContainer(); 563 final String member1Uri = serverAddress + member1Id; 564 565 // Create proxies to members 566 final String proxy1Id = indirect1Id + "/proxy1"; 567 createProxy(proxy1Id, member1Uri); 568 569 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 570 571 TimeUnit.MILLISECONDS.sleep(1050l); 572 573 // Edit the proxy in a way that doesn't change membership 574 setProperty(proxy1Id, DC.title.getURI(), "Proxying"); 575 576 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 577 578 executeAndClose(deleteObjMethod(proxy1Id)); 579 580 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 581 } 582 583 @Test 584 public void indirectContainerDeleteInTx() throws Exception { 585 final var membershipRescId = createBasicContainer(); 586 final var membershipRescURI = serverAddress + membershipRescId; 587 588 final var indirectId = createIndirectContainer(membershipRescURI); 589 590 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 591 592 // Create the members 593 final String member1Id = createBasicContainer(); 594 final String member1Uri = serverAddress + member1Id; 595 596 // Create proxies to members 597 final String proxy1Id = indirectId + "/proxy1"; 598 createProxy(proxy1Id, member1Uri); 599 600 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 601 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 602 603 final var txUri = createTransaction(); 604 605 final HttpDelete deleteMethod = deleteObjMethod(indirectId); 606 deleteMethod.addHeader(ATOMIC_ID_HEADER, txUri); 607 executeAndClose(deleteMethod); 608 609 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 610 assertHasMembers(txUri, membershipRescId, membershipRescId, PCDM_HAS_MEMBER_PROP); 611 612 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 613 614 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 615 } 616 617 @Test 618 public void indirectContainerModifyProxyAutoVersioning() throws Exception { 619 final var membershipRescId = createBasicContainer(); 620 final var membershipRescURI = serverAddress + membershipRescId; 621 622 final var indirect1Id = createIndirectContainer(membershipRescURI); 623 624 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 625 626 // Create the members 627 final String member1Id = createBasicContainer(); 628 final String member1Uri = serverAddress + member1Id; 629 final String member2Id = createBasicContainer(); 630 final String member2Uri = serverAddress + member2Id; 631 632 TimeUnit.MILLISECONDS.sleep(1000); 633 634 // Create proxy to member1 635 final String proxy1Id = indirect1Id + "/proxy1"; 636 final String proxy1Uri = serverAddress + proxy1Id; 637 createProxy(proxy1Id, member1Uri); 638 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 639 640 // Memento preceeds addition of proxy, so no membership 641 final var mementos = listMementoIds(membershipRescId); 642 assertEquals(1, mementos.size()); 643 assertMementoHasMembers(mementos.get(0), PCDM_HAS_MEMBER_PROP); 644 645 changeProxyMember(proxy1Uri, member1Uri, member2Uri, null); 646 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 647 648 // History sohuld be unchanged since the membership resource hasn't been versioned 649 final var mementos2 = listMementoIds(membershipRescId); 650 assertEquals(1, mementos2.size()); 651 assertMementoHasMembers(mementos2.get(0), PCDM_HAS_MEMBER_PROP); 652 653 TimeUnit.MILLISECONDS.sleep(1000); 654 655 // Change proxy inside of a transaction 656 final var txUri = createTransaction(); 657 final String member3Id = createBasicContainer(null, getRandomUniqueId(), txUri); 658 final String member3Uri = serverAddress + member3Id; 659 changeProxyMember(proxy1Uri, member2Uri, member3Uri, txUri); 660 661 // Unchanged outside of tx, changed in tx 662 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 663 assertHasMembers(txUri, membershipRescId, membershipRescId, PCDM_HAS_MEMBER_PROP, member3Id); 664 665 // Commit tx 666 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 667 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member3Id); 668 669 final var mementos3 = listMementoIds(membershipRescId); 670 assertEquals(1, mementos3.size()); 671 assertMementoHasMembers(mementos3.get(0), PCDM_HAS_MEMBER_PROP); 672 } 673 674 @Test 675 public void indirectContainerModifyProxyOnDemandVersioning() throws Exception { 676 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 677 ocflPropsConfig.setAutoVersioningEnabled(false); 678 679 final var membershipRescId = createBasicContainer(); 680 final var membershipRescURI = serverAddress + membershipRescId; 681 682 final var indirect1Id = createIndirectContainer(membershipRescURI); 683 684 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 685 686 // Create the members 687 final String member1Id = createBasicContainer(); 688 final String member1Uri = serverAddress + member1Id; 689 final String member2Id = createBasicContainer(); 690 final String member2Uri = serverAddress + member2Id; 691 692 // Create proxy to member1 693 final String proxy1Id = indirect1Id + "/proxy1"; 694 final String proxy1Uri = serverAddress + proxy1Id; 695 createProxy(proxy1Id, member1Uri); 696 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 697 698 changeProxyMember(proxy1Uri, member1Uri, member2Uri, null); 699 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 700 701 TimeUnit.MILLISECONDS.sleep(1000); 702 703 createMemento(membershipRescURI); 704 final var mementos = listMementoIds(membershipRescId); 705 assertEquals(1, mementos.size()); 706 assertMementoHasMembers(mementos.get(0), PCDM_HAS_MEMBER_PROP, member2Id); 707 708 // Change proxy target again, should rewrite history of membership resc 709 changeProxyMember(proxy1Uri, member2Uri, member1Uri, null); 710 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 711 final var mementos2 = listMementoIds(membershipRescId); 712 assertEquals(1, mementos2.size()); 713 assertMementoHasMembers(mementos2.get(0), PCDM_HAS_MEMBER_PROP, member1Id); 714 715 TimeUnit.MILLISECONDS.sleep(1000); 716 717 // Create memento of the proxy, the membership state of the memento should be immutable now 718 createMemento(proxy1Uri); 719 changeProxyMember(proxy1Uri, member1Uri, member2Uri, null); 720 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 721 final var mementos3 = listMementoIds(membershipRescId); 722 assertEquals(1, mementos3.size()); 723 assertMementoHasMembers(mementos3.get(0), PCDM_HAS_MEMBER_PROP, member1Id); 724 725 final var txUri = createTransaction(); 726 final String member3Id = createBasicContainer(null, getRandomUniqueId(), txUri); 727 final String member3Uri = serverAddress + member3Id; 728 changeProxyMember(proxy1Uri, member2Uri, member3Uri, txUri); 729 730 // Unchanged outside of tx, changed in tx 731 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 732 assertHasMembers(txUri, membershipRescId, membershipRescId, PCDM_HAS_MEMBER_PROP, member3Id); 733 734 // Commit tx 735 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 736 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member3Id); 737 } 738 739 private void changeProxyMember(final String proxyUri, final String oldMember, final String newMember, 740 final String txUri) throws Exception { 741 final HttpPatch patch = new HttpPatch(proxyUri); 742 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 743 if (txUri != null) { 744 patch.addHeader(ATOMIC_ID_HEADER, txUri); 745 } 746 patch.setEntity(new StringEntity( 747 "DELETE { <> <" + PROXY_FOR + "> <" + oldMember + "> . }" + 748 "INSERT { <> <" + PROXY_FOR + "> <" + newMember + "> } WHERE {}\n")); 749 try (final CloseableHttpResponse response = execute(patch)) { 750 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 751 } 752 } 753 754 @Test 755 public void indirectContainerHasMemberMembershipHistory() throws Exception { 756 final var membershipRescId = createBasicContainer(); 757 final var membershipRescURI = serverAddress + membershipRescId; 758 759 final var indirectId = createIndirectContainer(membershipRescURI); 760 761 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 762 763 final String initialEtag = getEtag(membershipRescURI); 764 765 // First version will have no membership 766 TimeUnit.MILLISECONDS.sleep(1500); 767 768 // Create the members 769 final String member1Id = createBasicContainer(); 770 final String member1Uri = serverAddress + member1Id; 771 final String member2Id = createBasicContainer(); 772 final String member2Uri = serverAddress + member2Id; 773 774 // Create proxies to members 775 final String proxy1Id = indirectId + "/proxy1"; 776 createProxy(proxy1Id, member1Uri); 777 final String proxy2Id = indirectId + "/proxy2"; 778 createProxy(proxy2Id, member2Uri); 779 780 final String afterPropEtag1 = getEtag(membershipRescURI); 781 assertNotEquals("Etag must change after additions", initialEtag, afterPropEtag1); 782 783 // Trigger an update of the membership resource to create version with two members 784 setProperty(membershipRescId, DC.subject.getURI(), "Updated"); 785 786 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 787 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 788 789 final String afterPropEtag2 = getEtag(membershipRescURI); 790 assertNotEquals("Etag must change after modification", afterPropEtag1, afterPropEtag2); 791 792 TimeUnit.MILLISECONDS.sleep(1500); 793 794 // Change the membership relation 795 final String indirectURI = serverAddress + indirectId; 796 final HttpPatch patch = new HttpPatch(indirectURI); 797 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 798 patch.setEntity(new StringEntity( 799 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 800 "DELETE { <> ldp:hasMemberRelation <" + PCDM_HAS_MEMBER + "> . }" + 801 "INSERT { <> ldp:hasMemberRelation ldp:member } WHERE {}\n")); 802 try (final CloseableHttpResponse response = execute(patch)) { 803 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 804 } 805 806 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id); 807 assertHasNoMembershipWhenOmitted(membershipRescId, RdfLexicon.LDP_MEMBER); 808 809 final String afterRelChangeEtag = getEtag(membershipRescURI); 810 assertNotEquals("Etag must change after relation changes", afterPropEtag2, afterRelChangeEtag); 811 812 // Update membership resc to create version where the membership rel has changed 813 setProperty(membershipRescId, DC.subject.getURI(), "Updated again"); 814 815 final String afterRelChangeEtag2 = getEtag(membershipRescURI); 816 817 TimeUnit.MILLISECONDS.sleep(1500); 818 819 // Delete a proxy, which will disappear from the head version's membership 820 executeAndClose(deleteObjMethod(proxy1Id)); 821 822 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member2Id); 823 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 824 825 final String afterDeleteEtag = getEtag(membershipRescURI); 826 assertNotEquals("Etag must change after member delete", afterRelChangeEtag2, afterDeleteEtag); 827 828 final var mementos = listMementoIds(membershipRescId); 829 assertEquals(3, mementos.size()); 830 831 // verify membership at each of the mementos of the membership resource 832 assertMementoHasNoMembership(mementos.get(0), PCDM_HAS_MEMBER_PROP); 833 assertEquals(initialEtag, getEtag(serverAddress + mementos.get(0))); 834 835 assertMementoHasMembers(mementos.get(1), PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 836 assertEquals(afterPropEtag2, getEtag(serverAddress + mementos.get(1))); 837 838 assertMementoHasMembers(mementos.get(2), RdfLexicon.LDP_MEMBER, member1Id, member2Id); 839 assertEquals(afterRelChangeEtag2, getEtag(serverAddress + mementos.get(2))); 840 841 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member2Id); 842 } 843 844 @Test 845 public void indirectContainerIsMemberOfMembershipHistory() throws Exception { 846 final var membershipRescId = createBasicContainer(); 847 final var membershipRescURI = serverAddress + membershipRescId; 848 849 final var indirectId = createIndirectContainerIsMemberOf(membershipRescURI); 850 851 // Get IDs the members 852 final String member1Id = createBasicContainer(); 853 final String member1Uri = serverAddress + member1Id; 854 // Second member will be updated to create an extra version of it 855 final String member2Id = createBasicContainer(); 856 final String member2Uri = serverAddress + member2Id; 857 // Create one member after creating proxy for it 858 final String member3Id = getRandomUniqueId(); 859 final String member3Uri = serverAddress + member3Id; 860 861 TimeUnit.SECONDS.sleep(1); 862 863 // Create proxies to members 864 final String proxy1Id = indirectId + "/proxy1"; 865 createProxy(proxy1Id, member1Uri); 866 final String proxy2Id = indirectId + "/proxy2"; 867 createProxy(proxy2Id, member2Uri); 868 final String proxy3Id = indirectId + "/proxy3"; 869 createProxy(proxy3Id, member3Uri); 870 871 TimeUnit.SECONDS.sleep(1); 872 873 // Create member3 after its proxy 874 createBasicContainer(null, member3Id); 875 876 TimeUnit.MILLISECONDS.sleep(1500); 877 878 // Change the membership relation 879 final String directURI = serverAddress + indirectId; 880 final HttpPatch patch = new HttpPatch(directURI); 881 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 882 patch.setEntity(new StringEntity( 883 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 884 "DELETE { <> ldp:isMemberOfRelation <" + EX_IS_MEMBER_PROP + "> . }" + 885 "INSERT { <> ldp:isMemberOfRelation ldp:member } WHERE {}\n")); 886 try (final CloseableHttpResponse response = execute(patch)) { 887 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 888 } 889 890 setProperty(member2Id, DC.subject.getURI(), "Updated"); 891 892 final var member1Mementos = listMementoIds(member1Id); 893 assertEquals(1, member1Mementos.size()); 894 assertHasNoMembership(member1Mementos.get(0), EX_IS_MEMBER_PROP); 895 assertHasNoMembership(member1Id, EX_IS_MEMBER_PROP); 896 assertIsMemberOf(member1Id, RdfLexicon.LDP_MEMBER, membershipRescId); 897 898 final var member2Mementos = listMementoIds(member2Id); 899 assertEquals(2, member2Mementos.size()); 900 await().atMost(2, TimeUnit.SECONDS).until(() -> { 901 assertHasNoMembership(member2Mementos.get(0), EX_IS_MEMBER_PROP); 902 assertMementoIsMemberOf(member2Mementos.get(1), RdfLexicon.LDP_MEMBER, membershipRescId); 903 return true; 904 }); 905 906 assertIsMemberOf(member2Id, RdfLexicon.LDP_MEMBER, membershipRescId); 907 908 final var member3Mementos = listMementoIds(member3Id); 909 assertEquals(1, member3Mementos.size()); 910 assertMementoIsMemberOf(member3Mementos.get(0), EX_IS_MEMBER_PROP, membershipRescId); 911 assertHasNoMembership(member3Id, EX_IS_MEMBER_PROP); 912 assertIsMemberOf(member1Id, RdfLexicon.LDP_MEMBER, membershipRescId); 913 } 914 915 @Test 916 public void indirectContainerHasMemberWithTransaction() throws Exception { 917 final var membershipRescId = createBasicContainer(); 918 final var membershipRescURI = serverAddress + membershipRescId; 919 920 final var indirectId = createIndirectContainer(membershipRescURI); 921 922 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 923 924 // Create the members 925 final var member1Id = createBasicContainer(null, "member1"); 926 final String member1Uri = serverAddress + member1Id; 927 final var member2Id = createBasicContainer(null, "member2"); 928 final String member2Uri = serverAddress + member2Id; 929 final var member3Id = createBasicContainer(null, "member3"); 930 final String member3Uri = serverAddress + member3Id; 931 932 // Create one proxy outside of the TX 933 final String proxy1Id = indirectId + "/proxy1"; 934 createProxy(proxy1Id, member1Uri); 935 936 // Create second proxy in TX 937 final var txUri = createTransaction(); 938 final String proxy2Id = indirectId + "/proxy2"; 939 createProxy(proxy2Id, member2Uri, PROXY_FOR.getURI(), txUri); 940 941 // Outside the tx, only 1 member should be visible 942 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 943 // In the tx, both should be 944 assertHasMembers(txUri, membershipRescId, membershipRescId, PCDM_HAS_MEMBER_PROP, 945 member1Id, member2Id); 946 947 // commit tx 948 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 949 // After committing, both members should be present 950 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 951 952 // Create another tx in order to add a member then roll it back 953 final var txUri2 = createTransaction(); 954 955 final String proxy3Id = indirectId + "/proxy3"; 956 createProxy(proxy3Id, member3Uri, PROXY_FOR.getURI(), txUri2); 957 958 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 959 assertHasMembers(txUri2, membershipRescId, membershipRescId, PCDM_HAS_MEMBER_PROP, 960 member1Id, member2Id, member3Id); 961 962 // rollback 963 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(txUri2))); 964 965 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 966 assertEquals("Rolled back transaction should be gone", 967 GONE.getStatusCode(), getStatus(new HttpGet(txUri2))); 968 } 969 970 @Test 971 public void testEtagForIndirectContainerHasMemberPatch() throws Exception { 972 final var membershipRescId = createBasicContainer(); 973 final var membershipRescURI = serverAddress + membershipRescId; 974 975 final String indirectId = createIndirectContainer(membershipRescURI); 976 977 // Capture baseline etag before any membership is added 978 final String initialEtag = getEtag(membershipRescURI); 979 980 final var member1Id = createBasicContainer(null, "member1"); 981 final String member1Uri = serverAddress + member1Id; 982 final String proxy1Id = indirectId + "/proxy1"; 983 createProxy(proxy1Id, member1Uri); 984 985 final String committedMembershipEtag = getEtag(membershipRescURI); 986 assertNotEquals("Committed etag must not match original etag", initialEtag, committedMembershipEtag); 987 988 // Update the membership resource using the etags 989 addTitleWithEtag(membershipRescURI, "title1", deweakify(initialEtag), Status.PRECONDITION_FAILED); 990 addTitleWithEtag(membershipRescURI, "title2", deweakify(committedMembershipEtag), Status.NO_CONTENT); 991 992 assertNotEquals("Etag must update after modification", committedMembershipEtag, getEtag(membershipRescURI)); 993 } 994 995 @Test 996 public void indirectContainerModifyProxyReferencedMember() throws Exception { 997 final var membershipRescId = createBasicContainer(); 998 final var membershipRescURI = serverAddress + membershipRescId; 999 1000 final var indirectId = createIndirectContainer(membershipRescURI); 1001 1002 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1003 1004 // Create the members 1005 final String member1Id = createBasicContainer(); 1006 final String member1Uri = serverAddress + member1Id; 1007 final String member2Id = createBasicContainer(); 1008 final String member2Uri = serverAddress + member2Id; 1009 1010 // Create proxies to member1 1011 final String proxy1Id = indirectId + "/proxy1"; 1012 createProxy(proxy1Id, member1Uri); 1013 1014 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 1015 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 1016 1017 // Change the proxy to reference member2 instead 1018 final HttpPatch patch = patchObjMethod(proxy1Id); 1019 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 1020 patch.setEntity(new StringEntity( 1021 "DELETE { <> <" + PROXY_FOR + "> ?existing . }" + 1022 " INSERT { <> <" + PROXY_FOR + "> <" + member2Uri + "> }" + 1023 " WHERE { <> <" + PROXY_FOR + "> ?existing}")); 1024 assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch)); 1025 1026 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 1027 } 1028 1029 @Test 1030 public void indirectContainerRemoveProxyReferencedMember() throws Exception { 1031 final var membershipRescId = createBasicContainer(); 1032 final var membershipRescURI = serverAddress + membershipRescId; 1033 1034 final var indirectId = createIndirectContainer(membershipRescURI); 1035 1036 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1037 1038 // Create the members 1039 final String member1Id = createBasicContainer(); 1040 final String member1Uri = serverAddress + member1Id; 1041 1042 // Create proxies to member1 1043 final String proxy1Id = indirectId + "/proxy1"; 1044 createProxy(proxy1Id, member1Uri); 1045 1046 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 1047 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 1048 1049 // Change the proxy to remove the member reference 1050 final HttpPatch patch = patchObjMethod(proxy1Id); 1051 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 1052 patch.setEntity(new StringEntity( 1053 "DELETE { <> <" + PROXY_FOR + "> ?existing . }" + 1054 " WHERE { <> <" + PROXY_FOR + "> ?existing}")); 1055 assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch)); 1056 1057 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1058 } 1059 1060 @Test 1061 public void indirectContainerMultipleProxyForSameMember() throws Exception { 1062 final var membershipRescId = createBasicContainer(); 1063 final var membershipRescURI = serverAddress + membershipRescId; 1064 1065 final var indirectId = createIndirectContainer(membershipRescURI); 1066 1067 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1068 1069 // Create the members 1070 final String member1Id = createBasicContainer(); 1071 final String member1Uri = serverAddress + member1Id; 1072 1073 // Create proxies to member1 1074 final String proxy1Id = indirectId + "/proxy1"; 1075 createProxy(proxy1Id, member1Uri); 1076 final String proxy2Id = indirectId + "/proxy2"; 1077 createProxy(proxy2Id, member1Uri); 1078 1079 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 1080 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 1081 1082 // Delete 1 proxy, membership should remain 1083 executeAndClose(deleteObjMethod(proxy1Id)); 1084 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 1085 1086 // Delete both proxies, membership should be gone 1087 executeAndClose(deleteObjMethod(proxy2Id)); 1088 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1089 } 1090 1091 private String createIndirectContainer(final String membershipURI) throws Exception { 1092 return createIndirectContainer(membershipURI, PCDM_HAS_MEMBER_PROP, PROXY_FOR.getURI(), false); 1093 } 1094 1095 private String createIndirectContainerIsMemberOf(final String membershipURI) throws Exception { 1096 return createIndirectContainer(membershipURI, EX_IS_MEMBER_PROP, PROXY_FOR.getURI(), true); 1097 } 1098 1099 private String createIndirectContainer(final String membershipURI, final Property hasMemberRelation, 1100 final String insertedContentRelation, final boolean isMemberOf) throws Exception { 1101 final String indirectId = getRandomUniqueId(); 1102 return createIndirectContainer(indirectId, membershipURI, hasMemberRelation, 1103 insertedContentRelation, isMemberOf); 1104 } 1105 1106 private String createIndirectContainer(final String indirectId, final String membershipURI, 1107 final Property hasMemberRelation, final String insertedContentRelation, final boolean isMemberOf) 1108 throws Exception { 1109 final HttpPut putIndirect = putObjMethod(indirectId); 1110 putIndirect.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER); 1111 putIndirect.addHeader(CONTENT_TYPE, "text/turtle"); 1112 final String memberRelation = isMemberOf ? "ldp:isMemberOfRelation" : "ldp:hasMemberRelation"; 1113 final String membersRDF = "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1114 "<> " + memberRelation + " <" + hasMemberRelation + "> ; " + 1115 "ldp:insertedContentRelation <" + insertedContentRelation + ">; " + 1116 "ldp:membershipResource <" + membershipURI + "> . "; 1117 putIndirect.setEntity(new StringEntity(membersRDF)); 1118 assertEquals(Status.CREATED.getStatusCode(), getStatus(putIndirect)); 1119 1120 return indirectId; 1121 } 1122 1123 private void createProxy(final String proxyId, final String memberURI) throws Exception { 1124 createProxy(proxyId, memberURI, PROXY_FOR.getURI(), null); 1125 } 1126 1127 private void createProxy(final String proxyId, final String memberURI, final String insertedContentRelation, 1128 final String txUri) throws Exception { 1129 final HttpPut putProxy = putObjMethod(proxyId); 1130 putProxy.addHeader(CONTENT_TYPE, "text/turtle"); 1131 final String proxyRDF = "<> <" + insertedContentRelation + "> <" + memberURI + "> ."; 1132 putProxy.setEntity(new StringEntity(proxyRDF)); 1133 if (txUri != null) { 1134 putProxy.addHeader(ATOMIC_ID_HEADER, txUri); 1135 } 1136 assertEquals(Status.CREATED.getStatusCode(), getStatus(putProxy)); 1137 } 1138 1139 @Test 1140 public void testDirectContainerDefaults() throws Exception { 1141 final String id = getRandomUniqueId(); 1142 final String subjectURI = serverAddress + id; 1143 final HttpPut put = putObjMethod(id); 1144 put.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER); 1145 executeAndClose(put); 1146 1147 final Model model = getModel(id); 1148 1149 final Resource resc = model.getResource(subjectURI); 1150 assertTrue("Must have container type", resc.hasProperty(RDF.type, DIRECT_CONTAINER)); 1151 1152 assertTrue("Default ldp:membershipResource must be set", 1153 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 1154 assertTrue("Default ldp:hasMemberRelation must be set", 1155 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 1156 } 1157 1158 @Test 1159 public void testDirectContainerOverrides() throws Exception { 1160 final String parentId = createBasicContainer(); 1161 final String parentURI = serverAddress + parentId; 1162 1163 final var directId = createDirectContainer(parentURI); 1164 final String directURI = serverAddress + directId; 1165 1166 final Model model = getModel(directId); 1167 final Resource resc = model.getResource(directURI); 1168 assertTrue("Must have container type", resc.hasProperty(RDF.type, DIRECT_CONTAINER)); 1169 1170 assertTrue("Provided ldp:membershipResource must be present", 1171 resc.hasProperty(MEMBERSHIP_RESOURCE, createResource(parentURI))); 1172 assertFalse("Default ldp:membershipResource must not be present", 1173 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 1174 1175 assertTrue("Provided ldp:hasMemberRelation must be set", 1176 resc.hasProperty(HAS_MEMBER_RELATION, PCDM_HAS_MEMBER_PROP)); 1177 assertFalse("Default ldp:hasMemberRelation must not be present", 1178 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 1179 } 1180 1181 @Test 1182 public void testDirectContainerDefaultsAfterPUT() throws Exception { 1183 final String parentId = createBasicContainer(); 1184 final String parentURI = serverAddress + parentId; 1185 1186 final var directId = createDirectContainer(parentURI); 1187 final String directURI = serverAddress + directId; 1188 1189 final Model replaceModel = getModel(directId); 1190 replaceModel.removeAll(null, MEMBERSHIP_RESOURCE, null); 1191 replaceModel.removeAll(null, HAS_MEMBER_RELATION, null); 1192 1193 replacePropertiesWithPUT(directURI, replaceModel); 1194 1195 final Model model = getModel(directId); 1196 final Resource resc = model.getResource(directURI); 1197 assertTrue("Must have container type", resc.hasProperty(RDF.type, DIRECT_CONTAINER)); 1198 1199 assertFalse("Provided ldp:membershipResource must be removed", 1200 resc.hasProperty(MEMBERSHIP_RESOURCE, createResource(parentURI))); 1201 assertTrue("Default ldp:membershipResource must be set", 1202 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 1203 1204 assertFalse("Provided ldp:hasMemberRelation must be removed", 1205 resc.hasProperty(HAS_MEMBER_RELATION, createResource(parentURI))); 1206 assertTrue("Default ldp:hasMemberRelation must be set", 1207 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 1208 } 1209 1210 @Test 1211 public void testDirectContainerDefaultsAfterPatch() throws Exception { 1212 final String parentId = createBasicContainer(); 1213 final String parentURI = serverAddress + parentId; 1214 1215 final var directId = createDirectContainer(parentURI); 1216 final String directURI = serverAddress + directId; 1217 1218 final HttpPatch patch = new HttpPatch(directURI); 1219 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1220 patch.setEntity(new StringEntity( 1221 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1222 "DELETE { <> ldp:membershipResource <" + parentURI + "> ;\n" + 1223 "ldp:hasMemberRelation <" + PCDM_HAS_MEMBER + "> . } WHERE {}")); 1224 try (final CloseableHttpResponse response = execute(patch)) { 1225 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 1226 } 1227 1228 final Model model = getModel(directId); 1229 final Resource resc = model.getResource(directURI); 1230 assertTrue("Must have container type", resc.hasProperty(RDF.type, DIRECT_CONTAINER)); 1231 1232 assertFalse("Provided ldp:membershipResource must be removed", 1233 resc.hasProperty(MEMBERSHIP_RESOURCE, createResource(parentURI))); 1234 assertTrue("Default ldp:membershipResource must be set", 1235 resc.hasProperty(MEMBERSHIP_RESOURCE, resc)); 1236 1237 assertFalse("Provided ldp:hasMemberRelation must be removed", 1238 resc.hasProperty(HAS_MEMBER_RELATION, createResource(parentURI))); 1239 assertTrue("Default ldp:hasMemberRelation must be set", 1240 resc.hasProperty(HAS_MEMBER_RELATION, LDP_MEMBER)); 1241 } 1242 1243 @Test 1244 public void testDirectContainerPatchWithLdpContainsHasMember() throws IOException { 1245 final String id = getRandomUniqueId(); 1246 final HttpPut put = putObjMethod(id); 1247 put.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER); 1248 executeAndClose(put); 1249 1250 final HttpPatch patch = patchObjMethod(id); 1251 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 1252 patch.setEntity(new StringEntity( 1253 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1254 "DELETE { <> ldp:hasMemberRelation ?memRel . }\n" + 1255 "INSERT { <> ldp:hasMemberRelation <" + RdfLexicon.CONTAINS + "> }\n" + 1256 " WHERE { <> ldp:hasMemberRelation ?memRel}")); 1257 assertEquals("Patch with sparql update allowed ldp:contains in direct container!", 1258 CONFLICT.getStatusCode(), getStatus(patch)); 1259 } 1260 1261 @Test 1262 public void testDirectContainerPatchWithLdpContainsIsMember() throws IOException { 1263 final String id = getRandomUniqueId(); 1264 final HttpPut put = putObjMethod(id); 1265 put.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER); 1266 executeAndClose(put); 1267 1268 final HttpPatch patch = patchObjMethod(id); 1269 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 1270 patch.setEntity(new StringEntity( 1271 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1272 "DELETE { <> ldp:hasMemberRelation ?memRel . }\n" + 1273 "INSERT { <> ldp:isMemberOfRelation <" + RdfLexicon.CONTAINS + "> }\n" + 1274 " WHERE { <> ldp:hasMemberRelation ?memRel}")); 1275 assertEquals("Patch with sparql update allowed ldp:contains in direct container!", 1276 CONFLICT.getStatusCode(), getStatus(patch)); 1277 } 1278 1279 @Test 1280 public void testDirectContainerPatchWithoutLdpContains() throws IOException { 1281 final String id = getRandomUniqueId(); 1282 final HttpPut put = putObjMethod(id); 1283 put.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER); 1284 executeAndClose(put); 1285 1286 final HttpPatch patch = patchObjMethod(id); 1287 patch.setHeader(CONTENT_TYPE, "application/sparql-update"); 1288 patch.setEntity(new StringEntity( 1289 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1290 "DELETE { <> ldp:hasMemberRelation ?memRel . }\n" + 1291 "INSERT { <> ldp:hasMemberRelation <info:some/relation> }\n" + 1292 " WHERE { <> ldp:hasMemberRelation ?memRel}")); 1293 assertEquals("Patch with sparql update did not allow non SMT relation in direct container!", 1294 NO_CONTENT.getStatusCode(), getStatus(patch)); 1295 } 1296 1297 @Test 1298 public void testDirectContainerInvalidMembershipRelationProperties() throws Exception { 1299 final String membershipResc1Id = createBasicContainer(); 1300 final String membershipResc1URI = serverAddress + membershipResc1Id; 1301 1302 final var directId = createDirectContainer(membershipResc1URI); 1303 final String directURI = serverAddress + directId; 1304 1305 // Change the membership relation via PATCH 1306 final HttpPatch patch = new HttpPatch(directURI); 1307 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1308 patch.setEntity(new StringEntity( 1309 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1310 "INSERT { <> ldp:hasMemberRelation ldp:member } WHERE {}\n")); 1311 try (final CloseableHttpResponse response = execute(patch)) { 1312 assertEquals(BAD_REQUEST.getStatusCode(), getStatus(response)); 1313 } 1314 1315 // Try setting two via PUT 1316 final Model replaceModel = ModelFactory.createDefaultModel(); 1317 replaceModel.add(createResource(directURI), RdfLexicon.HAS_MEMBER_RELATION, RdfLexicon.LDP_MEMBER); 1318 replaceModel.add(createResource(directURI), RdfLexicon.HAS_MEMBER_RELATION, PCDM_HAS_MEMBER_PROP); 1319 replacePropertiesWithPUT(directURI, replaceModel, BAD_REQUEST); 1320 1321 // Try setting both ldp:hasMemberOf and ldp:isMemberOf 1322 final Model replaceModel2 = ModelFactory.createDefaultModel(); 1323 replaceModel2.add(createResource(directURI), RdfLexicon.HAS_MEMBER_RELATION, RdfLexicon.LDP_MEMBER); 1324 replaceModel2.add(createResource(directURI), RdfLexicon.IS_MEMBER_OF_RELATION, PCDM_HAS_MEMBER_PROP); 1325 replacePropertiesWithPUT(directURI, replaceModel2, BAD_REQUEST); 1326 1327 // Set an invalid object value 1328 final Model replaceModel3 = ModelFactory.createDefaultModel(); 1329 replaceModel3.add(createResource(directURI), RdfLexicon.HAS_MEMBER_RELATION, "what"); 1330 replacePropertiesWithPUT(directURI, replaceModel3, BAD_REQUEST); 1331 } 1332 1333 @Test 1334 public void testDirectContainerInvalidMembershipResourceProperties() throws Exception { 1335 final String membershipResc1Id = createBasicContainer(); 1336 final String membershipResc1URI = serverAddress + membershipResc1Id; 1337 1338 final String membershipResc2Id = createBasicContainer(); 1339 final String membershipResc2URI = serverAddress + membershipResc2Id; 1340 1341 final var directId = createDirectContainer(membershipResc1URI); 1342 final String directURI = serverAddress + directId; 1343 1344 // Change the membership relation 1345 final HttpPatch patch = new HttpPatch(directURI); 1346 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1347 patch.setEntity(new StringEntity( 1348 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1349 "INSERT { <> ldp:membershipResource <" + membershipResc2URI + "> } WHERE {}\n")); 1350 try (final CloseableHttpResponse response = execute(patch)) { 1351 assertEquals(BAD_REQUEST.getStatusCode(), getStatus(response)); 1352 } 1353 1354 // Try setting two via PUT 1355 final Model replaceModel = ModelFactory.createDefaultModel(); 1356 replaceModel.add(createResource(directURI), RdfLexicon.MEMBERSHIP_RESOURCE, 1357 createResource(membershipResc1URI)); 1358 replaceModel.add(createResource(directURI), RdfLexicon.MEMBERSHIP_RESOURCE, 1359 createResource(directURI)); 1360 replacePropertiesWithPUT(directURI, replaceModel, BAD_REQUEST); 1361 1362 // Set object to non-resource 1363 final Model replaceModel2 = ModelFactory.createDefaultModel(); 1364 replaceModel2.add(createResource(directURI), RdfLexicon.MEMBERSHIP_RESOURCE, "ohno"); 1365 replacePropertiesWithPUT(directURI, replaceModel2, BAD_REQUEST); 1366 } 1367 1368 @Test 1369 public void directContainerPopulatesHasMember() throws Exception { 1370 final var membershipRescId = createBasicContainer(); 1371 final var membershipRescURI = serverAddress + membershipRescId; 1372 1373 final var directId = createDirectContainer(membershipRescURI); 1374 1375 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1376 1377 // Add some members 1378 final var member1Id = createBasicContainer(directId, "member1"); 1379 final var member2Id = createBasicContainer(directId, "member2"); 1380 1381 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1382 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 1383 1384 executeAndClose(deleteObjMethod(member1Id)); 1385 1386 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member2Id); 1387 1388 executeAndClose(deleteObjMethod(directId)); 1389 1390 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1391 } 1392 1393 @Test 1394 public void directContainerPopulatesIsMemberOf() throws Exception { 1395 final var membershipRescId = createBasicContainer(); 1396 final var membershipRescURI = serverAddress + membershipRescId; 1397 1398 final var directId = createDirectContainer(membershipRescURI, EX_IS_MEMBER_PROP, true); 1399 1400 assertHasNoMembership(membershipRescId, EX_IS_MEMBER_PROP); 1401 assertHasNoMembership(directId, EX_IS_MEMBER_PROP); 1402 1403 // Add some members 1404 final var member1Id = createBasicContainer(directId, "member1"); 1405 final var member2Id = createBasicContainer(directId, "member2"); 1406 1407 assertHasNoMembership(membershipRescId, EX_IS_MEMBER_PROP); 1408 assertIsMemberOf(member1Id, EX_IS_MEMBER_PROP, membershipRescId); 1409 assertIsMemberOf(member2Id, EX_IS_MEMBER_PROP, membershipRescId); 1410 assertHasNoMembershipWhenOmitted(member1Id, EX_IS_MEMBER_PROP); 1411 assertHasNoMembershipWhenOmitted(member2Id, EX_IS_MEMBER_PROP); 1412 } 1413 1414 @Test 1415 public void directContainerHasMemberMembershipHistory() throws Exception { 1416 final var membershipRescId = createBasicContainer(); 1417 final var membershipRescURI = serverAddress + membershipRescId; 1418 1419 final var directId = createDirectContainer(membershipRescURI); 1420 1421 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1422 1423 final String initialEtag = getEtag(membershipRescURI); 1424 1425 // First version will have no membership 1426 TimeUnit.MILLISECONDS.sleep(1500); 1427 1428 final var member1Id = createBasicContainer(directId, "member1"); 1429 final var member2Id = createBasicContainer(directId, "member2"); 1430 1431 final String afterPropEtag1 = getEtag(membershipRescURI); 1432 assertNotEquals("Etag must change after additions", initialEtag, afterPropEtag1); 1433 1434 // Trigger an update of the membership resource to create version with two members 1435 setProperty(membershipRescId, DC.subject.getURI(), "Updated"); 1436 1437 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1438 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 1439 1440 final String afterPropEtag2 = getEtag(membershipRescURI); 1441 assertNotEquals("Etag must change after modification", afterPropEtag1, afterPropEtag2); 1442 1443 TimeUnit.MILLISECONDS.sleep(1500); 1444 1445 // Change the membership relation 1446 final String directURI = serverAddress + directId; 1447 final HttpPatch patch = new HttpPatch(directURI); 1448 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1449 patch.setEntity(new StringEntity( 1450 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1451 "DELETE { <> ldp:hasMemberRelation <" + PCDM_HAS_MEMBER + "> . }" + 1452 "INSERT { <> ldp:hasMemberRelation ldp:member } WHERE {}\n")); 1453 try (final CloseableHttpResponse response = execute(patch)) { 1454 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 1455 } 1456 1457 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id); 1458 assertHasNoMembershipWhenOmitted(membershipRescId, RdfLexicon.LDP_MEMBER); 1459 1460 final String afterRelChangeEtag = getEtag(membershipRescURI); 1461 assertNotEquals("Etag must change after relation changes", afterPropEtag2, afterRelChangeEtag); 1462 1463 // Update membership resc to create version where the membership rel has changed 1464 setProperty(membershipRescId, DC.subject.getURI(), "Updated again"); 1465 1466 final String afterRelChangeEtag2 = getEtag(membershipRescURI); 1467 1468 TimeUnit.MILLISECONDS.sleep(1500); 1469 1470 // Delete a member, which will disappear from the head version's membership 1471 executeAndClose(deleteObjMethod(member1Id)); 1472 1473 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member2Id); 1474 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 1475 1476 final String afterDeleteEtag = getEtag(membershipRescURI); 1477 assertNotEquals("Etag must change after member delete", afterRelChangeEtag2, afterDeleteEtag); 1478 1479 final var mementos = listMementoIds(membershipRescId); 1480 assertEquals(3, mementos.size()); 1481 1482 // verify membership at each of the mementos of the membership resource 1483 assertMementoHasNoMembership(mementos.get(0), PCDM_HAS_MEMBER_PROP); 1484 assertEquals(initialEtag, getEtag(serverAddress + mementos.get(0))); 1485 1486 assertMementoHasMembers(mementos.get(1), PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1487 assertEquals(afterPropEtag2, getEtag(serverAddress + mementos.get(1))); 1488 1489 assertMementoHasMembers(mementos.get(2), RdfLexicon.LDP_MEMBER, member1Id, member2Id); 1490 assertEquals(afterRelChangeEtag2, getEtag(serverAddress + mementos.get(2))); 1491 1492 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member2Id); 1493 } 1494 1495 @Test 1496 public void directContaineIsMemberOfMembershipHistory() throws Exception { 1497 final var membershipRescId = createBasicContainer(); 1498 final var membershipRescURI = serverAddress + membershipRescId; 1499 1500 final var directId = createDirectContainer(membershipRescURI, EX_IS_MEMBER_PROP, true); 1501 1502 // Add some members 1503 final var member1Id = createBasicContainer(directId, "member1"); 1504 final var member2Id = createBasicContainer(directId, "member2"); 1505 1506 TimeUnit.MILLISECONDS.sleep(1500); 1507 1508 // Change the membership relation 1509 final String directURI = serverAddress + directId; 1510 final HttpPatch patch = new HttpPatch(directURI); 1511 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1512 patch.setEntity(new StringEntity( 1513 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1514 "DELETE { <> ldp:isMemberOfRelation <" + EX_IS_MEMBER_PROP + "> . }" + 1515 "INSERT { <> ldp:isMemberOfRelation ldp:member } WHERE {}\n")); 1516 try (final CloseableHttpResponse response = execute(patch)) { 1517 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 1518 } 1519 1520 setProperty(member2Id, DC.subject.getURI(), "Updated"); 1521 1522 final var member1Mementos = listMementoIds(member1Id); 1523 assertEquals(1, member1Mementos.size()); 1524 assertMementoIsMemberOf(member1Mementos.get(0), EX_IS_MEMBER_PROP, membershipRescId); 1525 assertIsMemberOf(member1Id, RdfLexicon.LDP_MEMBER, membershipRescId); 1526 assertHasNoMembership(member1Id, EX_IS_MEMBER_PROP); 1527 1528 final var member2Mementos = listMementoIds(member2Id); 1529 assertEquals(2, member2Mementos.size()); 1530 assertMementoIsMemberOf(member2Mementos.get(0), EX_IS_MEMBER_PROP, membershipRescId); 1531 assertMementoIsMemberOf(member2Mementos.get(1), RdfLexicon.LDP_MEMBER, membershipRescId); 1532 assertIsMemberOf(member2Id, RdfLexicon.LDP_MEMBER, membershipRescId); 1533 assertHasNoMembership(member2Id, EX_IS_MEMBER_PROP); 1534 } 1535 1536 @Test 1537 public void directContainerHasMemberWithTransaction() throws Exception { 1538 final var membershipRescId = createBasicContainer(); 1539 final var membershipRescURI = serverAddress + membershipRescId; 1540 1541 final var directId = createDirectContainer(membershipRescURI); 1542 1543 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1544 1545 // Add some members 1546 final var member1Id = createBasicContainer(directId, "member1"); 1547 1548 final var txUri = createTransaction(); 1549 1550 final var member2Id = createBasicContainer(directId, "member2", txUri); 1551 1552 // Outside the tx, only 1 member should be visible 1553 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id); 1554 // In the tx, both should be 1555 assertHasMembers(txUri, membershipRescId, membershipRescId, PCDM_HAS_MEMBER_PROP, 1556 member1Id, member2Id); 1557 1558 // commit tx 1559 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 1560 // After committing, both members should be present 1561 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1562 1563 // Create another tx in order to add a member then roll it back 1564 final var txUri2 = createTransaction(); 1565 1566 final var member3Id = createBasicContainer(directId, "member3", txUri2); 1567 1568 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1569 assertHasMembers(txUri2, membershipRescId, membershipRescId, PCDM_HAS_MEMBER_PROP, 1570 member1Id, member2Id, member3Id); 1571 1572 // rollback 1573 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(txUri2))); 1574 1575 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1576 assertEquals("Rolled back transaction should be gone", 1577 GONE.getStatusCode(), getStatus(new HttpGet(txUri2))); 1578 } 1579 1580 @Test 1581 public void directContainerChangeDCOnDemandVersioning() throws Exception { 1582 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 1583 ocflPropsConfig.setAutoVersioningEnabled(false); 1584 1585 final var membershipRescId = createBasicContainer(); 1586 final var membershipRescURI = serverAddress + membershipRescId; 1587 1588 final var directId = createDirectContainer(membershipRescURI); 1589 1590 assertHasNoMembership(membershipRescId, PCDM_HAS_MEMBER_PROP); 1591 1592 final String initialEtag = getEtag(membershipRescURI); 1593 1594 final var member1Id = createBasicContainer(directId, "member1"); 1595 final var member2Id = createBasicContainer(directId, "member2"); 1596 1597 final String afterPropEtag1 = getEtag(membershipRescURI); 1598 assertNotEquals("Etag must change after additions", initialEtag, afterPropEtag1); 1599 1600 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1601 assertHasNoMembershipWhenOmitted(membershipRescId, PCDM_HAS_MEMBER_PROP); 1602 1603 // Change the membership relation 1604 final String directURI = serverAddress + directId; 1605 changeMembershipRelation(directURI, PCDM_HAS_MEMBER_PROP, RdfLexicon.LDP_MEMBER, null); 1606 1607 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id); 1608 assertHasNoMembershipWhenOmitted(membershipRescId, RdfLexicon.LDP_MEMBER); 1609 1610 // Version the membership resc 1611 createMemento(membershipRescURI); 1612 1613 TimeUnit.MILLISECONDS.sleep(1000); 1614 1615 // Verify membership in the created memento 1616 final var mementos = listMementoIds(membershipRescId); 1617 assertEquals(1, mementos.size()); 1618 assertMementoHasMembers(mementos.get(0), RdfLexicon.LDP_MEMBER, member1Id, member2Id); 1619 1620 // Change membership relation, should rewrite history of membership resc 1621 changeMembershipRelation(directURI, RdfLexicon.LDP_MEMBER, PCDM_HAS_MEMBER_PROP, null); 1622 1623 assertHasMembers(membershipRescId, PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1624 final var mementos2 = listMementoIds(membershipRescId); 1625 assertEquals(1, mementos2.size()); 1626 assertMementoHasMembers(mementos2.get(0), PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1627 1628 // Version the DC and change property again, should preserve membership in memento 1629 createMemento(directURI); 1630 changeMembershipRelation(directURI, PCDM_HAS_MEMBER_PROP, RdfLexicon.LDP_MEMBER, null); 1631 1632 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id); 1633 final var mementos3 = listMementoIds(membershipRescId); 1634 assertEquals(1, mementos3.size()); 1635 assertMementoHasMembers(mementos3.get(0), PCDM_HAS_MEMBER_PROP, member1Id, member2Id); 1636 1637 // Modify within a transaction 1638 final var txUri = createTransaction(); 1639 final Property otherMembership = createProperty("http://example.com/member"); 1640 changeMembershipRelation(directURI, RdfLexicon.LDP_MEMBER, otherMembership, txUri); 1641 1642 // Unchanged outside of tx, changed in tx 1643 assertHasMembers(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id); 1644 assertHasMembers(txUri, membershipRescId, membershipRescId, otherMembership, member1Id, member2Id); 1645 1646 // Commit tx 1647 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 1648 assertHasMembers(membershipRescId, otherMembership, member1Id, member2Id); 1649 } 1650 1651 private void changeMembershipRelation(final String containerUri, final Property oldRel, 1652 final Property newRel, final String txUri) throws Exception { 1653 final HttpPatch patch = new HttpPatch(containerUri); 1654 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1655 if (txUri != null) { 1656 patch.addHeader(ATOMIC_ID_HEADER, txUri); 1657 } 1658 patch.setEntity(new StringEntity( 1659 "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 1660 "DELETE { <> ldp:hasMemberRelation <" + oldRel + "> . }" + 1661 "INSERT { <> ldp:hasMemberRelation <" + newRel + "> } WHERE {}\n")); 1662 try (final CloseableHttpResponse response = execute(patch)) { 1663 assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); 1664 } 1665 } 1666 1667 private String createMemento(final String subjectUri) throws Exception { 1668 final HttpPost createVersionMethod = new HttpPost(subjectUri + "/" + FCR_VERSIONS); 1669 1670 // Create new memento of resource with updated body 1671 try (final CloseableHttpResponse response = execute(createVersionMethod)) { 1672 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1673 return response.getFirstHeader(LOCATION).getValue(); 1674 } 1675 } 1676 1677 @Test 1678 public void testEtagForDirectContainerHasMember() throws Exception { 1679 final var membershipRescId = createBasicContainer(); 1680 final var membershipRescURI = serverAddress + membershipRescId; 1681 1682 final var directId = createDirectContainer(membershipRescURI); 1683 1684 // Capture baseline etag before any membership is added 1685 final String initialEtag = getEtag(membershipRescURI); 1686 final String initialGetEtag = getEtag(new HttpGet(membershipRescURI)); 1687 assertEquals("HEAD and basic GET should produce same etag", initialEtag, initialGetEtag); 1688 1689 // Add member in transaction 1690 final var txUri = createTransaction(); 1691 1692 createBasicContainer(directId, "member1", txUri); 1693 1694 final String txMembershipEtag = getEtag(addTxTo(new HttpHead(membershipRescURI), txUri)); 1695 1696 assertEquals("Etag outside of tx must be unchanged", initialEtag, getEtag(membershipRescURI)); 1697 assertNotEquals("Etag within the tx must have changed", initialEtag, txMembershipEtag); 1698 1699 // Commit tx 1700 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 1701 1702 final String committedMembershipEtag = getEtag(membershipRescURI); 1703 assertEquals("Committed etag must match pre-commit etag", txMembershipEtag, committedMembershipEtag); 1704 assertNotEquals("Committed etag must not match original etag", initialEtag, committedMembershipEtag); 1705 1706 assertEquals("Committed GET and HEAD etags must match", 1707 committedMembershipEtag, getEtag(new HttpGet(membershipRescURI))); 1708 1709 // Verify that etag of membership resource is the same when excluding membership as before there was any 1710 final String excludeMembershipEtag = getEtag(getOmitMembership(membershipRescURI)); 1711 assertEquals("Etag without membership should match initial etag", initialEtag, excludeMembershipEtag); 1712 } 1713 1714 @Test 1715 public void testEtagForDirectContainerHasMemberPatch() throws Exception { 1716 final var membershipRescId = createBasicContainer(); 1717 final var membershipRescURI = serverAddress + membershipRescId; 1718 1719 final var directId = createDirectContainer(membershipRescURI); 1720 1721 // Capture baseline etag before any membership is added 1722 final String initialEtag = getEtag(membershipRescURI); 1723 1724 createBasicContainer(directId, "member1"); 1725 1726 final String committedMembershipEtag = getEtag(membershipRescURI); 1727 assertNotEquals("Committed etag must not match original etag", initialEtag, committedMembershipEtag); 1728 1729 // Update the membership resource using the etags 1730 addTitleWithEtag(membershipRescURI, "title1", deweakify(initialEtag), Status.PRECONDITION_FAILED); 1731 addTitleWithEtag(membershipRescURI, "title2", deweakify(committedMembershipEtag), Status.NO_CONTENT); 1732 1733 assertNotEquals("Etag must update after modification", committedMembershipEtag, getEtag(membershipRescURI)); 1734 } 1735 1736 @Test 1737 public void testEtagForDirectContainerHasMemberPut() throws Exception { 1738 final var membershipRescId = createBasicContainer(); 1739 final var membershipRescURI = serverAddress + membershipRescId; 1740 1741 final var directId = createDirectContainer(membershipRescURI); 1742 1743 // Capture baseline etag before any membership is added 1744 final String initialEtag = getEtag(membershipRescURI); 1745 1746 createBasicContainer(directId, "member1"); 1747 1748 final String committedMembershipEtag = getEtag(membershipRescURI); 1749 assertNotEquals("Committed etag must not match original etag", initialEtag, committedMembershipEtag); 1750 1751 // Update the membership resource using the etags 1752 putPropertiesWithEtag(membershipRescURI, deweakify(initialEtag), Status.PRECONDITION_FAILED); 1753 putPropertiesWithEtag(membershipRescURI, deweakify(committedMembershipEtag), Status.NO_CONTENT); 1754 1755 assertNotEquals("Etag must update after modification", committedMembershipEtag, getEtag(membershipRescURI)); 1756 } 1757 1758 @Test 1759 public void testEtagForDirectContainerIsMemberOf() throws Exception { 1760 final var membershipRescId = createBasicContainer(); 1761 final var membershipRescURI = serverAddress + membershipRescId; 1762 1763 final var directId = createDirectContainer(membershipRescURI, EX_IS_MEMBER_PROP, true); 1764 1765 // Capture starting etag for membership resource 1766 final String membershipEtag = getEtag(membershipRescURI); 1767 assertNotNull(membershipEtag); 1768 1769 // Add member in transaction 1770 final var txUri = createTransaction(); 1771 1772 final var memberId = createBasicContainer(directId, "member1", txUri); 1773 final var memberUri = serverAddress + memberId; 1774 1775 final String txMemberEtag = getEtag(addTxTo(new HttpHead(memberUri), txUri)); 1776 assertNotNull("Member etag must not be null in tx", txMemberEtag); 1777 1778 // Commit tx 1779 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txUri))); 1780 1781 final String committedMemberEtag = getEtag(memberUri); 1782 assertEquals("Committed etag must match pre-commit etag", txMemberEtag, committedMemberEtag); 1783 1784 assertEquals("Membership resc etag must not change", membershipEtag, getEtag(membershipRescURI)); 1785 1786 // Verify etag varies appropriately when excluding membership 1787 final String excludeMemberEtag = getEtag(getOmitMembership(memberUri)); 1788 assertNotEquals("Etag for member must change when excluding membership", 1789 committedMemberEtag, excludeMemberEtag); 1790 1791 final String excludeMembershipEtag = getEtag(getOmitMembership(membershipRescURI)); 1792 assertEquals("Etag for membership resc should not change when excluding membership", 1793 membershipEtag, excludeMembershipEtag); 1794 1795 // Update the member resource using the etags 1796 addTitleWithEtag(memberUri, "title1", deweakify(excludeMemberEtag), Status.PRECONDITION_FAILED); 1797 addTitleWithEtag(memberUri, "title2", deweakify(committedMemberEtag), Status.NO_CONTENT); 1798 } 1799 1800 @Test 1801 public void testEtagForDirectContainerIsMemberOfPatch() throws Exception { 1802 final var membershipRescId = createBasicContainer(); 1803 final var membershipRescURI = serverAddress + membershipRescId; 1804 1805 final var directId = createDirectContainer(membershipRescURI, EX_IS_MEMBER_PROP, true); 1806 1807 final var memberId = createBasicContainer(directId, "member1"); 1808 final var memberUri = serverAddress + memberId; 1809 1810 final String committedMemberEtag = getEtag(memberUri); 1811 assertNotNull("Member resource must have etag", committedMemberEtag); 1812 1813 // Update the member resource using the etags 1814 addTitleWithEtag(memberUri, "title1", deweakify("W/\"fake\""), Status.PRECONDITION_FAILED); 1815 addTitleWithEtag(memberUri, "title2", deweakify(committedMemberEtag), Status.NO_CONTENT); 1816 1817 assertNotEquals("Etag must update after modification", committedMemberEtag, getEtag(membershipRescURI)); 1818 } 1819 1820 @Test 1821 public void testEtagForDirectContainerIsMemberOfPut() throws Exception { 1822 final var membershipRescId = createBasicContainer(); 1823 final var membershipRescURI = serverAddress + membershipRescId; 1824 1825 final var directId = createDirectContainer(membershipRescURI, EX_IS_MEMBER_PROP, true); 1826 1827 final var memberId = createBasicContainer(directId, "member1"); 1828 final var memberUri = serverAddress + memberId; 1829 1830 final String committedMemberEtag = getEtag(memberUri); 1831 assertNotNull("Member resource must have etag", committedMemberEtag); 1832 1833 putPropertiesWithEtag(memberUri, deweakify("W/\"fake\""), Status.PRECONDITION_FAILED); 1834 putPropertiesWithEtag(memberUri, deweakify(committedMemberEtag), Status.NO_CONTENT); 1835 1836 assertNotEquals("Etag must update after modification", committedMemberEtag, getEtag(membershipRescURI)); 1837 } 1838 1839 @Test 1840 public void testETagOnDeletedLdpDirectContainerChild() throws Exception { 1841 final String membershipRescId = getRandomUniqueId(); 1842 final String members = membershipRescId + "/members"; 1843 final String child = members + "/child"; 1844 1845 createObjectAndClose(membershipRescId); 1846 1847 final String etag0 = getEtag(serverAddress + membershipRescId); 1848 1849 // Create the DirectContainer 1850 final HttpPut createContainer = new HttpPut(serverAddress + members); 1851 createContainer.addHeader(CONTENT_TYPE, "text/turtle"); 1852 createContainer.addHeader(LINK, DIRECT_CONTAINER_LINK_HEADER); 1853 final String membersRDF = "<> <http://www.w3.org/ns/ldp#hasMemberRelation> <http://pcdm.org/models#hasMember>;" 1854 + " <http://www.w3.org/ns/ldp#membershipResource> <" + serverAddress + membershipRescId + "> . "; 1855 createContainer.setEntity(new StringEntity(membersRDF)); 1856 assertEquals("Membership container not created!", CREATED.getStatusCode(), getStatus(createContainer)); 1857 1858 // Create the child resource 1859 createObjectAndClose(child); 1860 1861 final String etag1 = getEtag(serverAddress + membershipRescId); 1862 assertNotEquals("Adding child must change etag of membership resc", etag0, etag1); 1863 1864 // Wait a second so that the creation and deletion of the child are not simultaneous 1865 TimeUnit.SECONDS.sleep(1); 1866 1867 // Delete the child resource 1868 assertEquals("Child resource not deleted!", NO_CONTENT.getStatusCode(), 1869 getStatus(new HttpDelete(serverAddress + child))); 1870 1871 final String etag2 = getEtag(serverAddress + membershipRescId); 1872 1873 assertNotEquals("ETag didn't change!", etag1, etag2); 1874 } 1875 1876 private void putPropertiesWithEtag(final String rescUri, final String etag, final Status status) 1877 throws IOException { 1878 final HttpPut replaceMethod = new HttpPut(rescUri); 1879 replaceMethod.setHeader(HttpHeaders.IF_MATCH, etag); 1880 replaceMethod.addHeader(CONTENT_TYPE, "text/n3"); 1881 replaceMethod.setEntity(new StringEntity("<" + rescUri + "> <info:test#label> \"foo\"")); 1882 try (final CloseableHttpResponse response = execute(replaceMethod)) { 1883 assertEquals(status.getStatusCode(), getStatus(response)); 1884 } 1885 } 1886 1887 private void addTitleWithEtag(final String rescUri, final String title, final String etag, final Status status) 1888 throws IOException { 1889 final HttpPatch postProp = new HttpPatch(rescUri); 1890 postProp.setHeader(CONTENT_TYPE, "application/sparql-update"); 1891 postProp.setHeader(HttpHeaders.IF_MATCH, etag); 1892 final String updateString = 1893 "INSERT { <" + rescUri + "> <" + DC.title.getURI() + "> \"" + title + "\" } WHERE { }"; 1894 postProp.setEntity(new StringEntity(updateString)); 1895 try (final CloseableHttpResponse dcResp = execute(postProp)) { 1896 assertEquals(dcResp.getStatusLine().toString(), status.getStatusCode(), getStatus(dcResp)); 1897 } 1898 } 1899 1900 private String deweakify(final String etag) { 1901 return etag.replaceFirst("W/", ""); 1902 } 1903 1904 private String toId(final String rescUri) { 1905 return rescUri.replaceFirst(serverAddress, ""); 1906 } 1907 1908 private List<String> listMementoIds(final String rescId) throws IOException { 1909 final HttpGet httpGet = getObjMethod(rescId + "/" + FCR_VERSIONS); 1910 httpGet.setHeader("Accept", APPLICATION_LINK_FORMAT); 1911 1912 final String responseBody; 1913 try (final CloseableHttpResponse response = execute(httpGet)) { 1914 assertEquals("Didn't get a OK response!", OK.getStatusCode(), getStatus(response)); 1915 responseBody = EntityUtils.toString(response.getEntity()); 1916 } 1917 1918 final List<String> bodyList = Arrays.asList(responseBody.split("," + System.lineSeparator())); 1919 return bodyList.stream() 1920 .map(String::trim) 1921 .filter(t -> !t.isEmpty()) 1922 .map(Link::valueOf) 1923 .filter(l -> "memento".equals(l.getRel())) 1924 .map(l -> l.getUri().toString()) 1925 .map(this::toId) 1926 .sorted() 1927 .collect(Collectors.toList()); 1928 } 1929 1930 private void assertMementoHasMembers(final String membershipMementoRescId, 1931 final Property hasMemberRelation, final String... memberIds) throws Exception { 1932 final var subjId = StringUtils.substringBefore(membershipMementoRescId, "/fcr:versions"); 1933 assertHasMembers(null, membershipMementoRescId, subjId, hasMemberRelation, memberIds); 1934 } 1935 1936 private void assertHasMembers(final String membershipRescId, 1937 final Property hasMemberRelation, final String... memberIds) throws Exception { 1938 assertHasMembers(null, membershipRescId, membershipRescId, hasMemberRelation, memberIds); 1939 } 1940 1941 private void assertHasMembers(final String txUri, final String membershipRescId, final String subjectId, 1942 final Property hasMemberRelation, final String... memberIds) throws Exception { 1943 final var model = getModel(txUri, membershipRescId); 1944 final var membershipResc = model.getResource(serverAddress + subjectId); 1945 1946 assertEquals(memberIds.length, membershipResc.listProperties(hasMemberRelation).toList().size()); 1947 for (final String memberId : memberIds) { 1948 final var memberUri = serverAddress + memberId; 1949 assertTrue("Did not contain expected member " + memberId, 1950 membershipResc.hasProperty(hasMemberRelation, createResource(memberUri))); 1951 } 1952 } 1953 1954 private void assertMementoIsMemberOf(final String memberId, final Property isMemberOfRelation, 1955 final String membershipRescId) throws Exception { 1956 final var subjId = StringUtils.substringBefore(memberId, "/fcr:versions"); 1957 assertIsMemberOf(memberId, subjId, isMemberOfRelation, membershipRescId); 1958 } 1959 1960 private void assertIsMemberOf(final String memberId, final Property isMemberOfRelation, 1961 final String membershipRescId) throws Exception { 1962 assertIsMemberOf(memberId, memberId, isMemberOfRelation, membershipRescId); 1963 } 1964 1965 private void assertIsMemberOf(final String memberId, final String subjectId, 1966 final Property isMemberOfRelation, final String membershipRescId) throws Exception { 1967 final var model = getModel(memberId); 1968 final var memberResc = model.getResource(serverAddress + subjectId); 1969 1970 final var membershipUri = serverAddress + membershipRescId; 1971 assertTrue("Did not contain expected membership " + isMemberOfRelation + " " + membershipRescId, 1972 memberResc.hasProperty(isMemberOfRelation, createResource(membershipUri))); 1973 } 1974 1975 private void assertMementoHasNoMembership(final String mementoId, 1976 final Property memberRelation) throws Exception { 1977 final var subjId = StringUtils.substringBefore(mementoId, "/fcr:versions"); 1978 assertHasNoMembership(getModel(mementoId), subjId, memberRelation); 1979 } 1980 1981 private void assertHasNoMembership(final String subjectId, final Property memberRelation) throws Exception { 1982 assertHasNoMembership(getModel(subjectId), subjectId, memberRelation); 1983 } 1984 1985 private void assertHasNoMembership(final Model model, final String subjectId, final Property memberRelation) 1986 throws Exception { 1987 final var membershipResc = model.getResource(serverAddress + subjectId); 1988 assertFalse("Expect " + subjectId + " to have no membership", 1989 membershipResc.hasProperty(memberRelation)); 1990 } 1991 1992 private void assertHasNoMembershipWhenOmitted(final String subjectId, final Property memberRelation) 1993 throws Exception { 1994 assertHasNoMembership(getModelOmitMembership(subjectId), subjectId, memberRelation); 1995 } 1996 1997 private Model getModelOmitMembership(final String pid) throws Exception { 1998 final Model model = createDefaultModel(); 1999 try (final CloseableHttpResponse response = execute(getOmitMembership(serverAddress + pid))) { 2000 model.read(response.getEntity().getContent(), serverAddress + pid, "TURTLE"); 2001 } 2002 return model; 2003 } 2004 2005 private HttpGet getOmitMembership(final String uri) { 2006 final var httpGet = new HttpGet(uri); 2007 httpGet.addHeader("Prefer", "return=representation; omit=\"" + PREFER_MEMBERSHIP + "\""); 2008 return httpGet; 2009 } 2010 2011 private String createBasicContainer() { 2012 return createBasicContainer(null, getRandomUniqueId()); 2013 } 2014 2015 private String createBasicContainer(final String parentId, final String id) { 2016 return createBasicContainer(parentId, id, null); 2017 } 2018 2019 private String createBasicContainer(final String parentId, final String id, 2020 final String txUri) { 2021 final String containerId; 2022 if (parentId != null) { 2023 containerId = parentId + "/" + id; 2024 } else { 2025 containerId = id; 2026 } 2027 final HttpPut putContainer = putObjMethod(containerId); 2028 if (txUri != null) { 2029 putContainer.addHeader(ATOMIC_ID_HEADER, txUri); 2030 } 2031 executeAndClose(putContainer); 2032 2033 return containerId; 2034 } 2035 2036 private String createDirectContainer(final String membershipURI) 2037 throws Exception { 2038 return createDirectContainer(membershipURI, PCDM_HAS_MEMBER_PROP, false); 2039 } 2040 2041 private String createDirectContainer(final String membershipURI, final Property memberRelation, 2042 final boolean isMemberOf) throws Exception { 2043 final String containerId = getRandomUniqueId(); 2044 2045 final HttpPost postIndirect = postObjMethod(); 2046 postIndirect.setHeader("Slug", containerId); 2047 postIndirect.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER); 2048 postIndirect.addHeader(CONTENT_TYPE, "text/turtle"); 2049 2050 final var relationProperty = isMemberOf ? "ldp:isMemberOfRelation" : "ldp:hasMemberRelation"; 2051 final String membersRDF = "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" + 2052 "<> " + relationProperty + " <" + memberRelation.getURI() + "> ; " + 2053 "ldp:membershipResource <" + membershipURI + "> . "; 2054 postIndirect.setEntity(new StringEntity(membersRDF)); 2055 executeAndClose(postIndirect); 2056 2057 return containerId; 2058 } 2059 2060 private void replacePropertiesWithPUT(final String resourceURI, final Model replaceModel) 2061 throws Exception { 2062 replacePropertiesWithPUT(resourceURI, replaceModel, NO_CONTENT); 2063 } 2064 2065 private void replacePropertiesWithPUT(final String resourceURI, final Model replaceModel, 2066 final Status expectedStatus) throws Exception { 2067 final HttpPut replaceMethod = new HttpPut(resourceURI); 2068 replaceMethod.addHeader(CONTENT_TYPE, "text/turtle"); 2069 replaceMethod.setHeader("Prefer", "handling=lenient; received=\"minimal\""); 2070 try (final StringWriter w = new StringWriter()) { 2071 replaceModel.write(w, "TURTLE"); 2072 replaceMethod.setEntity(new StringEntity(w.toString())); 2073 } 2074 assertEquals(expectedStatus.getStatusCode(), getStatus(replaceMethod)); 2075 } 2076}