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}