001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.integration.http.api;
019
020import static java.lang.Thread.sleep;
021import static java.nio.charset.StandardCharsets.UTF_8;
022import static java.time.ZoneId.of;
023import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
024import static java.util.Arrays.asList;
025import static java.util.regex.Pattern.compile;
026import static javax.ws.rs.core.HttpHeaders.ACCEPT;
027import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION;
028import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
029import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
030import static javax.ws.rs.core.HttpHeaders.LINK;
031import static javax.ws.rs.core.Link.fromUri;
032import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
033import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
034import static javax.ws.rs.core.Response.Status.CONFLICT;
035import static javax.ws.rs.core.Response.Status.CREATED;
036import static javax.ws.rs.core.Response.Status.GONE;
037import static javax.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED;
038import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE;
039import static javax.ws.rs.core.Response.Status.NOT_FOUND;
040import static javax.ws.rs.core.Response.Status.NOT_MODIFIED;
041import static javax.ws.rs.core.Response.Status.NO_CONTENT;
042import static javax.ws.rs.core.Response.Status.OK;
043import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT;
044import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED;
045import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE;
046import static nu.validator.htmlparser.common.DoctypeExpectation.NO_DOCTYPE_ERRORS;
047import static nu.validator.htmlparser.common.XmlViolationPolicy.ALLOW;
048import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
049import static org.apache.http.HttpStatus.SC_CREATED;
050import static org.apache.http.HttpStatus.SC_OK;
051import static org.apache.http.entity.ContentType.parse;
052import static org.apache.http.impl.client.cache.CacheConfig.DEFAULT;
053import static org.apache.jena.datatypes.TypeMapper.getInstance;
054import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDinteger;
055import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDlong;
056import static org.apache.jena.graph.Node.ANY;
057import static org.apache.jena.graph.NodeFactory.createLiteral;
058import static org.apache.jena.graph.NodeFactory.createURI;
059import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
060import static org.apache.jena.rdf.model.ModelFactory.createModelForGraph;
061import static org.apache.jena.rdf.model.ResourceFactory.createResource;
062import static org.apache.jena.riot.RDFLanguages.contentTypeToLang;
063import static org.apache.jena.riot.WebContent.contentTypeN3;
064import static org.apache.jena.riot.WebContent.contentTypeN3Alt2;
065import static org.apache.jena.riot.WebContent.contentTypeNTriples;
066import static org.apache.jena.riot.WebContent.contentTypeRDFXML;
067import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate;
068import static org.apache.jena.riot.WebContent.contentTypeTurtle;
069import static org.apache.jena.vocabulary.DC_11.title;
070import static org.apache.jena.vocabulary.RDF.type;
071import static org.fcrepo.http.commons.domain.RDFMediaType.POSSIBLE_RDF_RESPONSE_VARIANTS_STRING;
072import static org.fcrepo.http.commons.domain.RDFMediaType.POSSIBLE_RDF_VARIANTS;
073import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL;
074import static org.fcrepo.kernel.api.FedoraTypes.FCR_FIXITY;
075import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
076import static org.fcrepo.kernel.api.FedoraTypes.FCR_TOMBSTONE;
077import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS;
078import static org.fcrepo.kernel.api.RdfLexicon.ARCHIVAL_GROUP;
079import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
080import static org.fcrepo.kernel.api.RdfLexicon.CONSTRAINED_BY;
081import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER;
082import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS;
083import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE;
084import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
085import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_BINARY;
086import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_CONTAINER;
087import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_RESOURCE;
088import static org.fcrepo.kernel.api.RdfLexicon.HAS_FIXITY_SERVICE;
089import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION;
090import static org.fcrepo.kernel.api.RdfLexicon.HAS_MESSAGE_DIGEST;
091import static org.fcrepo.kernel.api.RdfLexicon.HAS_MIME_TYPE;
092import static org.fcrepo.kernel.api.RdfLexicon.HAS_ORIGINAL_NAME;
093import static org.fcrepo.kernel.api.RdfLexicon.HAS_SIZE;
094import static org.fcrepo.kernel.api.RdfLexicon.INBOUND_REFERENCES;
095import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
096import static org.fcrepo.kernel.api.RdfLexicon.INSERTED_CONTENT_RELATION;
097import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE;
098import static org.fcrepo.kernel.api.RdfLexicon.LDP_MEMBER;
099import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE;
100import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_NAMESPACE;
101import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE;
102import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
103import static org.fcrepo.kernel.api.RdfLexicon.PREFER_MINIMAL_CONTAINER;
104import static org.fcrepo.kernel.api.RdfLexicon.PREFER_SERVER_MANAGED;
105import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE;
106import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
107import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_ROOT;
108import static org.fcrepo.kernel.api.RdfLexicon.RESOURCE;
109import static org.fcrepo.kernel.api.RdfLexicon.VERSIONED_RESOURCE;
110import static org.fcrepo.kernel.api.RdfLexicon.VERSIONING_TIMEGATE_TYPE;
111import static org.fcrepo.kernel.api.models.ExternalContent.COPY;
112import static org.fcrepo.kernel.api.models.ExternalContent.PROXY;
113import static org.fcrepo.kernel.api.models.ExternalContent.REDIRECT;
114import static org.junit.Assert.assertEquals;
115import static org.junit.Assert.assertFalse;
116import static org.junit.Assert.assertNotEquals;
117import static org.junit.Assert.assertNotNull;
118import static org.junit.Assert.assertNull;
119import static org.junit.Assert.assertTrue;
120import static org.junit.Assert.fail;
121import static org.slf4j.LoggerFactory.getLogger;
122
123import java.io.ByteArrayInputStream;
124import java.io.ByteArrayOutputStream;
125import java.io.File;
126import java.io.IOException;
127import java.io.InputStream;
128import java.io.OutputStreamWriter;
129import java.io.StringReader;
130import java.io.StringWriter;
131import java.io.UnsupportedEncodingException;
132import java.io.Writer;
133import java.net.URI;
134import java.net.URLEncoder;
135import java.text.ParseException;
136import java.time.Instant;
137import java.time.ZoneId;
138import java.time.format.DateTimeFormatter;
139import java.util.ArrayList;
140import java.util.Arrays;
141import java.util.Collection;
142import java.util.Iterator;
143import java.util.List;
144import java.util.Locale;
145import java.util.NoSuchElementException;
146import java.util.Optional;
147import java.util.Random;
148import java.util.concurrent.Phaser;
149import java.util.concurrent.TimeUnit;
150
151import javax.ws.rs.core.Link;
152import javax.ws.rs.core.Response.Status;
153import javax.ws.rs.core.Variant;
154
155import com.fasterxml.jackson.databind.JsonNode;
156import com.fasterxml.jackson.databind.ObjectMapper;
157import com.google.common.collect.Iterators;
158import nu.validator.htmlparser.sax.HtmlParser;
159import nu.validator.saxtree.TreeBuilder;
160import org.apache.commons.io.IOUtils;
161import org.apache.http.Header;
162import org.apache.http.HttpEntity;
163import org.apache.http.HttpResponse;
164import org.apache.http.client.config.RequestConfig;
165import org.apache.http.client.methods.CloseableHttpResponse;
166import org.apache.http.client.methods.HttpDelete;
167import org.apache.http.client.methods.HttpGet;
168import org.apache.http.client.methods.HttpHead;
169import org.apache.http.client.methods.HttpOptions;
170import org.apache.http.client.methods.HttpPatch;
171import org.apache.http.client.methods.HttpPost;
172import org.apache.http.client.methods.HttpPut;
173import org.apache.http.client.methods.HttpUriRequest;
174import org.apache.http.entity.BasicHttpEntity;
175import org.apache.http.entity.ByteArrayEntity;
176import org.apache.http.entity.FileEntity;
177import org.apache.http.entity.InputStreamEntity;
178import org.apache.http.entity.StringEntity;
179import org.apache.http.impl.client.CloseableHttpClient;
180import org.apache.http.impl.client.cache.CachingHttpClientBuilder;
181import org.apache.http.util.EntityUtils;
182import org.apache.jena.graph.Node;
183import org.apache.jena.graph.NodeFactory;
184import org.apache.jena.query.Dataset;
185import org.apache.jena.rdf.model.Model;
186import org.apache.jena.rdf.model.Property;
187import org.apache.jena.rdf.model.Resource;
188import org.apache.jena.rdf.model.StmtIterator;
189import org.apache.jena.riot.RDFFormat;
190import org.apache.jena.sparql.core.DatasetGraph;
191import org.apache.jena.sparql.core.Quad;
192import org.apache.jena.vocabulary.DC_11;
193import org.apache.jena.vocabulary.RDF;
194import org.fcrepo.http.commons.domain.RDFMediaType;
195import org.fcrepo.http.commons.test.util.CloseableDataset;
196import org.fcrepo.kernel.api.RdfLexicon;
197import org.glassfish.jersey.media.multipart.ContentDisposition;
198import org.junit.Ignore;
199import org.junit.Test;
200import org.slf4j.Logger;
201import org.springframework.test.context.TestExecutionListeners;
202import org.xml.sax.ErrorHandler;
203import org.xml.sax.InputSource;
204import org.xml.sax.SAXException;
205import org.xml.sax.SAXParseException;
206
207/**
208 * @author cabeer
209 * @author ajs6f
210 */
211@TestExecutionListeners(
212        listeners = { TestIsolationExecutionListener.class },
213        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
214public class FedoraLdpIT extends AbstractResourceIT {
215
216    private static final Node DC_IDENTIFIER = DC_11.identifier.asNode();
217
218    private static final String LDP_RESOURCE_LINK_HEADER = "<" + RESOURCE.getURI() + ">; rel=\"type\"";
219
220    private static final Node rdfType = type.asNode();
221
222    private static final Resource PCDM_FILE_TYPE = createResource("http://pcdm.org/models#File");
223
224    private static final String CONTAINER_LINK_HEADER = "<" + CONTAINER.getURI() + ">; rel=\"type\"";
225
226    private static final String BASIC_CONTAINER_LINK_HEADER = "<" + BASIC_CONTAINER.getURI() + ">; rel=\"type\"";
227    private static final String INDIRECT_CONTAINER_LINK_HEADER = "<" + INDIRECT_CONTAINER.getURI() + ">; rel=\"type\"";
228    private static final String ARCHIVAL_GROUP_LINK_HEADER = "<" + ARCHIVAL_GROUP.getURI() + ">; rel=\"type\"";
229
230    private static final String RESOURCE_LINK_HEADER = "<" + RESOURCE.getURI() + ">; rel=\"type\"";
231    private static final String RDF_SOURCE_LINK_HEADER = "<" + RDF_SOURCE.getURI() + ">; rel=\"type\"";
232    private static final String NON_RDF_SOURCE_LINK_HEADER = "<" + NON_RDF_SOURCE.getURI() + ">; rel=\"type\"";
233    private static final String VERSIONED_RESOURCE_LINK_HEADER = "<" + VERSIONED_RESOURCE.getURI() + ">; rel=\"type\"";
234
235    private static final String SERVER_MANAGED_TYPE_CONSTRAINT_URI = serverAddress +
236            "static/constraints/ServerManagedTypeException.rdf";
237
238    private static final String SERVER_MANAGED_PROPERTY_CONSTRAINT_URI = serverAddress +
239            "static/constraints/ServerManagedPropertyException.rdf";
240
241    private static final String INBOUND_REFERENCE_PREFER_HEADER = "return=representation; include=\"" +
242            INBOUND_REFERENCES + "\"";
243
244    private static final String WANT_DIGEST = "Want-Digest";
245
246    private static final String DIGEST = "Digest";
247
248    private static final String TEST_BINARY_CONTENT = "01234567890123456789012345678901234567890123456789";
249
250    private static final String TEST_SHA_DIGEST_HEADER_VALUE = "sha=9578f951955d37f20b601c26591e260c1e5389bf";
251
252    private static final String TEST_MD5_DIGEST_HEADER_VALUE = "md5=baed005300234f3d1503c50a48ce8e6f";
253
254    private static final String TEST_SHA256_DIGEST_HEADER_VALUE =
255            "sha-256=fb871ff8cce8fea83dfaeab41784305a1461e008dc02a371ed26d856c766c903";
256
257    private static final Logger LOGGER = getLogger(FedoraLdpIT.class);
258
259    private static final DateTimeFormatter headerFormat =
260            RFC_1123_DATE_TIME.withLocale(Locale.US).withZone(ZoneId.of("GMT"));
261
262    private static final DateTimeFormatter tripleFormat =
263      DateTimeFormatter.ISO_INSTANT.withZone(of("GMT"));
264
265    @Test
266    public void testHeadRepositoryGraph() throws IOException {
267        final HttpHead headObjMethod = new HttpHead(serverAddress);
268        try (final CloseableHttpResponse response = execute(headObjMethod)) {
269          assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
270          checkForLinkHeader(response, serverAddress + FCR_ACL, "acl");
271        }
272    }
273
274    @Test
275    public void testHeadObject() throws IOException {
276        final String id = getRandomUniqueId();
277        createObject(id).close();
278        final HttpHead headObjMethod = headObjMethod(id);
279        assertEquals(OK.getStatusCode(), getStatus(headObjMethod));
280    }
281
282    @Test
283    public void testHeadDefaultContainer() throws IOException {
284        final String id = getRandomUniqueId();
285        createObject(id).close();
286        final HttpHead headObjMethod = headObjMethod(id);
287        try (final CloseableHttpResponse response = execute(headObjMethod)) {
288            checkForLinkHeader(response, RDF_SOURCE.getURI(), "type");
289            assertTrue("Didn't find LDP container link header!", getLinkHeaders(response).contains(
290                    BASIC_CONTAINER_LINK_HEADER));
291        }
292    }
293
294    @Test
295    public void testHeadBasicContainer() throws IOException {
296        final String id = getRandomUniqueId();
297
298        createObjectAndClose(id, BASIC_CONTAINER_LINK_HEADER);
299
300        final HttpHead headObjMethod = headObjMethod(id);
301        try (final CloseableHttpResponse response = execute(headObjMethod)) {
302            final Collection<String> links = getLinkHeaders(response);
303            checkForLinkHeader(response, RDF_SOURCE.getURI(), "type");
304            assertTrue("Didn't find LDP container link header!", links.contains(BASIC_CONTAINER_LINK_HEADER));
305        }
306    }
307
308    @Test
309    public void testCreateArchivalGroup() throws Exception {
310        final var id = getRandomUniqueId();
311        final var childId = id + "/child";
312        final var grandChildId = childId + "/grandchild";
313
314        createObjectAndClose(id, BASIC_CONTAINER_LINK_HEADER, ARCHIVAL_GROUP_LINK_HEADER);
315
316        final var headObjMethod = headObjMethod(id);
317        try (final CloseableHttpResponse response = execute(headObjMethod)) {
318            final Collection<String> links = getLinkHeaders(response);
319            assertTrue("Didn't find ArchivalGroup link header!", links.contains(ARCHIVAL_GROUP_LINK_HEADER));
320            assertTrue("Didn't find LDP container link header!", links.contains(BASIC_CONTAINER_LINK_HEADER));
321        }
322
323
324        executeAndClose(putObjMethod(childId, "text/turtle", "<> a <http://example.com/Foo> ."));
325
326        final var childHeadObjMethod = headObjMethod(childId);
327        try (final CloseableHttpResponse response = execute(childHeadObjMethod)) {
328            final Collection<String> links = getLinkHeaders(response);
329            assertTrue("Didn't find LDP container link header!", links.contains(BASIC_CONTAINER_LINK_HEADER));
330            assertFalse("Unexpectedly found ArchivalGroup link header!",
331                    links.contains(ARCHIVAL_GROUP_LINK_HEADER));
332        }
333
334        executeAndClose(putObjMethod(grandChildId, "text/turtle", "<> a <http://example.com/Foo> ."));
335
336        final var grandChildHeadObjMethod = headObjMethod(grandChildId);
337        try (final CloseableHttpResponse response = execute(grandChildHeadObjMethod)) {
338            final Collection<String> links = getLinkHeaders(response);
339            assertTrue("Didn't find LDP container link header!", links.contains(BASIC_CONTAINER_LINK_HEADER));
340            assertFalse("Unexpectedly found ArchivalGroup link header!",
341                    links.contains(ARCHIVAL_GROUP_LINK_HEADER));
342        }
343    }
344
345    @Test
346    public void testCreateArchivalGroupWithinAnArchivalGroupFails() throws Exception {
347        final var id = getRandomUniqueId();
348        final var childId = id + "/child";
349
350        createObjectAndClose(id, BASIC_CONTAINER_LINK_HEADER, ARCHIVAL_GROUP_LINK_HEADER);
351
352        final var headObjMethod = headObjMethod(id);
353        try (final CloseableHttpResponse response = execute(headObjMethod)) {
354            final Collection<String> links = getLinkHeaders(response);
355            assertTrue("Didn't find ArchivalGroup link header!", links.contains(ARCHIVAL_GROUP_LINK_HEADER));
356            assertTrue("Didn't find LDP container link header!", links.contains(BASIC_CONTAINER_LINK_HEADER));
357        }
358
359        final var putObjMethod = putObjMethod(childId, "text/turtle", "<> a <http://example.com/Foo> .");
360        putObjMethod.setHeader("Link", ARCHIVAL_GROUP_LINK_HEADER);
361        try (final CloseableHttpResponse response = execute(putObjMethod)) {
362            assertEquals("Expected Conflict response", CONFLICT.getStatusCode(),
363                    response.getStatusLine().getStatusCode());
364        }
365    }
366
367    @Test
368    public void testCreateBinaryAsArchivalGroupWithPostFails() throws Exception {
369        final var id = getRandomUniqueId();
370        final var postMethod = postObjMethod();
371        postMethod.setHeader("Slug", id);
372        postMethod.setEntity(new StringEntity("test"));
373        postMethod.addHeader("Link", ARCHIVAL_GROUP_LINK_HEADER);
374        postMethod.addHeader("Link", NON_RDF_SOURCE_LINK_HEADER);
375        postMethod.addHeader("Content-Type", "text/plain");
376
377        try (final CloseableHttpResponse response = execute(postMethod)) {
378            assertEquals("Expected Bad Request response", BAD_REQUEST.getStatusCode(),
379                    response.getStatusLine().getStatusCode());
380        }
381    }
382
383    @Test
384    public void testCreateBinaryAsArchivalGroupWithPutFails() throws Exception {
385        final var id = getRandomUniqueId();
386        final var putMethod = putObjMethod(id, "text/plain", "testcontent");
387        putMethod.addHeader("Link", ARCHIVAL_GROUP_LINK_HEADER);
388        putMethod.addHeader("Link", NON_RDF_SOURCE_LINK_HEADER);
389
390        try (final CloseableHttpResponse response = execute(putMethod)) {
391            assertEquals("Expected Bad Request response", BAD_REQUEST.getStatusCode(),
392                    response.getStatusLine().getStatusCode());
393        }
394    }
395
396    @Test
397    public void testHeadTurtleContentType() throws IOException {
398        testHeadDefaultContentType(RDFMediaType.TURTLE_WITH_CHARSET);
399    }
400
401    @Test
402    public void testHeadRDFContentType() throws IOException {
403        testHeadDefaultContentType(RDFMediaType.RDF_XML);
404    }
405
406    @Test
407    public void testHeadJSONLDContentType() throws IOException {
408        testHeadDefaultContentType(RDFMediaType.JSON_LD);
409    }
410
411    @Test
412    public void testHeadDefaultContentType() throws IOException {
413        testHeadDefaultContentType(null);
414    }
415
416    private void testHeadDefaultContentType(final String mimeType) throws IOException {
417        final String id = getRandomUniqueId();
418        createObjectAndClose(id);
419
420        final HttpHead headObjMethod = headObjMethod(id);
421        String mt = mimeType;
422        if (mt != null) {
423            headObjMethod.addHeader("Accept", mt);
424        } else {
425            mt = RDFMediaType.TURTLE_WITH_CHARSET;
426        }
427        try (final CloseableHttpResponse response = execute(headObjMethod)) {
428            final Collection<String> contentTypes = getHeader(response, CONTENT_TYPE);
429            final String contentType = contentTypes.iterator().next();
430            assertTrue("Didn't find LDP valid content-type header: " + contentType +
431                    "; expected result: " + mt, contentType.contains(mt));
432            testHeadVaryAndPreferHeaders(response);
433        }
434    }
435
436    private void testHeadVaryAndPreferHeaders(final CloseableHttpResponse response) {
437        final Collection<String> preferenceApplied = getHeader(response, "Preference-Applied");
438        final Collection<String> vary = getHeader(response, "Vary");
439        assertTrue("Didn't find valid Preference-Applied header", preferenceApplied.contains("return=representation"));
440        assertTrue("Didn't find valid Vary Prefer header", vary.contains("Prefer"));
441        assertTrue("Didn't find valid Vary header",
442                vary.contains("Accept"));
443        assertTrue("Didn't find valid Vary header",
444            vary.contains("Range"));
445        assertTrue("Didn't find valid Vary header",
446            vary.contains("Accept-Encoding"));
447        assertTrue("Didn't find valid Vary header",
448            vary.contains("Accept-Language"));
449
450    }
451
452    @Test
453    public void testHeadDefaultRDF() throws IOException {
454        final String id = getRandomUniqueId();
455        final HttpPut put = putObjMethod(id, "text/turtle", "<> a <http://example.com/Foo> .");
456        executeAndClose(put);
457
458        final HttpHead headObjMethod = headObjMethod(id);
459        try (final CloseableHttpResponse response = execute(headObjMethod)) {
460            final Collection<String> links = getLinkHeaders(response);
461            assertTrue("Didn't find LDP BasicContainer link header!", links.contains(BASIC_CONTAINER_LINK_HEADER));
462            testHeadVaryAndPreferHeaders(response);
463        }
464    }
465
466    @Test
467    public void testHeadDefaultNonRDF() throws IOException {
468        final String id = getRandomUniqueId();
469        final HttpPut put = putObjMethod(id, "text/plain", "<> a <http://example.com/Foo> .");
470        executeAndClose(put);
471
472        final HttpHead headObjMethod = headObjMethod(id);
473        try (final CloseableHttpResponse response = execute(headObjMethod)) {
474            final Collection<String> links = getLinkHeaders(response);
475            assertTrue("Didn't find LDP NonRDFSource link header!", links.contains(NON_RDF_SOURCE_LINK_HEADER));
476        }
477    }
478
479    @Test
480    public void testHeadDirectContainer() throws IOException {
481        final String id = getRandomUniqueId();
482        final HttpPut put = putObjMethod(id);
483        put.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
484        executeAndClose(put);
485
486        final HttpHead headObjMethod = headObjMethod(id);
487        try (final CloseableHttpResponse response = execute(headObjMethod)) {
488            final Collection<String> links = getLinkHeaders(response);
489            checkForLinkHeader(response, RDF_SOURCE.getURI(), "type");
490            assertTrue("Didn't find LDP container link header!", links.contains(DIRECT_CONTAINER_LINK_HEADER));
491            testHeadVaryAndPreferHeaders(response);
492        }
493    }
494
495    @Test
496    public void testHeadIndirectContainer() throws IOException {
497        final String id = getRandomUniqueId();
498        final HttpPut put = putObjMethod(id);
499        put.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER);
500        executeAndClose(put);
501
502        final HttpHead headObjMethod = headObjMethod(id);
503        try (final CloseableHttpResponse response = execute(headObjMethod)) {
504            final Collection<String> links = getLinkHeaders(response);
505            assertTrue("Didn't find LDP container link header!", links.contains(INDIRECT_CONTAINER_LINK_HEADER));
506            testHeadVaryAndPreferHeaders(response);
507        }
508    }
509
510    @Test
511    public void testHeadDatastream() throws IOException, ParseException {
512        final String id = getRandomUniqueId();
513        createDatastream(id, "x", "123");
514
515        final HttpHead headObjMethod = headObjMethod(id + "/x");
516        try (final CloseableHttpResponse response = execute(headObjMethod)) {
517            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
518            assertEquals(TEXT_PLAIN, response.getFirstHeader(CONTENT_TYPE).getValue());
519            assertEquals("3", response.getFirstHeader(CONTENT_LENGTH).getValue());
520            assertEquals("bytes", response.getFirstHeader("Accept-Ranges").getValue());
521            final ContentDisposition disposition =
522                    new ContentDisposition(response.getFirstHeader(CONTENT_DISPOSITION).getValue());
523            assertEquals("attachment", disposition.getType());
524        }
525    }
526
527    @Test
528    public void testHeadDatastreamWithWantDigest() throws IOException {
529        final String id = getRandomUniqueId();
530        createDatastream(id, "x", TEST_BINARY_CONTENT);
531
532        final HttpHead headObjMethod = headObjMethod(id + "/x");
533        headObjMethod.addHeader(WANT_DIGEST, "SHA");
534        try (final CloseableHttpResponse response = execute(headObjMethod)) {
535            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
536            assertEquals(TEXT_PLAIN, response.getFirstHeader(CONTENT_TYPE).getValue());
537            assertTrue(response.getHeaders(DIGEST).length > 0);
538            final String digesterHeaderValue = response.getHeaders(DIGEST)[0].getValue();
539            assertTrue("Fixity Checksum doesn't match",
540                    digesterHeaderValue.equals(TEST_SHA_DIGEST_HEADER_VALUE));
541        }
542    }
543
544    @Test
545    public void testHeadDatastreamWithWantDigestMultiple() throws IOException {
546        final String id = getRandomUniqueId();
547        createDatastream(id, "x", TEST_BINARY_CONTENT);
548
549        final HttpHead headObjMethod = headObjMethod(id + "/x");
550        headObjMethod.addHeader(WANT_DIGEST, "SHA, md5;q=0.3");
551        try (final CloseableHttpResponse response = execute(headObjMethod)) {
552            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
553            assertEquals(TEXT_PLAIN, response.getFirstHeader(CONTENT_TYPE).getValue());
554            assertTrue(response.getHeaders(DIGEST).length > 0);
555
556            final String digesterHeaderValue = response.getHeaders(DIGEST)[0].getValue();
557            assertTrue("SHA-1 Fixity Checksum doesn't match",
558                    digesterHeaderValue.contains(TEST_SHA_DIGEST_HEADER_VALUE));
559            assertTrue("MD5 fixity checksum doesn't match",
560                    digesterHeaderValue.contains(TEST_MD5_DIGEST_HEADER_VALUE));
561        }
562    }
563
564    @Test
565    public void testHeadDatastreamWithWantDigestMultipleOneUnsupported() throws IOException {
566        final String id = getRandomUniqueId();
567        createDatastream(id, "x", TEST_BINARY_CONTENT);
568
569        final HttpHead headObjMethod = headObjMethod(id + "/x");
570        headObjMethod.addHeader(WANT_DIGEST, "md5, Indigestion");
571        try (final CloseableHttpResponse response = execute(headObjMethod)) {
572            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
573            assertEquals(1, response.getHeaders(DIGEST).length);
574            final String digesterHeaderValue = response.getHeaders(DIGEST)[0].getValue();
575            assertTrue("MD5 fixity checksum doesn't match",
576                    digesterHeaderValue.contains(TEST_MD5_DIGEST_HEADER_VALUE));
577        }
578    }
579
580    @Test
581    public void testHeadDatastreamWithWantDigestSha256() throws IOException {
582        final String id = getRandomUniqueId();
583        createDatastream(id, "x", TEST_BINARY_CONTENT);
584
585        final HttpHead headObjMethod = headObjMethod(id + "/x");
586        headObjMethod.addHeader(WANT_DIGEST, "sha-256");
587        try (final CloseableHttpResponse response = execute(headObjMethod)) {
588            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
589            assertEquals(TEXT_PLAIN, response.getFirstHeader(CONTENT_TYPE).getValue());
590            assertTrue(response.getHeaders(DIGEST).length > 0);
591
592            final String digesterHeaderValue = response.getHeaders(DIGEST)[0].getValue();
593            assertTrue("SHA-256 Fixity Checksum doesn't match",
594                    digesterHeaderValue.contains(TEST_SHA256_DIGEST_HEADER_VALUE));
595        }
596    }
597
598    @Test
599    public void testHeadRdfResourceHeaders() throws IOException {
600        final String id = getRandomUniqueId();
601        createObject(id).close();
602
603        final String location = serverAddress + id;
604        final HttpHead headObjMethod = headObjMethod(id);
605        try (final CloseableHttpResponse response = execute(headObjMethod)) {
606            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
607            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
608        }
609    }
610
611    @Test
612    public void testHeadNonRdfHeaders() throws IOException {
613        final String id = getRandomUniqueId();
614        final HttpPut put = putObjMethod(id, "text/plain", "<> a <http://example.com/Foo> .");
615        executeAndClose(put);
616
617        final String location = serverAddress + id;
618        final HttpHead headObjMethod = headObjMethod(id);
619        try (final CloseableHttpResponse response = execute(headObjMethod)) {
620            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
621            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
622        }
623    }
624
625    @Test
626    public void testOptions() throws IOException {
627        final String id = getRandomUniqueId();
628        createObjectAndClose(id);
629
630        final HttpOptions optionsRequest = new HttpOptions(serverAddress + id);
631        try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) {
632            assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode());
633            assertContainerOptionsHeaders(optionsResponse);
634        }
635    }
636
637    @Test
638    public void testOptionsBinary() throws IOException {
639        final String id = getRandomUniqueId();
640        createDatastream(id, "x", id);
641
642        final HttpOptions optionsRequest = new HttpOptions(serverAddress + id + "/x");
643        try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) {
644            assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode());
645            assertResourceOptionsHeaders(optionsResponse);
646            assertEquals("0", optionsResponse.getFirstHeader(CONTENT_LENGTH).getValue());
647        }
648    }
649
650    @Test
651    public void testOptionsBinaryMetadata() throws IOException {
652        final String id = getRandomUniqueId();
653        createDatastream(id, "x", null);
654
655        final HttpOptions optionsRequest = new HttpOptions(serverAddress + id + "/x/fcr:metadata");
656        try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) {
657            assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode());
658            assertNonRdfResourceDescriptionOptionsHeaders(optionsResponse);
659        }
660    }
661
662    @Test
663    public void testOptionsBinaryMetadataWithUriEncoding() throws Exception {
664        final String id = getRandomUniqueId();
665        createDatastream(id, "x", null);
666        final String location = serverAddress + id + "/x/fcr%3Ametadata";
667
668        final HttpOptions optionsRequest = new HttpOptions(location);
669        try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) {
670            assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode());
671            assertNonRdfResourceDescriptionOptionsHeaders(optionsResponse);
672        }
673    }
674
675    private static void assertContainerOptionsHeaders(final HttpResponse httpResponse) {
676        assertRdfOptionsHeaders(httpResponse);
677        final List<String> methods = headerValues(httpResponse, "Allow");
678        assertTrue("Should allow POST", methods.contains(HttpPost.METHOD_NAME));
679
680        final List<String> postTypes = headerValues(httpResponse, "Accept-Post");
681        assertTrue("POST should support text/turtle", postTypes.contains(contentTypeTurtle));
682        assertTrue("POST should support text/rdf+n3", postTypes.contains(contentTypeN3));
683        assertTrue("POST should support text/n3", postTypes.contains(contentTypeN3Alt2));
684        assertTrue("POST should support application/rdf+xml", postTypes.contains(contentTypeRDFXML));
685        assertTrue("POST should support application/n-triples", postTypes.contains(contentTypeNTriples));
686
687        final List<String> externalTypes = headerValues(httpResponse, "Accept-External-Content-Handling");
688        assertTrue("COPY should be advertised for accepted external content.", externalTypes.contains(COPY));
689        assertTrue("PROXY should be advertised for accepted external content.", externalTypes.contains(PROXY));
690        assertTrue("REDIRECT should be advertised for accepted external content.", externalTypes.contains(REDIRECT));
691    }
692
693    private static void assertRdfOptionsHeaders(final HttpResponse httpResponse) {
694        final List<String> methods = headerValues(httpResponse, "Allow");
695        assertTrue("Should allow PATCH", methods.contains(HttpPatch.METHOD_NAME));
696        assertTrue("Should allow DELETE", methods.contains("DELETE"));
697        assertTrue("Should allow HEAD", methods.contains("HEAD"));
698
699        final List<String> patchTypes = headerValues(httpResponse, "Accept-Patch");
700        assertTrue("PATCH should support application/sparql-update", patchTypes.contains(contentTypeSPARQLUpdate));
701        assertResourceOptionsHeaders(httpResponse);
702    }
703
704    private static void assertNonRdfResourceDescriptionOptionsHeaders(final HttpResponse httpResponse) {
705        final List<String> methods = headerValues(httpResponse, "Allow");
706        assertTrue("Should allow PATCH", methods.contains(HttpPatch.METHOD_NAME));
707        assertTrue("Should allow HEAD", methods.contains(HttpHead.METHOD_NAME));
708        final List<String> patchTypes = headerValues(httpResponse, "Accept-Patch");
709        assertTrue("PATCH should support application/sparql-update", patchTypes.contains(contentTypeSPARQLUpdate));
710        assertResourceOptionsHeaders(httpResponse);
711    }
712
713    private static void assertResourceOptionsHeaders(final HttpResponse httpResponse) {
714        final List<String> methods = headerValues(httpResponse, "Allow");
715        assertTrue("Should allow GET", methods.contains(HttpGet.METHOD_NAME));
716        assertTrue("Should allow PUT", methods.contains(HttpPut.METHOD_NAME));
717        assertTrue("Should allow DELETE", methods.contains(HttpDelete.METHOD_NAME));
718        assertTrue("Should allow OPTIONS", methods.contains(HttpOptions.METHOD_NAME));
719    }
720
721    @Test
722    public void testGetRDFSource() throws IOException {
723        final String id = getRandomUniqueId();
724        createObjectAndClose(id);
725
726        final String location = serverAddress + id;
727        try (final CloseableHttpResponse response = execute(new HttpGet(serverAddress + id))) {
728            assertEquals(OK.getStatusCode(), getStatus(response));
729            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
730            checkForLinkHeader(response, RDF_SOURCE.getURI(), "type");
731            final HttpEntity entity = response.getEntity();
732            final String contentType = parse(entity.getContentType().getValue()).getMimeType();
733            assertNotNull("Entity is not an RDF serialization!", contentTypeToLang(contentType));
734        }
735    }
736
737    @Test
738    public void testCreateContainerWithCharset() throws IOException {
739        final String id = getRandomUniqueId();
740
741        final HttpPut put = putObjMethod(id, "text/turtle; charset=ISO-8859-1", "<> <http://test.org/title> 'hello'");
742        put.setHeader(LINK, BASIC_CONTAINER_LINK_HEADER);
743        try (final CloseableHttpResponse response = execute(put)) {
744            assertEquals(CREATED.getStatusCode(), response.getStatusLine().getStatusCode());
745
746        }
747    }
748
749    @Test
750    public void testCheckGetAclResourceHeaders() throws IOException {
751        final String aclUri = createAcl();
752
753        try (final CloseableHttpResponse response = execute(new HttpGet(aclUri))) {
754            assertEquals(OK.getStatusCode(), getStatus(response));
755            final Collection<String> links = getLinkHeaders(response);
756            final String aclLink = "<" + aclUri + "/" + FCR_ACL + ">;rel=\"acl\"";
757            assertFalse("ACL link header exists in ACL resource!", links.contains(aclLink));
758        }
759    }
760
761    @Test
762    public void testGetRDFSourceWithPreferRepresentation() throws IOException {
763        final String id = getRandomUniqueId();
764        createObjectAndClose(id);
765
766        final HttpGet getMethod = new HttpGet(serverAddress + id);
767        final String preferHeader = "return=representation;"
768                + " include=\"http://fedora.info/definitions/fcrepo#PreferInboundReferences\";"
769                + " omit=\"http://www.w3.org/ns/ldp#PreferMembership http://www.w3.org/ns/ldp#PreferContainment\"";
770        getMethod.addHeader("Prefer", preferHeader);
771
772        try (final CloseableHttpResponse response = execute(getMethod)) {
773            assertEquals(OK.getStatusCode(), getStatus(response));
774            final Collection<String> preferenceApplied = getHeader(response, "Preference-Applied");
775            assertTrue("Preference-Applied header doesn't matched", preferenceApplied.contains(preferHeader));
776        }
777    }
778
779    @Test
780    public void testGetNonRDFSource() throws IOException {
781        final String id = getRandomUniqueId();
782        createDatastream(id, "x", "some content");
783
784        final String location = serverAddress + id + "/x";
785        try (final CloseableHttpResponse response = execute(getDSMethod(id, "x"))) {
786            final HttpEntity entity = response.getEntity();
787            final String content = EntityUtils.toString(entity);
788            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
789            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
790            assertEquals("some content", content);
791        }
792    }
793
794    @Test
795    public void testGetNonRDFSourceDescription() throws IOException {
796        final String id = getRandomUniqueId();
797        createDatastream(id, "x", "some content");
798
799        final String location = serverAddress + id + "/x";
800        try (final CloseableHttpResponse response = execute(getDSDescMethod(id, "x"));
801                final CloseableDataset dataset = getDataset(response)) {
802            checkForLinkHeader(response, location, "describes");
803            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
804            final DatasetGraph graph = dataset.asDatasetGraph();
805            final Node correctDSSubject = createURI(serverAddress + id + "/x");
806            assertTrue("Binary should be a ldp:NonRDFSource", graph.contains(ANY,
807                    correctDSSubject, rdfType, NON_RDF_SOURCE.asNode()));
808            // every triple in the response should have a subject of the actual resource described
809            LOGGER.info("Found graph:\n{}", graph);
810            graph.find().forEachRemaining(quad -> {
811                assertEquals("Found a triple with incorrect subject!", correctDSSubject, quad.getSubject());
812            });
813        }
814    }
815
816    @Test
817    public void testGetNonRDFSourceWithWantDigest() throws IOException {
818        final String id = getRandomUniqueId();
819        createDatastream(id, "x", TEST_BINARY_CONTENT);
820
821        final HttpGet getMethod = getDSMethod(id, "x");
822        getMethod.addHeader(WANT_DIGEST, "SHA");
823        try (final CloseableHttpResponse response = execute(getMethod)) {
824            final HttpEntity entity = response.getEntity();
825            final String content = EntityUtils.toString(entity);
826            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
827            assertEquals(TEST_BINARY_CONTENT, content);
828
829            final String digesterHeaderValue = response.getHeaders(DIGEST)[0].getValue();
830            assertTrue("Fixity Checksum doesn't match",
831                    digesterHeaderValue.equals(TEST_SHA_DIGEST_HEADER_VALUE));
832        }
833    }
834
835    @Test
836    public void testGetNonRDFSourceWithWantDigestMultiple() throws IOException {
837        final String id = getRandomUniqueId();
838        createDatastream(id, "x", TEST_BINARY_CONTENT);
839
840        final HttpGet getMethod = getDSMethod(id, "x");
841        getMethod.addHeader(WANT_DIGEST, "SHA,md5;q=0.3,sha-256;q=0.2");
842        try (final CloseableHttpResponse response = execute(getMethod)) {
843            final HttpEntity entity = response.getEntity();
844            final String content = EntityUtils.toString(entity);
845            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
846            assertEquals(TEST_BINARY_CONTENT, content);
847
848            final String digesterHeaderValue = response.getHeaders(DIGEST)[0].getValue();
849            assertTrue("SHA-1 Fixity Checksum doesn't match",
850                    digesterHeaderValue.contains(TEST_SHA_DIGEST_HEADER_VALUE));
851            assertTrue("MD5 fixity checksum doesn't match",
852                    digesterHeaderValue.contains(TEST_MD5_DIGEST_HEADER_VALUE));
853            assertTrue("SHA-256 fixity checksum doesn't match",digesterHeaderValue.contains(
854                    "sha-256=fb871ff8cce8fea83dfaeab41784305a1461e008dc02a371ed26d856c766c903"));
855        }
856    }
857
858    /**
859     * Test that a 406 gets returned in the event of an invalid or unsupported
860     * format being requested.
861     */
862    @Test
863    public void testGetRDFSourceWrongAccept() {
864        final String id = getRandomUniqueId();
865        createObjectAndClose(id);
866
867        final HttpGet get = new HttpGet(serverAddress + id);
868        get.addHeader(ACCEPT, "application/turtle");
869
870        assertEquals(NOT_ACCEPTABLE.getStatusCode(), getStatus(get));
871    }
872
873    @Test
874    public void testGetRDFSourceWithUserTypes() throws IOException {
875        final String id = getRandomUniqueId();
876        createObjectAndClose(id);
877
878        verifyPresenceOfUserTypeHeader(id);
879    }
880
881    @Test
882    public void testGetNonRDFSourceAndDescriptionWithUserTypes() throws IOException {
883        final String id = getRandomUniqueId();
884        createDatastream(id, "ds", "sample-content");
885
886        verifyPresenceOfUserTypeHeader(id + "/ds/" + FCR_METADATA);
887    }
888
889    private void verifyPresenceOfUserTypeHeader(final String id) throws IOException {
890        final HttpHead headMethod = new HttpHead(serverAddress + id);
891
892        final int numInitialHeaders;
893        try (final CloseableHttpResponse response = execute(headMethod)) {
894            assertEquals(headMethod.toString(), OK.getStatusCode(), response.getStatusLine().getStatusCode());
895            numInitialHeaders = response.getAllHeaders().length;
896        }
897
898        // Add user type
899        final URI userType = URI.create("http://example.org/ObjectType");
900        setProperty(id, type.toString(), userType);
901
902        try (final CloseableHttpResponse response = execute(headMethod)) {
903            assertEquals(headMethod.toString(), OK.getStatusCode(), response.getStatusLine().getStatusCode());
904
905            // Should be an additional header from the previous GET request
906            assertEquals(numInitialHeaders + 1, response.getAllHeaders().length);
907
908            // Verify presence of user type
909            checkForLinkHeader(response, userType.toString(), "type");
910        }
911
912        // Verify presence of user type on NonRDFSource... if applicable
913        if (id.endsWith(FCR_METADATA)) {
914            // Strip the trailing /fcr:metadata
915            final HttpHead headBinary = new HttpHead(serverAddress + id.replace("/" + FCR_METADATA, ""));
916
917            try (final CloseableHttpResponse response = execute(headBinary)) {
918                assertEquals(headBinary.toString(), OK.getStatusCode(), response.getStatusLine().getStatusCode());
919
920                // Verify presence of user type
921                checkForLinkHeader(response, userType.toString(), "type");
922            }
923        }
924    }
925
926    @Test
927    public void testDeleteObject() {
928        final String id = getRandomUniqueId();
929        createObjectAndClose(id);
930        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id)));
931        assertDeleted(id);
932    }
933
934    @Test
935    public void testDeleteContainerWithDepthHeaderSet() {
936        final String id = getRandomUniqueId();
937        createObjectAndClose(id);
938        final HttpDelete httpDelete = deleteObjMethod(id);
939        httpDelete.addHeader("Depth", "infinity");
940        assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpDelete));
941        assertDeleted(id);
942    }
943
944    @Test
945    public void testDeleteContainerWithIncorrectDepthHeaderSet() {
946        final String id = getRandomUniqueId();
947        createObjectAndClose(id);
948        final HttpDelete httpDelete = deleteObjMethod(id);
949        httpDelete.addHeader("Depth", "0");
950        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(httpDelete));
951        assertNotDeleted(id);
952    }
953
954    @Test
955    public void testDeleteHierarchy() {
956        final String id = getRandomUniqueId();
957        createObjectAndClose(id);
958        createObjectAndClose(id + "/foo");
959        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id)));
960        assertDeleted(id);
961        assertDeleted(id + "/foo");
962    }
963
964    @Test
965    public void testEmptyPatch() {
966        final String id = getRandomUniqueId();
967        createObjectAndClose(id);
968
969        final HttpPatch patch = patchObjMethod(id);
970        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
971        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(patch));
972    }
973
974    @Test
975    public void testUpdateObjectGraph() throws IOException {
976        final String id = getRandomUniqueId();
977        createObjectAndClose(id);
978
979        final String location = serverAddress + id;
980        final HttpPatch updateObjectGraphMethod = new HttpPatch(location);
981        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
982        updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" + location + "> " +
983                "<http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\" } WHERE {}"));
984        assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod));
985    }
986
987    @Test
988    public void testDeleteMultipleMultiValuedProperties() throws IOException {
989        final String id = getRandomUniqueId();
990        createObjectAndClose(id);
991
992        final String location = serverAddress + id;
993        final HttpPatch addTriplesGraphMethod = new HttpPatch(location);
994        addTriplesGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
995
996        final String insertStatement = "INSERT DATA { \n" +
997                "  <> <http://example.org/test/x> \"x\" . \n" +
998                "  <> <http://example.org/test/a> \"1\" . \n" +
999                "  <> <http://example.org/test/a> \"2\" . \n" +
1000                "  <> <http://example.org/test/a> \"3\" . \n" +
1001                "  <> <http://example.org/test/a> \"4\" . \n" +
1002                "  <> <http://example.org/test/b> \"1\" . \n" +
1003                "  <> <http://example.org/test/b> \"2\" . \n" +
1004                "  <> <http://example.org/test/b> \"3\" . \n" +
1005                "  <> <http://example.org/test/b> \"4\" . \n" +
1006                "}";
1007
1008        addTriplesGraphMethod.setEntity(new StringEntity(insertStatement));
1009        assertEquals(NO_CONTENT.getStatusCode(), getStatus(addTriplesGraphMethod));
1010
1011        // ensure that the triples are there.
1012        try (final CloseableDataset dataset = getDataset(getObjMethod(id))) {
1013            final DatasetGraph graph = dataset.asDatasetGraph();
1014            assertTrue("Didn't find a triple we expected!", graph.contains(ANY,
1015                    createURI(location), createURI("http://example.org/test/x"), createLiteral("x")));
1016            assertTrue("Didn't find a triple we expected!", graph.contains(ANY,
1017                    createURI(location), createURI("http://example.org/test/a"), createLiteral("1")));
1018
1019        }
1020
1021        final HttpPatch deleteQuery = new HttpPatch(location);
1022        deleteQuery.addHeader(CONTENT_TYPE, "application/sparql-update");
1023
1024        final String deleteQueryStatement = "" +
1025                "DELETE \n" +
1026                "WHERE \n" +
1027                "{ \n" +
1028                "  <> <http://example.org/test/a> ?a . \n" +
1029                "  <> <http://example.org/test/b> ?b . \n" +
1030                "} ";
1031
1032        deleteQuery.setEntity(new StringEntity(deleteQueryStatement));
1033        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteQuery));
1034
1035        // ensure that the expected triples removed.
1036        try (final CloseableDataset dataset = getDataset(getObjMethod(id))) {
1037            final DatasetGraph graph = dataset.asDatasetGraph();
1038            assertTrue("Didn't find a triple we expected!", graph.contains(ANY,
1039                    createURI(location), createURI("http://example.org/test/x"), createLiteral("x")));
1040            for (int i = 0; i < 4; ++i) {
1041                for (final String suffix : Arrays.asList("a", "b")) {
1042                    assertFalse("Found a triple we deleted!", graph.contains(ANY,
1043                            createURI(location), createURI("http://example.org/test/" + suffix), createLiteral(i +
1044                                    "")));
1045                }
1046            }
1047        }
1048    }
1049
1050    @Test
1051    public void testPatchBinary() throws IOException {
1052        final String id = getRandomUniqueId();
1053        createDatastream(id, "x", "some content");
1054
1055        final HttpPatch patch = patchObjMethod(id + "/x");
1056        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
1057        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(patch));
1058    }
1059
1060    /**
1061     * Descriptions of bitstreams contain triples about the described thing, so only triples with the described thing
1062     * as their subject are legal.
1063     *
1064     * @throws IOException in case of IOException
1065     */
1066    @Test
1067    public void testPatchBinaryDescription() throws IOException {
1068        final String id = getRandomUniqueId();
1069        createDatastream(id, "x", "some content");
1070
1071        final String location = serverAddress + id + "/x/fcr:metadata";
1072        final HttpPatch patch = new HttpPatch(location);
1073        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
1074        patch.setEntity(new StringEntity("INSERT { <" + serverAddress + id + "/x> "
1075                + "<http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\" } WHERE {}"));
1076        assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch));
1077    }
1078
1079    /**
1080     * Descriptions of bitstreams contain only triples about the described thing, so only triples with the described
1081     * thing as their subject are legal.
1082     *
1083     * @throws IOException on error
1084     */
1085    @Test
1086    public void testPatchBinaryDescriptionWithBinaryProperties() throws IOException {
1087        final String id = getRandomUniqueId();
1088        createDatastream(id, "x", "some content");
1089        final String resource = serverAddress + id;
1090        final String location = resource + "/x/fcr:metadata";
1091
1092        final HttpPatch patch = new HttpPatch(location);
1093        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
1094        patch.setEntity(new StringEntity("INSERT { <" +
1095                resource + "/x>" + " <" + DC_IDENTIFIER + "> \"identifier\" } WHERE {}"));
1096        try (final CloseableHttpResponse response = execute(patch)) {
1097            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
1098            try (final CloseableDataset dataset = getDataset(new HttpGet(location))) {
1099                final DatasetGraph graph = dataset.asDatasetGraph();
1100                assertTrue(graph.contains(ANY,
1101                        createURI(resource + "/x"), DC_IDENTIFIER, createLiteral("identifier")));
1102            }
1103        }
1104    }
1105
1106    @Test
1107    public void testPatchBinaryNameAndType() throws IOException {
1108        final String pid = getRandomUniqueId();
1109
1110        createDatastream(pid, "x", "some content");
1111
1112        final String location = serverAddress + pid + "/x/fcr:metadata";
1113        final HttpPatch patch = new HttpPatch(location);
1114        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
1115        patch.setEntity(new StringEntity("DELETE { " +
1116                "<" + serverAddress + pid + "/x> <" + HAS_MIME_TYPE + "> ?any . } " +
1117                "WHERE {" +
1118                "<" + serverAddress + pid + "/x> <" + HAS_MIME_TYPE + "> ?any . } ; " +
1119                "INSERT {" +
1120                "<" + serverAddress + pid + "/x> <" + HAS_MIME_TYPE + "> \"text/awesome\" ." +
1121                "<" + serverAddress + pid + "/x> <" + HAS_ORIGINAL_NAME + "> \"x.txt\" }" +
1122                "WHERE {}"));
1123
1124        try (final CloseableHttpResponse response = client.execute(patch)) {
1125            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
1126            try (final CloseableDataset dataset = getDataset(new HttpGet(location))) {
1127                final DatasetGraph graphStore = dataset.asDatasetGraph();
1128                final Node subject = createURI(serverAddress + pid + "/x");
1129                assertTrue(graphStore.contains(ANY, subject, HAS_MIME_TYPE.asNode(), createLiteral("text/awesome")));
1130                assertTrue(graphStore.contains(ANY, subject, HAS_ORIGINAL_NAME.asNode(), createLiteral("x.txt")));
1131                assertFalse("Should not contain old mime type property", graphStore.contains(ANY,
1132                        subject, createURI(REPOSITORY_NAMESPACE + "mimeType"), ANY));
1133            }
1134        }
1135
1136        // Ensure binary can be downloaded (test against regression of: https://jira.duraspace.org/browse/FCREPO-1720)
1137        assertEquals(OK.getStatusCode(), getStatus(getDSMethod(pid, "x")));
1138    }
1139
1140    @Test
1141    public void testPatchWithBlankNode() throws Exception {
1142        final String id = getRandomUniqueId();
1143        createObjectAndClose(id);
1144
1145        final String location = serverAddress + id;
1146        final HttpPatch updateObjectGraphMethod = patchObjMethod(id);
1147        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
1148        updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" +
1149                location + "> <info:some-predicate> _:a .\n " +
1150                "_:a <http://purl.org/dc/elements/1.1/title> \"this is a title\"\n" + " } WHERE {}"));
1151        assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod));
1152
1153        try (final CloseableDataset dataset = getDataset(new HttpGet(location))) {
1154            final DatasetGraph graphStore = dataset.asDatasetGraph();
1155            assertTrue(graphStore.contains(ANY, createURI(location), createURI("info:some-predicate"), ANY));
1156            final Node bnode = graphStore.find(ANY,
1157                    createURI(location), createURI("info:some-predicate"), ANY).next().getObject();
1158            assertTrue(graphStore.contains(ANY, bnode, DCTITLE, createLiteral("this is a title")));
1159        }
1160    }
1161
1162    @Test
1163    public void testReplaceGraph() throws IOException {
1164        final String id = getRandomUniqueId();
1165        createObjectAndClose(id);
1166
1167        final String subjectURI = serverAddress + id;
1168        final String initialContent;
1169        final HttpGet getMethod = getObjMethod(id);
1170        getMethod.addHeader("Prefer", "return=representation; omit=\"" + PREFER_SERVER_MANAGED + "\"");
1171        try (final CloseableHttpResponse subjectResponse = execute(getMethod)) {
1172            initialContent = EntityUtils.toString(subjectResponse.getEntity());
1173        }
1174        final HttpPut replaceMethod = putObjMethod(id);
1175        replaceMethod.addHeader(CONTENT_TYPE, "text/n3");
1176        replaceMethod
1177                .setEntity(new StringEntity(initialContent + "\n<" + subjectURI + "> <info:test#label> \"foo\""));
1178        try (final CloseableHttpResponse response = execute(replaceMethod)) {
1179            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
1180            assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified"));
1181            assertTrue("Didn't find ETag header!", response.containsHeader("ETag"));
1182        }
1183        try (final CloseableDataset dataset = getDataset(getObjMethod(id))) {
1184            final DatasetGraph graph = dataset.asDatasetGraph();
1185            assertTrue("Didn't find a triple we tried to create!", graph.contains(ANY,
1186                    createURI(subjectURI), createURI("info:test#label"), createLiteral("foo")));
1187        }
1188    }
1189
1190    @Test
1191    public void testCreateGraph() throws IOException {
1192        final String subjectURI = serverAddress + getRandomUniqueId();
1193        final HttpPut createMethod = new HttpPut(subjectURI);
1194        createMethod.addHeader(CONTENT_TYPE, "text/n3");
1195        createMethod.setEntity(new StringEntity("<" + subjectURI + "> <info:test#label> \"foo\""));
1196        assertEquals(CREATED.getStatusCode(), getStatus(createMethod));
1197
1198        try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) {
1199            final DatasetGraph graph = dataset.asDatasetGraph();
1200            assertTrue("Didn't find a triple we tried to create!", graph.contains(ANY,
1201                    createURI(subjectURI), createURI("info:test#label"), createLiteral("foo")));
1202        }
1203    }
1204
1205    @Test
1206    public void testCreateVersionedRDFResource() throws IOException {
1207        createVersionedRDFResource();
1208    }
1209
1210    @Test
1211    public void testGetVersionedResourceHeaders() throws IOException {
1212        final String subjectURI = createVersionedRDFResource();
1213        try (final CloseableHttpResponse response = execute(new HttpGet(subjectURI))) {
1214            verifyVersionedResourceResponseHeaders(subjectURI, response);
1215            checkForLinkHeader(response, RDF_SOURCE.getURI(), "type");
1216        }
1217    }
1218
1219    @Test
1220    public void testHeadVersionedResourceHeaders() throws IOException {
1221        final String subjectURI = createVersionedRDFResource();
1222        try (final CloseableHttpResponse response = execute(new HttpHead(subjectURI))) {
1223            verifyVersionedResourceResponseHeaders(subjectURI, response);
1224        }
1225    }
1226
1227    private void verifyVersionedResourceResponseHeaders(final String subjectURI,
1228            final CloseableHttpResponse response) {
1229        assertEquals("Didn't get an OK (200) response!", OK.getStatusCode(), getStatus(response));
1230        checkForVersionedResourceLinkHeader(response);
1231        checkForMementoTimeGateLinkHeader(response);
1232        checkForLinkHeader(response, subjectURI, "original");
1233        checkForLinkHeader(response, subjectURI, "timegate");
1234        checkForLinkHeader(response, subjectURI + "/" + FCR_VERSIONS, "timemap");
1235        checkForLinkHeader(response, subjectURI + "/" + FCR_ACL, "acl");
1236        assertEquals(1, Arrays.stream(response.getHeaders("Vary")).filter(x -> x.getValue().contains(
1237                "Accept-Datetime")).count());
1238    }
1239
1240    private String createVersionedRDFResource() throws IOException {
1241        final String id = getRandomUniqueId();
1242        final String subjectURI = serverAddress + id;
1243        final HttpPost createMethod = postObjMethod();
1244        createMethod.addHeader("Slug", id);
1245        createMethod.addHeader(CONTENT_TYPE, "text/n3");
1246        createMethod.setEntity(new StringEntity("<" + subjectURI + "> <info:test#label> \"foo\""));
1247
1248        try (final CloseableHttpResponse response = execute(createMethod)) {
1249            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
1250            checkForVersionedResourceLinkHeader(response);
1251        }
1252        return subjectURI;
1253    }
1254
1255    private void checkForVersionedResourceLinkHeader(final CloseableHttpResponse response) {
1256        checkForLinkHeader(response, VERSIONED_RESOURCE.getURI(), "type");
1257    }
1258
1259    private void checkForMementoTimeGateLinkHeader(final CloseableHttpResponse response) {
1260        checkForLinkHeader(response, VERSIONING_TIMEGATE_TYPE, "type");
1261    }
1262
1263    @Test
1264    public void testCreateVersionedBinaryResource() throws IOException {
1265        final HttpPost method = postObjMethod();
1266        final String id = getRandomUniqueId();
1267        method.addHeader("Slug", id);
1268        method.addHeader(CONTENT_TYPE, "text/plain");
1269        method.setEntity(new StringEntity("test content"));
1270
1271        method.addHeader(LINK, VERSIONED_RESOURCE_LINK_HEADER);
1272        try (final CloseableHttpResponse response = execute(method)) {
1273            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
1274            checkForVersionedResourceLinkHeader(response);
1275        }
1276    }
1277
1278    private String createAcl() throws UnsupportedEncodingException {
1279        final String aclPid = "acl" + getRandomUniqueId();
1280        final String aclURI = serverAddress + aclPid;
1281        createObjectAndClose(aclPid);
1282        final HttpPatch patch = patchObjMethod(aclPid);
1283        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
1284        // add webac:Acl type to aclURI
1285        patch.setEntity(new StringEntity(
1286                "INSERT { <> a <http://fedora.info/definitions/v4/webac#Acl> } WHERE {}"));
1287        assertEquals("Couldn't add webac:Acl type", NO_CONTENT.getStatusCode(), getStatus(patch));
1288        return aclURI;
1289    }
1290
1291    @Test
1292    public void testCreateGraphWithBlanknodes() throws IOException {
1293        final String subjectURI = serverAddress + getRandomUniqueId();
1294        final HttpPut createMethod = new HttpPut(subjectURI);
1295        createMethod.addHeader(CONTENT_TYPE, "text/n3");
1296        createMethod.setEntity(new StringEntity("<" + subjectURI + "> <info:some-predicate> _:a ." +
1297                "_:a <info:test#label> \"asdfg\""));
1298        assertEquals(CREATED.getStatusCode(), getStatus(createMethod));
1299
1300        final HttpGet getObjMethod = new HttpGet(subjectURI);
1301        getObjMethod.addHeader(ACCEPT, "application/rdf+xml");
1302        try (final CloseableDataset dataset = getDataset(getObjMethod)) {
1303            final DatasetGraph graph = dataset.asDatasetGraph();
1304            final Iterator<Quad> quads =
1305                    graph.find(ANY, createURI(subjectURI), createURI("info:some-predicate"), ANY);
1306            assertTrue("Didn't find skolemized blank node assertion", quads.hasNext());
1307            final Node skolemizedNode = quads.next().getObject();
1308            assertTrue("Didn't find a triple we tried to create!", graph.contains(ANY,
1309                    skolemizedNode, createURI("info:test#label"), createLiteral("asdfg")));
1310        }
1311    }
1312
1313    @Test
1314    public void testRoundTripReplaceGraph() throws IOException {
1315
1316        final String subjectURI = getLocation(postObjMethod());
1317        final HttpGet getObjMethod = new HttpGet(subjectURI);
1318        getObjMethod.addHeader(ACCEPT, "text/turtle");
1319        getObjMethod.addHeader("Prefer", "return=representation; omit=\"" + PREFER_SERVER_MANAGED + "\"");
1320
1321        final Model model = createDefaultModel();
1322        try (final CloseableHttpResponse getResponse = execute(getObjMethod)) {
1323            model.read(getResponse.getEntity().getContent(), subjectURI, "TURTLE");
1324        }
1325        final HttpPut replaceMethod = new HttpPut(subjectURI);
1326        replaceMethod.addHeader(CONTENT_TYPE, "application/n-triples");
1327        try (final StringWriter w = new StringWriter()) {
1328            model.write(w, "N-TRIPLE");
1329            replaceMethod.setEntity(new StringEntity(w.toString()));
1330            logger.trace("Retrieved object graph for testRoundTripReplaceGraph():\n {}", w);
1331        }
1332        assertEquals(NO_CONTENT.getStatusCode(), getStatus(replaceMethod));
1333    }
1334
1335    @Test
1336    public void testPutBinary() throws IOException {
1337
1338        final String id = getRandomUniqueId();
1339        createObjectAndClose(id);
1340
1341        final String location = serverAddress + id + "/x";
1342        final HttpPut method = new HttpPut(location);
1343        method.setEntity(new StringEntity("foo"));
1344        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1345        try (final CloseableHttpResponse response = execute(method)) {
1346            assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified"));
1347            assertTrue("Didn't find ETag header!", response.containsHeader("ETag"));
1348
1349            final String receivedLocation = response.getFirstHeader("Location").getValue();
1350            assertEquals(CREATED.getStatusCode(), getStatus(response));
1351            assertEquals("Got wrong URI in Location header for datastream creation!", location, receivedLocation);
1352
1353            assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified"));
1354            final String lastmod = response.getFirstHeader("Last-Modified").getValue();
1355            assertNotNull("Should set Last-Modified for new nodes", lastmod);
1356            assertNotEquals("Last-Modified should not be blank for new nodes", lastmod.trim(), "");
1357            final Link link = Link.valueOf(response.getFirstHeader(LINK).getValue());
1358            assertEquals("describedby", link.getRel());
1359        }
1360    }
1361
1362    @Test
1363    public void testPutBinaryChildViolation() throws IOException {
1364        final String id = getRandomUniqueId();
1365        createObject(id);
1366        createDatastream(id, "binary", "some-content");
1367
1368        final String location = serverAddress + id + "/binary/xx";
1369        assertEquals("Should be a 409 Conflict!", CONFLICT.getStatusCode(), getStatus(new HttpPut(location)));
1370    }
1371
1372    @Test
1373    public void testBinaryEtags() throws IOException, InterruptedException {
1374        final String id = getRandomUniqueId();
1375        createObjectAndClose(id);
1376        final String binaryLocation = serverAddress + id + "/binary";
1377        final HttpPut method = putDSMethod(id, "binary", "foo");
1378
1379        final String binaryEtag1, binaryEtag2, binaryEtag3, descEtag1, descEtag2, descEtag3;
1380        final String binaryLastModed1, binaryLastModed2, binaryLastModed3;
1381        final String descLastModed1, descLastModed2, descLastModed3;
1382        final String descLocation;
1383
1384        try (final CloseableHttpResponse response = execute(method)) {
1385            binaryEtag1 = response.getFirstHeader("ETag").getValue();
1386            binaryLastModed1 = response.getFirstHeader("Last-Modified").getValue();
1387            descLocation = Link.valueOf(response.getFirstHeader(LINK).getValue()).getUri().toString();
1388        }
1389
1390        // First check ETags and Last-Modified headers for the binary
1391        final HttpGet get1 = new HttpGet(binaryLocation);
1392        get1.addHeader("If-None-Match", binaryEtag1);
1393        assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get1));
1394
1395        final HttpGet get2 = new HttpGet(binaryLocation);
1396        get2.addHeader("If-Modified-Since", binaryLastModed1);
1397        assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get2));
1398
1399        // Next, check ETags and Last-Modified headers on the description
1400        final HttpGet get3 = new HttpGet(descLocation);
1401        try (final CloseableHttpResponse response = execute(get3)) {
1402            descEtag1 = response.getFirstHeader("ETag").getValue();
1403            descLastModed1 = response.getFirstHeader("Last-Modified").getValue();
1404        }
1405        assertNotEquals("Binary, description ETags should be different", binaryEtag1, descEtag1);
1406
1407        final HttpGet get4 = new HttpGet(descLocation);
1408        get4.addHeader("If-None-Match", descEtag1);
1409        assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get4));
1410
1411        final HttpGet get5 = new HttpGet(descLocation);
1412        get5.addHeader("If-Modified-Since", descLastModed1);
1413        assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get5));
1414
1415        // Pause two seconds before updating the description
1416        sleep(2000);
1417
1418        // Next, update the description
1419        final HttpPatch httpPatch = patchObjMethod(id + "/binary/fcr:metadata");
1420        assertTrue("Expected weak ETag", descEtag1.startsWith("W/"));
1421        httpPatch.addHeader(CONTENT_TYPE, "application/sparql-update");
1422        httpPatch.addHeader("If-Match", descEtag1.substring(2));
1423        httpPatch.setEntity(new StringEntity(
1424                "INSERT { <> <http://purl.org/dc/elements/1.1/title> 'this is a title' } WHERE {}"));
1425        assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPatch));
1426
1427        // Next, check headers for the binary; they should not have changed
1428        final HttpHead head1 = new HttpHead(binaryLocation);
1429        try (final CloseableHttpResponse response = execute(head1)) {
1430            binaryEtag2 = response.getFirstHeader("ETag").getValue();
1431            binaryLastModed2 = response.getFirstHeader("Last-Modified").getValue();
1432        }
1433
1434        assertEquals("ETags should be the same", binaryEtag1, binaryEtag2);
1435        assertEquals("Last-Modified should be the same", binaryLastModed1, binaryLastModed2);
1436
1437        final HttpGet get6 = new HttpGet(binaryLocation);
1438        get6.addHeader("If-Match", binaryEtag1);
1439        assertEquals("Expected 200", OK.getStatusCode(), getStatus(get6));
1440
1441        final HttpGet get7 = new HttpGet(binaryLocation);
1442        get7.addHeader("If-Unmodified-Since", binaryLastModed1);
1443        assertEquals("Expected 200", OK.getStatusCode(), getStatus(get7));
1444
1445        // Next, check headers for the description; they should have changed
1446        final HttpHead head2 = new HttpHead(descLocation);
1447        try (final CloseableHttpResponse response = execute(head2)) {
1448            descEtag2 = response.getFirstHeader("ETag").getValue();
1449            descLastModed2 = response.getFirstHeader("Last-Modified").getValue();
1450        }
1451
1452        assertNotEquals("ETags should not be the same", descEtag1, descEtag2);
1453        assertNotEquals("Last-Modified should not be the same", descLastModed1, descLastModed2);
1454
1455        final HttpGet get8 = new HttpGet(descLocation);
1456        get8.addHeader("If-None-Match", descEtag2);
1457        assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get8));
1458
1459        final HttpGet get9 = new HttpGet(descLocation);
1460        get9.addHeader("If-Modified-Since", descLastModed2);
1461        assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get9));
1462
1463        sleep(1000);
1464
1465        // Next, update the binary itself
1466        final HttpPut method2 = new HttpPut(binaryLocation);
1467        assertFalse("Expected strong ETag", binaryEtag2.startsWith("W/"));
1468        method2.addHeader("If-Match", binaryEtag2);
1469        method2.setEntity(new StringEntity("foobar"));
1470        try (final CloseableHttpResponse response = execute(method2)) {
1471            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
1472            binaryEtag3 = response.getFirstHeader("ETag").getValue();
1473            binaryLastModed3 = response.getFirstHeader("Last-Modified").getValue();
1474        }
1475
1476        final HttpGet get10 = new HttpGet(binaryLocation);
1477        get10.addHeader("If-None-Match", binaryEtag2);
1478        assertEquals("Expected 200 OK", OK.getStatusCode(), getStatus(get10));
1479
1480        final HttpGet get11 = new HttpGet(binaryLocation);
1481        get11.addHeader("If-Modified-Since", binaryLastModed1);
1482        assertEquals("Expected 200 OK", OK.getStatusCode(), getStatus(get11));
1483
1484        assertNotEquals("ETags should have changed", binaryEtag1, binaryEtag3);
1485        assertNotEquals("Last-Modified should have changed", binaryLastModed1, binaryLastModed3);
1486
1487        // Next, check headers for the description; they should not have changed
1488        final HttpHead head3 = new HttpHead(descLocation);
1489        try (final CloseableHttpResponse response = execute(head3)) {
1490            descEtag3 = response.getFirstHeader("ETag").getValue();
1491            descLastModed3 = response.getFirstHeader("Last-Modified").getValue();
1492        }
1493
1494        assertEquals("ETags should have changed", descEtag2, descEtag3);
1495        assertEquals("Last-Modified should have changed", descLastModed2, descLastModed3);
1496    }
1497
1498    @Test
1499    public void testPutBinaryRdfChanges() throws Exception {
1500        final String id = getRandomUniqueId();
1501        final Model model = createDefaultModel();
1502        final Resource subject = model.createResource(serverAddress + id);
1503        final Property property = model.createProperty("http://purl.org/dcterms/", "title");
1504
1505        // Create the binary
1506        createObjectAndClose(id, "<" + NON_RDF_SOURCE + ">; rel=\"type\"");
1507
1508        // Get the current description
1509        final HttpGet get = getObjMethod(id + "/" + FCR_METADATA);
1510        get.addHeader("Prefer",
1511            "return=representation; omit=\"http://fedora.info/definitions/v4/repository#ServerManaged\"");
1512        get.addHeader("Accept", "text/turtle");
1513        try (final CloseableHttpResponse response = execute(get)) {
1514            assertEquals("Expected 200 OK", OK.getStatusCode(), getStatus(response));
1515            model.read(response.getEntity().getContent(), null, "TURTLE");
1516        }
1517
1518        // Put triples to the description.
1519        model.add(subject, property, model.createLiteral("ABC"));
1520        final ByteArrayOutputStream out1 = new ByteArrayOutputStream();
1521        final Writer writer1 = new OutputStreamWriter(out1, "UTF-8");
1522        model.write(writer1, "TURTLE");
1523
1524        final HttpPut put = putObjMethod(id + "/" + FCR_METADATA);
1525        final String outputModel = out1.toString("UTF-8");
1526        put.setEntity(new StringEntity(outputModel, "UTF-8"));
1527        put.setHeader("Content-type", "text/turtle");
1528        put.setHeader("Prefer", "handling=lenient; received=\"minimal\"");
1529        assertEquals("Did put the binary description", NO_CONTENT.getStatusCode(), getStatus(put));
1530
1531        // Get the description and verify the content.
1532        final Model model2 = createDefaultModel();
1533        try (final CloseableHttpResponse response = execute(get)) {
1534            assertEquals("Expected 200 OK", OK.getStatusCode(), getStatus(response));
1535            model2.read(response.getEntity().getContent(), serverAddress, "TURTLE");
1536            final StmtIterator st = model.listStatements(subject, property, "ABC");
1537            assertTrue(st.hasNext());
1538        }
1539
1540        // Alter the triples.
1541        model2.remove(subject, property, model.createLiteral("ABC"));
1542        model2.add(subject, property, model.createLiteral("XYZ"));
1543
1544        // Put it again.
1545        final ByteArrayOutputStream out2 = new ByteArrayOutputStream();
1546        final Writer writer2 = new OutputStreamWriter(out2, "UTF-8");
1547        model2.write(writer2, "TURTLE");
1548        final HttpPut put2 = putObjMethod(id + "/" + FCR_METADATA);
1549        put2.setHeader("Content-type", "text/turtle");
1550        put2.setHeader("Prefer", "handling=lenient; received=\"minimal\"");
1551        put2.setEntity(new ByteArrayEntity(out2.toByteArray()));
1552        assertEquals("Did not update binary description", NO_CONTENT.getStatusCode(), getStatus(put2));
1553
1554        // Get the description and verify the content again.
1555        final Model model3 = createDefaultModel();
1556        try (final CloseableHttpResponse response = execute(get)) {
1557            assertEquals("Expected 200 OK", OK.getStatusCode(), getStatus(response));
1558            model3.read(response.getEntity().getContent(), serverAddress, "TURTLE");
1559            final StmtIterator st1 = model3.listStatements(subject, property, "XYZ");
1560            assertTrue(st1.hasNext());
1561            final StmtIterator st2 = model3.listStatements(subject, property, "ABC");
1562            assertFalse(st2.hasNext());
1563        }
1564    }
1565
1566    @Test
1567    public void testETagOnDeletedChild() throws Exception {
1568        final String id = getRandomUniqueId();
1569        final String child = id + "/child";
1570        createObjectAndClose(id);
1571        createObjectAndClose(child);
1572
1573        final HttpGet get = new HttpGet(serverAddress + id);
1574        final String etag1;
1575        try (final CloseableHttpResponse response = execute(get)) {
1576            etag1 = response.getFirstHeader("ETag").getValue();
1577        }
1578
1579        // If the child is created and deleted in the same second the eTag would be the same.
1580        // Wait to delete.
1581        TimeUnit.SECONDS.sleep(1);
1582
1583        assertEquals("Child resource not deleted!", NO_CONTENT.getStatusCode(),
1584                getStatus(new HttpDelete(serverAddress + child)));
1585        final String etag2;
1586        try (final CloseableHttpResponse response = execute(get)) {
1587            etag2 = response.getFirstHeader("ETag").getValue();
1588        }
1589
1590        assertNotEquals("ETag didn't change!", etag1, etag2);
1591    }
1592
1593    @Test
1594    public void testContainmentHashChanges() throws Exception {
1595        final String parentUri;
1596        final String createdEtag;
1597        // Create a resource and note its ETag
1598        try (final var response = execute(postObjMethod())) {
1599            assertEquals(CREATED.getStatusCode(), getStatus(response));
1600            parentUri = getLocation(response);
1601            createdEtag = getEtag(response);
1602        }
1603
1604        // Create one child and see the parent's ETag changes
1605        final String oneChildEtag;
1606        assertEquals(CREATED.getStatusCode(), getStatus(new HttpPost(parentUri)));
1607        try (final var response = execute(new HttpGet(parentUri))) {
1608            oneChildEtag = getEtag(response);
1609            assertEquals(OK.getStatusCode(), getStatus(response));
1610            assertNotEquals(createdEtag, oneChildEtag);
1611        }
1612
1613        // We use the last created time for the eTag, if 2 children are created in the same second there is no
1614        // difference, so we wait.
1615        TimeUnit.SECONDS.sleep(1);
1616
1617        // Create another child
1618        final String otherChild;
1619        try (final var response = execute(new HttpPost(parentUri))) {
1620            assertEquals(CREATED.getStatusCode(), getStatus(response));
1621            otherChild = getLocation(response);
1622        }
1623        // See the parent's ETag changes again.
1624        final var currentETag = getEtag(parentUri);
1625        assertNotEquals(createdEtag, currentETag);
1626        assertNotEquals(oneChildEtag, currentETag);
1627
1628        TimeUnit.SECONDS.sleep(1);
1629
1630        // Delete the second child.
1631        assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(otherChild)));
1632        // See the parent's ETag change again.
1633        final var currentETag2 = getEtag(parentUri);
1634        assertNotEquals(createdEtag, currentETag2);
1635        assertNotEquals(oneChildEtag, currentETag2);
1636        assertNotEquals(currentETag, currentETag2);
1637
1638        // Request parent with containment omitted, etag should change
1639        final var httpGetOmitContainment = new HttpGet(parentUri);
1640        final String preferHeader2 = "return=representation; omit=\"" +
1641                RdfLexicon.PREFER_CONTAINMENT.getURI() + "\"";
1642        httpGetOmitContainment.setHeader("Prefer", preferHeader2);
1643        final String omitEtag = getEtag(httpGetOmitContainment);
1644        assertEquals("Etag should match the etag before adding a child", createdEtag, omitEtag);
1645        assertNotEquals(oneChildEtag, omitEtag);
1646        assertNotEquals(currentETag, omitEtag);
1647        assertNotEquals(currentETag2, omitEtag);
1648    }
1649
1650    @Test
1651    public void testPutDatastreamContentOnObject() throws IOException {
1652        final String content = "foo";
1653        final String id = getRandomUniqueId();
1654        createObjectAndClose(id);
1655
1656        final HttpPut put = putObjMethod(id);
1657        put.setEntity(new StringEntity(content));
1658        put.setHeader(CONTENT_TYPE, "application/octet-stream");
1659        assertEquals(
1660                "Expected UNSUPPORTED MEDIA TYPE response when PUTing content to an object (as opposed to datastream)",
1661                UNSUPPORTED_MEDIA_TYPE.getStatusCode(), getStatus(put));
1662    }
1663
1664    @Test
1665    public void testPutMalformedRDFOnObject() throws IOException {
1666        final String content = "this is not legitimate RDF";
1667        final String id = getRandomUniqueId();
1668        createObjectAndClose(id);
1669
1670        final HttpPut put = putObjMethod(id);
1671        put.setEntity(new StringEntity(content));
1672        put.setHeader(CONTENT_TYPE, "text/plain");
1673        assertEquals("Expected BAD REQUEST response code when PUTing malformed RDF on an object",
1674                BAD_REQUEST.getStatusCode(), getStatus(put));
1675    }
1676
1677    @Test
1678    public void testIngest() throws IOException {
1679        final String id = getRandomUniqueId();
1680        try (final CloseableHttpResponse response = createObject(id)) {
1681            final String content = EntityUtils.toString(response.getEntity());
1682            assertIdentifierness(content);
1683            assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified"));
1684            assertTrue("Didn't find ETag header!", response.containsHeader("ETag"));
1685            assertTrue("Didn't find Location header!", response.containsHeader("Location"));
1686        }
1687    }
1688
1689    @Test
1690    public void testIngestWithNewAndSparqlQuery() throws IOException {
1691        final HttpPost method = postObjMethod();
1692        method.addHeader(CONTENT_TYPE, "application/sparql-update");
1693        method.setEntity(new StringEntity(
1694                "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"title\" } WHERE {}"));
1695        assertEquals(UNSUPPORTED_MEDIA_TYPE.getStatusCode(), getStatus(method));
1696
1697        final String id = getRandomUniqueId();
1698        final HttpPut putMethod = putObjMethod(id);
1699        putMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
1700        putMethod.setEntity(new StringEntity(
1701                "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"title\" } WHERE {}"));
1702        assertEquals(UNSUPPORTED_MEDIA_TYPE.getStatusCode(), getStatus(putMethod));
1703    }
1704
1705    @Test
1706    public void testIngestWithNewAndGraph() throws IOException {
1707        final HttpPost method = postObjMethod();
1708        method.addHeader(CONTENT_TYPE, "text/n3");
1709        method.setEntity(new StringEntity("<> <http://purl.org/dc/elements/1.1/title> \"title\"."));
1710
1711        try (final CloseableHttpResponse response = execute(method)) {
1712            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
1713            assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified"));
1714            final String lastmod = response.getFirstHeader("Last-Modified").getValue();
1715            assertNotNull("Should set Last-Modified for new nodes", lastmod);
1716            assertNotEquals("Last-Modified should not be blank for new nodes", lastmod.trim(), "");
1717            final String location = getLocation(response);
1718            try (final CloseableDataset dataset = getDataset(new HttpGet(location))) {
1719                final DatasetGraph graphStore = dataset.asDatasetGraph();
1720                assertTrue(graphStore.contains(ANY, createURI(location), DCTITLE, createLiteral("title")));
1721            }
1722        }
1723    }
1724
1725    @Test
1726    public void testIngestWithSlug() throws IOException {
1727        final HttpPost method = postObjMethod();
1728        method.addHeader("Slug", getRandomUniqueId());
1729        try (final CloseableHttpResponse response = execute(method)) {
1730            final String content = EntityUtils.toString(response.getEntity());
1731            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
1732            assertIdentifierness(content);
1733            final String location = getLocation(response);
1734            assertNotEquals(serverAddress + "/objects", location);
1735            assertEquals("Object wasn't created!", OK.getStatusCode(), getStatus(new HttpGet(location)));
1736        }
1737    }
1738
1739    @Test
1740    public void testIngestWithRepeatedSlug() {
1741        final String id = getRandomUniqueId();
1742        assertEquals(CREATED.getStatusCode(), getStatus(putObjMethod(id)));
1743        final HttpPost method = postObjMethod();
1744        method.addHeader("Slug", id);
1745        assertEquals(CREATED.getStatusCode(), getStatus(method));
1746    }
1747
1748    // TODO this was copied from extant tests as a refactoring, but is this a good test for identifier-ness?
1749    public static void assertIdentifierness(final String content) {
1750        assertTrue("Response wasn't a PID", compile("[a-z]+").matcher(content).find());
1751    }
1752
1753    @Test
1754    public void testIngestWithBinary() throws IOException {
1755        final HttpPost method = postObjMethod();
1756        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1757        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1758        final String binaryContent = "xyz";
1759        method.setEntity(new StringEntity(binaryContent));
1760
1761        try (final CloseableHttpResponse response = execute(method)) {
1762            final String content = EntityUtils.toString(response.getEntity());
1763            final int status = getStatus(response);
1764            assertEquals("Didn't get a CREATED response! Got content:\n" + content, CREATED.getStatusCode(), status);
1765            assertIdentifierness(content);
1766            final String location = getLocation(response);
1767
1768            try (final CloseableHttpResponse getResponse = execute(new HttpGet(location))) {
1769                assertEquals("Object wasn't created!", OK.getStatusCode(), getResponse.getStatusLine().getStatusCode());
1770                final String resp = IOUtils.toString(getResponse.getEntity().getContent(), UTF_8);
1771                assertEquals("application/octet-stream", getResponse.getFirstHeader(CONTENT_TYPE).getValue());
1772                assertEquals(binaryContent, resp);
1773            }
1774
1775            final Link link = Link.valueOf(response.getFirstHeader(LINK).getValue());
1776
1777            assertEquals("describedby", link.getRel());
1778            assertTrue("Expected an anchor to the newly created resource", link.getParams().containsKey("anchor"));
1779            assertEquals("Expected anchor at the newly created resource", location,
1780                    link.getParams().get("anchor"));
1781            assertEquals("Expected describedBy link", location + "/" + FCR_METADATA, link.getUri().toString());
1782        }
1783    }
1784
1785    /* Verifies RDF persisted as binary is retrieved with byte-for-byte fidelity */
1786    @Test
1787    public void testIngestOpaqueRdfAsBinary() throws IOException {
1788        final HttpPost method = postObjMethod();
1789        method.addHeader(CONTENT_TYPE, "application/n-triples");
1790        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1791
1792        final String rdf = "<test:/subject> <test:/predicate> <test:/object> .";
1793        method.setEntity(new StringEntity(rdf));
1794
1795        try (final CloseableHttpResponse response = execute(method)) {
1796            assertEquals(CREATED.getStatusCode(), getStatus(response));
1797
1798            final String location = getLocation(response);
1799            final HttpGet get = new HttpGet(location);
1800
1801            try (final CloseableHttpResponse getResponse = execute(get)) {
1802                final String resp = IOUtils.toString(getResponse.getEntity().getContent(), UTF_8);
1803                assertEquals("application/n-triples", getResponse.getFirstHeader(CONTENT_TYPE).getValue());
1804                assertEquals(rdf, resp);
1805            }
1806        }
1807    }
1808
1809    /**
1810     * Ensure that the objects can be created with a Digest header
1811     * with a SHA1 sum of the binary content
1812     */
1813    @Test
1814    public void testIngestWithBinaryAndChecksum() {
1815        final HttpPost method = postObjMethod();
1816        final File img = new File("src/test/resources/test-objects/img.png");
1817        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1818        method.addHeader("Digest", "SHA=f0b632679fab4f22e031010bd81a3b0544294730");
1819        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1820        method.setEntity(new FileEntity(img));
1821
1822        assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(method));
1823    }
1824
1825    /**
1826     * Ensure that the objects cannot be created when a Digest header
1827     * contains a SHA1 sum that does not match the uploaded binary
1828     * content
1829     */
1830    @Test
1831    public void testIngestWithBinaryAndChecksumMismatch() {
1832        final HttpPost method = postObjMethod();
1833        final File img = new File("src/test/resources/test-objects/img.png");
1834        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1835        method.addHeader("Digest", "SHA=fedoraicon");
1836        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1837        method.setEntity(new FileEntity(img));
1838
1839        assertEquals("Should be a 409 Conflict!", CONFLICT.getStatusCode(), getStatus(method));
1840    }
1841
1842    /**
1843     * Ensure that the a malformed Digest header returns a 400 Bad Request
1844     */
1845    @Test
1846    public void testIngestWithBinaryAndMalformedDigestHeader() {
1847        final HttpPost method = postObjMethod();
1848        final File img = new File("src/test/resources/test-objects/img.png");
1849        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1850        method.addHeader("Digest", "md5=not a valid hash,SHA:thisisbadtoo");
1851        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1852        method.setEntity(new FileEntity(img));
1853        assertEquals("Should be a 400 BAD REQUEST!", BAD_REQUEST.getStatusCode(), getStatus(method));
1854    }
1855
1856    /**
1857     * Ensure that a non-SHA1 Digest header returns a 409 Conflict
1858     */
1859    @Test
1860    public void testIngestWithBinaryAndNonSha1DigestHeader() {
1861        final HttpPost method = postObjMethod();
1862        final File img = new File("src/test/resources/test-objects/img.png");
1863        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1864        method.addHeader("Digest", "md5=anything");
1865        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1866        method.setEntity(new FileEntity(img));
1867
1868        assertEquals("Should be a 409 Conflict!", CONFLICT.getStatusCode(), getStatus(method));
1869    }
1870
1871    @Test
1872    public void testIngestWithBinaryAndMD5DigestHeader() {
1873        final HttpPost method = postObjMethod();
1874        final File img = new File("src/test/resources/test-objects/img.png");
1875        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1876        method.addHeader("Digest", "md5=6668675a91f39ca1afe46c084e8406ba");
1877        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1878        method.setEntity(new FileEntity(img));
1879
1880        assertEquals("Should be a 201 Created!", CREATED.getStatusCode(), getStatus(method));
1881    }
1882
1883    @Test
1884    public void testIngestWithBinaryAndTwoValidHeadersDigestHeaders() throws Exception {
1885        final HttpPost method = postObjMethod();
1886        final File img = new File("src/test/resources/test-objects/img.png");
1887        final var md5 = "6668675a91f39ca1afe46c084e8406ba";
1888        final var sha256 = "7b115a72978fe138287c1a6dfe6cc1afce4720fb3610a81d32e4ad518700c923";
1889        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1890        method.addHeader("Digest", "md5=" + md5 + "," +
1891                " sha-256=" + sha256);
1892        method.setEntity(new FileEntity(img));
1893        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1894
1895        final String location;
1896        try (final CloseableHttpResponse response = execute(method)) {
1897            assertEquals("Should be a 201 Created!", CREATED.getStatusCode(), getStatus(method));
1898            location = getLocation(response);
1899        }
1900
1901        // verify that both headers are returned in the metadata.
1902        try (final var response = execute(new HttpGet(location + "/" + FCR_METADATA))) {
1903            try (final var dataset = getDataset(response)) {
1904                final var graph = dataset.asDatasetGraph();
1905                final var model = createModelForGraph(graph.getDefaultGraph());
1906                final var nodeUri = createResource(location);
1907                final var stmts = model.listStatements(nodeUri, HAS_MESSAGE_DIGEST, (String) null);
1908                final var digests = new ArrayList<String>();
1909                while (stmts.hasNext()) {
1910                    digests.add(stmts.nextStatement().getObject().asResource().getURI());
1911                }
1912                assertTrue("contains md5", digests.contains("urn:md5:" + md5));
1913                assertTrue("contains sha-256", digests.contains("urn:sha-256:" + sha256));
1914            }
1915        }
1916    }
1917
1918    @Test
1919    public void testIngestWithBinaryAndValidAndInvalidDigestHeaders() {
1920        final HttpPost method = postObjMethod();
1921        final File img = new File("src/test/resources/test-objects/img.png");
1922        method.addHeader(CONTENT_TYPE, "application/octet-stream");
1923        method.addHeader("Digest", "md5=6668675a91f39ca1afe46c084e8406ba," +
1924                " sha99=7b115a72978fe138287c1a6dfe6cc1afce4720fb3610a81d32e4ad518700c923");
1925        method.setEntity(new FileEntity(img));
1926        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1927
1928        assertEquals("Should be a 400 Bad Request!", BAD_REQUEST.getStatusCode(), getStatus(method));
1929    }
1930
1931    @Test
1932    public void testContentDispositionHeader() throws ParseException, IOException {
1933        final HttpPost method = postObjMethod();
1934        final File img = new File("src/test/resources/test-objects/img.png");
1935        final String filename = "some-file.png";
1936        method.addHeader(CONTENT_TYPE, "application/png");
1937        method.addHeader(CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"");
1938        method.setEntity(new FileEntity(img));
1939        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
1940
1941        // Create a binary resource with content-disposition
1942        final String location;
1943        try (final CloseableHttpResponse response = execute(method)) {
1944            assertEquals("Should be a 201 Created!", CREATED.getStatusCode(), getStatus(response));
1945            location = getLocation(response);
1946        }
1947
1948        // Retrieve the new resource and verify the content-disposition
1949        verifyContentDispositionFilename(location, filename);
1950
1951        // TODO enable once PATCH is working
1952        // DO WE WANT TO ALTER THE USER'S RDF BY ADDING THE TRIPLES?
1953        // Update the filename
1954        // final String filename1 = "new-file.png";
1955        // final HttpPatch patch = new HttpPatch(location + "/" + FCR_METADATA);
1956        // patch.setHeader(CONTENT_TYPE, "application/sparql-update");
1957        // final String updateString = "PREFIX ebucore: <http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#>\n" +
1958        // "DELETE { <> ebucore:filename ?x}\n" +
1959        // "INSERT { <> ebucore:filename \"" + filename1 + "\"}\n" +
1960        // "WHERE { <> ebucore:filename ?x}";
1961        //
1962        // patch.setEntity(new StringEntity(updateString));
1963        // assertEquals(location, NO_CONTENT.getStatusCode(), getStatus(patch));
1964        //
1965        // // Retrieve the new resource and verify the content-disposition
1966        // verifyContentDispositionFilename(location, filename1);
1967    }
1968
1969    private void verifyContentDispositionFilename(final String location, final String filename)
1970            throws IOException, ParseException {
1971        final HttpHead head = new HttpHead(location);
1972        try (final CloseableHttpResponse headResponse = execute(head)) {
1973            final Header header = headResponse.getFirstHeader(CONTENT_DISPOSITION);
1974            assertNotNull(header);
1975
1976            final ContentDisposition contentDisposition = new ContentDisposition(header.getValue());
1977            assertEquals(filename, contentDisposition.getFileName());
1978        }
1979    }
1980
1981    @Test
1982    public void testIngestOnSubtree() throws IOException {
1983        final String id = getRandomUniqueId();
1984        createObjectAndClose(id);
1985        final HttpPost method = postObjMethod(id);
1986        method.addHeader("Slug", "x");
1987        assertEquals(serverAddress + id + "/x", getLocation(method));
1988    }
1989
1990    @Test
1991    public void testIngestWithRDFLang() throws IOException {
1992        final HttpPost method = postObjMethod();
1993        method.addHeader(CONTENT_TYPE, "text/n3");
1994        method.setEntity(new StringEntity("<> <http://purl.org/dc/elements/1.1/title> \"french title\"@fr ."
1995                + "<> <http://purl.org/dc/elements/1.1/title> \"english title\"@en ."));
1996
1997        try (final CloseableHttpResponse response = execute(method)) {
1998            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
1999            final String location = getLocation(response);
2000            try (final CloseableDataset dataset = getDataset(new HttpGet(location))) {
2001                final DatasetGraph graphStore = dataset.asDatasetGraph();
2002                final Node subj = createURI(location);
2003                assertTrue(graphStore.contains(ANY, subj, DCTITLE, createLiteral("english title", "en", false)));
2004                assertTrue(graphStore.contains(ANY, subj, DCTITLE, createLiteral("french title", "fr", false)));
2005            }
2006        }
2007    }
2008
2009    @Test
2010    public void testDeleteWithBadEtag() throws IOException {
2011        try (final CloseableHttpResponse response = execute(postObjMethod())) {
2012            assertEquals(CREATED.getStatusCode(), getStatus(response));
2013            final HttpDelete request = new HttpDelete(getLocation(response));
2014            request.addHeader("If-Match", "\"doesnt-match\"");
2015            assertEquals(PRECONDITION_FAILED.getStatusCode(), getStatus(request));
2016        }
2017    }
2018
2019    @Test
2020    public void testGetDatastream() throws IOException, ParseException {
2021        final String id = getRandomUniqueId();
2022        createObjectAndClose(id);
2023        createDatastream(id, "ds1", "foo");
2024
2025        try (final CloseableHttpResponse response = execute(getDSMethod(id, "ds1"))) {
2026            assertEquals("Wasn't able to retrieve a datastream!", OK.getStatusCode(), getStatus(response));
2027            assertEquals(TEXT_PLAIN, response.getFirstHeader(CONTENT_TYPE).getValue());
2028            assertEquals("3", response.getFirstHeader(CONTENT_LENGTH).getValue());
2029            assertEquals("bytes", response.getFirstHeader("Accept-Ranges").getValue());
2030            final ContentDisposition disposition =
2031                    new ContentDisposition(response.getFirstHeader(CONTENT_DISPOSITION).getValue());
2032            assertEquals("attachment", disposition.getType());
2033
2034            final Collection<String> links = getLinkHeaders(response);
2035            final String describedByHeader =
2036                    "<" + serverAddress + id + "/ds1/" + FCR_METADATA + ">; rel=\"describedby\"";
2037            assertTrue("Didn't find 'describedby' link header!", links.contains(describedByHeader));
2038        }
2039    }
2040
2041    @Test
2042    public void testGetLongRange() throws IOException {
2043        final String id = getRandomUniqueId();
2044        createObjectAndClose(id);
2045        final StringBuilder buf = new StringBuilder();
2046        while ( buf.length() < 9000 ) {
2047            buf.append("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
2048        }
2049        createDatastream(id, "ds1", buf.toString());
2050
2051        final HttpGet get = getDSMethod(id, "ds1");
2052        get.setHeader("Range", "bytes=0-8199");
2053        try (final CloseableHttpResponse response = execute(get)) {
2054            assertEquals("Expected 206 Partial Content!", PARTIAL_CONTENT.getStatusCode(), getStatus(response));
2055            assertEquals("Expected range length (8200)!", "8200", response.getFirstHeader(CONTENT_LENGTH).getValue());
2056        }
2057    }
2058
2059    @Test
2060    public void testDeleteDatastream() throws IOException {
2061        final String id = getRandomUniqueId();
2062        createObjectAndClose(id);
2063        createDatastream(id, "ds1", "foo");
2064        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id + "/ds1")));
2065        assertDeleted(id + "/ds1");
2066    }
2067
2068    @Test
2069    public void testDeleteBinaryDescription() throws IOException {
2070        final String id = getRandomUniqueId();
2071        createObjectAndClose(id, NON_RDF_SOURCE_LINK_HEADER);
2072
2073        // Check that the binary and description exist.
2074        binaryExists(id);
2075
2076        // Try to delete the description.
2077        final HttpDelete deleteBinaryDesc = deleteObjMethod(id + "/" + FCR_METADATA);
2078        try (final CloseableHttpResponse response = execute(deleteBinaryDesc)) {
2079            assertEquals(METHOD_NOT_ALLOWED.getStatusCode(), getStatus(response));
2080        }
2081
2082        // ensure the binary and description still exist.
2083        binaryExists(id);
2084
2085        // Delete the binary
2086        final HttpDelete deleteBinary = deleteObjMethod(id);
2087        try (final CloseableHttpResponse response = execute(deleteBinary)) {
2088            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
2089        }
2090
2091        // Ensure the binary and description are gone.
2092        binaryDoesntExist(id);
2093
2094    }
2095
2096    /**
2097     * Check if a binary and its description exist.
2098     *
2099     * @param id id of the binary.
2100     * @throws IOException on error with http communication.
2101     */
2102    private void binaryExists(final String id) throws IOException {
2103        binaryStatus(id, OK);
2104    }
2105
2106    /**
2107     * Check if a binary and its description don't exist.
2108     *
2109     * @param id id of the binary.
2110     * @throws IOException on error with http communication.
2111     */
2112    private void binaryDoesntExist(final String id) throws IOException {
2113        binaryStatus(id, GONE);
2114    }
2115
2116    /**
2117     * Utility function to confirm status of both a binary and its description
2118     *
2119     * @param id id of the binary.
2120     * @param status the expected status.
2121     * @throws IOException on error with http communication.
2122     */
2123    private void binaryStatus(final String id, final Status status) throws IOException {
2124        final HttpHead headBinary = headObjMethod(id);
2125        final var tombstoneUri = serverAddress + id + "/" + FCR_TOMBSTONE;
2126        try (final CloseableHttpResponse response = execute(headBinary)) {
2127            assertEquals(status.getStatusCode(), getStatus(response));
2128            if (status.equals(GONE)) {
2129                checkForLinkHeader(response, tombstoneUri, "hasTombstone");
2130            }
2131        }
2132
2133        final HttpHead headBinaryDesc = headObjMethod(id + "/" + FCR_METADATA);
2134        try (final CloseableHttpResponse response = execute(headBinaryDesc)) {
2135            assertEquals(status.getStatusCode(), getStatus(response));
2136            if (status.equals(GONE)) {
2137                checkForLinkHeader(response, tombstoneUri, "hasTombstone");
2138            }
2139        }
2140    }
2141
2142    @Test
2143    public void testGetObjectGraphHtml() throws IOException {
2144        final HttpGet getObjMethod = new HttpGet(getLocation(postObjMethod()));
2145        getObjMethod.addHeader(ACCEPT, "text/html");
2146        assertEquals(OK.getStatusCode(), getStatus(getObjMethod));
2147    }
2148
2149    @Test
2150    public void testGetObjectGraphVariants() throws IOException {
2151        final String location = getLocation(postObjMethod());
2152        for (final Variant variant : POSSIBLE_RDF_VARIANTS) {
2153            final HttpGet getObjMethod = new HttpGet(location);
2154            final String type = variant.getMediaType().getType();
2155            getObjMethod.addHeader(ACCEPT, type);
2156            assertEquals("Got bad response for type " + type + " !", OK.getStatusCode(), getStatus(getObjMethod));
2157        }
2158    }
2159
2160    @Test
2161    public void testGetObjectGraph() throws IOException {
2162        logger.trace("Entering testGetObjectGraph()...");
2163        final String location = getLocation(postObjMethod());
2164        final HttpGet getObjMethod = new HttpGet(location);
2165
2166        try (final CloseableHttpResponse response = execute(getObjMethod)) {
2167            assertEquals(OK.getStatusCode(), getStatus(response));
2168            assertResourceOptionsHeaders(response);
2169            checkForLinkHeader(response, RDF_SOURCE.getURI(), "type");
2170            assertTrue("Didn't find LDP link header!", getLinkHeaders(response).contains(LDP_RESOURCE_LINK_HEADER));
2171            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
2172            try (final CloseableDataset dataset = getDataset(response)) {
2173                assertTrue("Didn't find any type triples!", dataset.asDatasetGraph().contains(ANY,
2174                        createURI(location), rdfType, ANY));
2175            }
2176            logger.trace("Leaving testGetObjectGraph()...");
2177        }
2178    }
2179
2180    @Test
2181    public void verifyFullSetOfRdfTypes() throws IOException {
2182        logger.trace("Entering verifyFullSetOfRdfTypes()...");
2183        final String id = getRandomUniqueId();
2184        createObjectAndClose(id);
2185
2186        try (final CloseableDataset dataset = getDataset(getObjMethod(id))) {
2187            final DatasetGraph graph = dataset.asDatasetGraph();
2188            final Node resource = createURI(serverAddress + id);
2189            verifyResource(graph, resource, REPOSITORY_NAMESPACE, "Container");
2190            verifyResource(graph, resource, REPOSITORY_NAMESPACE, "Resource");
2191        }
2192        logger.trace("Leaving verifyFullSetOfRdfTypes()...");
2193    }
2194
2195    private static void verifyResource(final DatasetGraph g, final Node subject, final String ns, final String type) {
2196        assertTrue("Should find type: " + ns + type, g.contains(ANY, subject, rdfType, createURI(ns + type)));
2197    }
2198
2199    @Test
2200    public void testGetObjectGraphWithChild() throws IOException {
2201        final String id = getRandomUniqueId();
2202        final String location = getLocation(createObject(id));
2203        createObjectAndClose(id + "/c");
2204
2205        try (final CloseableHttpResponse response = execute(getObjMethod(id))) {
2206            try (final CloseableDataset dataset = getDataset(response)) {
2207                assertTrue("Didn't find child node!", dataset.asDatasetGraph().contains(ANY,
2208                        createURI(location), CONTAINS.asNode(), createURI(location + "/c")));
2209
2210                final Collection<String> links = getLinkHeaders(response);
2211                assertTrue("Didn't find LDP resource link header!", links.contains(LDP_RESOURCE_LINK_HEADER));
2212            }
2213        }
2214    }
2215
2216    @Test
2217    public void testGetObjectGraphWithChildAndRemove() throws IOException {
2218        final String id = getRandomUniqueId();
2219        final String location = getLocation(createObject(id));
2220        createObjectAndClose(id + "/c");
2221        try (final CloseableHttpResponse response = execute(getObjMethod(id))) {
2222            try (final CloseableDataset dataset = getDataset(response)) {
2223                assertTrue("Didn't find child node!", dataset.asDatasetGraph().contains(ANY,
2224                        createURI(location), CONTAINS.asNode(), createURI(location + "/c")));
2225            }
2226        }
2227
2228        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id + "/c")));
2229
2230        try (final CloseableHttpResponse response1 = execute(getObjMethod(id))) {
2231            try (final CloseableDataset dataset = getDataset(response1)) {
2232                assertFalse("Found child node!", dataset.asDatasetGraph().contains(ANY,
2233                        createURI(location), CONTAINS.asNode(), createURI(location + "/c")));
2234            }
2235        }
2236    }
2237
2238    @Test
2239    public void testGetObjectGraphWithChildren() throws IOException {
2240        final String id = getRandomUniqueId();
2241        final String location = getLocation(createObject(id));
2242
2243        // Create some children
2244        final int CHILDREN_TOTAL = 20;
2245        for (int x = 0; x < CHILDREN_TOTAL; ++x) {
2246            createObjectAndClose(id + "/child-" + x);
2247        }
2248
2249        final int CHILDREN_LIMIT = 12;
2250        final HttpGet httpGet = getObjMethod(id);
2251        httpGet.setHeader("Limit", Integer.toString(CHILDREN_LIMIT));
2252        try (final CloseableHttpResponse response = execute(httpGet)) {
2253            try (final CloseableDataset dataset = getDataset(response)) {
2254                final DatasetGraph graph = dataset.asDatasetGraph();
2255                final Iterator<Quad> contains = graph.find(ANY, createURI(location), CONTAINS.asNode(), ANY);
2256                assertTrue("Should find contained child!", contains.hasNext());
2257                assertEquals(CHILDREN_LIMIT, Iterators.size(contains));
2258            }
2259        }
2260    }
2261
2262    @Test
2263    public void testGetObjectGraphWithBadLimit() throws IOException {
2264        final String id = getRandomUniqueId();
2265        getLocation(createObject(id));
2266
2267        final HttpGet httpGet = getObjMethod(id);
2268        httpGet.setHeader("Limit", "not-an-integer");
2269        try (final CloseableHttpResponse response = execute(httpGet)) {
2270            assertEquals(SC_BAD_REQUEST, response.getStatusLine().getStatusCode());
2271        }
2272    }
2273
2274
2275
2276    @Test
2277    public void testPatchToCreateDirectContainerInSparqlUpdateFails() throws IOException {
2278        final String id = getRandomUniqueId();
2279        createObjectAndClose(id);
2280        final HttpPatch patch = patchObjMethod(id);
2281        patch.setHeader(CONTENT_TYPE, "application/sparql-update");
2282        final String updateString =
2283                "INSERT DATA { <> a <" + DIRECT_CONTAINER.getURI() + "> ; <" + MEMBERSHIP_RESOURCE.getURI() +
2284                        "> <> ; <" + HAS_MEMBER_RELATION + "> <" + LDP_MEMBER + "> .}";
2285        patch.setEntity(new StringEntity(updateString));
2286        assertEquals("Patch with sparql update created direct container from basic container!",
2287                CONFLICT.getStatusCode(), getStatus(patch));
2288    }
2289
2290    @Test
2291    public void testPatchToDeleteNonRdfSourceInteractionModel() throws IOException {
2292        final String pid = getRandomUniqueId();
2293
2294        createDatastream(pid, "x", "some content");
2295
2296        final String location = serverAddress + pid + "/x/fcr:metadata";
2297        final HttpPatch patchDeleteMethod = new HttpPatch(location);
2298        patchDeleteMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
2299        patchDeleteMethod.setEntity(new StringEntity("PREFIX ldp: <http://www.w3.org/ns/ldp#> " +
2300                "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> DELETE { " +
2301                "<> rdf:type  ldp:NonRDFSource .} WHERE {}"));
2302        assertEquals("Delete interaction model got status 409!\n",
2303                CONFLICT.getStatusCode(), getStatus(patchDeleteMethod));
2304    }
2305
2306    @Test
2307    public void testPutToChangeNonRdfSourceToRdfSource() throws IOException {
2308        final String pid = getRandomUniqueId();
2309
2310        createDatastream(pid, "x", "some content");
2311
2312        final String ttl = "<> <http://purl.org/dc/elements/1.1/title> \"this is a title\" .";
2313        final HttpPut put = putObjMethod(pid + "/x/fcr:metadata", "text/turtle", ttl);
2314        put.setHeader(LINK, BASIC_CONTAINER_LINK_HEADER);
2315        assertEquals("Changed the NonRdfSource ixn to basic container",
2316                CONFLICT.getStatusCode(), getStatus(put));
2317    }
2318
2319    @Test
2320    public void testPutChangeBinaryTypeNotAllowed() throws IOException {
2321        final HttpPost postMethod = new HttpPost(serverAddress);
2322        postMethod.setEntity(new StringEntity("TestString."));
2323        postMethod.addHeader(CONTENT_DISPOSITION, "attachment; filename=\"postCreate.txt\"");
2324
2325        final String location;
2326        try (final CloseableHttpResponse response = execute(postMethod)) {
2327            location = getLocation(response);
2328        }
2329
2330        // Change to RDFSource
2331        final HttpPut put1Method = new HttpPut(location);
2332        put1Method.setEntity(new StringEntity("TestString2."));
2333        put1Method.addHeader(CONTENT_DISPOSITION, "attachment; filename=\"putUpdate.txt\"");
2334        put1Method.setHeader(LINK, RDF_SOURCE_LINK_HEADER);
2335        assertEquals("Changed the NonRdfSource interaction model to RdfSource",
2336                BAD_REQUEST.getStatusCode(), getStatus(put1Method));
2337
2338        // Change to Basic Container
2339        final HttpPut put2Method = new HttpPut(location);
2340        put2Method.setEntity(new StringEntity("TestString2."));
2341        put2Method.addHeader(CONTENT_DISPOSITION, "attachment; filename=\"putUpdate.txt\"");
2342        put2Method.setHeader(LINK, BASIC_CONTAINER_LINK_HEADER);
2343        assertEquals("Changed the NonRdfSource interaction model to RdfSource",
2344                CONFLICT.getStatusCode(), getStatus(put2Method));
2345    }
2346
2347    @Test
2348    public void testPutChangeBasicContainerToParent() throws Exception {
2349        final String pid = getRandomUniqueId();
2350        final String location = serverAddress + pid;
2351        createObjectAndClose(pid);
2352
2353        // Change to RDFSource
2354        final HttpPut put1Method = new HttpPut(location);
2355        put1Method.setHeader(LINK, CONTAINER_LINK_HEADER);
2356        assertEquals("Changed the BasicContainer interaction model to Container",
2357                BAD_REQUEST.getStatusCode(), getStatus(put1Method));
2358
2359        final HttpPut put2Method = new HttpPut(location);
2360        put2Method.setHeader(LINK, RESOURCE_LINK_HEADER);
2361        assertEquals("Changed the BasicContainer interaction model to Resource",
2362                BAD_REQUEST.getStatusCode(), getStatus(put2Method));
2363
2364        final HttpPut put3Method = new HttpPut(location);
2365        put3Method.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
2366        assertEquals("Changed the BasicContainer interaction model to DirectContainer",
2367                CONFLICT.getStatusCode(), getStatus(put3Method));
2368    }
2369
2370    @Test
2371    public void testPutToChangeInteractionModelWithRdf() throws IOException {
2372        final String pid = getRandomUniqueId();
2373        final String resource = serverAddress + pid;
2374        final String container = serverAddress + pid + "/c";
2375
2376        createObjectAndClose(pid);
2377        createObjectAndClose(pid + "/a");
2378        createObjectAndClose(pid + "/c");
2379
2380        // attempt to change basic container to NonRdfSource
2381        final String ttl1 = "<> a <" + NON_RDF_SOURCE.getURI() + "> .";
2382        final HttpPut put1 = putObjMethod(pid + "/a", "text/turtle", ttl1);
2383        assertEquals("Changed the basic container ixn to NonRdfSource through PUT with RDF content!",
2384                CONFLICT.getStatusCode(), getStatus(put1));
2385
2386        // attempt to change basic container to direct container
2387        final String ttl2 = "<> a <" + DIRECT_CONTAINER.getURI() + "> ; <" + MEMBERSHIP_RESOURCE.getURI() +
2388                "> <" + resource + "> ; <" + HAS_MEMBER_RELATION + "> <" + LDP_MEMBER + "> .";
2389        final HttpPut put2 = putObjMethod(pid + "/a", "text/turtle", ttl2);
2390        assertEquals("Changed the basic container ixn to Direct Container through PUT with RDF content!",
2391                CONFLICT.getStatusCode(), getStatus(put2));
2392
2393        // create direct container
2394        final String ttl = "<> <" + MEMBERSHIP_RESOURCE.getURI() +
2395                "> <" + resource + "> ; <" + HAS_MEMBER_RELATION + "> <" + LDP_MEMBER + "> .";
2396        final HttpPut put = putObjMethod(pid + "/b", "text/turtle", ttl);
2397        put.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
2398        assertEquals(CREATED.getStatusCode(), getStatus(put));
2399        assertTrue(getLinkHeaders(getObjMethod(pid + "/b")).contains(DIRECT_CONTAINER_LINK_HEADER));
2400
2401        // successful update the properties with the interaction mode
2402        final String ttla = "<> <" + MEMBERSHIP_RESOURCE + "> <" + container + ">;\n"
2403                + "<" + HAS_MEMBER_RELATION + "> <info:some/relation> .\n";
2404        final HttpPut puta = putObjMethod(pid + "/b", "text/turtle", ttla);
2405        puta.addHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
2406        assertEquals(NO_CONTENT.getStatusCode(), getStatus(puta));
2407
2408        // attempt to change direct container to basic container
2409        final String ttl3 = "<> a <" + BASIC_CONTAINER.getURI() +
2410                "> ; <" + title + "> \"this is a title\".";
2411        final HttpPut put3 = putObjMethod(pid + "/b", "text/turtle", ttl3);
2412        assertEquals("Changed the direct container ixn to basic container through PUT with RDF content!",
2413                CONFLICT.getStatusCode(), getStatus(put3));
2414
2415        // attempt to change direct container to indirect container
2416        final String ttl4 = "<> a <" + INDIRECT_CONTAINER.getURI()
2417                + "> ; <" + MEMBERSHIP_RESOURCE + "> <" + container + ">;\n"
2418                + "<" + HAS_MEMBER_RELATION + "> <info:some/relation>;\n"
2419                + "<" + INSERTED_CONTENT_RELATION + "> <info:proxy/for> .\n";
2420        final HttpPut put4 = putObjMethod(pid + "/b", "text/turtle", ttl4);
2421        assertEquals("Changed the direct container ixn to indirect container through PUT with RDF content!",
2422                CONFLICT.getStatusCode(), getStatus(put4));
2423    }
2424
2425    @Test
2426    public void testChangeInteractionModelWithPut() throws IOException {
2427        final String pid = getRandomUniqueId();
2428        final String resource = serverAddress + pid;
2429        final String container = serverAddress + pid + "/c";
2430
2431        createObjectAndClose(pid);
2432        createObjectAndClose(pid + "/a");
2433        createObjectAndClose(pid + "/c");
2434
2435        final String ttl1 = "<> <" + MEMBERSHIP_RESOURCE.getURI() +
2436                "> <" + resource + "> ; <" + HAS_MEMBER_RELATION + "> <" + LDP_MEMBER + "> .";
2437        final HttpPut put1 = putObjMethod(pid + "/a", "text/turtle", ttl1);
2438        put1.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
2439        put1.addHeader("Prefer", "handling=lenient");
2440        assertEquals("Changed the basic container ixn to direct container!",
2441                CONFLICT.getStatusCode(), getStatus(put1));
2442
2443        // create direct container
2444        final String ttl = "<> <" + MEMBERSHIP_RESOURCE.getURI() +
2445                "> <" + resource + "> ; <" + HAS_MEMBER_RELATION + "> <" + LDP_MEMBER + "> .";
2446        final HttpPut put = putObjMethod(pid + "/b", "text/turtle", ttl);
2447        put.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
2448        assertEquals(CREATED.getStatusCode(), getStatus(put));
2449        assertTrue(getLinkHeaders(getObjMethod(pid + "/b")).contains(DIRECT_CONTAINER_LINK_HEADER));
2450
2451        final String ttl2 = "<> <" + title + "> \"this is a title\"";
2452        final HttpPut put2 = putObjMethod(pid + "/b", "text/turtle", ttl2);
2453        put2.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER);
2454        put2.addHeader("Prefer", "handling=lenient");
2455        assertEquals("Changed the direct container ixn to basic container",
2456                CONFLICT.getStatusCode(), getStatus(put2));
2457
2458        final String ttl3 = "<> <" + MEMBERSHIP_RESOURCE + "> <" + container + ">;\n"
2459                + "<" + HAS_MEMBER_RELATION + "> <info:some/relation>;\n"
2460                + "<" + INSERTED_CONTENT_RELATION + "> <info:proxy/for> .\n";
2461        final HttpPut put3 = putObjMethod(pid + "/b", "text/turtle", ttl3);
2462        put3.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER);
2463        put3.addHeader("Prefer", "handling=lenient");
2464        assertEquals("Changed the direct container ixn to indirect container!",
2465                CONFLICT.getStatusCode(), getStatus(put3));
2466    }
2467
2468    @Test
2469    public void testGetObjectReferences() throws IOException {
2470        final String id = getRandomUniqueId();
2471        final String resource = serverAddress + id;
2472        final String resourcea = resource + "/a";
2473        final String resourceb = resource + "/b";
2474
2475        createObjectAndClose(id);
2476        createObjectAndClose(id + "/a");
2477        createObjectAndClose(id + "/b");
2478        final HttpPatch updateObjectGraphMethod = patchObjMethod(id + "/a");
2479        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
2480        updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" +
2481                resourcea + "> <http://purl.org/dc/terms/isPartOf> <" + resourceb + "> . \n <" +
2482                resourcea + "> <info:xyz#some-other-property> <" + resourceb + "> } WHERE {}"));
2483        assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod));
2484
2485        final String withoutRefsEtag = getEtag(resourceb);
2486
2487        final HttpGet getObjMethod = new HttpGet(resourceb);
2488        getObjMethod.addHeader("Prefer", INBOUND_REFERENCE_PREFER_HEADER);
2489
2490        assertNotEquals(withoutRefsEtag, getEtag(getObjMethod));
2491
2492        try (final CloseableDataset dataset = getDataset(getObjMethod)) {
2493            final DatasetGraph graph = dataset.asDatasetGraph();
2494            assertTrue(graph.contains(ANY,
2495                    createURI(resourcea), createURI("http://purl.org/dc/terms/isPartOf"), createURI(resourceb)));
2496
2497            assertTrue(graph.contains(ANY,
2498                    createURI(resourcea), createURI("info:xyz#some-other-property"), createURI(resourceb)));
2499        }
2500    }
2501
2502    @Test
2503    public void testInboundReferencesFromBinary() throws Exception {
2504        final Node referenceProp = NodeFactory.createURI("http://awoods.com/pointsAt");
2505        final HttpPost postContainer = postObjMethod();
2506        final String containerUri;
2507        try (final CloseableHttpResponse response = execute(postContainer)) {
2508            assertEquals(CREATED.getStatusCode(), getStatus(response));
2509            containerUri = getLocation(response);
2510        }
2511
2512        final HttpPost postBinary = postObjMethod();
2513        postBinary.setHeader(CONTENT_TYPE, TEXT_PLAIN);
2514        postBinary.setEntity(new StringEntity("Test text"));
2515        final String binaryUri;
2516        try (final CloseableHttpResponse response = execute(postBinary)) {
2517            assertEquals(CREATED.getStatusCode(), getStatus(response));
2518            binaryUri = getLocation(response);
2519        }
2520
2521        setProperty(binaryUri.replace(serverAddress, "") + "/" + FCR_METADATA, referenceProp.getURI(),
2522                URI.create(containerUri));
2523
2524        final String withoutRefsEtag = getEtag(containerUri);
2525
2526        final HttpGet getContainer = new HttpGet(containerUri);
2527        getContainer.setHeader("Prefer", INBOUND_REFERENCE_PREFER_HEADER);
2528
2529        assertNotEquals(withoutRefsEtag, getEtag(getContainer));
2530        try (final CloseableDataset dataset = getDataset(getContainer)) {
2531            final DatasetGraph graph = dataset.asDatasetGraph();
2532            assertTrue(graph.contains(ANY, NodeFactory.createURI(binaryUri), referenceProp,
2533                    NodeFactory.createURI(containerUri)));
2534        }
2535    }
2536
2537    /**
2538     * Create the same reference from two resources, check that one exists when the other is deleted.
2539     */
2540    @Test
2541    public void testMultipleReferences() throws Exception {
2542        // Need to use something different or they come out as a single triple in the serialization.
2543        final Node referenceProp1 = NodeFactory.createURI("http://awoods.com/pointsAt");
2544        final Node referenceProp2 = createURI("http://example.org/lookAt");
2545        final HttpPost postContainer = postObjMethod();
2546        final String targetUri;
2547        try (final CloseableHttpResponse response = execute(postContainer)) {
2548            assertEquals(CREATED.getStatusCode(), getStatus(response));
2549            targetUri = getLocation(response);
2550        }
2551        final Node targetNode = createURI(targetUri);
2552
2553        final HttpPost post1 = postObjMethod();
2554        post1.setHeader(CONTENT_TYPE, "text/turtle");
2555        post1.setEntity(new StringEntity("<> <" + referenceProp1.getURI() + "> <" + targetUri + "> ."));
2556        final String firstContainerUri;
2557        try (final CloseableHttpResponse response = execute(post1)) {
2558            assertEquals(CREATED.getStatusCode(), getStatus(response));
2559            firstContainerUri = getLocation(response);
2560        }
2561        final Node subjectNode = createURI(firstContainerUri);
2562
2563        final HttpPost post2 = postObjMethod();
2564        post2.setHeader(CONTENT_TYPE, "text/turtle");
2565        post2.setEntity(new StringEntity("<" + firstContainerUri + "> <" + referenceProp2.getURI() + "> <" + targetUri +
2566                "> ."));
2567        final String secondContainerUri;
2568        try (final CloseableHttpResponse response = execute(post2)) {
2569            assertEquals(CREATED.getStatusCode(), getStatus(response));
2570            secondContainerUri = getLocation(response);
2571        }
2572
2573        final HttpGet getReferences = new HttpGet(targetUri);
2574        getReferences.setHeader("Prefer", INBOUND_REFERENCE_PREFER_HEADER);
2575        try (final CloseableDataset dataset = getDataset(getReferences)) {
2576            final DatasetGraph graph = dataset.asDatasetGraph();
2577            assertTrue(graph.contains(ANY, subjectNode, referenceProp1, targetNode));
2578            assertTrue(graph.contains(ANY, subjectNode, referenceProp2, targetNode));
2579        }
2580        // Delete the first resource.
2581        final HttpDelete deleteFirst = new HttpDelete(firstContainerUri);
2582        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteFirst));
2583        // Still one reference.
2584        final HttpGet getReferences2 = new HttpGet(targetUri);
2585        getReferences2.setHeader("Prefer", INBOUND_REFERENCE_PREFER_HEADER);
2586        try (final CloseableDataset dataset = getDataset(getReferences2)) {
2587            final DatasetGraph graph = dataset.asDatasetGraph();
2588            assertFalse(graph.contains(ANY, subjectNode, referenceProp1, targetNode));
2589            assertTrue(graph.contains(ANY, subjectNode, referenceProp2, targetNode));
2590        }
2591        // Delete the second resource
2592        final HttpDelete deleteSecond = new HttpDelete(secondContainerUri);
2593        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteSecond));
2594        // No more references.
2595        final HttpGet getReferences3 = new HttpGet(targetUri);
2596        getReferences3.setHeader("Prefer", INBOUND_REFERENCE_PREFER_HEADER);
2597        try (final CloseableDataset dataset = getDataset(getReferences3)) {
2598            final DatasetGraph graph = dataset.asDatasetGraph();
2599            assertFalse(graph.contains(ANY, subjectNode, referenceProp1, targetNode));
2600            assertFalse(graph.contains(ANY, subjectNode, referenceProp2, targetNode));
2601        }
2602    }
2603
2604    @Test
2605    public void testReferenceRemovedOnDelete() throws Exception {
2606        final String id = getRandomUniqueId();
2607        final String resource = serverAddress + id;
2608        final String resourcea = resource + "/a";
2609        final String resourceb = resource + "/b";
2610        final String resourcec = resource + "/c";
2611
2612        createObjectAndClose(id);
2613        createObjectAndClose(id + "/a");
2614        createObjectAndClose(id + "/b");
2615        createObjectAndClose(id + "/c");
2616        // Patch a to point to c.
2617        final HttpPatch updateObjectGraphMethodA = patchObjMethod(id + "/a");
2618        updateObjectGraphMethodA.addHeader(CONTENT_TYPE, "application/sparql-update");
2619        updateObjectGraphMethodA.setEntity(new StringEntity("INSERT { <" +
2620                resourcea + "> <http://purl.org/dc/terms/isPartOf> <" + resourcec + "> . } WHERE {}"));
2621        assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethodA));
2622        // Patch b to point to c as well.
2623        final HttpPatch updateObjectGraphMethodB = patchObjMethod(id + "/b");
2624        updateObjectGraphMethodB.addHeader(CONTENT_TYPE, "application/sparql-update");
2625        updateObjectGraphMethodB.setEntity(new StringEntity("INSERT { <" +
2626                resourceb + "> <http://purl.org/dc/terms/isPartOf> <" + resourcec + "> . } WHERE {}"));
2627        assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethodB));
2628
2629        // Verify the inbound references.
2630        final HttpGet getObjMethod = new HttpGet(resourcec);
2631        getObjMethod.addHeader("Prefer", INBOUND_REFERENCE_PREFER_HEADER);
2632        try (final CloseableDataset dataset = getDataset(getObjMethod)) {
2633            final DatasetGraph graph = dataset.asDatasetGraph();
2634            assertTrue(graph.contains(ANY,
2635                    createURI(resourcea), createURI("http://purl.org/dc/terms/isPartOf"), createURI(resourcec)));
2636
2637            assertTrue(graph.contains(ANY,
2638                    createURI(resourceb), createURI("http://purl.org/dc/terms/isPartOf"), createURI(resourcec)));
2639        }
2640        // Delete a.
2641        final HttpDelete deleteReferrer = deleteObjMethod(id + "/a");
2642        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteReferrer));
2643        // Verify only inbound reference from b
2644        final HttpGet getObjMethod2 = new HttpGet(resourcec);
2645        getObjMethod2.addHeader("Prefer", INBOUND_REFERENCE_PREFER_HEADER);
2646        try (final CloseableDataset dataset = getDataset(getObjMethod2)) {
2647            final DatasetGraph graph = dataset.asDatasetGraph();
2648            assertFalse(graph.contains(ANY,
2649                    createURI(resourcea), createURI("http://purl.org/dc/terms/isPartOf"), createURI(resourceb)));
2650
2651            assertTrue(graph.contains(ANY,
2652                    createURI(resourceb), createURI("http://purl.org/dc/terms/isPartOf"), createURI(resourcec)));
2653        }
2654    }
2655
2656    @Test
2657@Ignore("Membership/containment not included as inbound references - FCREPO-3589")
2658    public void testGetObjectReferencesIndirect() throws Exception {
2659        final String uuid = getRandomUniqueId();
2660        final String pid1 = uuid + "/parent";
2661        final String pid2 = uuid + "/child1";
2662        final String pid3 = uuid + "/child2";
2663        createObjectAndClose(pid1);
2664        createObjectAndClose(pid2);
2665        createObjectAndClose(pid3);
2666
2667        final String memberRelation = "http://pcdm.org/models#hasMember";
2668
2669        // create an indirect container
2670        final HttpPut createContainer = new HttpPut(serverAddress + pid1 + "/members");
2671        createContainer.addHeader(CONTENT_TYPE, "text/turtle");
2672        createContainer.addHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER);
2673        final String membersRDF = "<> <http://www.w3.org/ns/ldp#hasMemberRelation> <" + memberRelation + ">; "
2674            + "<http://www.w3.org/ns/ldp#insertedContentRelation> <http://www.openarchives.org/ore/terms/proxyFor>; "
2675            + "<http://www.w3.org/ns/ldp#membershipResource> <" + serverAddress + pid1 + "> . ";
2676        createContainer.setEntity(new StringEntity(membersRDF));
2677        assertEquals(CREATED.getStatusCode(), getStatus(createContainer));
2678
2679        // create proxies for the children in the indirect container
2680        createProxy(pid1, pid2);
2681        createProxy(pid1, pid3);
2682
2683        // retrieve the parent and verify the outbound triples exist
2684        final HttpGet getParent =  new HttpGet(serverAddress + pid1);
2685        getParent.addHeader(ACCEPT, "application/n-triples");
2686        try (final CloseableDataset dataset = getDataset(getParent)) {
2687            final DatasetGraph parentGraph = dataset.asDatasetGraph();
2688            assertTrue(parentGraph.contains(Node.ANY,
2689                    createURI(serverAddress + pid1),
2690                    createURI(memberRelation),
2691                    createURI(serverAddress + pid2)));
2692            assertTrue(parentGraph.contains(Node.ANY,
2693                    createURI(serverAddress + pid1),
2694                    createURI(memberRelation),
2695                    createURI(serverAddress + pid3)));
2696        }
2697
2698        // retrieve the members container and verify the LDP triples exist
2699        final HttpGet getContainer =  new HttpGet(serverAddress + pid1 + "/members");
2700        getContainer.addHeader("Prefer", "return=representation;include=\"http://www.w3.org/ns/ldp#PreferMembership\"");
2701        getContainer.addHeader(ACCEPT, "application/n-triples");
2702        try (final CloseableDataset dataset = getDataset(getContainer)) {
2703            final DatasetGraph containerGraph = dataset.asDatasetGraph();
2704            assertTrue(containerGraph.contains(Node.ANY,
2705                    createURI(serverAddress + pid1 + "/members"),
2706                    createURI("http://www.w3.org/ns/ldp#hasMemberRelation"),
2707                    createURI(memberRelation)));
2708
2709            assertTrue(containerGraph.contains(Node.ANY,
2710                    createURI(serverAddress + pid1 + "/members"),
2711                    createURI("http://www.w3.org/ns/ldp#insertedContentRelation"),
2712                    createURI("http://www.openarchives.org/ore/terms/proxyFor")));
2713
2714            assertTrue(containerGraph.contains(Node.ANY,
2715                    createURI(serverAddress + pid1 + "/members"),
2716                    createURI("http://www.w3.org/ns/ldp#membershipResource"),
2717                    createURI(serverAddress + pid1)));
2718        }
2719
2720
2721        // retrieve the member and verify inbound triples exist
2722        final HttpGet getMember =  new HttpGet(serverAddress + pid2);
2723        getMember.addHeader("Prefer", "return=representation; include=\"" + INBOUND_REFERENCES.toString() + "\"");
2724        getMember.addHeader(ACCEPT, "application/n-triples");
2725        try (final CloseableDataset dataset = getDataset(getMember)) {
2726            final DatasetGraph memberGraph = dataset.asDatasetGraph();
2727            assertTrue(memberGraph.contains(Node.ANY,
2728                    Node.ANY,
2729                    createURI("http://www.openarchives.org/ore/terms/proxyFor"),
2730                    createURI(serverAddress + pid2)));
2731
2732            assertTrue(memberGraph.contains(Node.ANY,
2733                    createURI(serverAddress + pid1),
2734                    createURI(memberRelation),
2735                    createURI(serverAddress + pid2)));
2736
2737            assertFalse("Should not contain inbound references to the other child", memberGraph.contains(Node.ANY,
2738                    createURI(serverAddress + pid1),
2739                    createURI(memberRelation),
2740                    createURI(serverAddress + pid3)));
2741        }
2742    }
2743    private static void createProxy(final String parent, final String child) {
2744        final HttpPost createProxy = new HttpPost(serverAddress + parent + "/members");
2745        createProxy.addHeader(CONTENT_TYPE, "text/turtle");
2746        final String proxyRDF = "<> <http://www.openarchives.org/ore/terms/proxyFor> <" + serverAddress + child + ">;"
2747            + " <http://www.openarchives.org/ore/terms/proxyIn> <" + serverAddress + parent + "> .";
2748        createProxy.setEntity(new StringEntity(proxyRDF, UTF_8));
2749        assertEquals(CREATED.getStatusCode(), getStatus(createProxy));
2750    }
2751
2752    @Test
2753    public void testNoReferentialIntegrity() throws IOException {
2754        final HttpPatch patch = new HttpPatch(getLocation(postObjMethod()));
2755        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
2756        patch.setEntity(new StringEntity("INSERT { " +
2757                "<> <http://some-vocabulary#isMemberOfCollection> <" + serverAddress + "non-existant> } WHERE {}"));
2758        assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch));
2759    }
2760
2761    @Test
2762    public void testUpdateAndReplaceObjectGraph() throws IOException {
2763        final String subjectURI = getLocation(postObjMethod());
2764        final HttpPatch updateObjectGraphMethod = new HttpPatch(subjectURI);
2765        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
2766        updateObjectGraphMethod.setEntity(new StringEntity("INSERT {<" + subjectURI + "> <info:test#label> \"foo\" ; " +
2767                " <info:test#number> 42 ; " +
2768                " <info:test#date> \"1953?\"^^<http://id.loc.gov/datatypes/edtf/EDTF> }" +
2769                " WHERE {}"));
2770        executeAndClose(updateObjectGraphMethod);
2771        try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) {
2772            final DatasetGraph graph = dataset.asDatasetGraph();
2773            assertTrue("Didn't find a triple we thought we added.", graph.contains(ANY,
2774                    createURI(subjectURI), createURI("info:test#label"), createLiteral("foo")));
2775            assertTrue("Didn't find a triple we thought we added.", graph.contains(ANY,
2776                    createURI(subjectURI), createURI("info:test#number"), createLiteral("42", XSDinteger)));
2777            assertTrue("Didn't find a triple we thought we added.", graph.contains(ANY,
2778                    createURI(subjectURI), createURI("info:test#date"), createLiteral("1953?",
2779                        getInstance().getSafeTypeByName("http://id.loc.gov/datatypes/edtf/EDTF"))));
2780        }
2781        updateObjectGraphMethod.setEntity(new StringEntity("DELETE WHERE { " +
2782                    "<" + subjectURI + "> <info:test#label> \"foo\"." +
2783                    "<" + subjectURI + "> <info:test#number> 42 . " +
2784                    "<" + subjectURI + "> <info:test#date> \"1953?\"^^<http://id.loc.gov/datatypes/edtf/EDTF> . }; \n" +
2785                "INSERT {<" + subjectURI + "> <info:test#label> \"qwerty\" ; " +
2786                "<info:test#number> 43 ; " +
2787                "<info:test#date> \"1953\"^^<http://id.loc.gov/datatypes/edtf/EDTF> . } \n" +
2788                "WHERE { }"));
2789
2790        try (final CloseableHttpResponse response = execute(updateObjectGraphMethod)) {
2791            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
2792            assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified"));
2793            assertTrue("Didn't find ETag header!", response.containsHeader("ETag"));
2794        }
2795        try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) {
2796            final DatasetGraph graph = dataset.asDatasetGraph();
2797            assertFalse("Found a triple we thought we deleted.", graph.contains(ANY,
2798                    createURI(subjectURI), createURI("info:test#label"), createLiteral("foo")));
2799            assertFalse("Found a triple we thought we deleted.", graph.contains(ANY,
2800                    createURI(subjectURI), createURI("info:test#number"), createLiteral("42", XSDinteger)));
2801            assertFalse("Found a triple we thought we deleted.", graph.contains(ANY,
2802                    createURI(subjectURI), createURI("info:test#date"), createLiteral("1953?",
2803                        getInstance().getSafeTypeByName("http://id.loc.gov/datatypes/edtf/EDTF"))));
2804        }
2805    }
2806
2807    @Test
2808    public void testUpdateObjectGraphWithProblems() throws IOException {
2809        final String subjectURI = getLocation(postObjMethod());
2810        final Link ex = fromUri(URI.create(serverAddress + "static/constraints/ServerManagedPropertyException.rdf"))
2811                .rel(CONSTRAINED_BY.getURI()).build();
2812
2813        final HttpPatch patchObjMethod = new HttpPatch(subjectURI);
2814        patchObjMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
2815        patchObjMethod.setEntity(new StringEntity("INSERT { <" +
2816                subjectURI + "> <" + REPOSITORY_NAMESPACE + "uuid> \"value-doesn't-matter\" } WHERE {}\n"));
2817        try (final CloseableHttpResponse response = execute(patchObjMethod)) {
2818            assertEquals(CONFLICT.getStatusCode(), getStatus(response));
2819            assertEquals(ex.toString(), response.getFirstHeader(LINK).getValue());
2820        }
2821    }
2822
2823    @Test
2824    public void testPutResourceBadRdf() throws IOException {
2825        final HttpPut httpPut = new HttpPut(serverAddress + getRandomUniqueId());
2826        httpPut.setHeader(CONTENT_TYPE, "text/turtle");
2827        httpPut.setEntity(new StringEntity("<> a \"still image\"."));
2828        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(httpPut));
2829    }
2830
2831    @Test
2832    public void testCreateResourceWithoutContentType() {
2833        assertEquals(CREATED.getStatusCode(), getStatus(new HttpPut(serverAddress + getRandomUniqueId())));
2834    }
2835
2836    @Test
2837    public void testUpdateObjectWithoutContentType() throws IOException {
2838        final HttpPut httpPut = new HttpPut(getLocation(postObjMethod()));
2839        // use a bytestream-based entity to avoid settin a content type
2840        httpPut.setEntity(new ByteArrayEntity("bogus content".getBytes(UTF_8)));
2841        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(httpPut));
2842    }
2843
2844    @Test
2845    public void testUpdateBinaryWithoutContentType() throws IOException {
2846        final String id = getRandomUniqueId();
2847        assertEquals(CREATED.getStatusCode(), getStatus(putObjMethod(id)));
2848        createDatastream(id, "x", "xyz");
2849        final HttpPut httpPut = new HttpPut(serverAddress + id + "/x");
2850        assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPut));
2851    }
2852
2853    @Test
2854    public void testCreateBinaryUpperCaseMimeType() throws IOException {
2855        final String subjectURI = serverAddress + getRandomUniqueId();
2856        final HttpPut createMethod = new HttpPut(subjectURI);
2857        createMethod.addHeader(CONTENT_TYPE, "TEXT/PlAiN");
2858        createMethod.setEntity(new StringEntity("Some text here."));
2859        createMethod.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
2860
2861        try (final CloseableHttpResponse response = execute(createMethod)) {
2862            assertEquals(CREATED.getStatusCode(), getStatus(response));
2863
2864            final String receivedLocation = response.getFirstHeader("Location").getValue();
2865            assertEquals("Got wrong URI in Location header for datastream creation!", subjectURI, receivedLocation);
2866
2867            final Link link = Link.valueOf(response.getFirstHeader(LINK).getValue());
2868            // ensure it's a binary; if it doesn't have describedby, then it's not
2869            assertEquals("No described by header!", "describedby", link.getRel());
2870        }
2871    }
2872
2873    @Test
2874    public void testBinarySetBadMimeType() throws IOException {
2875        final String subjectURI = serverAddress + getRandomUniqueId();
2876        final HttpPut createMethod = new HttpPut(subjectURI);
2877        createMethod.addHeader(CONTENT_TYPE, "text/plain");
2878        createMethod.setEntity(new StringEntity("Some text here."));
2879        createMethod.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
2880
2881        assertEquals(CREATED.getStatusCode(), getStatus(createMethod));
2882
2883        final HttpPatch patch = new HttpPatch(subjectURI + "/fcr:metadata");
2884        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
2885        patch.setEntity(new StringEntity(
2886                "PREFIX ebucore: <http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#> " +
2887                "INSERT { <" + subjectURI + "> ebucore:hasMimeType \"-- invalid syntax! --\" } WHERE {}")
2888        );
2889
2890        assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch));
2891
2892        // make sure it's still retrievable
2893        final HttpGet getMethod = new HttpGet(subjectURI);
2894        try (final CloseableHttpResponse response = execute(getMethod)) {
2895            assertEquals(OK.getStatusCode(), getStatus(response));
2896            final Collection<String> contentTypes = getHeader(response, CONTENT_TYPE);
2897            final String contentType = contentTypes.iterator().next();
2898            assertTrue("GET: Expected 'text/plain' instead got: '" + contentType + "'",
2899                    contentType.contains("text/plain"));
2900        }
2901
2902        final HttpHead httpHead = new HttpHead(subjectURI);
2903        try (final CloseableHttpResponse response = execute(httpHead)) {
2904            final Collection<String> contentTypes = getHeader(response, CONTENT_TYPE);
2905            final String contentType = contentTypes.iterator().next();
2906            assertTrue("HEAD: Expected 'text/plain' instead got: '" + contentType + "'",
2907                    contentType.contains("text/plain"));
2908        }
2909    }
2910
2911    @Test
2912    public void testCreateBinaryCSV() throws IOException {
2913        final String subjectURI = serverAddress + getRandomUniqueId();
2914        final HttpPut createMethod = new HttpPut(subjectURI);
2915        createMethod.addHeader(CONTENT_TYPE, "text/csv");
2916        createMethod.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
2917        createMethod.setEntity(new StringEntity("Header 1, Header 2, Header 3\r1,2,3\r1,2,3"));
2918
2919        try (final CloseableHttpResponse response = execute(createMethod)) {
2920            assertEquals(CREATED.getStatusCode(), getStatus(response));
2921
2922            final String receivedLocation = response.getFirstHeader("Location").getValue();
2923
2924            assertEquals("Got wrong URI in Location header for datastream creation!", subjectURI, receivedLocation);
2925
2926            final Link link = Link.valueOf(response.getFirstHeader(LINK).getValue());
2927            // ensure it's a binary; if it doesn't have describedby, then it's not
2928            assertEquals("No described by header!", "describedby", link.getRel());
2929        }
2930    }
2931
2932    @Test
2933    public void testRoundTripReplaceGraphForDatastreamDescription() throws IOException {
2934        final String id = getRandomUniqueId();
2935        final String subjectURI = serverAddress + id + "/ds1";
2936        createDatastream(id, "ds1", "some-content");
2937
2938        final HttpGet getObjMethod = new HttpGet(subjectURI + "/" + FCR_METADATA);
2939        getObjMethod.addHeader(ACCEPT, "text/turtle");
2940        getObjMethod.addHeader("Prefer", "return=representation; omit=\"" + PREFER_SERVER_MANAGED + "\"");
2941        final Model model = createDefaultModel();
2942        try (final CloseableHttpResponse getResponse = execute(getObjMethod)) {
2943            final String graph = EntityUtils.toString(getResponse.getEntity());
2944            logger.trace("Got serialized object graph for testRoundTripReplaceGraphForDatastream():\n {}", graph);
2945            try (final StringReader r = new StringReader(graph)) {
2946                model.read(r, subjectURI, "TURTLE");
2947            }
2948        }
2949        final HttpPut replaceMethod = new HttpPut(subjectURI + "/" + FCR_METADATA);
2950        try (final StringWriter w = new StringWriter()) {
2951            model.write(w, "N-TRIPLE");
2952            final String updatedMetadata = w.toString() +
2953                "<" + subjectURI + "> <http://www.w3.org/2000/01/rdf-schema#label> 'foo' .";
2954            replaceMethod.setEntity(new StringEntity(updatedMetadata));
2955            logger.trace("Transmitting object graph for testRoundTripReplaceGraphForDatastream():\n {}", w);
2956        }
2957        replaceMethod.addHeader(CONTENT_TYPE, "application/n-triples");
2958        assertEquals(NO_CONTENT.getStatusCode(), getStatus(replaceMethod));
2959    }
2960
2961    @Test
2962    public void testDefaultBinaryDescriptionTriples() throws Exception {
2963        final String id = getRandomUniqueId();
2964        final HttpPost post = postObjMethod();
2965        final String binaryUri = serverAddress + id;
2966        final Node binaryNode = createURI(binaryUri);
2967
2968        post.setHeader("Slug", id);
2969        post.setHeader(CONTENT_TYPE, TEXT_PLAIN);
2970        post.setEntity(new StringEntity("some text", UTF_8));
2971        post.setHeader(CONTENT_DISPOSITION, "attachment; filename=\"mytest.txt\"");
2972        assertEquals(CREATED.getStatusCode(), getStatus(post));
2973
2974        final HttpGet get = getObjMethod(id + "/" + FCR_METADATA);
2975        try (final CloseableHttpResponse response = execute(get)) {
2976            assertEquals(OK.getStatusCode(), getStatus(response));
2977            try (final CloseableDataset dataset = getDataset(response)) {
2978                final DatasetGraph graph = dataset.asDatasetGraph();
2979                // Not checking for CreatedBy and LastModifiedBy as they require authentication.
2980                assertFalse(dataset.isEmpty());
2981                assertTrue(graph.contains(ANY, binaryNode, CREATED_DATE.asNode(), ANY));
2982                assertTrue(graph.contains(ANY, binaryNode, LAST_MODIFIED_DATE.asNode(), ANY));
2983                assertTrue(graph.contains(ANY, binaryNode, type.asNode(), NON_RDF_SOURCE.asNode()));
2984                assertTrue(graph.contains(ANY, binaryNode, type.asNode(), RESOURCE.asNode()));
2985                assertTrue(graph.contains(ANY, binaryNode, type.asNode(), FEDORA_RESOURCE.asNode()));
2986                assertTrue(graph.contains(ANY, binaryNode, type.asNode(), FEDORA_BINARY.asNode()));
2987                assertTrue(graph.contains(ANY, binaryNode, HAS_SIZE.asNode(), createLiteral("9", XSDlong)));
2988                assertTrue(graph.contains(ANY, binaryNode, HAS_MESSAGE_DIGEST.asNode(), ANY));
2989                assertTrue(graph.contains(ANY, binaryNode, HAS_FIXITY_SERVICE.asNode(),
2990                        createURI(binaryUri + "/" + FCR_FIXITY)));
2991                assertTrue(graph.contains(ANY, binaryNode, HAS_ORIGINAL_NAME.asNode(),
2992                        createLiteral("mytest.txt")));
2993            }
2994        }
2995
2996        // Now get with ServerManaged omitted
2997        final HttpGet get2 = getObjMethod(id + "/" + FCR_METADATA);
2998        get2.setHeader("Prefer", "return=representation; omit=\"" + PREFER_SERVER_MANAGED + "\"");
2999        try (final CloseableHttpResponse response = execute(get2)) {
3000            assertEquals(OK.getStatusCode(), getStatus(response));
3001            try (final CloseableDataset dataset = getDataset(response)) {
3002                assertTrue(dataset.isEmpty());
3003            }
3004        }
3005    }
3006
3007    @Test
3008    public void testResponseContentTypes() throws Exception {
3009        final String id = getRandomUniqueId();
3010        createObjectAndClose(id);
3011        for (final String type : POSSIBLE_RDF_RESPONSE_VARIANTS_STRING) {
3012            final HttpGet method = new HttpGet(serverAddress + id);
3013            method.addHeader(ACCEPT, type);
3014            assertEquals(type, getContentType(method));
3015        }
3016    }
3017
3018    @Test
3019    public void testDescribeRdfCached() throws IOException {
3020        try (final CloseableHttpClient cachClient = CachingHttpClientBuilder.create().setCacheConfig(DEFAULT).build()) {
3021            final String location = getLocation(postObjMethod());
3022            try (final CloseableHttpResponse response = cachClient.execute(new HttpGet(location))) {
3023                assertEquals("Client didn't return a OK!", OK.getStatusCode(), getStatus(response));
3024                logger.debug("Found HTTP headers:\n{}", asList(response.getAllHeaders()));
3025                assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified"));
3026                final String lastModed = response.getFirstHeader("Last-Modified").getValue();
3027                final String etag = response.getFirstHeader("ETag").getValue();
3028                final HttpGet getObjMethod2 = new HttpGet(location);
3029                getObjMethod2.setHeader("If-Modified-Since", lastModed);
3030                getObjMethod2.setHeader("If-None-Match", etag);
3031                assertEquals("Client didn't get a NOT_MODIFIED!", NOT_MODIFIED.getStatusCode(),
3032                        getStatus(getObjMethod2));
3033            }
3034        }
3035    }
3036
3037    @Test
3038    public void testValidHTMLForRepo() throws IOException, SAXException {
3039        validateHTML("");
3040    }
3041
3042    @Test
3043    public void testValidHTMLForObject() throws IOException, SAXException {
3044        final String id = getRandomUniqueId();
3045        createObjectAndClose(id);
3046        validateHTML(id);
3047    }
3048
3049    @Test
3050    public void testValidHTMLForDS() throws IOException, SAXException {
3051        final String id = getRandomUniqueId();
3052        createDatastream(id, "ds", "content");
3053        validateHTML(id + "/ds/" + FCR_METADATA);
3054    }
3055
3056    private static void validateHTML(final String path) throws IOException, SAXException {
3057        final HttpGet getMethod = getObjMethod(path);
3058        getMethod.addHeader(ACCEPT, "text/html");
3059        try (final CloseableHttpResponse response = execute(getMethod)) {
3060            assertEquals(OK.getStatusCode(), getStatus(response));
3061            final String content = EntityUtils.toString(response.getEntity());
3062            logger.trace("Retrieved HTML view:\n" + content);
3063            final HtmlParser htmlParser = new HtmlParser(ALLOW);
3064            htmlParser.setDoctypeExpectation(NO_DOCTYPE_ERRORS);
3065            htmlParser.setErrorHandler(new HTMLErrorHandler());
3066            htmlParser.setContentHandler(new TreeBuilder());
3067            try (final InputStream htmlStream = new ByteArrayInputStream(content.getBytes(UTF_8))) {
3068                htmlParser.parse(new InputSource(htmlStream));
3069            }
3070            logger.debug("HTML found to be valid.");
3071        }
3072    }
3073
3074    private static class HTMLErrorHandler implements ErrorHandler {
3075
3076        @Override
3077        public void warning(final SAXParseException e) {
3078            fail(e.toString());
3079        }
3080
3081        @Override
3082        public void error(final SAXParseException e) {
3083            fail(e.toString());
3084        }
3085
3086        @Override
3087        public void fatalError(final SAXParseException e) {
3088            fail(e.toString());
3089        }
3090    }
3091
3092    @Test
3093    public void testLinkedDeletion() {
3094        final String linkedFrom = getRandomUniqueId();
3095        final String linkedTo = getRandomUniqueId();
3096        createObjectAndClose(linkedFrom);
3097        createObjectAndClose(linkedTo);
3098
3099        final String sparql =
3100                "INSERT DATA { <" + serverAddress + linkedFrom + "> " +
3101                        "<http://some-vocabulary#isMemberOfCollection> <" + serverAddress + linkedTo + "> . }";
3102        final HttpPatch patch = patchObjMethod(linkedFrom);
3103        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
3104        patch.setEntity(new ByteArrayEntity(sparql.getBytes(UTF_8)));
3105        assertEquals("Couldn't link resources!", NO_CONTENT.getStatusCode(), getStatus(patch));
3106        assertEquals("Error deleting linked-to!", NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(linkedTo)));
3107        assertEquals("Linked to should still exist!", OK.getStatusCode(), getStatus(getObjMethod(linkedFrom)));
3108    }
3109
3110    @Test
3111    public void testUpdateObjectWithSpacesPost() throws IOException {
3112        final String id = getRandomUniqueId() + " 2";
3113        final String expectedUri = serverAddress + id.replace(" ", "%20");
3114        final String notExpectedUri = expectedUri.replace("%", "%25");
3115        try (final CloseableHttpResponse createResponse = createObject(id)) {
3116            final String subjectURI = getLocation(createResponse);
3117            assertEquals(expectedUri, subjectURI);
3118            final HttpPatch updateObjectGraphMethod = new HttpPatch(subjectURI);
3119            updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
3120            updateObjectGraphMethod.setEntity(new StringEntity(
3121                    "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"test\" } WHERE {}"));
3122            assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod));
3123        }
3124        final HttpGet httpGet = new HttpGet(expectedUri);
3125        try (final var dataset = getDataset(httpGet)) {
3126            final var graph  = dataset.asDatasetGraph();
3127            // Ensure the graph has an encoded URI too.
3128            assertTrue(graph.contains(
3129                    ANY,
3130                    createURI(expectedUri),
3131                    ANY,
3132                    ANY
3133            ));
3134            // Ensure it does not have a double encoded URI.
3135            assertFalse(graph.contains(
3136                    ANY,
3137                    createURI(notExpectedUri),
3138                    ANY,
3139                    ANY
3140            ));
3141        }
3142    }
3143
3144    @Test
3145    public void testUpdateObjectWithSpacesPut() throws IOException {
3146        final String id = getRandomUniqueId() + "%202";
3147        final String expectedUri = serverAddress + id;
3148        final String notExpectedUri = expectedUri.replace("%", "%25");
3149        try (final CloseableHttpResponse createResponse = execute(putObjMethod(id))) {
3150            final String subjectURI = getLocation(createResponse);
3151            assertEquals(expectedUri, subjectURI);
3152            final HttpPatch updateObjectGraphMethod = new HttpPatch(subjectURI);
3153            updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
3154            updateObjectGraphMethod.setEntity(new StringEntity(
3155                    "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"test\" } WHERE {}"));
3156            assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod));
3157        }
3158        final HttpGet httpGet = new HttpGet(expectedUri);
3159        try (final var dataset = getDataset(httpGet)) {
3160            final var graph  = dataset.asDatasetGraph();
3161            // Ensure the graph has an encoded URI too.
3162            assertTrue(graph.contains(
3163                    ANY,
3164                    createURI(expectedUri),
3165                    ANY,
3166                    ANY
3167            ));
3168            // Ensure it does not have a double encoded URI.
3169            assertFalse(graph.contains(
3170                    ANY,
3171                    createURI(notExpectedUri),
3172                    ANY,
3173                    ANY
3174            ));
3175        }
3176    }
3177
3178    @Test
3179    public void testCreatedAndModifiedDates() throws IOException {
3180        final String location = getLocation(postObjMethod());
3181        final HttpGet getObjMethod = new HttpGet(location);
3182        try (final CloseableHttpResponse response = execute(getObjMethod)) {
3183            try (final CloseableDataset dataset = getDataset(response)) {
3184                final DatasetGraph graph = dataset.asDatasetGraph();
3185                final Model model = createModelForGraph(graph.getDefaultGraph());
3186                final Resource nodeUri = createResource(location);
3187                final String lastmodString = response.getFirstHeader("Last-Modified").getValue();
3188                final Optional<Instant> createdDateTriples =
3189                        getDateFromModel(model, nodeUri, CREATED_DATE);
3190                final Optional<Instant> lastmodDateTriples =
3191                        getDateFromModel(model, nodeUri, LAST_MODIFIED_DATE);
3192                assertTrue(createdDateTriples.isPresent());
3193                assertTrue(lastmodDateTriples.isPresent());
3194                // Reformatting lastModified header to ensure consistent formatting between it and fedora timestamps
3195                final Instant lastMod = headerFormat.parse(lastmodString, Instant::from);
3196                final String formattedLastModifiedHeader = headerFormat.format(lastMod);
3197                assertEquals(formattedLastModifiedHeader, headerFormat.format(createdDateTriples.get()));
3198                assertEquals(formattedLastModifiedHeader, headerFormat.format(lastmodDateTriples.get()));
3199            }
3200        }
3201    }
3202
3203    @Test
3204    public void testPostCreateDate() throws IOException {
3205        final HttpPost httpPost = postObjMethod("/");
3206        httpPost.addHeader("Slug", getRandomUniqueId());
3207        httpPost.addHeader(CONTENT_TYPE, "text/turtle");
3208        httpPost.setEntity(new StringEntity(getTTLThatUpdatesServerManagedTriples("fakeuser", null, null, null)));
3209        try (final CloseableHttpResponse response = execute(httpPost)) {
3210            assertEquals("Must not be able to update createdBy!", CONFLICT.getStatusCode(), getStatus(response));
3211            assertConstrainedByPresent(response);
3212            checkForLinkHeader(response, SERVER_MANAGED_PROPERTY_CONSTRAINT_URI, CONSTRAINED_BY.toString());
3213        }
3214    }
3215
3216    @Test
3217    public void testPutRdfLenient() throws IOException {
3218        final String location = getLocation(postObjMethod());
3219        final Node uriResource = createURI(location);
3220        final HttpGet getObjMethod = new HttpGet(location);
3221        String createdDate = null;
3222        try (final CloseableHttpResponse response = execute(getObjMethod)) {
3223            assertEquals(OK.getStatusCode(), getStatus(response));
3224            try (final CloseableDataset dataset = getDataset(response)) {
3225                final DatasetGraph graph = dataset.asDatasetGraph();
3226                final var iterator = graph.find(ANY, uriResource, CREATED_DATE.asNode(), ANY);
3227                while (iterator.hasNext()) {
3228                    final var quad = iterator.next();
3229                    createdDate = quad.getObject().getLiteral().toString();
3230                }
3231            }
3232        }
3233        final HttpPut putMethod = new HttpPut(location);
3234        putMethod.setHeader(CONTENT_TYPE, "text/turtle");
3235        putMethod.setEntity(new StringEntity("<> <" + CREATED_DATE + "> \"1979-01-01T00:00:00\" ;" +
3236                "<" + DCTITLE + "> \"This is a new title\" . "));
3237        // Can't set the created date
3238        assertEquals(CONFLICT.getStatusCode(), getStatus(putMethod));
3239
3240        // With lenient we ignore it.
3241        putMethod.setHeader("Prefer", "handling=\"lenient\"");
3242        assertEquals(NO_CONTENT.getStatusCode(), getStatus(putMethod));
3243
3244        final HttpGet getObj2 = new HttpGet(location);
3245        try (final CloseableHttpResponse response = execute(getObj2)) {
3246            assertEquals(OK.getStatusCode(), getStatus(response));
3247            try (final CloseableDataset dataset = getDataset(response)) {
3248                String createdDate2 = null;
3249                String title = null;
3250                final DatasetGraph graph = dataset.asDatasetGraph();
3251                final var iterator = graph.find(ANY, uriResource, CREATED_DATE.asNode(), ANY);
3252                while (iterator.hasNext()) {
3253                    final var quad = iterator.next();
3254                    createdDate2 = quad.getObject().getLiteral().toString();
3255                }
3256                final var iterator2 = graph.find(ANY, uriResource, DCTITLE, ANY);
3257                while (iterator2.hasNext()) {
3258                    final var quad = iterator2.next();
3259                    title = quad.getObject().getLiteral().toString();
3260                }
3261                // Created date change did not take effect.
3262                assertEquals(createdDate, createdDate2);
3263                // But the new title is there.
3264                assertEquals("This is a new title", title);
3265            }
3266        }
3267    }
3268
3269    @Test
3270    public void testNonRDFSourceInteraction() throws IOException {
3271        final String id = getRandomUniqueId();
3272        final HttpPut put = putObjMethod(id, "text/turtle", "<> a <http://example.com/Foo> .");
3273        put.setHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
3274        executeAndClose(put);
3275        assertTrue(getLinkHeaders(getObjMethod(id)).contains(NON_RDF_SOURCE_LINK_HEADER));
3276    }
3277
3278    @Test
3279    public void testContainerInteraction() throws IOException {
3280        final String id = getRandomUniqueId();
3281        final String location;
3282        final HttpPut put0 = putObjMethod(id);
3283        put0.setHeader(LINK, BASIC_CONTAINER_LINK_HEADER);
3284        try (final CloseableHttpResponse response = execute(put0)) {
3285            location = getLocation(response);
3286        }
3287        assertTrue(getLinkHeaders(getObjMethod(id)).contains(BASIC_CONTAINER_LINK_HEADER));
3288
3289        final String ttl = "<> <" + MEMBERSHIP_RESOURCE + "> <" + location + ">; "
3290                + "<" + HAS_MEMBER_RELATION + "> <info:some/relation> .\n";
3291        final HttpPut put1 = putObjMethod(id + "/a", "text/turtle", ttl);
3292        put1.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
3293        executeAndClose(put1);
3294        assertTrue(getLinkHeaders(getObjMethod(id + "/a")).contains(DIRECT_CONTAINER_LINK_HEADER));
3295        createObject(id + "/a/1");
3296
3297        final String ttl2 = "<> <" + MEMBERSHIP_RESOURCE + "> <" + location + ">; "
3298                + "<" + HAS_MEMBER_RELATION + "> <info:some/another-relation> .\n";
3299        final HttpPut put2 = putObjMethod(id + "/b", "text/turtle", ttl2);
3300        put2.setHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
3301        executeAndClose(put2);
3302        assertTrue(getLinkHeaders(getObjMethod(id + "/b")).contains(DIRECT_CONTAINER_LINK_HEADER));
3303        createObject(id + "/b/1");
3304
3305        try (final CloseableDataset dataset = getDataset(new HttpGet(location))) {
3306            final DatasetGraph graphStore = dataset.asDatasetGraph();
3307            final Node resource = createURI(location);
3308            assertTrue("Expected to have container t", graphStore.contains(ANY,
3309                    resource, CONTAINS.asNode(), createURI(location + "/a")));
3310            assertTrue("Expected to have container b", graphStore.contains(ANY,
3311                    resource, CONTAINS.asNode(), createURI(location + "/b")));
3312            assertTrue("Expected member relation", graphStore.contains(ANY,
3313                    resource, createURI("info:some/relation"), createURI(location + "/a/1")));
3314            assertTrue("Expected other member relation", graphStore.contains(ANY,
3315                    resource, createURI("info:some/another-relation"), createURI(location + "/b/1")));
3316        }
3317    }
3318
3319    @Test
3320    public void testIndirectContainerInteraction() throws IOException {
3321
3322        // Create resource (object)
3323        final String resourceId = getRandomUniqueId();
3324        final String resource;
3325        try (final CloseableHttpResponse createResponse = createObject(resourceId)) {
3326            resource = getLocation(createResponse);
3327        }
3328        // Create container (c0)
3329        final String containerId = getRandomUniqueId();
3330        final String container;
3331        try (final CloseableHttpResponse createResponse = createObject(containerId)) {
3332            container = getLocation(createResponse);
3333        }
3334        // Create indirect container (c0/members)
3335        final String indirectContainerId = containerId + "/t";
3336        final String indirectContainer;
3337        final String ttl = "<> <" + MEMBERSHIP_RESOURCE + "> <" + container + ">;\n"
3338                + "<" + HAS_MEMBER_RELATION + "> <info:some/relation>;\n"
3339                + "<" + INSERTED_CONTENT_RELATION + "> <info:proxy/for> .\n";
3340        final HttpPut put = putObjMethod(containerId + "/t", "text/turtle", ttl);
3341        put.setHeader(LINK, INDIRECT_CONTAINER_LINK_HEADER);
3342        try (final CloseableHttpResponse response = execute(put)) {
3343            indirectContainer = getLocation(response);
3344        }
3345        assertTrue(getLinkHeaders(getObjMethod(containerId + "/t")).contains(INDIRECT_CONTAINER_LINK_HEADER));
3346
3347        // Add indirect resource to indirect container
3348        final HttpPost postIndirectResource = postObjMethod(indirectContainerId);
3349        final String irRdf =
3350                "<> <info:proxy/in>  <" + container + "> ;\n" +
3351                        "   <info:proxy/for> <" + resource + "> .";
3352        postIndirectResource.setEntity(new StringEntity(irRdf));
3353        postIndirectResource.setHeader(CONTENT_TYPE, "text/turtle");
3354
3355        final String indirectResource;
3356        try (final CloseableHttpResponse postResponse = execute(postIndirectResource)) {
3357            indirectResource = getLocation(postResponse);
3358            assertEquals("Expected post to succeed", CREATED.getStatusCode(), getStatus(postResponse));
3359        }
3360        // Ensure container has been updated with relationship... indirectly
3361        try (final CloseableHttpResponse getResponse = execute(new HttpGet(container));
3362                final CloseableDataset dataset = getDataset(getResponse)) {
3363            final DatasetGraph graphStore = dataset.asDatasetGraph();
3364            assertTrue("Expected to have indirect container", graphStore.contains(ANY,
3365                    createURI(container), CONTAINS.asNode(), createURI(indirectContainer)));
3366
3367            assertTrue("Expected to have resource: " + graphStore.toString(), graphStore.contains(ANY,
3368                    createURI(container), createURI("info:some/relation"), createURI(resource)));
3369        }
3370        // Remove indirect resource
3371        assertEquals("Expected delete to succeed",
3372                NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(indirectResource)));
3373
3374        // Ensure container has been updated with relationship... indirectly
3375        try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(container));
3376                final CloseableDataset dataset = getDataset(getResponse1)) {
3377            final DatasetGraph graph = dataset.asDatasetGraph();
3378            assertFalse("Expected NOT to have resource: " + graph, graph.contains(ANY,
3379                    createURI(container), createURI("info:some/relation"), createURI(resource)));
3380        }
3381    }
3382
3383    @Test
3384    public void testWithHashUris() throws IOException {
3385        final HttpPost method = postObjMethod();
3386        method.addHeader(CONTENT_TYPE, "text/turtle");
3387        method.setEntity(new StringEntity("<> <info:some-predicate> <#abc> .\n"
3388                + "<#abc> <info:test#label> \"asdfg\" ."));
3389        try (final CloseableHttpResponse response = execute(method)) {
3390            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
3391            final String location = getLocation(response);
3392            try (final CloseableDataset dataset = getDataset(new HttpGet(location))) {
3393                final DatasetGraph graphStore = dataset.asDatasetGraph();
3394                assertTrue(graphStore.contains(ANY,
3395                        createURI(location), createURI("info:some-predicate"), createURI(location + "#abc")));
3396                assertTrue(graphStore.contains(ANY,
3397                        createURI(location + "#abc"), createURI("info:test#label"), createLiteral("asdfg")));
3398                assertFalse(graphStore.contains(ANY,
3399                        createURI(location + "#abc"), LAST_MODIFIED_DATE.asNode(), ANY));
3400                assertFalse(graphStore.contains(ANY,
3401                        createURI(location + "#abc"), rdfType, FEDORA_RESOURCE.asNode()));
3402            }
3403        }
3404    }
3405
3406    @Test
3407    public void testCreateAndReplaceGraphMinimal() throws IOException {
3408        LOGGER.trace("Entering testCreateAndReplaceGraphMinimal()...");
3409        final HttpPost httpPost = postObjMethod("/");
3410        httpPost.addHeader("Slug", getRandomUniqueId());
3411        httpPost.addHeader(CONTENT_TYPE, "text/turtle");
3412        httpPost.setEntity(new StringEntity("<> <" + DCTITLE.getURI() + "> \"abc\""));
3413        final String subjectURI;
3414        try (final CloseableHttpResponse response = execute(httpPost)) {
3415            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
3416            subjectURI = getLocation(response);
3417        }
3418        final HttpPut replaceMethod = new HttpPut(subjectURI);
3419        replaceMethod.addHeader(CONTENT_TYPE, "text/turtle");
3420        replaceMethod.addHeader("Prefer", "handling=lenient");
3421        replaceMethod.setEntity(new StringEntity("<> <" + DCTITLE.getURI() + "> \"xyz\""));
3422        assertEquals(NO_CONTENT.getStatusCode(), getStatus(replaceMethod));
3423
3424        final HttpGet get = new HttpGet(subjectURI);
3425        get.addHeader("Prefer", "return=representation; include=\"" + PREFER_MINIMAL_CONTAINER + "\"");
3426        try (final CloseableDataset dataset = getDataset(get)) {
3427            assertTrue(dataset.asDatasetGraph().contains(ANY, ANY, DCTITLE, createLiteral("xyz")));
3428        }
3429        LOGGER.trace("Done with testCreateAndReplaceGraphMinimal().");
3430    }
3431
3432    @Test
3433    public void testAclHeaderWithPost() throws IOException {
3434        final String pid = getRandomUniqueId();
3435        final HttpPost httpPost = postObjMethod("/");
3436        httpPost.addHeader("Slug", pid);
3437        final String location = serverAddress + pid;
3438        try (final CloseableHttpResponse response = execute(httpPost)) {
3439            assertEquals(CREATED.getStatusCode(), getStatus(response));
3440            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
3441        }
3442    }
3443
3444    @Test
3445    public void testAclHeaderWithPut() throws IOException {
3446        final String pid = getRandomUniqueId();
3447        final String location = serverAddress + pid;
3448
3449        final HttpPut httpPut = new HttpPut(location);
3450        try (final CloseableHttpResponse response = execute(httpPut)) {
3451            assertEquals(CREATED.getStatusCode(), getStatus(response));
3452            checkForLinkHeader(response, location + "/" + FCR_ACL, "acl");
3453        }
3454    }
3455
3456    @Test
3457    public void testJsonLdProfileCompacted() throws IOException {
3458        // Create a resource
3459        final HttpPost method = postObjMethod();
3460        method.addHeader(CONTENT_TYPE, "text/n3");
3461        final BasicHttpEntity entity = new BasicHttpEntity();
3462        final String rdf = "<> <http://purl.org/dc/elements/1.1/title> \"ceci n'est pas un titre français\"@fr ." +
3463                "<> <http://purl.org/dc/elements/1.1/title> \"this is an english title\"@en .";
3464        entity.setContent(new ByteArrayInputStream(rdf.getBytes(UTF_8)));
3465        method.setEntity(entity);
3466
3467        final String location;
3468        try (final CloseableHttpResponse response = execute(method)) {
3469            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
3470            location = response.getFirstHeader("Location").getValue();
3471        }
3472        // GET the resource with a JSON profile
3473        final HttpGet httpGet = new HttpGet(location);
3474        httpGet.setHeader(ACCEPT, "application/ld+json; profile=\"http://www.w3.org/ns/json-ld#compacted\"");
3475        final JsonNode json;
3476        try (final CloseableHttpResponse responseGET = execute(httpGet)) {
3477            // Inspect the response
3478            final ObjectMapper mapper = new ObjectMapper();
3479            json = mapper.readTree(responseGET.getEntity().getContent());
3480        }
3481
3482        final JsonNode titles = json.get("title");
3483        assertNotNull(titles);
3484        assertTrue("Should be a list", titles.isArray());
3485
3486        assertEquals("Should be two langs!", 2, titles.findValues("@language").size());
3487        assertEquals("Should be two values!", 2, titles.findValues("@value").size());
3488    }
3489
3490    @Test
3491    public void testJsonLdProfileExpanded() throws IOException {
3492        // Create a resource
3493        final HttpPost method = postObjMethod();
3494        method.addHeader(CONTENT_TYPE, "text/n3");
3495        final BasicHttpEntity entity = new BasicHttpEntity();
3496        final String rdf = "<> <http://purl.org/dc/elements/1.1/title> \"ceci n'est pas un titre français\"@fr ." +
3497                "<> <http://purl.org/dc/elements/1.1/title> \"this is an english title\"@en .";
3498        entity.setContent(new ByteArrayInputStream(rdf.getBytes(UTF_8)));
3499        method.setEntity(entity);
3500
3501        final String location;
3502        try (final CloseableHttpResponse response = execute(method)) {
3503            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
3504            location = response.getFirstHeader("Location").getValue();
3505        }
3506        // GET the resource with a JSON profile
3507        final HttpGet httpGet = new HttpGet(location);
3508        httpGet.setHeader(ACCEPT, "application/ld+json; profile=\"http://www.w3.org/ns/json-ld#expanded\"");
3509        final JsonNode json;
3510        try (final CloseableHttpResponse responseGET = execute(httpGet)) {
3511            // Inspect the response
3512            final ObjectMapper mapper = new ObjectMapper();
3513            json = mapper.readTree(responseGET.getEntity().getContent());
3514        }
3515
3516        final List<JsonNode> titlesList = json.findValues("http://purl.org/dc/elements/1.1/title");
3517        assertNotNull(titlesList);
3518        assertEquals("Should be list of lists", 1, titlesList.size());
3519
3520        final JsonNode titles = titlesList.get(0);
3521        assertEquals("Should be two langs!", 2, titles.findValues("@language").size());
3522        assertEquals("Should be two values!", 2, titles.findValues("@value").size());
3523    }
3524
3525    @Test
3526    public void testJsonLdProfileFlattened() throws IOException {
3527        // Create a resource
3528        final HttpPost method = postObjMethod();
3529        method.addHeader(CONTENT_TYPE, "text/n3");
3530        final BasicHttpEntity entity = new BasicHttpEntity();
3531        final String rdf = "<> <http://purl.org/dc/elements/1.1/title> \"ceci n'est pas un titre français\"@fr ." +
3532                "<> <http://purl.org/dc/elements/1.1/title> \"this is an english title\"@en .";
3533        entity.setContent(new ByteArrayInputStream(rdf.getBytes(UTF_8)));
3534        method.setEntity(entity);
3535
3536        final String location;
3537        try (final CloseableHttpResponse response = execute(method)) {
3538            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
3539            location = response.getFirstHeader("Location").getValue();
3540        }
3541        // GET the resource with a JSON profile
3542        final HttpGet httpGet = new HttpGet(location);
3543        httpGet.setHeader(ACCEPT, "application/ld+json; profile=\"http://www.w3.org/ns/json-ld#flattened\"");
3544        final JsonNode json;
3545        try (final CloseableHttpResponse responseGET = execute(httpGet)) {
3546            // Inspect the response
3547            final ObjectMapper mapper = new ObjectMapper();
3548            json = mapper.readTree(responseGET.getEntity().getContent());
3549        }
3550
3551        final List<JsonNode> titlesList = json.get("@graph").findValues("title");
3552        assertNotNull(titlesList);
3553        assertEquals("Should be list of lists", 1, titlesList.size());
3554
3555        final JsonNode titles = titlesList.get(0);
3556        assertEquals("Should be two langs!", 2, titles.findValues("@language").size());
3557        assertEquals("Should be two values!", 2, titles.findValues("@value").size());
3558    }
3559
3560    @Test
3561    public void testPathWithEmptySegment() {
3562        // Ensure HttpClient does not remove empty paths
3563        final RequestConfig config = RequestConfig.custom().setNormalizeUri(false).build();
3564        final String badLocation = "test/me/mb/er/s//members/9528a300-22da-40f2-bf3c-5b345d71affb";
3565        final var getEmpty = headObjMethod(badLocation);
3566        getEmpty.setConfig(config);
3567        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(getEmpty));
3568    }
3569
3570    private static Optional<Instant> getDateFromModel(final Model model, final Resource subj, final Property pred)
3571            throws NoSuchElementException {
3572        final StmtIterator stmts = model.listStatements(subj, pred, (String) null);
3573        return Optional.ofNullable(
3574           stmts.hasNext() ? Instant.from(tripleFormat.parse(stmts.nextStatement().getString())) : null);
3575    }
3576
3577    @Test
3578    public void testUpdateObjectGraphWithNonLocalTriples() throws IOException {
3579        final String pid = getRandomUniqueId();
3580        createObject(pid);
3581        final String otherPid = getRandomUniqueId();
3582        createObject(otherPid);
3583        final String location = serverAddress + pid;
3584        final String otherLocation = serverAddress + otherPid;
3585        final HttpPatch updateObjectGraphMethod = new HttpPatch(location);
3586        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
3587        updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" + location +
3588                "> <http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\". " + "<" + otherLocation +
3589                "> <http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\"" + " } WHERE {}"));
3590        assertEquals("It ought not be possible to use PATCH to create non-local triples!",
3591                NO_CONTENT.getStatusCode(),getStatus(updateObjectGraphMethod));
3592    }
3593
3594    @Test
3595    public void testPutMalformedHeader() throws IOException {
3596        // Create a resource
3597        final String id = getRandomUniqueId();
3598        executeAndClose(putObjMethod(id));
3599
3600        // Get the resource's etag
3601        final String etag;
3602        final HttpHead httpHead = headObjMethod(id);
3603        try (final CloseableHttpResponse response = execute(httpHead)) {
3604            etag = response.getFirstHeader("ETag").getValue();
3605            assertNotNull("ETag was missing!?", etag);
3606        }
3607
3608        // PUT properly formatted etag
3609        final HttpPut httpPut = putObjMethod(id);
3610        httpPut.addHeader("If-Match", etag);
3611
3612        try (final CloseableHttpResponse response = execute(httpPut)) {
3613            assertEquals("Should be a 412 Precondition Failed!", PRECONDITION_FAILED.getStatusCode(),
3614                    getStatus(response));
3615        }
3616
3617        // PUT improperly formatted etag ... not quoted.
3618        final HttpPut httpPut2 = putObjMethod(id);
3619        httpPut2.addHeader("If-Match", etag.replace("\"", ""));
3620
3621        try (final CloseableHttpResponse response = execute(httpPut2)) {
3622            assertEquals("Should be a 400 BAD REQUEST!", BAD_REQUEST.getStatusCode(), getStatus(response));
3623        }
3624    }
3625
3626    @Test
3627    public void testPutServerManagedPredicateInIndirectContainer() throws IOException {
3628        LOGGER.info("running testPutServerManagedPredicateInIndirectContainer");
3629        // Create a resource
3630        final String id = getRandomUniqueId();
3631        executeAndClose(putObjMethod(id));
3632
3633        final String child1 = id + "/1";
3634        executeAndClose(putObjMethod(child1));
3635
3636        // create indirect container using a server managed triple.
3637        final String body =
3638                "@prefix ore: <http://www.openarchives.org/ore/terms/> .\n" +
3639                        "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
3640                        "@prefix pcdm: <http://pcdm.org/models#> .\n" +
3641                        "<> a ldp:IndirectContainer ;\n" +
3642                        "   ldp:hasMemberRelation <http://fedora.info/definitions/v4/repository#lastModified> ; \n" +
3643                        "   ldp:insertedContentRelation ore:proxyFor ; \n" +
3644                        "   ldp:membershipResource <" + serverAddress + child1 + "> . ";
3645        final String child1MembersId = child1 + "/members";
3646        final HttpPut httpPut = putObjMethod(child1MembersId);
3647        httpPut.addHeader("Content-Type", "text/turtle");
3648        httpPut.setEntity(new StringEntity(body));
3649
3650        try (final CloseableHttpResponse response = execute(httpPut)) {
3651            assertEquals("Should be a 409 CONFLICT!", CONFLICT.getStatusCode(), getStatus(response));
3652        }
3653    }
3654
3655    @Test
3656    public void testPutEmptyBody() throws IOException {
3657        final HttpPut httpPut = putObjMethod(getRandomUniqueId());
3658        httpPut.addHeader(CONTENT_TYPE, "application/ld+json");
3659
3660        try (final CloseableHttpResponse response = execute(httpPut)) {
3661            assertEquals("Should be a client error", BAD_REQUEST.getStatusCode(), getStatus(response));
3662        }
3663    }
3664
3665    @Test
3666    public void testPutOgg() throws IOException {
3667        final String id = getRandomUniqueId();
3668        execute(putObjMethod(id));
3669        createDatastream(id, "x", "OggS");
3670    }
3671
3672    @Test
3673    public void testPutReferenceRoot() throws Exception {
3674        final HttpPut httpPut = putObjMethod(getRandomUniqueId());
3675        httpPut.addHeader(CONTENT_TYPE, "text/turtle");
3676        httpPut.setEntity(new StringEntity("@prefix acl: <http://www.w3.org/ns/auth/acl#> . " +
3677                "<> a acl:Authorization ; " +
3678                "acl:agent \"smith123\" ; " +
3679                "acl:mode acl:Read ;" +
3680                "acl:accessTo <" + serverAddress + "> ."));
3681
3682        try (final CloseableHttpResponse response = execute(httpPut)) {
3683            assertEquals(CREATED.getStatusCode(), getStatus(response));
3684        }
3685    }
3686
3687    @Test
3688    public void testDeleteLargeLiteralStoredAsBinary() throws IOException {
3689
3690        final String pid = getRandomUniqueId();
3691        createObject(pid);
3692
3693        final Node DC_TITLE = title.asNode();
3694        final String location = serverAddress + pid;
3695        final HttpPatch patch = new HttpPatch(location);
3696        final HttpPatch delPatch = new HttpPatch(location);
3697
3698        final String longLiteral =   // minimumBinaryInByteSize is currently 40 bytes
3699                TEST_BINARY_CONTENT +
3700                        TEST_BINARY_CONTENT +
3701                        TEST_BINARY_CONTENT +
3702                        TEST_BINARY_CONTENT;
3703
3704        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
3705        patch.setEntity(new StringEntity(
3706                "INSERT { <> <" + DC_TITLE + "> \"\"\"" + longLiteral + "\"\"\" } WHERE {}"));
3707
3708        assertEquals("Unable to add property value", NO_CONTENT.getStatusCode(), getStatus(patch));
3709
3710        delPatch.addHeader(CONTENT_TYPE, "application/sparql-update");
3711        delPatch.setEntity(new StringEntity(
3712                "DELETE WHERE { <> <" + DC_TITLE + "> \"\"\"" + longLiteral + "\"\"\"}"));
3713
3714        // delete that triple, or at least try to.
3715        assertEquals("Unable to complete delete property HTTP request", NO_CONTENT.getStatusCode(),
3716                getStatus(delPatch));
3717
3718        // now test if property exists anymore (it shouldn't).
3719        try (final CloseableDataset dataset = getDataset(getObjMethod(pid))) {
3720            assertFalse("Found the literal we tried to delete!", dataset.asDatasetGraph().contains(ANY,
3721                        createURI(location), DC_TITLE, createLiteral(longLiteral)));
3722        }
3723    }
3724
3725    @Test
3726    public void testInboundLinksDoNotUpdateEtag() throws IOException {
3727        final String id1 = getRandomUniqueId();
3728        final HttpPut httpPut = putObjMethod(id1);
3729        final String oldETag;
3730        final String oldMod;
3731        try (final CloseableHttpResponse response = execute(httpPut)) {
3732            assertEquals(CREATED.getStatusCode(), getStatus(response));
3733            oldETag = response.getFirstHeader("ETag").getValue();
3734            oldMod = response.getFirstHeader("Last-Modified").getValue();
3735        }
3736
3737        final String id2 = getRandomUniqueId();
3738        createObject(id2).close();
3739
3740        final HttpPatch patch = patchObjMethod(id2);
3741        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
3742        patch.setEntity(new StringEntity(
3743                "INSERT { <> <http://purl.org/dc/elements/1.1/relation> <" + serverAddress + id1 + "> } WHERE {}"));
3744        assertEquals(NO_CONTENT.getStatusCode(), getStatus(execute(patch)));
3745
3746        try (final CloseableHttpResponse response = execute(getObjMethod(id1))) {
3747            final String etag = response.getFirstHeader("ETag").getValue();
3748            final String lastmod = response.getFirstHeader("Last-Modified").getValue();
3749            assertEquals(oldMod, lastmod);
3750            assertEquals(oldETag, etag);
3751        }
3752    }
3753
3754    @Test
3755    public void testCreationResponseDefault() {
3756        testCreationResponse(null, null, CREATED, "text/plain");
3757        testCreationResponse(null, "application/ld+json", NOT_ACCEPTABLE, "text/html");
3758    }
3759
3760    @Test
3761    public void testCreationResponseMinimal() {
3762        testCreationResponse("minimal", null, CREATED, null);
3763        testCreationResponse("minimal", "application/ld+json", CREATED, null);
3764    }
3765
3766    @Test
3767    public void testCreationResponseRepresentation() {
3768        testCreationResponse("representation", null, CREATED, "text/turtle");
3769        testCreationResponse("representation", "application/ld+json", CREATED, "application/ld+json");
3770    }
3771
3772    private static void testCreationResponse(final String prefer,
3773                                      final String accept,
3774                                      final Status expectedStatus,
3775                                      final String expectedType) {
3776
3777        final HttpPost createMethod = new HttpPost(serverAddress);
3778        if (prefer != null) {
3779            createMethod.addHeader("Prefer", "return=" + prefer);
3780        }
3781        if (accept != null) {
3782            createMethod.addHeader(ACCEPT, accept);
3783        }
3784
3785        try (final CloseableHttpResponse createResponse = execute(createMethod)) {
3786            assertEquals(expectedStatus.getStatusCode(), createResponse.getStatusLine().getStatusCode());
3787            if (expectedType == null) {
3788                assertNull(createResponse.getFirstHeader(CONTENT_TYPE));
3789            } else {
3790                assertTrue(createResponse.getFirstHeader(CONTENT_TYPE).getValue().startsWith(expectedType));
3791            }
3792        } catch (final IOException e) {
3793            throw new AssertionError(e);
3794        }
3795    }
3796
3797    @Test
3798    public void testConcurrentPuts() throws InterruptedException, IOException {
3799        final String parent = getRandomUniqueId();
3800        executeAndClose(putObjMethod(parent));
3801        final String newResource = parent + "/test";
3802        final Runnable updateRunnable = () -> {
3803            executeAndClose(putObjMethod(newResource));
3804        };
3805        final Thread t1 = new Thread(updateRunnable);
3806        final Thread t2 = new Thread(updateRunnable);
3807        final Thread t3 = new Thread(updateRunnable);
3808        final Thread t4 = new Thread(updateRunnable);
3809        t1.start();
3810        t2.start();
3811        t3.start();
3812        t4.start();
3813        t1.join();
3814        t2.join();
3815        t3.join();
3816        t4.join();
3817
3818        try (final CloseableDataset dataset = getDataset(getObjMethod(parent))) {
3819            final DatasetGraph graphStore = dataset.asDatasetGraph();
3820            final Iterator<Quad> children = graphStore.find(ANY, ANY, CONTAINS.asNode(), ANY);
3821            assertTrue("One of the PUTs should have resulted in a child.", children.hasNext());
3822            children.next();
3823            if (children.hasNext()) {
3824                fail("Only one of the PUTs should have resulted in a child (unexpected child "
3825                       + children.next().getObject().getURI() + ")!");
3826            }
3827        }
3828    }
3829
3830    @Test
3831    public void testConcurrentPutsWithPairtrees() throws InterruptedException, IOException {
3832        final String parent = getRandomUniqueId();
3833        executeAndClose(putObjMethod(parent));
3834        final String first = parent + "/00/1";
3835        final String second = parent + "/00/2";
3836        final String third = parent + "/00/3";
3837        final String fourth = parent + "/00/4";
3838        final Thread t1 = new Thread(() -> {
3839            executeAndClose(putObjMethod(first));
3840        });
3841        final Thread t2 = new Thread(() -> {
3842            executeAndClose(putObjMethod(second));
3843        });
3844        final Thread t3 = new Thread(() -> {
3845            executeAndClose(putObjMethod(third));
3846        });
3847        final Thread t4 = new Thread(() -> {
3848            executeAndClose(putObjMethod(fourth));
3849        });
3850        t1.start();
3851        t2.start();
3852        t3.start();
3853        t4.start();
3854        t1.join();
3855        t2.join();
3856        t3.join();
3857        t4.join();
3858
3859        try (final CloseableDataset dataset = getDataset(getObjMethod(parent))) {
3860            final DatasetGraph graphStore = dataset.asDatasetGraph();
3861            final List<String> childPaths = new ArrayList<>();
3862            final Iterator<Quad> children = graphStore.find(ANY, ANY, CONTAINS.asNode(), ANY);
3863            assertTrue("Four children should have been created (none found).", children.hasNext());
3864            childPaths.add(children.next().getObject().getURI());
3865            LOGGER.info("Found child: {}", childPaths.get(0));
3866            assertTrue("Four children should have been created (only one found).", children.hasNext());
3867            childPaths.add(children.next().getObject().getURI());
3868            LOGGER.info("Found child: {}", childPaths.get(1));
3869            assertTrue("Four children should have been created (only two found).", children.hasNext());
3870            childPaths.add(children.next().getObject().getURI());
3871            LOGGER.info("Found child: {}", childPaths.get(2));
3872            assertTrue("Four children should have been created. (only three found)", children.hasNext());
3873            childPaths.add(children.next().getObject().getURI());
3874            LOGGER.info("Found child: {}", childPaths.get(3));
3875            assertFalse("Only four children should have been created.", children.hasNext());
3876            assertTrue(childPaths.contains(serverAddress + first));
3877            assertTrue(childPaths.contains(serverAddress + second));
3878            assertTrue(childPaths.contains(serverAddress + third));
3879            assertTrue(childPaths.contains(serverAddress + fourth));
3880        }
3881
3882    }
3883
3884    @Test
3885    public void testConcurrentUpdatesToBinary() throws IOException, InterruptedException {
3886        // create a binary
3887        final String path = getRandomUniqueId();
3888
3889        final HttpPut method = new HttpPut(serverAddress + path);
3890        method.setHeader(CONTENT_TYPE, "text/plain");
3891        method.setHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
3892        method.setEntity(new StringEntity("initial value"));
3893
3894        final String binaryEtag;
3895
3896        try (final CloseableHttpResponse response = execute(method)) {
3897            binaryEtag = response.getFirstHeader("ETag").getValue();
3898        }
3899
3900        final var phaser = new Phaser(5);
3901
3902        final RequestThread[] threads = new RequestThread[] {
3903                new RequestThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 1"), phaser),
3904                new RequestThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 2"), phaser),
3905                new RequestThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 3"), phaser),
3906                new RequestThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 4"), phaser)
3907        };
3908
3909        for (final RequestThread t : threads) {
3910            t.start();
3911        }
3912
3913        phaser.arriveAndAwaitAdvance();
3914
3915        final List<RequestThread> successfulThreads = new ArrayList<>();
3916        for (final RequestThread t : threads) {
3917            t.join(1000);
3918            assertFalse("Thread " + t.getId() + " could not perform its operation in time!", t.isAlive());
3919            final int status = t.response.getStatusLine().getStatusCode();
3920            LOGGER.info("{} received a {} status code.", t.getId(), status);
3921            if (status == 204) {
3922                successfulThreads.add(t);
3923            }
3924        }
3925
3926        assertEquals("Only one PUT request should have been successful!", 1, successfulThreads.size());
3927    }
3928
3929    private static class RequestThread extends Thread {
3930
3931        private final HttpUriRequest request;
3932
3933        private final Phaser phaser;
3934
3935        private HttpResponse response;
3936
3937        public RequestThread(final HttpUriRequest request, final Phaser phaser) {
3938            this.request = request;
3939            this.phaser = phaser;
3940        }
3941
3942        @Override
3943        public void run() {
3944            try {
3945                phaser.arriveAndAwaitAdvance();
3946                response = execute(request);
3947            } catch (final IOException e) {
3948                LOGGER.error("Thread " + Thread.currentThread().getId() + ", failed to request!", e);
3949            }
3950
3951        }
3952    }
3953
3954    private HttpPut putBinaryObjMethodIfMatch(final String location, final String etag, final String content)
3955            throws IOException {
3956        final HttpPut put = putObjMethod(location, "text/plain", content);
3957        put.setHeader("If-Match", etag);
3958        return put;
3959    }
3960
3961    @Test
3962    public void testBinaryLastModified() throws Exception {
3963        final String objid = getRandomUniqueId();
3964        final String objURI = serverAddress + objid;
3965        final String binURI = objURI + "/binary1";
3966
3967        final Instant lastmod1;
3968        try (final CloseableHttpResponse response = execute(putDSMethod(objid, "binary1", "some test content"))) {
3969            assertEquals(CREATED.getStatusCode(), getStatus(response));
3970            lastmod1 = Instant.from(headerFormat.parse(response.getFirstHeader("Last-Modified").getValue()));
3971        }
3972
3973        sleep(1000); // wait a second to make sure last-modified value will be different
3974
3975        try (final CloseableDataset dataset = getDataset(new HttpGet(binURI + "/fcr:metadata"))) {
3976            verifyModifiedMatchesCreated(dataset);
3977        }
3978
3979        final HttpPatch patchBinary = new HttpPatch(binURI + "/fcr:metadata");
3980        patchBinary.addHeader(CONTENT_TYPE, "application/sparql-update");
3981        patchBinary.setEntity(new StringEntity("INSERT { <" + binURI + "> " +
3982                "<http://www.w3.org/TR/rdf-schema/label> \"this is a label\" } WHERE {}"));
3983
3984        final Instant lastmod2;
3985        try (final CloseableHttpResponse response = execute(patchBinary)) {
3986            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
3987            lastmod2 = Instant.from(headerFormat.parse(response.getFirstHeader("Last-Modified").getValue()));
3988            assertTrue(lastmod2.isAfter(lastmod1));
3989        }
3990
3991        sleep(1000); // wait a second to make sure last-modified value will be different
3992
3993        final Instant lastmod3;
3994        try (final CloseableHttpResponse response = execute(putDSMethod(objid, "binary1", "new test content"))) {
3995            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
3996            lastmod3 = Instant.from(headerFormat.parse(response.getFirstHeader("Last-Modified").getValue()));
3997            assertTrue(lastmod3.isAfter(lastmod2));
3998        }
3999    }
4000
4001    @Test
4002    public void testContainerLastModified() throws Exception {
4003        final String objid = getRandomUniqueId();
4004        final String objURI = serverAddress + objid;
4005
4006        // create an object
4007        final long lastmod1;
4008        try (final CloseableHttpResponse response = execute(putObjMethod(objid))) {
4009            assertEquals(CREATED.getStatusCode(), getStatus(response));
4010            lastmod1 = Instant.from(
4011                headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())).toEpochMilli();
4012        }
4013
4014        sleep(1000); // wait a second to make sure last-modified value will be different
4015
4016        // initial created and last-modified properties should match
4017        try (final CloseableDataset dataset = getDataset(getObjMethod(objid))) {
4018            verifyModifiedMatchesCreated(dataset);
4019        }
4020
4021        // update the object properties (last-modified should be updated)
4022        final HttpPatch patchObject = new HttpPatch(objURI);
4023        patchObject.addHeader(CONTENT_TYPE, "application/sparql-update");
4024        patchObject.setEntity(new StringEntity("INSERT { <> " +
4025                "<http://www.w3.org/TR/rdf-schema/label> \"this is a label\" } WHERE {}"));
4026        final long lastmod2;
4027        try (final CloseableHttpResponse response = execute(patchObject)) {
4028            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
4029            lastmod2 = Instant.from(
4030                headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())).toEpochMilli();
4031            assertTrue(lastmod2 > lastmod1);
4032        }
4033
4034        sleep(1000); // wait a second to make sure last-modified value will be different
4035
4036        // create a direct container (last-modified should be updated)
4037        final long lastmod3;
4038        final HttpPut createContainer = new HttpPut(objURI + "/members");
4039        createContainer.addHeader(CONTENT_TYPE, "text/turtle");
4040        createContainer.addHeader(LINK, DIRECT_CONTAINER_LINK_HEADER);
4041        final String membersRDF = "<> <http://www.w3.org/ns/ldp#hasMemberRelation> <http://pcdm.org/models#hasMember>; "
4042            + "<http://www.w3.org/ns/ldp#membershipResource> <" + objURI + "> . ";
4043        createContainer.setEntity(new StringEntity(membersRDF));
4044        try (final CloseableHttpResponse response = execute(createContainer)) {
4045            assertEquals(CREATED.getStatusCode(), getStatus(response));
4046            lastmod3 = Instant.from(
4047                headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())).toEpochMilli();
4048            assertTrue(lastmod3 > lastmod2);
4049        }
4050    }
4051
4052    private static void verifyModifiedMatchesCreated(final Dataset dataset) {
4053        final DatasetGraph graph = dataset.asDatasetGraph();
4054        final Node cre = graph.find(ANY, ANY, CREATED_DATE.asNode(), ANY).next().getObject();
4055        final Node mod = graph.find(ANY, ANY, LAST_MODIFIED_DATE.asNode(), ANY).next().getObject();
4056        assertEquals(cre.getLiteralValue(), mod.getLiteralValue());
4057    }
4058
4059    @Test
4060    public void testDigestConsistency() throws IOException {
4061        final String id = getRandomUniqueId();
4062        executeAndClose(putDSMethod(id, "binary1", "some test content"));
4063
4064        final String headDigestValue;
4065        final HttpHead headObjMethod = headObjMethod(id + "/binary1");
4066        headObjMethod.addHeader(WANT_DIGEST, "sha, md5");
4067        try (final CloseableHttpResponse response = execute(headObjMethod)) {
4068            assertTrue(response.getHeaders(DIGEST).length > 0);
4069            headDigestValue = response.getHeaders(DIGEST)[0].getValue();
4070        }
4071
4072        final HttpGet getObjMethod = getObjMethod(id + "/binary1");
4073        getObjMethod.addHeader(WANT_DIGEST, "sha, md5");
4074        try (final CloseableHttpResponse response = execute(getObjMethod)) {
4075            assertTrue(response.getHeaders(DIGEST).length > 0);
4076            assertEquals(headDigestValue, response.getHeaders(DIGEST)[0].getValue());
4077        }
4078    }
4079
4080    @Test
4081    public void testDigestAbsence() throws IOException {
4082        final String id = getRandomUniqueId();
4083        executeAndClose(putDSMethod(id, "binary1", "some test content"));
4084
4085        final HttpHead headObjMethod = headObjMethod(id + "/binary1");
4086        try (final CloseableHttpResponse response = execute(headObjMethod)) {
4087            assertTrue(response.getHeaders(DIGEST).length == 0);
4088        }
4089
4090        final HttpGet getObjMethod = getObjMethod(id + "/binary1");
4091        try (final CloseableHttpResponse response = execute(getObjMethod)) {
4092            assertTrue(response.getHeaders(DIGEST).length == 0);
4093        }
4094    }
4095
4096    @Test
4097    public void testPostFcrSlug() throws IOException {
4098        final HttpPost httpPost = postObjMethod();
4099        httpPost.addHeader("Slug", "fcr:path");
4100        try (final CloseableHttpResponse response = execute(httpPost)) {
4101            assertEquals("Must not be able to POST with fcr namespaced Slug!", CONFLICT.getStatusCode(),
4102                getStatus(response));
4103        }
4104    }
4105
4106    @Test
4107    public void testPutFcrPath() throws IOException {
4108        final HttpPut httpPut = putObjMethod("/fcr:path");
4109        try (final CloseableHttpResponse response = execute(httpPut)) {
4110            assertEquals("Must not be able to PUT with fcr namespaced path!", CONFLICT.getStatusCode(),
4111                getStatus(response));
4112        }
4113    }
4114
4115    @Test
4116    public void testPutFcrTx() throws IOException {
4117        final HttpPut httpPut = putObjMethod("hello/fcr:tx");
4118        try (final CloseableHttpResponse response = execute(httpPut)) {
4119            assertEquals("Must not be able to PUT with fcr namespaced path!", CONFLICT.getStatusCode(),
4120                getStatus(response));
4121        }
4122    }
4123
4124    @Test
4125    public void testDeleteWithFcrPath() throws IOException {
4126        final String id = getRandomUniqueId() + "/fcr:delete";
4127        final HttpDelete httpDelete = deleteObjMethod(id);
4128        httpDelete.addHeader("Depth", "infinity");
4129        try (final CloseableHttpResponse response = execute(httpDelete)) {
4130            assertEquals("Must not be able to DELETE with fcr namespaced path!", CONFLICT.getStatusCode(),
4131                getStatus(response));
4132        }
4133    }
4134
4135    @Test
4136    public void testPostCreateNonRDFSourceWithAcl() throws IOException {
4137        final String aclURI = createAcl();
4138        final String subjectURI = getRandomUniqueId();
4139        final HttpPost createMethod = new HttpPost(serverAddress);
4140        createMethod.addHeader(CONTENT_TYPE, "text/plain");
4141        createMethod.addHeader("Link", "<" + aclURI + ">; rel=\"acl\"");
4142        createMethod.addHeader("Slug", subjectURI);
4143        createMethod.setEntity(new StringEntity("test body"));
4144
4145        checkResponseForMethodWithAcl(createMethod);
4146    }
4147
4148    @Test
4149    public void testPostCreateRDFSourceWithAcl() throws IOException {
4150        final String aclURI = createAcl();
4151        final String subjectURI = getRandomUniqueId();
4152        final HttpPost createMethod = new HttpPost(serverAddress);
4153        createMethod.addHeader(CONTENT_TYPE, "text/n3");
4154        createMethod.addHeader("Link", "<" + aclURI + ">; rel=\"acl\"");
4155        createMethod.addHeader("Slug", subjectURI);
4156        createMethod.setEntity(new StringEntity("<> <info:test#label> \"foo\""));
4157
4158        checkResponseForMethodWithAcl(createMethod);
4159}
4160
4161    @Test
4162    public void testPutCreateNonRDFSourceWithAcl() throws IOException {
4163        final String aclURI = createAcl();
4164        final String subjectURI = serverAddress + getRandomUniqueId();
4165        final HttpPut putMethod = new HttpPut(subjectURI);
4166        putMethod.addHeader(CONTENT_TYPE, "text/plain");
4167        putMethod.addHeader("Link", "<" + aclURI + ">; rel=\"acl\"");
4168        putMethod.setEntity(new StringEntity("test body"));
4169
4170        checkResponseForMethodWithAcl(putMethod);
4171    }
4172
4173    @Test
4174    public void testPutCreateRDFSourceWithAcl() throws IOException {
4175        final String aclURI = createAcl();
4176        final String subjectURI = serverAddress + getRandomUniqueId();
4177        final HttpPut putMethod = new HttpPut(subjectURI);
4178        putMethod.addHeader(CONTENT_TYPE, "text/n3");
4179        putMethod.addHeader("Link", "<" + aclURI + ">; rel=\"acl\"");
4180        putMethod.setEntity(new StringEntity("<" + subjectURI + "> <info:test#label> \"foo\""));
4181
4182        checkResponseForMethodWithAcl(putMethod);
4183    }
4184
4185    private void checkResponseForMethodWithAcl(final HttpUriRequest req) throws IOException {
4186        try (final CloseableHttpResponse response = execute(req)) {
4187            assertEquals(BAD_REQUEST.getStatusCode(), getStatus(response));
4188            assertConstrainedByPresent(response);
4189        }
4190    }
4191
4192    @Test
4193    public void testPostCreateRDFSourceWithMementoNamespaceType() throws IOException {
4194        final String subjectURI = getRandomUniqueId();
4195        final HttpPost createMethod = new HttpPost(serverAddress);
4196        createMethod.addHeader(CONTENT_TYPE, "text/turtle");
4197        createMethod.addHeader("Slug", subjectURI);
4198        createMethod.setEntity(new StringEntity("<> a <" + MEMENTO_TYPE + "> ."));
4199        try (final CloseableHttpResponse response = execute(createMethod)) {
4200            assertEquals("Must not be able to POST RDF that contains an \"<> a  <" + MEMENTO_TYPE + ">\"",
4201                         CONFLICT.getStatusCode(),
4202                         getStatus(response));
4203            assertConstrainedByPresent(response);
4204            checkForLinkHeader(response, SERVER_MANAGED_TYPE_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4205        }
4206    }
4207
4208    @Test
4209    public void testPutCreateRDFSourceWithMementoNamespaceType() throws IOException {
4210        final String subjectURI = serverAddress + getRandomUniqueId();
4211        final HttpPut putMethod = new HttpPut(subjectURI);
4212        putMethod.addHeader(CONTENT_TYPE, "text/turtle");
4213        putMethod.setEntity(new StringEntity("<> a <" + MEMENTO_TYPE + "> ."));
4214        try (final CloseableHttpResponse response = execute(putMethod)) {
4215            assertEquals("Must not be able to PUT RDF that contains an \"<> a  <" + MEMENTO_TYPE + ">\"",
4216                         CONFLICT.getStatusCode(),
4217                         getStatus(response));
4218            checkForLinkHeader(response, SERVER_MANAGED_TYPE_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4219        }
4220    }
4221
4222    @Test
4223    public void testPatchInsertMementoNamespaceType() throws IOException {
4224        final String pid =  getRandomUniqueId();
4225        final String subjectURI = serverAddress + pid;
4226        createObjectAndClose(pid);
4227        final HttpPatch patch = new HttpPatch(subjectURI);
4228        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
4229        patch.setEntity(new StringEntity("INSERT { <> a <" + MEMENTO_TYPE + "> . } WHERE {} "));
4230
4231        try (final CloseableHttpResponse response = execute(patch)) {
4232            assertEquals("Must not be able to INSERT  \"<>  a <" + MEMENTO_TYPE + ">\"",
4233                         CONFLICT.getStatusCode(), getStatus(patch));
4234            checkForLinkHeader(response, SERVER_MANAGED_TYPE_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4235        }
4236    }
4237
4238    @Test
4239    public void testPostCreateRDFSourceWithMementoNamespacePredicate() throws IOException {
4240        final String subjectURI = getRandomUniqueId();
4241        final HttpPost createMethod = new HttpPost(serverAddress);
4242        createMethod.addHeader(CONTENT_TYPE, "text/turtle");
4243        createMethod.addHeader("Slug", subjectURI);
4244        createMethod.setEntity(
4245            new StringEntity("<>  <" + MEMENTO_NAMESPACE + "mementoDatetime" + "> \"Thu, 21 Jan 2010 00:09:40 GMT\""));
4246        try (final CloseableHttpResponse response = execute(createMethod)) {
4247            assertEquals("Must not be able to POST RDF that contains a memento namespace predicate",
4248                         CONFLICT.getStatusCode(),
4249                         getStatus(response));
4250            checkForLinkHeader(response, SERVER_MANAGED_PROPERTY_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4251        }
4252    }
4253
4254    @Test
4255    public void testPutCreateRDFSourceWithMementoNamespacePredicate() throws IOException {
4256        final String subjectURI = serverAddress + getRandomUniqueId();
4257        final HttpPut putMethod = new HttpPut(subjectURI);
4258        putMethod.addHeader(CONTENT_TYPE, "text/turtle");
4259        putMethod.setEntity(
4260            new StringEntity("<>  <" + MEMENTO_NAMESPACE + "mementoDatetime" + "> \"Thu, 21 Jan 2010 00:09:40 GMT\""));
4261        try (final CloseableHttpResponse response = execute(putMethod)) {
4262            assertEquals("Must not be able to PUT RDF that contains a memento namespace predicate",
4263                         CONFLICT.getStatusCode(),
4264                         getStatus(response));
4265            checkForLinkHeader(response, SERVER_MANAGED_PROPERTY_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4266        }
4267    }
4268
4269    @Test
4270    public void testPatchInsertMementoNamespacePredicate() throws IOException {
4271        final String pid = getRandomUniqueId();
4272        final String subjectURI = serverAddress + pid;
4273        createObjectAndClose(pid);
4274        final HttpPatch patch = new HttpPatch(subjectURI);
4275        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
4276        patch.setEntity(new StringEntity(
4277            "INSERT { <>  <" + MEMENTO_NAMESPACE + "mementoDatetime" +
4278            "> \"Thu, 21 Jan 2010 00:09:40 GMT\". } WHERE {}"));
4279        try (final CloseableHttpResponse response = execute(patch)) {
4280            assertEquals("Must not be able to PATCH RDF that contains a memento namespace predicate",
4281                         CONFLICT.getStatusCode(), getStatus(response));
4282            checkForLinkHeader(response, SERVER_MANAGED_PROPERTY_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4283        }
4284    }
4285
4286    @Test
4287    public void testPostCreateRDFSourceWithMementoTypeAndProperty() throws IOException {
4288        final String subjectURI = getRandomUniqueId();
4289        final HttpPost createMethod = new HttpPost(serverAddress);
4290        createMethod.addHeader(CONTENT_TYPE, "text/turtle");
4291        createMethod.addHeader("Slug", subjectURI);
4292        createMethod.setEntity(
4293                new StringEntity("<> a <" + MEMENTO_TYPE + "> ; " +
4294                        "<" + MEMENTO_NAMESPACE + "mementoDatetime" + "> \"Thu, 21 Jan 2010 00:09:40 GMT\""));
4295        try (final CloseableHttpResponse response = execute(createMethod)) {
4296            assertEquals("Must not be able to POST RDF that contains a memento type and namespace predicate",
4297                    CONFLICT.getStatusCode(),
4298                    getStatus(response));
4299            checkForLinkHeader(response, SERVER_MANAGED_PROPERTY_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4300            checkForLinkHeader(response, SERVER_MANAGED_TYPE_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4301        }
4302    }
4303
4304    @Test
4305    public void testPutCreateRDFSourceWithMementoTypeAndProperty() throws IOException {
4306        final String subjectURI = serverAddress + getRandomUniqueId();
4307        final HttpPut putMethod = new HttpPut(subjectURI);
4308        putMethod.addHeader(CONTENT_TYPE, "text/turtle");
4309        putMethod.setEntity(
4310                new StringEntity("<> a <" + MEMENTO_TYPE + "> ; " +
4311                        "<" + MEMENTO_NAMESPACE + "mementoDatetime" + "> \"Thu, 21 Jan 2010 00:09:40 GMT\""));
4312        try (final CloseableHttpResponse response = execute(putMethod)) {
4313            assertEquals("Must not be able to PUT RDF that contains a memento type and namespace predicate",
4314                    CONFLICT.getStatusCode(),
4315                    getStatus(response));
4316            checkForLinkHeader(response, SERVER_MANAGED_PROPERTY_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4317            checkForLinkHeader(response, SERVER_MANAGED_TYPE_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4318        }
4319    }
4320
4321    @Test
4322    public void testPatchInsertMementoTypeAndProperty() throws IOException {
4323        final String pid = getRandomUniqueId();
4324        final String subjectURI = serverAddress + pid;
4325        createObjectAndClose(pid);
4326        final HttpPatch patch = new HttpPatch(subjectURI);
4327        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
4328        patch.setEntity(new StringEntity(
4329                "INSERT { <> a <" + MEMENTO_TYPE + "> ; <" + MEMENTO_NAMESPACE + "mementoDatetime" +
4330                        "> \"Thu, 21 Jan 2010 00:09:40 GMT\". } WHERE {}"));
4331        try (final CloseableHttpResponse response = execute(patch)) {
4332            assertEquals("Must not be able to PATCH RDF that contains a memento type and namespace predicate",
4333                    CONFLICT.getStatusCode(), getStatus(response));
4334            checkForLinkHeader(response, SERVER_MANAGED_PROPERTY_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4335            checkForLinkHeader(response, SERVER_MANAGED_TYPE_CONSTRAINT_URI, CONSTRAINED_BY.toString());
4336        }
4337    }
4338
4339    @Test
4340    public void testPatchUpdateIndirectContainerServerManaged() throws IOException {
4341        final String pid = getRandomUniqueId();
4342        final String subjectURI = serverAddress + pid;
4343        createObjectAndClose(pid);
4344        final String indirectContainerPid = getRandomUniqueId();
4345        final String indirectContainerURI = serverAddress + indirectContainerPid;
4346        createObjectAndClose(indirectContainerPid, INDIRECT_CONTAINER_LINK_HEADER);
4347        final HttpPatch patch = new HttpPatch(indirectContainerURI);
4348        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
4349        patch.setEntity(new StringEntity(
4350                "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" +
4351                "PREFIX fedora: <http://fedora.info/definitions/v4/repository#>\n" +
4352                "PREFIX ore: <http://www.openarchives.org/ore/terms/>\n" +
4353                "INSERT { <> ldp:membershipResource <" + subjectURI + "> ;\n" +
4354                    "ldp:hasMemberRelation fedora:createdBy ;\n" +
4355                    "ldp:insertedContentRelation ore:proxyFor. } WHERE {}"));
4356        try (final CloseableHttpResponse response = execute(patch)) {
4357            assertEquals("Must not be able to PATCH IndirectContainer updating Server Managed triples",
4358                         CONFLICT.getStatusCode(), getStatus(response));
4359        }
4360    }
4361
4362    @Test
4363    public void testPutUpdateBinaryWithType() throws Exception {
4364        final String objid = getRandomUniqueId();
4365        final String objURI = serverAddress + objid;
4366        final String binURI = objURI + "/binary1";
4367        final String descURI = objURI + "/binary1/fcr:metadata";
4368
4369        try (final CloseableHttpResponse response = execute(putDSMethod(objid, "binary1", "some test content"))) {
4370            assertEquals(CREATED.getStatusCode(), getStatus(response));
4371        }
4372
4373        // Add type using PUT
4374        final Model model1 = getModel(objid + "/binary1/fcr:metadata", true);
4375        final Resource resc = model1.getResource(binURI);
4376        resc.addProperty(RDF.type, PCDM_FILE_TYPE);
4377
4378        final HttpPut addMethod = new HttpPut(descURI);
4379        addMethod.addHeader(CONTENT_TYPE, "text/turtle");
4380        addMethod.setEntity(new InputStreamEntity(streamModel(model1, RDFFormat.TURTLE)));
4381        try (final CloseableHttpResponse response = execute(addMethod)) {
4382            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
4383        }
4384
4385        final Model model2 = getModel(objid + "/binary1/fcr:metadata", true);
4386        final Resource resc2 = model2.getResource(binURI);
4387        assertTrue(resc2.hasProperty(RDF.type, PCDM_FILE_TYPE));
4388
4389        // Remove type using PUT
4390        model2.remove(resc2, RDF.type, PCDM_FILE_TYPE);
4391
4392        final HttpPut removeMethod = new HttpPut(descURI);
4393        removeMethod.addHeader(CONTENT_TYPE, "text/turtle");
4394        removeMethod.setEntity(new InputStreamEntity(streamModel(model2, RDFFormat.TURTLE)));
4395        try (final CloseableHttpResponse response = execute(removeMethod)) {
4396            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
4397        }
4398
4399        final Model model3 = getModel(objid + "/binary1/fcr:metadata");
4400        final Resource resc3 = model3.getResource(binURI);
4401        assertFalse(resc3.hasProperty(RDF.type, PCDM_FILE_TYPE));
4402    }
4403
4404    @Test
4405    public void testPatchUpdateBinaryWithType() throws Exception {
4406        final String objid = getRandomUniqueId();
4407        final String objURI = serverAddress + objid;
4408        final String binURI = objURI + "/binary1";
4409        final String descURI = objURI + "/binary1/fcr:metadata";
4410
4411        try (final CloseableHttpResponse response = execute(putDSMethod(objid, "binary1", "some test content"))) {
4412            assertEquals(CREATED.getStatusCode(), getStatus(response));
4413        }
4414
4415        // Test adding an rdf:type
4416        final HttpPatch patchBinary = new HttpPatch(descURI);
4417        patchBinary.addHeader(CONTENT_TYPE, "application/sparql-update");
4418        patchBinary.setEntity(new StringEntity("INSERT { <" + binURI + "> " +
4419                "<" + RDF.type.getURI() + "> <" + PCDM_FILE_TYPE.getURI() + "> } WHERE {}"));
4420
4421        try (final CloseableHttpResponse response = execute(patchBinary)) {
4422            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
4423        }
4424
4425        final Model model = getModel(objid + "/binary1/fcr:metadata");
4426        final Resource resc = model.getResource(binURI);
4427        assertTrue(resc.hasProperty(RDF.type, PCDM_FILE_TYPE));
4428
4429        // Test removing an rdf:type
4430        final HttpPatch patchBinary2 = new HttpPatch(descURI);
4431        patchBinary2.addHeader(CONTENT_TYPE, "application/sparql-update");
4432        patchBinary2.setEntity(new StringEntity("DELETE { <" + binURI + "> " +
4433                "<" + RDF.type.getURI() + "> <" + PCDM_FILE_TYPE.getURI() + "> } WHERE {}"));
4434
4435        try (final CloseableHttpResponse response = execute(patchBinary2)) {
4436            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
4437        }
4438
4439        final Model model2 = getModel(objid + "/binary1/fcr:metadata");
4440        final Resource resc2 = model2.getResource(binURI);
4441        assertFalse(resc2.hasProperty(RDF.type, PCDM_FILE_TYPE));
4442    }
4443
4444    /**
4445     * Utility to assert a GET of id and id/fcr:versions
4446     *
4447     * @param id the path
4448     * @throws Exception issue with http communication.
4449     */
4450    private void doGetIdAndVersions(final String id) throws Exception {
4451        final HttpGet getMethod = getObjMethod(id);
4452        try (final CloseableHttpResponse response = execute(getMethod)) {
4453            assertEquals(OK.getStatusCode(), getStatus(response));
4454        }
4455
4456        final HttpGet getVersion = getObjMethod(id + "/" + FCR_VERSIONS);
4457        try (final CloseableHttpResponse response = execute(getVersion)) {
4458            assertEquals(OK.getStatusCode(), getStatus(response));
4459        }
4460    }
4461
4462    @Test
4463    public void testPutGetRdfSourceWithUndefinedPrefix() throws Exception {
4464        final String id = "some_prefix:" + getRandomUniqueId();
4465        final HttpPut putMethod = putObjMethod(id);
4466        try (final CloseableHttpResponse response = execute(putMethod)) {
4467            assertEquals(CREATED.getStatusCode(), getStatus(response));
4468        }
4469
4470        doGetIdAndVersions(id);
4471    }
4472
4473    @Test
4474    public void testPutGetNonRdfSourceWithUndefinedPrefix() throws Exception {
4475        final String id = "some_prefix:" + getRandomUniqueId();
4476        final String dsid = "anotherPrefix:" + getRandomUniqueId();
4477
4478        try (final CloseableHttpResponse response = execute(putDSMethod(id, dsid, "some test content"))) {
4479            assertEquals(CREATED.getStatusCode(), getStatus(response));
4480        }
4481
4482        // Check id/dsid
4483        doGetIdAndVersions(id + "/" + dsid);
4484
4485        // Check id/dsid/fcr:metadata
4486        doGetIdAndVersions(id + "/" + dsid + "/" + FCR_METADATA);
4487
4488    }
4489
4490    @Test
4491    public void testContentTypeWithCharset() throws Exception {
4492        final String uuid = getRandomUniqueId();
4493        final HttpPost postMethod = postObjMethod();
4494        postMethod.setHeader("Slug", uuid);
4495        postMethod.setHeader(CONTENT_TYPE, "text/turtle;charset=utf-8");
4496        final HttpEntity body = new StringEntity("@prefix acl: <http://www.w3.org/ns/auth/acl#>.\n" +
4497                "@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.\n" +
4498                "<> a vcard:Group;\n" +
4499                "  vcard:hasMember  \"someUser\".");
4500        postMethod.setEntity(body);
4501        assertEquals(CREATED.getStatusCode(), getStatus(postMethod));
4502
4503        final HttpGet getMethod = getObjMethod(uuid);
4504        try (final CloseableHttpResponse response = execute(getMethod)) {
4505            final Dataset dataset = getDataset(response);
4506            final DatasetGraph graph = dataset.asDatasetGraph();
4507            checkForLinkHeader(response, BASIC_CONTAINER.toString(), "type");
4508            assertTrue(graph.contains(
4509                    ANY,
4510                    ANY,
4511                    NodeFactory.createURI("http://www.w3.org/2006/vcard/ns#hasMember"),
4512                    NodeFactory.createLiteral("someUser")));
4513        }
4514    }
4515
4516    @Test
4517    public void testGhostNodes() throws IOException {
4518        final String shallowId = getRandomUniqueId();
4519        final String ghostSlug = getRandomUniqueId();
4520        final String ghostId = shallowId + "/" + ghostSlug;
4521        final String deepId = ghostId + "/" + getRandomUniqueId();
4522        // Create 'a'
4523        assertEquals(CREATED.getStatusCode(), getStatus(putObjMethod(shallowId)));
4524        assertEquals(OK.getStatusCode(), getStatus(getObjMethod(shallowId)));
4525
4526        // Create 'a/b/c' ('b' is a ghost node)
4527        assertEquals(CREATED.getStatusCode(), getStatus(putObjMethod(deepId)));
4528        // Ensure the resource exists.
4529        assertEquals(OK.getStatusCode(), getStatus(getObjMethod(deepId)));
4530
4531        // Ensure the ghost node does not exist.
4532        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getObjMethod(ghostId)));
4533        // Ensure you can't create at the ghost node location.
4534        assertEquals(CONFLICT.getStatusCode(), getStatus(putObjMethod(ghostId)));
4535        // Ensure you can't POST with a slug to get the ghost node location.
4536        final HttpPost ghostPost = postObjMethod(shallowId);
4537        ghostPost.setHeader("Slug", ghostSlug);
4538        try (final CloseableHttpResponse response = execute(ghostPost)) {
4539            assertEquals(CREATED.getStatusCode(), getStatus(response));
4540            assertFalse(getLocation(response).contains(ghostId));
4541        }
4542        // Ensure you can't create a child of the ghost node.
4543        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(postObjMethod(ghostId)));
4544
4545        // Delete 'a/b/c'
4546        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(deepId)));
4547
4548        // Assert 'a/b/c' is gone.
4549        assertEquals(GONE.getStatusCode(), getStatus(getObjMethod(deepId)));
4550
4551        // Still can't create because there is a tombstone
4552        assertEquals(CONFLICT.getStatusCode(), getStatus(putObjMethod(ghostId)));
4553
4554        // Delete the tombstone
4555        assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(deepId + "/" + FCR_TOMBSTONE)));
4556
4557        // Now you can create a/b
4558        assertEquals(CREATED.getStatusCode(), getStatus(putObjMethod(ghostId)));
4559
4560        // 'a' exists
4561        assertEquals(OK.getStatusCode(), getStatus(getObjMethod(shallowId)));
4562        // 'a/b' exists
4563        assertEquals(OK.getStatusCode(), getStatus(getObjMethod(ghostId)));
4564        // 'a/b/c' does not exist
4565        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getObjMethod(deepId)));
4566    }
4567
4568    @Test
4569    public void testRepositoryRootTypes() throws Exception {
4570        final HttpGet getRoot = new HttpGet(serverAddress);
4571        final Node rootNode = createURI(serverAddress);
4572        try (final CloseableHttpResponse response = execute(getRoot)) {
4573            final Dataset dataset = getDataset(response);
4574            final DatasetGraph graph = dataset.asDatasetGraph();
4575            assertTrue(graph.contains(ANY, rootNode, type.asNode(), REPOSITORY_ROOT.asNode()));
4576            assertTrue(graph.contains(ANY, rootNode, type.asNode(), RESOURCE.asNode()));
4577            assertTrue(graph.contains(ANY, rootNode, type.asNode(), FEDORA_RESOURCE.asNode()));
4578            assertTrue(graph.contains(ANY, rootNode, type.asNode(), RDF_SOURCE.asNode()));
4579            assertTrue(graph.contains(ANY, rootNode, type.asNode(), CONTAINER.asNode()));
4580            assertTrue(graph.contains(ANY, rootNode, type.asNode(), FEDORA_CONTAINER.asNode()));
4581            assertTrue(graph.contains(ANY, rootNode, type.asNode(), BASIC_CONTAINER.asNode()));
4582        }
4583    }
4584
4585    private static String genLongUrl(final String prefix, final int numChars) {
4586        String ret = prefix;
4587        final Random rand = new Random();
4588        int cnt = prefix.length();
4589
4590        while (cnt <= numChars) {
4591            ret = ret + "/" + (char)(rand.nextInt(26) + 'a');
4592            cnt = ret.length();
4593        }
4594        return ret;
4595    }
4596
4597    @Test
4598    public void testLongIdentifier1() throws IOException {
4599        final String url = genLongUrl(serverAddress,476);
4600        final HttpPut putMethod = new HttpPut(url);
4601        assertEquals(CREATED.getStatusCode(), getStatus(putMethod));
4602        final HttpPost postMethod = new HttpPost(url);
4603        postMethod.setHeader("Slug", getRandomUniqueId());
4604        try (final CloseableHttpResponse response = execute(postMethod)) {
4605            assertEquals(CREATED.getStatusCode(), getStatus(response));
4606        }
4607    }
4608
4609    @Test
4610    public void testLongIdentifier2() throws IOException {
4611        final String url = genLongUrl(serverAddress,477);
4612        final HttpPut putMethod = new HttpPut(url);
4613        assertEquals(CREATED.getStatusCode(), getStatus(putMethod));
4614        final HttpPost postMethod = new HttpPost(url);
4615        postMethod.setHeader("Slug", getRandomUniqueId());
4616        try (final CloseableHttpResponse response = execute(postMethod)) {
4617            assertEquals(BAD_REQUEST.getStatusCode(), getStatus(response));
4618        }
4619    }
4620
4621    @Test
4622    public void testIdConstraints() throws IOException {
4623        assertIdStringConstraint(".fcrepo");
4624        assertIdStringConstraint("fcr-root");
4625        assertIdStringConstraint("fcr-container.nt");
4626
4627        assertIdSuffixConstraint("~fcr-desc");
4628        assertIdSuffixConstraint("~fcr-acl");
4629        assertIdSuffixConstraint("~fcr-desc.nt");
4630        assertIdSuffixConstraint("~fcr-acl.nt");
4631    }
4632
4633    @Test
4634    public void testMapRelativeUris() throws Exception {
4635        final HttpPost post = postObjMethod();
4636        final String body = "<> <http://example.org/related> </fcrepo/rest/test1> ; <http://example.org/related> " +
4637                "</rest/test2> ; <http://example.org/related> <http://something.else/outThere> ; " +
4638                "<http://example.org/related> <test3> .";
4639        post.setHeader(CONTENT_TYPE, "text/turtle");
4640        post.setEntity(new StringEntity(body, UTF_8));
4641        final String location;
4642        try (final var response = execute(post)) {
4643            assertEquals(CREATED.getStatusCode(), getStatus(response));
4644            location = getLocation(response);
4645        }
4646        final HttpGet get = new HttpGet(location);
4647        try (final var dataset = getDataset(get)) {
4648            final var graph = dataset.asDatasetGraph();
4649            final var nodeIter = graph.find(ANY, ANY, createURI("http://example.org/related"), ANY);
4650            int nodeCounter = 0;
4651            final List<String> expectedUris = List.of(serverAddress + "fcrepo/rest/test1",
4652                    serverAddress + "rest/test2", "http://something.else/outThere", serverAddress + "test3");
4653            while (nodeIter.hasNext()) {
4654                final var quad = nodeIter.next();
4655                nodeCounter += 1;
4656                assertTrue(expectedUris.contains(quad.getObject().getURI()));
4657            }
4658            assertEquals(4, nodeCounter);
4659        }
4660    }
4661
4662    @Test
4663    public void testMapRelativeUrisDeeper() throws Exception {
4664        final String location1;
4665        final HttpPost post = postObjMethod();
4666        try (final var response = execute(post)) {
4667            assertEquals(CREATED.getStatusCode(), getStatus(response));
4668            location1 = getLocation(response);
4669        }
4670        final String location2;
4671        final HttpPost post2 = new HttpPost(location1);
4672        try (final var response = execute(post2)) {
4673            assertEquals(CREATED.getStatusCode(), getStatus(response));
4674            location2 = getLocation(response);
4675        }
4676        final HttpPost post3 = new HttpPost(location2);
4677        final String body = "<> <http://example.org/related> </fcrepo/rest/test1> ; <http://example.org/related> " +
4678                "</rest/test2> ; <http://example.org/related> <http://something.else/outThere> ; " +
4679                "<http://example.org/related> <test3> .";
4680        post3.setHeader(CONTENT_TYPE, "text/turtle");
4681        post3.setEntity(new StringEntity(body, UTF_8));
4682        final String location3;
4683        try (final var response = execute(post3)) {
4684            assertEquals(CREATED.getStatusCode(), getStatus(response));
4685            location3 = getLocation(response);
4686        }
4687        final HttpGet get = new HttpGet(location3);
4688        try (final var dataset = getDataset(get)) {
4689            final var graph = dataset.asDatasetGraph();
4690            final var nodeIter = graph.find(ANY, ANY, createURI("http://example.org/related"), ANY);
4691            int nodeCounter = 0;
4692            final List<String> expectedUris = List.of(serverAddress + "fcrepo/rest/test1",
4693                    serverAddress + "rest/test2", "http://something.else/outThere", location2 + "/test3");
4694            while (nodeIter.hasNext()) {
4695                final var quad = nodeIter.next();
4696                nodeCounter += 1;
4697                assertTrue(expectedUris.contains(quad.getObject().getURI()));
4698            }
4699            assertEquals(4, nodeCounter);
4700        }
4701    }
4702
4703    @Test
4704    public void testNonExistantFcrMetadata() throws Exception {
4705        final var id = getRandomUniqueId();
4706        // Ensure the object does not exist
4707        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getObjMethod(id)));
4708        // Ensure the metadata of a non-existant object also doesn't exist.
4709        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getObjMethod(id + "/" + FCR_METADATA)));
4710        // Ensure you can't PUT at non-existant binary.
4711        assertEquals(NOT_FOUND.getStatusCode(), getStatus(putObjMethod(id + "/" + FCR_METADATA)));
4712    }
4713
4714    @Test
4715    public void createObjectWithUnicodeInSlug() throws IOException {
4716        final var id = "☞☕☜☑";
4717        final var encodedId = URLEncoder.encode(id, UTF_8);
4718
4719        final String location;
4720
4721        try (final var response = createObject(id)) {
4722            assertEquals(SC_CREATED, response.getStatusLine().getStatusCode());
4723            location = getLocation(response);
4724            assertTrue(String.format("Expected location to end with '/%s'. Found: %s", encodedId, location),
4725                    location.endsWith("/" + encodedId));
4726        }
4727
4728        try (final var response = execute(new HttpGet(location))) {
4729            assertEquals(SC_OK, response.getStatusLine().getStatusCode());
4730        }
4731    }
4732
4733    @Test
4734    public void postToNonexistantResourceFails() throws IOException {
4735        final var id = getRandomUniqueId();
4736        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getObjMethod(id)));
4737        assertEquals(NOT_FOUND.getStatusCode(), getStatus(postObjMethod(id)));
4738    }
4739
4740    private void assertIdStringConstraint(final String id) throws IOException {
4741        assertInvalidId(id);
4742        assertValidId(id + "-suffix");
4743        assertValidId("prefix-" + id);
4744    }
4745
4746    private void assertIdSuffixConstraint(final String suffix) throws IOException {
4747        assertInvalidId("prefix" + suffix);
4748        assertValidId(suffix);
4749        assertValidId(suffix + "-suffix");
4750    }
4751
4752    private void assertInvalidId(final String id) throws IOException {
4753        final HttpPost postMethod = postObjMethod();
4754        postMethod.setHeader("Slug", id);
4755        try (final CloseableHttpResponse response = execute(postMethod)) {
4756            assertEquals(BAD_REQUEST.getStatusCode(), getStatus(response));
4757        }
4758    }
4759
4760    private void assertValidId(final String id) throws IOException {
4761        final HttpPost postMethod = postObjMethod();
4762        postMethod.setHeader("Slug", id);
4763        try (final CloseableHttpResponse response = execute(postMethod)) {
4764            assertEquals(CREATED.getStatusCode(), getStatus(response));
4765        }
4766    }
4767
4768}