/* * Licensed to DuraSpace under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * * DuraSpace licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.fcrepo.integration.http.api; import static java.lang.Thread.sleep; import static java.nio.charset.StandardCharsets.UTF_8; import static java.time.ZoneId.of; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.regex.Pattern.compile; import static java.util.stream.Collectors.toList; import static javax.ws.rs.core.HttpHeaders.ACCEPT; import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION; import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH; import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; import static javax.ws.rs.core.HttpHeaders.LINK; import static javax.ws.rs.core.Link.fromUri; import static javax.ws.rs.core.MediaType.TEXT_PLAIN; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.CONFLICT; import static javax.ws.rs.core.Response.Status.CREATED; import static javax.ws.rs.core.Response.Status.FORBIDDEN; import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static javax.ws.rs.core.Response.Status.NOT_MODIFIED; import static javax.ws.rs.core.Response.Status.NO_CONTENT; import static javax.ws.rs.core.Response.Status.OK; import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; import static javax.ws.rs.core.Response.Status.TEMPORARY_REDIRECT; import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE; import static nu.validator.htmlparser.common.DoctypeExpectation.NO_DOCTYPE_ERRORS; import static nu.validator.htmlparser.common.XmlViolationPolicy.ALLOW; import static org.apache.http.HttpStatus.SC_BAD_REQUEST; import static org.apache.http.entity.ContentType.parse; import static org.apache.http.impl.client.cache.CacheConfig.DEFAULT; import static org.apache.jena.datatypes.TypeMapper.getInstance; import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDinteger; import static org.apache.jena.graph.Node.ANY; import static org.apache.jena.graph.NodeFactory.createLiteral; import static org.apache.jena.graph.NodeFactory.createURI; import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; import static org.apache.jena.rdf.model.ModelFactory.createModelForGraph; import static org.apache.jena.rdf.model.ResourceFactory.createProperty; import static org.apache.jena.rdf.model.ResourceFactory.createResource; import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; import static org.apache.jena.riot.WebContent.contentTypeN3; import static org.apache.jena.riot.WebContent.contentTypeN3Alt2; import static org.apache.jena.riot.WebContent.contentTypeNTriples; import static org.apache.jena.riot.WebContent.contentTypeRDFXML; import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate; import static org.apache.jena.riot.WebContent.contentTypeTurtle; import static org.apache.jena.vocabulary.DC_11.title; import static org.apache.jena.vocabulary.RDF.type; import static org.fcrepo.http.commons.domain.RDFMediaType.POSSIBLE_RDF_RESPONSE_VARIANTS_STRING; import static org.fcrepo.http.commons.domain.RDFMediaType.POSSIBLE_RDF_VARIANTS; import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET; import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER; import static org.fcrepo.kernel.api.RdfLexicon.CONSTRAINED_BY; import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER; import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS; import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE; import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER; import static org.fcrepo.kernel.api.RdfLexicon.HAS_CHILD; import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION; import static org.fcrepo.kernel.api.RdfLexicon.HAS_MIME_TYPE; import static org.fcrepo.kernel.api.RdfLexicon.HAS_ORIGINAL_NAME; import static org.fcrepo.kernel.api.RdfLexicon.INBOUND_REFERENCES; import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER; import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE; import static org.fcrepo.kernel.api.RdfLexicon.LDP_MEMBER; import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE; import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE; import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.slf4j.LoggerFactory.getLogger; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URI; import java.text.ParseException; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; import java.util.Optional; import javax.ws.rs.core.Link; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Variant; import org.fcrepo.http.commons.domain.RDFMediaType; import org.fcrepo.http.commons.test.util.CloseableDataset; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.FileEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.cache.CachingHttpClientBuilder; import org.apache.http.util.EntityUtils; import org.apache.jena.graph.Node; import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.StmtIterator; import org.apache.jena.sparql.core.DatasetGraph; import org.apache.jena.sparql.core.Quad; import org.apache.jena.vocabulary.DC_11; import org.glassfish.jersey.media.multipart.ContentDisposition; import org.junit.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Iterators; import nu.validator.htmlparser.sax.HtmlParser; import nu.validator.saxtree.TreeBuilder; /** * @author cabeer * @author ajs6f */ public class FedoraLdpIT extends AbstractResourceIT { private static final Node DC_IDENTIFIER = DC_11.identifier.asNode(); private static final String LDP_RESOURCE_LINK_HEADER = "<" + LDP_NAMESPACE + "Resource>;rel=\"type\""; private static final Node rdfType = type.asNode(); private static final Node DCTITLE = title.asNode(); private static final String INDIRECT_CONTAINER_LINK_HEADER = "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\""; private static final String BASIC_CONTAINER_LINK_HEADER = "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""; private static final String DIRECT_CONTAINER_LINKHEADER = "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\""; private static final String TEST_ACTIVATION_PROPERTY = "RUN_TEST_CREATE_MANY"; private static final Logger LOGGER = getLogger(FedoraLdpIT.class); private static DateTimeFormatter headerFormat = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US).withZone(of("GMT")); private static DateTimeFormatter tripleFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(of("GMT")); @Test public void testHeadRepositoryGraph() { final HttpHead headObjMethod = new HttpHead(serverAddress); assertEquals(OK.getStatusCode(), getStatus(headObjMethod)); } @Test public void testHeadObject() throws IOException { final String id = getRandomUniqueId(); createObject(id).close(); final HttpHead headObjMethod = headObjMethod(id); assertEquals(OK.getStatusCode(), getStatus(headObjMethod)); } @Test public void testHeadDefaultContainer() throws IOException { final String id = getRandomUniqueId(); createObject(id).close(); final HttpHead headObjMethod = headObjMethod(id); try (final CloseableHttpResponse response = execute(headObjMethod)) { assertTrue("Didn't find LDP container link header!", getLinkHeaders(response).contains( BASIC_CONTAINER_LINK_HEADER)); } } @Test public void testHeadBasicContainer() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); addMixin(id, BASIC_CONTAINER.getURI()); final HttpHead headObjMethod = headObjMethod(id); try (final CloseableHttpResponse response = execute(headObjMethod)) { final Collection<String> links = getLinkHeaders(response); assertTrue("Didn't find LDP container link header!", links.contains(BASIC_CONTAINER_LINK_HEADER)); } } @Test public void testHeadTurtleContentType() throws IOException { testHeadDefaultContentType(RDFMediaType.TURTLE_WITH_CHARSET); } @Test public void testHeadRDFContentType() throws IOException { testHeadDefaultContentType(RDFMediaType.RDF_XML); } @Test public void testHeadJSONLDContentType() throws IOException { testHeadDefaultContentType(RDFMediaType.JSON_LD); } @Test public void testHeadDefaultContentType() throws IOException { testHeadDefaultContentType(null); } private void testHeadDefaultContentType(final String mimeType) throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); addMixin(id, CONTAINER.getURI()); final HttpHead headObjMethod = headObjMethod(id); String mt = mimeType; if (mt != null) { headObjMethod.addHeader("Accept", mt); } else { mt = RDFMediaType.TURTLE_WITH_CHARSET; } try (final CloseableHttpResponse response = execute(headObjMethod)) { final Collection<String> contentTypes = getHeader(response, CONTENT_TYPE); final String contentType = contentTypes.iterator().next(); assertTrue("Didn't find LDP valid content-type header: " + contentType + "; expected result: " + mt, contentType.contains(mt)); } } @Test public void testHeadDirectContainer() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); addMixin(id, DIRECT_CONTAINER.getURI()); final HttpHead headObjMethod = headObjMethod(id); try (final CloseableHttpResponse response = execute(headObjMethod)) { final Collection<String> links = getLinkHeaders(response); assertTrue("Didn't find LDP container link header!", links.contains(DIRECT_CONTAINER_LINKHEADER)); } } @Test public void testHeadIndirectContainer() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); addMixin(id, INDIRECT_CONTAINER.getURI()); final HttpHead headObjMethod = headObjMethod(id); try (final CloseableHttpResponse response = execute(headObjMethod)) { final Collection<String> links = getLinkHeaders(response); assertTrue("Didn't find LDP container link header!", links.contains(INDIRECT_CONTAINER_LINK_HEADER)); } } @Test public void testHeadDatastream() throws IOException, ParseException { final String id = getRandomUniqueId(); createDatastream(id, "x", "123"); final HttpHead headObjMethod = headObjMethod(id + "/x"); try (final CloseableHttpResponse response = execute(headObjMethod)) { assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode()); assertEquals(TEXT_PLAIN, response.getFirstHeader(CONTENT_TYPE).getValue()); assertEquals("3", response.getFirstHeader(CONTENT_LENGTH).getValue()); assertEquals("bytes", response.getFirstHeader("Accept-Ranges").getValue()); final ContentDisposition disposition = new ContentDisposition(response.getFirstHeader(CONTENT_DISPOSITION).getValue()); assertEquals("attachment", disposition.getType()); } } @Test public void testHeadExternalDatastream() throws IOException, ParseException { final String externalContentType = "message/external-body;access-type=URL;url=\"some:uri\""; final String id = getRandomUniqueId(); final HttpPut put = putObjMethod(id); put.addHeader(CONTENT_TYPE, externalContentType); assertEquals(CREATED.getStatusCode(), getStatus(put)); // Configure HEAD request to NOT follow redirects final HttpHead headObjMethod = headObjMethod(id); final RequestConfig.Builder requestConfig = RequestConfig.custom(); requestConfig.setRedirectsEnabled(false); headObjMethod.setConfig(requestConfig.build()); try (final CloseableHttpResponse response = execute(headObjMethod)) { assertEquals(TEMPORARY_REDIRECT.getStatusCode(), response.getStatusLine().getStatusCode()); assertEquals(externalContentType, response.getFirstHeader(CONTENT_TYPE).getValue()); assertEquals("bytes", response.getFirstHeader("Accept-Ranges").getValue()); final ContentDisposition disposition = new ContentDisposition(response.getFirstHeader(CONTENT_DISPOSITION).getValue()); assertEquals("attachment", disposition.getType()); } } @Test public void testOptions() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); final HttpOptions optionsRequest = new HttpOptions(serverAddress + id); try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) { assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode()); assertContainerOptionsHeaders(optionsResponse); } } @Test public void testOptionsBinary() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", id); final HttpOptions optionsRequest = new HttpOptions(serverAddress + id + "/x"); try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) { assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode()); assertResourceOptionsHeaders(optionsResponse); assertEquals("0", optionsResponse.getFirstHeader(CONTENT_LENGTH).getValue()); } } @Test public void testOptionsBinaryMetadata() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", null); final HttpOptions optionsRequest = new HttpOptions(serverAddress + id + "/x/fcr:metadata"); try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) { assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode()); assertNonRdfResourceDescriptionOptionsHeaders(optionsResponse); } } @Test public void testOptionsBinaryMetadataWithUriEncoding() throws Exception { final String id = getRandomUniqueId(); createDatastream(id, "x", null); final String location = serverAddress + id + "/x/fcr%3Ametadata"; final HttpOptions optionsRequest = new HttpOptions(location); try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) { assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode()); assertNonRdfResourceDescriptionOptionsHeaders(optionsResponse); } } private static void assertContainerOptionsHeaders(final HttpResponse httpResponse) { assertRdfOptionsHeaders(httpResponse); final List<String> methods = headerValues(httpResponse, "Allow"); assertTrue("Should allow POST", methods.contains(HttpPost.METHOD_NAME)); final List<String> postTypes = headerValues(httpResponse, "Accept-Post"); assertTrue("POST should support application/sparql-update", postTypes.contains(contentTypeSPARQLUpdate)); assertTrue("POST should support text/turtle", postTypes.contains(contentTypeTurtle)); assertTrue("POST should support text/rdf+n3", postTypes.contains(contentTypeN3)); assertTrue("POST should support text/n3", postTypes.contains(contentTypeN3Alt2)); assertTrue("POST should support application/rdf+xml", postTypes.contains(contentTypeRDFXML)); assertTrue("POST should support application/n-triples", postTypes.contains(contentTypeNTriples)); assertTrue("POST should support multipart/form-data", postTypes.contains("multipart/form-data")); } private static void assertRdfOptionsHeaders(final HttpResponse httpResponse) { final List<String> methods = headerValues(httpResponse, "Allow"); assertTrue("Should allow PATCH", methods.contains(HttpPatch.METHOD_NAME)); assertTrue("Should allow MOVE", methods.contains("MOVE")); assertTrue("Should allow COPY", methods.contains("COPY")); final List<String> patchTypes = headerValues(httpResponse, "Accept-Patch"); assertTrue("PATCH should support application/sparql-update", patchTypes.contains(contentTypeSPARQLUpdate)); assertResourceOptionsHeaders(httpResponse); } private static void assertNonRdfResourceDescriptionOptionsHeaders(final HttpResponse httpResponse) { final List<String> methods = headerValues(httpResponse, "Allow"); assertTrue("Should allow PATCH", methods.contains(HttpPatch.METHOD_NAME)); assertTrue("Should allow HEAD", methods.contains(HttpHead.METHOD_NAME)); final List<String> patchTypes = headerValues(httpResponse, "Accept-Patch"); assertTrue("PATCH should support application/sparql-update", patchTypes.contains(contentTypeSPARQLUpdate)); assertResourceOptionsHeaders(httpResponse); } private static void assertResourceOptionsHeaders(final HttpResponse httpResponse) { final List<String> methods = headerValues(httpResponse, "Allow"); assertTrue("Should allow GET", methods.contains(HttpGet.METHOD_NAME)); assertTrue("Should allow PUT", methods.contains(HttpPut.METHOD_NAME)); assertTrue("Should allow DELETE", methods.contains(HttpDelete.METHOD_NAME)); assertTrue("Should allow OPTIONS", methods.contains(HttpOptions.METHOD_NAME)); } private static List<String> headerValues(final HttpResponse response, final String headerName) { return stream(response.getHeaders(headerName)).map(Header::getValue).map(s -> s.split(",")).flatMap( Arrays::stream).map(String::trim).collect(toList()); } @Test public void testGetRDFSource() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); try (final CloseableHttpResponse response = execute(new HttpGet(serverAddress + id))) { assertEquals(OK.getStatusCode(), getStatus(response)); final HttpEntity entity = response.getEntity(); final String contentType = parse(entity.getContentType().getValue()).getMimeType(); assertNotNull("Entity is not an RDF serialization!", contentTypeToLang(contentType)); } } @Test public void testGetNonRDFSource() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "some content"); try (final CloseableHttpResponse response = execute(getDSMethod(id, "x"))) { final HttpEntity entity = response.getEntity(); final String content = EntityUtils.toString(entity); assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode()); assertEquals("some content", content); } } @Test public void testGetNonRDFSourceDescription() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "some content"); try (final CloseableHttpResponse response = execute(getDSDescMethod(id, "x")); final CloseableDataset dataset = getDataset(response)) { final DatasetGraph graph = dataset.asDatasetGraph(); final Node correctDSSubject = createURI(serverAddress + id + "/x"); assertTrue("Binary should be a ldp:NonRDFSource", graph.contains(ANY, correctDSSubject, rdfType, NON_RDF_SOURCE.asNode())); // every triple in the response should have a subject of the actual resource described logger.info("Found graph:\n{}", graph); graph.find().forEachRemaining(quad -> { assertEquals("Found a triple with incorrect subject!", correctDSSubject, quad.getSubject()); }); } } /** * Test that a 406 gets returned in the event of an invalid or unsupported * format being requested. */ @Test public void testGetRDFSourceWrongAccept() { final String id = getRandomUniqueId(); createObjectAndClose(id); final HttpGet get = new HttpGet(serverAddress + id); get.addHeader(ACCEPT, "application/turtle"); assertEquals(NOT_ACCEPTABLE.getStatusCode(), getStatus(get)); } @Test public void testDeleteObject() { final String id = getRandomUniqueId(); createObjectAndClose(id); assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id))); assertDeleted(id); } @Test public void testDeleteHierarchy() { final String id = getRandomUniqueId(); createObjectAndClose(id + "/foo"); assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id))); assertDeleted(id); assertDeleted(id + "/foo"); } @Test public void testDeleteBinary() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "some content"); assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id + "/x"))); assertDeleted(id + "/x"); } @Test public void testDeleteObjectAndTombstone() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(serverAddress + id))); assertDeleted(id); final HttpGet httpGet = getObjMethod(id); final Link tombstone; try (final CloseableHttpResponse response = execute(httpGet)) { tombstone = Link.valueOf(response.getFirstHeader(LINK).getValue()); } assertEquals("hasTombstone", tombstone.getRel()); assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(tombstone.getUri()))); assertEquals(NOT_FOUND.getStatusCode(), getStatus(httpGet)); } @Test public void testEmptyPatch() { final String id = getRandomUniqueId(); createObjectAndClose(id); final HttpPatch patch = patchObjMethod(id); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(patch)); } @Test public void testUpdateObjectGraph() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); final String location = serverAddress + id; final HttpPatch updateObjectGraphMethod = new HttpPatch(location); updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" + location + "> " + "<http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\" } WHERE {}")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod)); } @Test public void testDeleteMultipleMultiValuedProperties() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); final String location = serverAddress + id; final HttpPatch addTriplesGraphMethod = new HttpPatch(location); addTriplesGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); final String insertStatement = "INSERT DATA { \n" + " <> <http://example.org/test/x> \"x\" . \n" + " <> <http://example.org/test/a> \"1\" . \n" + " <> <http://example.org/test/a> \"2\" . \n" + " <> <http://example.org/test/a> \"3\" . \n" + " <> <http://example.org/test/a> \"4\" . \n" + " <> <http://example.org/test/b> \"1\" . \n" + " <> <http://example.org/test/b> \"2\" . \n" + " <> <http://example.org/test/b> \"3\" . \n" + " <> <http://example.org/test/b> \"4\" . \n" + "}"; addTriplesGraphMethod.setEntity(new StringEntity(insertStatement)); assertEquals(NO_CONTENT.getStatusCode(), getStatus(addTriplesGraphMethod)); // ensure that the triples are there. try (final CloseableDataset dataset = getDataset(getObjMethod(id))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue("Didn't find a triple we expected!", graph.contains(ANY, createURI(location), createURI("http://example.org/test/x"), createLiteral("x"))); assertTrue("Didn't find a triple we expected!", graph.contains(ANY, createURI(location), createURI("http://example.org/test/a"), createLiteral("1"))); } final HttpPatch deleteQuery = new HttpPatch(location); deleteQuery.addHeader(CONTENT_TYPE, "application/sparql-update"); final String deleteQueryStatement = "" + "DELETE \n" + "WHERE \n" + "{ \n" + " <> <http://example.org/test/a> ?a . \n" + " <> <http://example.org/test/b> ?b . \n" + "} "; deleteQuery.setEntity(new StringEntity(deleteQueryStatement)); assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteQuery)); // ensure that the expected triples removed. try (final CloseableDataset dataset = getDataset(getObjMethod(id))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue("Didn't find a triple we expected!", graph.contains(ANY, createURI(location), createURI("http://example.org/test/x"), createLiteral("x"))); for (int i = 0; i < 4; ++i) { for (String suffix : Arrays.asList("a", "b")) { assertFalse("Found a triple we deleted!", graph.contains(ANY, createURI(location), createURI("http://example.org/test/" + suffix), createLiteral(i + ""))); } } } } @Test public void testPatchBinary() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "some content"); final HttpPatch patch = patchObjMethod(id + "/x"); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(patch)); } /** * Descriptions of bitstreams contain triples about the described thing, so only triples with the described thing * as their subject are legal. * * @throws IOException in case of IOException */ @Test public void testPatchBinaryDescription() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "some content"); final String location = serverAddress + id + "/x/fcr:metadata"; final HttpPatch patch = new HttpPatch(location); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new StringEntity("INSERT { <" + serverAddress + id + "/x> " + "<http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\" } WHERE {}")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch)); } /** * Descriptions of bitstreams contain only triples about the described thing, so only triples with the described * thing as their subject are legal. * * @throws IOException on error */ @Test public void testPatchBinaryDescriptionWithBinaryProperties() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "some content"); final String resource = serverAddress + id; final String location = resource + "/x/fcr:metadata"; final HttpPatch patch = new HttpPatch(location); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new StringEntity("INSERT { <" + resource + "/x>" + " <" + DC_IDENTIFIER + "> \"identifier\" } WHERE {}")); try (final CloseableHttpResponse response = execute(patch)) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue(graph.contains(ANY, createURI(resource + "/x"), DC_IDENTIFIER, createLiteral("identifier"))); } } } @Test public void testPatchBinaryNameAndType() throws IOException { final String pid = getRandomUniqueId(); createDatastream(pid, "x", "some content"); final String location = serverAddress + pid + "/x/fcr:metadata"; final HttpPatch patch = new HttpPatch(location); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new StringEntity("DELETE { " + "<" + serverAddress + pid + "/x> <" + HAS_MIME_TYPE + "> ?any . } " + "WHERE {" + "<" + serverAddress + pid + "/x> <" + HAS_MIME_TYPE + "> ?any . } ; " + "INSERT {" + "<" + serverAddress + pid + "/x> <" + HAS_MIME_TYPE + "> \"text/awesome\" ." + "<" + serverAddress + pid + "/x> <" + HAS_ORIGINAL_NAME + "> \"x.txt\" }" + "WHERE {}")); try (final CloseableHttpResponse response = client.execute(patch)) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); final Node subject = createURI(serverAddress + pid + "/x"); assertTrue(graphStore.contains(ANY, subject, HAS_MIME_TYPE.asNode(), createLiteral("text/awesome"))); assertTrue(graphStore.contains(ANY, subject, HAS_ORIGINAL_NAME.asNode(), createLiteral("x.txt"))); assertFalse("Should not contain old mime type property", graphStore.contains(ANY, subject, createURI(REPOSITORY_NAMESPACE + "mimeType"), ANY)); } } // Ensure binary can be downloaded (test against regression of: https://jira.duraspace.org/browse/FCREPO-1720) assertEquals(OK.getStatusCode(), getStatus(getDSMethod(pid, "x"))); } @Test public void testPatchWithBlankNode() throws Exception { final String id = getRandomUniqueId(); createObjectAndClose(id); final String location = serverAddress + id; final HttpPatch updateObjectGraphMethod = patchObjMethod(id); updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" + location + "> <info:some-predicate> _:a .\n " + "_:a <http://purl.org/dc/elements/1.1/title> \"this is a title\"\n" + " } WHERE {}")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod)); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); assertTrue(graphStore.contains(ANY, createURI(location), createURI("info:some-predicate"), ANY)); final Node bnode = graphStore.find(ANY, createURI(location), createURI("info:some-predicate"), ANY).next().getObject(); try (final CloseableDataset dataset2 = getDataset(new HttpGet(bnode.getURI()))) { assertTrue(dataset2.asDatasetGraph().contains(ANY, bnode, DCTITLE, createLiteral("this is a title"))); } } } @Test public void testReplaceGraph() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); final String subjectURI = serverAddress + id; final String initialContent; try (final CloseableHttpResponse subjectResponse = execute(getObjMethod(id))) { initialContent = EntityUtils.toString(subjectResponse.getEntity()); } final HttpPut replaceMethod = putObjMethod(id); replaceMethod.addHeader(CONTENT_TYPE, "application/n3"); replaceMethod .setEntity(new StringEntity(initialContent + "\n<" + subjectURI + "> <info:test#label> \"foo\"")); try (final CloseableHttpResponse response = execute(replaceMethod)) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); assertTrue("Didn't find ETag header!", response.containsHeader("ETag")); } try (final CloseableDataset dataset = getDataset(getObjMethod(id))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue("Didn't find a triple we tried to create!", graph.contains(ANY, createURI(subjectURI), createURI("info:test#label"), createLiteral("foo"))); } } @Test public void testCreateGraph() throws IOException { final String subjectURI = serverAddress + getRandomUniqueId(); final HttpPut createMethod = new HttpPut(subjectURI); createMethod.addHeader(CONTENT_TYPE, "application/n3"); createMethod.setEntity(new StringEntity("<" + subjectURI + "> <info:test#label> \"foo\"")); assertEquals(CREATED.getStatusCode(), getStatus(createMethod)); try (final CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue("Didn't find a triple we tried to create!", graph.contains(ANY, createURI(subjectURI), createURI("info:test#label"), createLiteral("foo"))); } } @Test public void testCreateGraphWithBlanknodes() throws IOException { final String subjectURI = serverAddress + getRandomUniqueId(); final HttpPut createMethod = new HttpPut(subjectURI); createMethod.addHeader(CONTENT_TYPE, "application/n3"); createMethod.setEntity(new StringEntity("<" + subjectURI + "> <info:some-predicate> _:a ." + "_:a <info:test#label> \"asdfg\"")); assertEquals(CREATED.getStatusCode(), getStatus(createMethod)); final HttpGet getObjMethod = new HttpGet(subjectURI); getObjMethod.addHeader(ACCEPT, "application/rdf+xml"); try (final CloseableDataset dataset = getDataset(getObjMethod)) { final DatasetGraph graph = dataset.asDatasetGraph(); final Iterator<Quad> quads = graph.find(ANY, createURI(subjectURI), createURI("info:some-predicate"), ANY); assertTrue("Didn't find skolemized blank node assertion", quads.hasNext()); final Node skolemizedNode = quads.next().getObject(); assertTrue("Didn't find a triple we tried to create!", graph.contains(ANY, skolemizedNode, createURI("info:test#label"), createLiteral("asdfg"))); } } @Test public void testRoundTripReplaceGraph() throws IOException { final String subjectURI = getLocation(postObjMethod()); final HttpGet getObjMethod = new HttpGet(subjectURI); getObjMethod.addHeader(ACCEPT, "text/turtle"); final Model model = createDefaultModel(); try (final CloseableHttpResponse getResponse = execute(getObjMethod)) { model.read(getResponse.getEntity().getContent(), subjectURI, "TURTLE"); } final HttpPut replaceMethod = new HttpPut(subjectURI); replaceMethod.addHeader(CONTENT_TYPE, "application/n-triples"); try (final StringWriter w = new StringWriter()) { model.write(w, "N-TRIPLE"); replaceMethod.setEntity(new StringEntity(w.toString())); logger.trace("Retrieved object graph for testRoundTripReplaceGraph():\n {}", w); } assertEquals(NO_CONTENT.getStatusCode(), getStatus(replaceMethod)); } @Test public void testPutBinary() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); final String location = serverAddress + id + "/x"; final HttpPut method = new HttpPut(location); method.setEntity(new StringEntity("foo")); try (final CloseableHttpResponse response = execute(method)) { assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); assertTrue("Didn't find ETag header!", response.containsHeader("ETag")); final String receivedLocation = response.getFirstHeader("Location").getValue(); assertEquals(CREATED.getStatusCode(), getStatus(response)); assertEquals("Got wrong URI in Location header for datastream creation!", location, receivedLocation); assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); final String lastmod = response.getFirstHeader("Last-Modified").getValue(); assertNotNull("Should set Last-Modified for new nodes", lastmod); assertNotEquals("Last-Modified should not be blank for new nodes", lastmod.trim(), ""); final Link link = Link.valueOf(response.getFirstHeader(LINK).getValue()); assertEquals("describedby", link.getRel()); } } @Test public void testBinaryEtags() throws IOException, InterruptedException { final String id = getRandomUniqueId(); createObjectAndClose(id); final String location = serverAddress + id + "/binary"; final HttpPut method = new HttpPut(location); method.setEntity(new StringEntity("foo")); final String binaryEtag1, binaryEtag2, binaryEtag3, descEtag1, descEtag2, descEtag3; final String binaryLastModed1, binaryLastModed2, binaryLastModed3; final String descLastModed1, descLastModed2, descLastModed3; final String descLocation; try (final CloseableHttpResponse response = execute(method)) { binaryEtag1 = response.getFirstHeader("ETag").getValue(); binaryLastModed1 = response.getFirstHeader("Last-Modified").getValue(); descLocation = Link.valueOf(response.getFirstHeader(LINK).getValue()).getUri().toString(); } // First check ETags and Last-Modified headers for the binary final HttpGet get1 = new HttpGet(location); get1.addHeader("If-None-Match", binaryEtag1); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get1)); final HttpGet get2 = new HttpGet(location); get2.addHeader("If-Modified-Since", binaryLastModed1); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get2)); // Next, check ETags and Last-Modified headers on the description final HttpGet get3 = new HttpGet(descLocation); try (final CloseableHttpResponse response = execute(get3)) { descEtag1 = response.getFirstHeader("ETag").getValue(); descLastModed1 = response.getFirstHeader("Last-Modified").getValue(); } assertNotEquals("Binary, description ETags should be different", binaryEtag1, descEtag1); final HttpGet get4 = new HttpGet(descLocation); get4.addHeader("If-None-Match", descEtag1); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get4)); final HttpGet get5 = new HttpGet(descLocation); get5.addHeader("If-Modified-Since", descLastModed1); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get5)); // Pause two seconds before updating the description sleep(2000); // Next, update the description final HttpPatch httpPatch = patchObjMethod(id + "/binary/fcr:metadata"); assertTrue("Expected weak ETag", descEtag1.startsWith("W/")); httpPatch.addHeader(CONTENT_TYPE, "application/sparql-update"); httpPatch.addHeader("If-Match", descEtag1.substring(2)); httpPatch.setEntity(new StringEntity( "INSERT { <> <http://purl.org/dc/elements/1.1/title> 'this is a title' } WHERE {}")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPatch)); // Next, check headers for the binary; they should not have changed final HttpHead head1 = new HttpHead(location); try (final CloseableHttpResponse response = execute(head1)) { binaryEtag2 = response.getFirstHeader("ETag").getValue(); binaryLastModed2 = response.getFirstHeader("Last-Modified").getValue(); } assertEquals("ETags should be the same", binaryEtag1, binaryEtag2); assertEquals("Last-Modified should be the same", binaryLastModed1, binaryLastModed2); final HttpGet get6 = new HttpGet(location); get6.addHeader("If-None-Match", binaryEtag1); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get6)); final HttpGet get7 = new HttpGet(location); get7.addHeader("If-Modified-Since", binaryLastModed1); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get7)); // Next, check headers for the description; they should have changed final HttpHead head2 = new HttpHead(descLocation); try (final CloseableHttpResponse response = execute(head2)) { descEtag2 = response.getFirstHeader("ETag").getValue(); descLastModed2 = response.getFirstHeader("Last-Modified").getValue(); } assertNotEquals("ETags should not be the same", descEtag1, descEtag2); assertNotEquals("Last-Modified should not be the same", descLastModed1, descLastModed2); final HttpGet get8 = new HttpGet(descLocation); get8.addHeader("If-None-Match", descEtag2); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get8)); final HttpGet get9 = new HttpGet(descLocation); get9.addHeader("If-Modified-Since", descLastModed2); assertEquals("Expected 304 Not Modified", NOT_MODIFIED.getStatusCode(), getStatus(get9)); sleep(1000); // Next, update the binary itself final HttpPut method2 = new HttpPut(location); assertFalse("Expected strong ETag", binaryEtag1.startsWith("W/")); method2.addHeader("If-Match", binaryEtag1); method2.setEntity(new StringEntity("foobar")); try (final CloseableHttpResponse response = execute(method2)) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); binaryEtag3 = response.getFirstHeader("ETag").getValue(); binaryLastModed3 = response.getFirstHeader("Last-Modified").getValue(); } final HttpGet get10 = new HttpGet(location); get10.addHeader("If-None-Match", binaryEtag1); assertEquals("Expected 200 OK", OK.getStatusCode(), getStatus(get10)); final HttpGet get11 = new HttpGet(location); get11.addHeader("If-Modified-Since", binaryLastModed1); assertEquals("Expected 200 OK", OK.getStatusCode(), getStatus(get11)); assertNotEquals("ETags should have changed", binaryEtag1, binaryEtag3); assertNotEquals("Last-Modified should have changed", binaryLastModed1, binaryLastModed3); // Next, check headers for the description; they should have changed final HttpHead head3 = new HttpHead(descLocation); try (final CloseableHttpResponse response = execute(head3)) { descEtag3 = response.getFirstHeader("ETag").getValue(); descLastModed3 = response.getFirstHeader("Last-Modified").getValue(); } assertNotEquals("ETags should have changed", descEtag2, descEtag3); assertNotEquals("Last-Modified should have changed", descLastModed2, descLastModed3); } @Test public void testETagOnDeletedChild() throws Exception { final String id = getRandomUniqueId(); final String child = id + "/child"; createObjectAndClose(id); createObjectAndClose(child); final HttpGet get = new HttpGet(serverAddress + id); final String etag1; try (final CloseableHttpResponse response = execute(get)) { etag1 = response.getFirstHeader("ETag").getValue(); } assertEquals("Child resource not deleted!", NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(serverAddress + child))); final String etag2; try (final CloseableHttpResponse response = execute(get)) { etag2 = response.getFirstHeader("ETag").getValue(); } assertNotEquals("ETag didn't change!", etag1, etag2); } @Test public void testETagOnDeletedLdpDirectContainerChild() throws Exception { final String id = getRandomUniqueId(); final String members = id + "/members"; final String child = members + "/child"; createObjectAndClose(id); // Create the DirectContainer final HttpPut createContainer = new HttpPut(serverAddress + members); createContainer.addHeader(CONTENT_TYPE, "text/turtle"); final String membersRDF = "<> a <http://www.w3.org/ns/ldp#DirectContainer>; " + "<http://www.w3.org/ns/ldp#hasMemberRelation> <http://pcdm.org/models#hasMember>; " + "<http://www.w3.org/ns/ldp#membershipResource> <" + serverAddress + id + "> . "; createContainer.setEntity(new StringEntity(membersRDF)); assertEquals("Membership container not created!", CREATED.getStatusCode(), getStatus(createContainer)); // Create the child resource createObjectAndClose(child); final HttpGet get = new HttpGet(serverAddress + id); final String etag1; try (final CloseableHttpResponse response = execute(get)) { etag1 = response.getFirstHeader("ETag").getValue(); } assertEquals("Child resource not deleted!", NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(serverAddress + child))); final String etag2; try (final CloseableHttpResponse response = execute(get)) { etag2 = response.getFirstHeader("ETag").getValue(); } assertNotEquals("ETag didn't change!", etag1, etag2); } @Test public void testETagOnDeletedLdpIndirectContainerChild() throws Exception { final String id = getRandomUniqueId(); final String members = id + "/members"; final String child = members + "/child"; createObjectAndClose(id); // Create the IndirectContainer final HttpPut createContainer = new HttpPut(serverAddress + members); createContainer.addHeader(CONTENT_TYPE, "text/turtle"); final String membersRDF = "<> a <http://www.w3.org/ns/ldp#IndirectContainer>; " + "<http://www.w3.org/ns/ldp#hasMemberRelation> <info:fedora/test/hasTitle> ; " + "<http://www.w3.org/ns/ldp#insertedContentRelation> <http://www.w3.org/2004/02/skos/core#prefLabel>; " + "<http://www.w3.org/ns/ldp#membershipResource> <" + serverAddress + id + "> . "; createContainer.setEntity(new StringEntity(membersRDF)); assertEquals("Membership container not created!", CREATED.getStatusCode(), getStatus(createContainer)); // Create a child with the appropriate property final HttpPut createChild = new HttpPut(serverAddress + child); createChild.addHeader(CONTENT_TYPE, "text/turtle"); final String childRDF = "<> <http://www.w3.org/2004/02/skos/core#prefLabel> \"A title\"."; createChild.setEntity(new StringEntity(childRDF)); assertEquals("Child container not created!", CREATED.getStatusCode(), getStatus(createChild)); final HttpGet get = new HttpGet(serverAddress + id); final String etag1; try (final CloseableHttpResponse response = execute(get)) { etag1 = response.getFirstHeader("ETag").getValue(); final String resp = IOUtils.toString(response.getEntity().getContent(), UTF_8); } assertEquals("Child resource not deleted!", NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(serverAddress + child))); final String etag2; try (final CloseableHttpResponse response = execute(get)) { etag2 = response.getFirstHeader("ETag").getValue(); final String resp = IOUtils.toString(response.getEntity().getContent(), UTF_8); } assertNotEquals("ETag didn't change!", etag1, etag2); } @Test public void testPutDatastreamContentOnObject() throws IOException { final String content = "foo"; final String id = getRandomUniqueId(); createObjectAndClose(id); final HttpPut put = putObjMethod(id); put.setEntity(new StringEntity(content)); put.setHeader(CONTENT_TYPE, "application/octet-stream"); assertEquals( "Expected UNSUPPORTED MEDIA TYPE response when PUTing content to an object (as opposed to datastream)", UNSUPPORTED_MEDIA_TYPE.getStatusCode(), getStatus(put)); } @Test public void testInvalidNamespaceOnHeadReturnsCorrectContentType() throws IOException { assertEquals("Expected " + TEXT_PLAIN_WITH_CHARSET, TEXT_PLAIN_WITH_CHARSET, getContentType(headObjMethod("/fcr:accessroles"), Status.BAD_REQUEST)); } @Test public void testEmptyPutToExistingObject() { final String id = getRandomUniqueId(); createObjectAndClose(id); assertEquals("Expected CONFLICT response code when doing empty PUT on an existing object.", CONFLICT.getStatusCode(), getStatus(putObjMethod(id))); } @Test public void testPutMalformedRDFOnObject() throws IOException { final String content = "this is not legitimate RDF"; final String id = getRandomUniqueId(); createObjectAndClose(id); final HttpPut put = putObjMethod(id); put.setEntity(new StringEntity(content)); put.setHeader(CONTENT_TYPE, "text/plain"); assertEquals("Expected BAD REQUEST response code when PUTing malformed RDF on an object", BAD_REQUEST.getStatusCode(), getStatus(put)); } @Test public void testIngest() throws IOException { final String id = getRandomUniqueId(); try (final CloseableHttpResponse response = createObject(id)) { final String content = EntityUtils.toString(response.getEntity()); assertIdentifierness(content); assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); assertTrue("Didn't find ETag header!", response.containsHeader("ETag")); assertTrue("Didn't find Location header!", response.containsHeader("Location")); } } @Test public void testIngestWithNewAndSparqlQuery() throws IOException { final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/sparql-update"); method.setEntity(new StringEntity( "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"title\" } WHERE {}")); try (final CloseableHttpResponse response = execute(method)) { final String content = EntityUtils.toString(response.getEntity()); final int status = getStatus(response); assertEquals("Didn't get a CREATED response! Got content:\n" + content, CREATED.getStatusCode(), status); final String lastmod = response.getFirstHeader("Last-Modified").getValue(); assertNotNull("Should set Last-Modified for new nodes", lastmod); assertNotEquals("Last-Modified should not be blank for new nodes", lastmod.trim(), ""); assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); final String location = getLocation(response); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); assertTrue(graphStore.contains(ANY, createURI(location), DCTITLE, createLiteral("title"))); } } } @Test public void testIngestWithSparqlQueryBadNS() throws IOException { final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/sparql-update"); method.setEntity(new StringEntity("PREFIX fcr: <http://xmlns.com/my-fcr/> " + "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"this is a title\" } WHERE {}")); assertNotEquals("Should not get a CREATED response with bad namspace prefix!", CREATED.getStatusCode(), getStatus(method)); } @Test public void testIngestWithNewAndGraph() throws IOException { final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/n3"); method.setEntity(new StringEntity("<> <http://purl.org/dc/elements/1.1/title> \"title\".")); try (final CloseableHttpResponse response = execute(method)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); final String lastmod = response.getFirstHeader("Last-Modified").getValue(); assertNotNull("Should set Last-Modified for new nodes", lastmod); assertNotEquals("Last-Modified should not be blank for new nodes", lastmod.trim(), ""); final String location = getLocation(response); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); assertTrue(graphStore.contains(ANY, createURI(location), DCTITLE, createLiteral("title"))); } } } @Test public void testIngestWithSlug() throws IOException { final HttpPost method = postObjMethod(); method.addHeader("Slug", getRandomUniqueId()); try (final CloseableHttpResponse response = execute(method)) { final String content = EntityUtils.toString(response.getEntity()); assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); assertIdentifierness(content); final String location = getLocation(response); assertNotEquals(serverAddress + "/objects", location); assertEquals("Object wasn't created!", OK.getStatusCode(), getStatus(new HttpGet(location))); } } @Test public void testIngestWithRepeatedSlug() { final String id = getRandomUniqueId(); assertEquals(CREATED.getStatusCode(), getStatus(putObjMethod(id))); final HttpPost method = postObjMethod(); method.addHeader("Slug", id); assertEquals(CREATED.getStatusCode(), getStatus(method)); } // TODO this was copied from extant tests as a refactoring, but is this a good test for identifier-ness? public static void assertIdentifierness(final String content) { assertTrue("Response wasn't a PID", compile("[a-z]+").matcher(content).find()); } @Test public void testIngestWithBinary() throws IOException { final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.setEntity(new StringEntity("xyz")); try (final CloseableHttpResponse response = execute(method)) { final String content = EntityUtils.toString(response.getEntity()); final int status = getStatus(response); assertEquals("Didn't get a CREATED response! Got content:\n" + content, CREATED.getStatusCode(), status); assertIdentifierness(content); final String location = getLocation(response); assertNotEquals(serverAddress + "/objects", location); assertEquals("Object wasn't created!", OK.getStatusCode(), getStatus(new HttpGet(location))); final Link link = Link.valueOf(response.getFirstHeader(LINK).getValue()); assertEquals("describedby", link.getRel()); assertTrue("Expected an anchor to the newly created resource", link.getParams().containsKey("anchor")); assertEquals("Expected anchor at the newly created resource", location, link.getParams().get("anchor")); assertEquals("Expected describedBy link", location + "/" + FCR_METADATA, link.getUri().toString()); } } /* Verifies RDF persisted as binary is retrieved with byte-for-byte fidelity */ @Test public void testIngestOpaqueRdfAsBinary() throws IOException { final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/n-triples"); method.addHeader(CONTENT_DISPOSITION, "attachment"); final String rdf = "<test:/subject> <test:/predicate> <test:/object> ."; method.setEntity(new StringEntity(rdf)); try (final CloseableHttpResponse response = execute(method)) { assertEquals(CREATED.getStatusCode(), getStatus(response)); final String location = getLocation(response); final HttpGet get = new HttpGet(location); try (final CloseableHttpResponse getResponse = execute(get)) { final String resp = IOUtils.toString(getResponse.getEntity().getContent(), UTF_8); assertEquals("application/n-triples", getResponse.getFirstHeader(CONTENT_TYPE).getValue()); assertEquals(rdf, resp); } } } /** * Ensure that the objects can be created with a Digest header * with a SHA1 sum of the binary content */ @Test public void testIngestWithBinaryAndChecksum() { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.addHeader("Digest", "SHA1=f0b632679fab4f22e031010bd81a3b0544294730"); method.setEntity(new FileEntity(img)); assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(method)); } /** * Ensure that the objects cannot be created when a Digest header * contains a SHA1 sum that does not match the uploaded binary * content */ @Test public void testIngestWithBinaryAndChecksumMismatch() { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.addHeader("Digest", "SHA1=fedoraicon"); method.setEntity(new FileEntity(img)); assertEquals("Should be a 409 Conflict!", CONFLICT.getStatusCode(), getStatus(method)); } /** * Ensure that the a malformed Digest header returns a 400 Bad Request */ @Test public void testIngestWithBinaryAndMalformedDigestHeader() { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.addHeader("Digest", "md5=not a valid hash,SHA1:thisisbadtoo"); method.setEntity(new FileEntity(img)); assertEquals("Should be a 400 BAD REQUEST!", BAD_REQUEST.getStatusCode(), getStatus(method)); } /** * Ensure that a non-SHA1 Digest header returns a 409 Conflict */ @Test public void testIngestWithBinaryAndNonSha1DigestHeader() { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.addHeader("Digest", "md5=anything"); method.setEntity(new FileEntity(img)); assertEquals("Should be a 409 Conflict!", CONFLICT.getStatusCode(), getStatus(method)); } @Test public void testIngestWithBinaryAndMD5DigestHeader() { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.addHeader("Digest", "md5=6668675a91f39ca1afe46c084e8406ba"); method.setEntity(new FileEntity(img)); assertEquals("Should be a 201 Created!", CREATED.getStatusCode(), getStatus(method)); } @Test public void testIngestWithBinaryAndTwoValidHeadersDigestHeaders() { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.addHeader("Digest", "md5=6668675a91f39ca1afe46c084e8406ba," + " sha256=7b115a72978fe138287c1a6dfe6cc1afce4720fb3610a81d32e4ad518700c923"); method.setEntity(new FileEntity(img)); assertEquals("Should be a 201 Created!", CREATED.getStatusCode(), getStatus(method)); } @Test public void testIngestWithBinaryAndValidAndInvalidDigestHeaders() { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); method.addHeader(CONTENT_TYPE, "application/octet-stream"); method.addHeader("Digest", "md5=6668675a91f39ca1afe46c084e8406ba," + " sha99=7b115a72978fe138287c1a6dfe6cc1afce4720fb3610a81d32e4ad518700c923"); method.setEntity(new FileEntity(img)); assertEquals("Should be a 409 Conflict!", CONFLICT.getStatusCode(), getStatus(method)); } @Test public void testContentDispositionHeader() throws ParseException, IOException { final HttpPost method = postObjMethod(); final File img = new File("src/test/resources/test-objects/img.png"); final String filename = "some-file.png"; method.addHeader(CONTENT_TYPE, "application/png"); method.addHeader(CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\""); method.setEntity(new FileEntity(img)); // Create a binary resource with content-disposition final String location; try (final CloseableHttpResponse response = execute(method)) { assertEquals("Should be a 201 Created!", CREATED.getStatusCode(), getStatus(response)); location = getLocation(response); } // Retrieve the new resource and verify the content-disposition verifyContentDispositionFilename(location, filename); // Update the filename final String filename1 = "new-file.png"; final HttpPatch patch = new HttpPatch(location + "/" + FCR_METADATA); patch.setHeader(CONTENT_TYPE, "application/sparql-update"); final String updateString = "PREFIX ebucore: <http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#>\n" + "DELETE { <> ebucore:filename ?x}\n" + "INSERT { <> ebucore:filename \"" + filename1 + "\"}\n" + "WHERE { <> ebucore:filename ?x}"; patch.setEntity(new StringEntity(updateString)); assertEquals(location, NO_CONTENT.getStatusCode(), getStatus(patch)); // Retrieve the new resource and verify the content-disposition verifyContentDispositionFilename(location, filename1); } private void verifyContentDispositionFilename(final String location, final String filename) throws IOException, ParseException { final HttpHead head = new HttpHead(location); try (final CloseableHttpResponse headResponse = execute(head)) { final Header header = headResponse.getFirstHeader(CONTENT_DISPOSITION); assertNotNull(header); final ContentDisposition contentDisposition = new ContentDisposition(header.getValue()); assertEquals(filename, contentDisposition.getFileName()); } } @Test public void testIngestOnSubtree() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); final HttpPost method = postObjMethod(id); method.addHeader("Slug", "x"); assertEquals(serverAddress + id + "/x", getLocation(method)); } /** * Ensure that the objects cannot be pairtree child resources * * @throws IOException in case of IOException */ @Test public void testIngestOnPairtree() throws IOException { // Following the approach undertaken for FedoraExportIT#shouldRoundTripOnePairtree final String objName = getLocation(postObjMethod()); final String pairtreeName = objName.substring(serverAddress.length(), objName.lastIndexOf('/')); try (final CloseableDataset dataset = getDataset(getObjMethod(pairtreeName))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue("Resource \"" + objName + " " + pairtreeName + "\" must be pairtree.", graph.contains(ANY, createURI(serverAddress + pairtreeName), type.asNode(), createURI(REPOSITORY_NAMESPACE + "Pairtree"))); } // Attempting to POST to the child of the pairtree node... final int status = getStatus(postObjMethod(pairtreeName)); assertEquals("Created an Object under a pairtree node!", FORBIDDEN.getStatusCode(), status); } @Test public void testIngestWithRDFLang() throws IOException { final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/n3"); method.setEntity(new StringEntity("<> <http://purl.org/dc/elements/1.1/title> \"french title\"@fr ." + "<> <http://purl.org/dc/elements/1.1/title> \"english title\"@en .")); try (final CloseableHttpResponse response = execute(method)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); final String location = getLocation(response); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); final Node subj = createURI(location); assertTrue(graphStore.contains(ANY, subj, DCTITLE, createLiteral("english title", "en", false))); assertTrue(graphStore.contains(ANY, subj, DCTITLE, createLiteral("french title", "fr", false))); } } } @Test // TODO It's not clear what this test is actually testing, or why it sleeps while running public void testCreateManyObjects() throws IOException, InterruptedException { if (System.getProperty(TEST_ACTIVATION_PROPERTY) == null) { logger.info("Not running testCreateManyObjects because system property TEST_ACTIVATION_PROPERTY not set."); return; } final int manyObjects = 2000; for (int i = 0; i < manyObjects; i++) { sleep(10); // needed to prevent overloading TODO why? createObject().close(); } } @Test public void testDeleteWithBadEtag() throws IOException { try (final CloseableHttpResponse response = execute(postObjMethod())) { assertEquals(CREATED.getStatusCode(), getStatus(response)); final HttpDelete request = new HttpDelete(getLocation(response)); request.addHeader("If-Match", "\"doesnt-match\""); assertEquals(PRECONDITION_FAILED.getStatusCode(), getStatus(request)); } } @Test public void testGetDatastream() throws IOException, ParseException { final String id = getRandomUniqueId(); createObjectAndClose(id); createDatastream(id, "ds1", "foo"); try (final CloseableHttpResponse response = execute(getDSMethod(id, "ds1"))) { assertEquals("Wasn't able to retrieve a datastream!", OK.getStatusCode(), getStatus(response)); assertEquals(TEXT_PLAIN, response.getFirstHeader(CONTENT_TYPE).getValue()); assertEquals("3", response.getFirstHeader(CONTENT_LENGTH).getValue()); assertEquals("bytes", response.getFirstHeader("Accept-Ranges").getValue()); final ContentDisposition disposition = new ContentDisposition(response.getFirstHeader(CONTENT_DISPOSITION).getValue()); assertEquals("attachment", disposition.getType()); final Collection<String> links = getLinkHeaders(response); final String describedByHeader = "<" + serverAddress + id + "/ds1/" + FCR_METADATA + ">; rel=\"describedby\""; assertTrue("Didn't find 'describedby' link header!", links.contains(describedByHeader)); } } @Test public void testDeleteDatastream() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); createDatastream(id, "ds1", "foo"); assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(id + "/ds1"))); assertDeleted(id + "/ds1"); } @Test public void testGetObjectGraphHtml() throws IOException { final HttpGet getObjMethod = new HttpGet(getLocation(postObjMethod())); getObjMethod.addHeader(ACCEPT, "text/html"); assertEquals(OK.getStatusCode(), getStatus(getObjMethod)); } @Test public void testGetObjectGraphVariants() throws IOException { final String location = getLocation(postObjMethod()); for (final Variant variant : POSSIBLE_RDF_VARIANTS) { final HttpGet getObjMethod = new HttpGet(location); final String type = variant.getMediaType().getType(); getObjMethod.addHeader(ACCEPT, type); assertEquals("Got bad response for type " + type + " !", OK.getStatusCode(), getStatus(getObjMethod)); } } @Test public void testGetObjectGraph() throws IOException { logger.trace("Entering testGetObjectGraph()..."); final String location = getLocation(postObjMethod()); final HttpGet getObjMethod = new HttpGet(location); try (final CloseableHttpResponse response = execute(getObjMethod)) { assertEquals(OK.getStatusCode(), getStatus(response)); assertResourceOptionsHeaders(response); assertTrue("Didn't find LDP link header!", getLinkHeaders(response).contains(LDP_RESOURCE_LINK_HEADER)); try (final CloseableDataset dataset = getDataset(response)) { assertTrue("Didn't find any type triples!", dataset.asDatasetGraph().contains(ANY, createURI(location), rdfType, ANY)); } logger.trace("Leaving testGetObjectGraph()..."); } } @Test public void verifyFullSetOfRdfTypes() throws IOException { logger.trace("Entering verifyFullSetOfRdfTypes()..."); final String id = getRandomUniqueId(); createObjectAndClose(id); try (final CloseableDataset dataset = getDataset(getObjMethod(id))) { final DatasetGraph graph = dataset.asDatasetGraph(); final Node resource = createURI(serverAddress + id); verifyResource(graph, resource, REPOSITORY_NAMESPACE, "Container"); verifyResource(graph, resource, REPOSITORY_NAMESPACE, "Resource"); } logger.trace("Leaving verifyFullSetOfRdfTypes()..."); } private static void verifyResource(final DatasetGraph g, final Node subject, final String ns, final String type) { assertTrue("Should find type: " + ns + type, g.contains(ANY, subject, rdfType, createURI(ns + type))); } @Test public void testGetObjectGraphWithChild() throws IOException { final String id = getRandomUniqueId(); final String location = getLocation(createObject(id)); createObjectAndClose(id + "/c"); try (final CloseableHttpResponse response = execute(getObjMethod(id))) { try (final CloseableDataset dataset = getDataset(response)) { assertTrue("Didn't find child node!", dataset.asDatasetGraph().contains(ANY, createURI(location), createURI(LDP_NAMESPACE + "contains"), createURI(location + "/c"))); final Collection<String> links = getLinkHeaders(response); assertTrue("Didn't find LDP resource link header!", links.contains(LDP_RESOURCE_LINK_HEADER)); } } } @Test public void testGetObjectGraphWithChildren() throws IOException { final String id = getRandomUniqueId(); final String location = getLocation(createObject(id)); // Create some children final int CHILDREN_TOTAL = 20; for (int x = 0; x < CHILDREN_TOTAL; ++x) { createObjectAndClose(id + "/child-" + x); } final int CHILDREN_LIMIT = 12; final HttpGet httpGet = getObjMethod(id); httpGet.setHeader("Limit", Integer.toString(CHILDREN_LIMIT)); try (final CloseableHttpResponse response = execute(httpGet)) { try (final CloseableDataset dataset = getDataset(response)) { final DatasetGraph graph = dataset.asDatasetGraph(); final Iterator<Quad> contains = graph.find(ANY, createURI(location), CONTAINS.asNode(), ANY); assertTrue("Should find contained child!", contains.hasNext()); assertEquals(CHILDREN_LIMIT, Iterators.size(contains)); } } } @Test public void testGetObjectGraphWithBadLimit() throws IOException { final String id = getRandomUniqueId(); getLocation(createObject(id)); final HttpGet httpGet = getObjMethod(id); httpGet.setHeader("Limit", "not-an-integer"); try (final CloseableHttpResponse response = execute(httpGet)) { assertEquals(SC_BAD_REQUEST, response.getStatusLine().getStatusCode()); } } @Test public void testGetObjectGraphMinimal() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); addMixin(id, BASIC_CONTAINER.getURI()); createObjectAndClose(id + "/a"); final HttpGet getObjMethod = getObjMethod(id); getObjMethod.addHeader("Prefer", "return=minimal"); try (final CloseableDataset dataset = getDataset(getObjMethod)) { final DatasetGraph graph = dataset.asDatasetGraph(); final Node resource = createURI(serverAddress + id); assertFalse("Didn't expect members", graph.find(ANY, resource, HAS_CHILD.asNode(), ANY).hasNext()); assertFalse("Didn't expect members", graph.find(ANY, resource, CONTAINS.asNode(), ANY).hasNext()); } } @Test public void testGetObjectOmitMembership() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); addMixin(id, BASIC_CONTAINER.getURI()); createObjectAndClose(id + "/a"); final HttpGet getObjMethod = getObjMethod(id); getObjMethod.addHeader("Prefer", "return=representation; " + "omit=\"http://www.w3.org/ns/ldp#PreferContainment http://www.w3.org/ns/ldp#PreferMembership\""); try (final CloseableDataset dataset = getDataset(getObjMethod)) { final DatasetGraph graph = dataset.asDatasetGraph(); assertFalse("Didn't expect inlined member resources", graph.find(ANY, createURI(serverAddress + id), HAS_CHILD.asNode(), ANY).hasNext()); } } @Test public void testGetObjectOmitContainment() throws IOException { final String id = getRandomUniqueId(); createObjectAndClose(id); final HttpPatch patch = patchObjMethod(id); patch.setHeader(CONTENT_TYPE, "application/sparql-update"); final String updateString = "INSERT DATA { <> a <" + DIRECT_CONTAINER.getURI() + "> ; <" + MEMBERSHIP_RESOURCE.getURI() + "> <> ; <" + HAS_MEMBER_RELATION + "> <" + LDP_NAMESPACE + "member> .}"; patch.setEntity(new StringEntity(updateString)); assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch)); createObjectAndClose(id + "/a"); final HttpGet getObjMethod = getObjMethod(id); getObjMethod .addHeader("Prefer", "return=representation; omit=\"http://www.w3.org/ns/ldp#PreferContainment\""); try (final CloseableDataset dataset = getDataset(getObjMethod)) { final DatasetGraph graph = dataset.asDatasetGraph(); final Node resource = createURI(serverAddress + id); assertTrue("Didn't find member resources", graph.find(ANY, resource, LDP_MEMBER.asNode(), ANY).hasNext()); assertFalse("Expected nothing contained", graph.find(ANY, resource, CONTAINS.asNode(), ANY).hasNext()); } } @Test public void testGetObjectReferences() throws IOException { final String id = getRandomUniqueId(); final String resource = serverAddress + id; final String resourcea = resource + "/a"; final String resourceb = resource + "/b"; createObjectAndClose(id); createObjectAndClose(id + "/a"); createObjectAndClose(id + "/b"); final HttpPatch updateObjectGraphMethod = patchObjMethod(id + "/a"); updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" + resourcea + "> <http://purl.org/dc/terms/isPartOf> <" + resourceb + "> . \n <" + resourcea + "> <info:xyz#some-other-property> <" + resourceb + "> } WHERE {}")); executeAndClose(updateObjectGraphMethod); final HttpGet getObjMethod = new HttpGet(resourceb); getObjMethod.addHeader("Prefer", "return=representation; include=\"" + INBOUND_REFERENCES + "\""); try (final CloseableDataset dataset = getDataset(getObjMethod)) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue(graph.contains(ANY, createURI(resourcea), createURI("http://purl.org/dc/terms/isPartOf"), createURI(resourceb))); assertTrue(graph.contains(ANY, createURI(resourcea), createURI("info:xyz#some-other-property"), createURI(resourceb))); } } @Test public void testGetObjectReferencesIndirect() throws Exception { final String uuid = getRandomUniqueId(); final String pid1 = uuid + "/parent"; final String pid2 = uuid + "/child1"; final String pid3 = uuid + "/child2"; createObjectAndClose(pid1); createObjectAndClose(pid2); createObjectAndClose(pid3); final String memberRelation = "http://pcdm.org/models#hasMember"; // create an indirect container final HttpPut createContainer = new HttpPut(serverAddress + pid1 + "/members"); createContainer.addHeader(CONTENT_TYPE, "text/turtle"); final String membersRDF = "<> a <http://www.w3.org/ns/ldp#IndirectContainer>; " + "<http://www.w3.org/ns/ldp#hasMemberRelation> <" + memberRelation + ">; " + "<http://www.w3.org/ns/ldp#insertedContentRelation> <http://www.openarchives.org/ore/terms/proxyFor>; " + "<http://www.w3.org/ns/ldp#membershipResource> <" + serverAddress + pid1 + "> . "; createContainer.setEntity(new StringEntity(membersRDF)); assertEquals(CREATED.getStatusCode(), getStatus(createContainer)); // create proxies for the children in the indirect container createProxy(pid1, pid2); createProxy(pid1, pid3); // retrieve the parent and verify the outbound triples exist final HttpGet getParent = new HttpGet(serverAddress + pid1); getParent.addHeader(ACCEPT, "application/n-triples"); try (final CloseableDataset dataset = getDataset(getParent)) { final DatasetGraph parentGraph = dataset.asDatasetGraph(); assertTrue(parentGraph.contains(Node.ANY, createURI(serverAddress + pid1), createURI(memberRelation), createURI(serverAddress + pid2))); assertTrue(parentGraph.contains(Node.ANY, createURI(serverAddress + pid1), createURI(memberRelation), createURI(serverAddress + pid3))); } // retrieve the members container and verify the LDP triples exist final HttpGet getContainer = new HttpGet(serverAddress + pid1 + "/members"); getContainer.addHeader("Prefer", "return=representation;include=\"http://www.w3.org/ns/ldp#PreferMembership\""); getContainer.addHeader(ACCEPT, "application/n-triples"); try (final CloseableDataset dataset = getDataset(getContainer)) { final DatasetGraph containerGraph = dataset.asDatasetGraph(); assertTrue(containerGraph.contains(Node.ANY, createURI(serverAddress + pid1 + "/members"), createURI("http://www.w3.org/ns/ldp#hasMemberRelation"), createURI(memberRelation))); assertTrue(containerGraph.contains(Node.ANY, createURI(serverAddress + pid1 + "/members"), createURI("http://www.w3.org/ns/ldp#insertedContentRelation"), createURI("http://www.openarchives.org/ore/terms/proxyFor"))); assertTrue(containerGraph.contains(Node.ANY, createURI(serverAddress + pid1 + "/members"), createURI("http://www.w3.org/ns/ldp#membershipResource"), createURI(serverAddress + pid1))); } // retrieve the member and verify inbound triples exist final HttpGet getMember = new HttpGet(serverAddress + pid2); getMember.addHeader("Prefer", "return=representation; include=\"" + INBOUND_REFERENCES.toString() + "\""); getMember.addHeader(ACCEPT, "application/n-triples"); try (final CloseableDataset dataset = getDataset(getMember)) { final DatasetGraph memberGraph = dataset.asDatasetGraph(); assertTrue(memberGraph.contains(Node.ANY, Node.ANY, createURI("http://www.openarchives.org/ore/terms/proxyFor"), createURI(serverAddress + pid2))); assertTrue(memberGraph.contains(Node.ANY, createURI(serverAddress + pid1), createURI(memberRelation), createURI(serverAddress + pid2))); assertFalse("Should not contain inbound references to the other child", memberGraph.contains(Node.ANY, createURI(serverAddress + pid1), createURI(memberRelation), createURI(serverAddress + pid3))); } } private static void createProxy(final String parent, final String child) { final HttpPost createProxy = new HttpPost(serverAddress + parent + "/members"); createProxy.addHeader(CONTENT_TYPE, "text/turtle"); final String proxyRDF = "<> <http://www.openarchives.org/ore/terms/proxyFor> <" + serverAddress + child + ">;" + " <http://www.openarchives.org/ore/terms/proxyIn> <" + serverAddress + parent + "> ."; createProxy.setEntity(new StringEntity(proxyRDF, UTF_8)); assertEquals(CREATED.getStatusCode(), getStatus(createProxy)); } @Test public void testLinkToNonExistent() throws IOException { final HttpPatch patch = new HttpPatch(getLocation(postObjMethod())); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new StringEntity("INSERT { " + "<> <http://some-vocabulary#isMemberOfCollection> <" + serverAddress + "non-existant> } WHERE {}")); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(patch)); } @Test public void testUpdateAndReplaceObjectGraph() throws IOException { final String subjectURI = getLocation(postObjMethod()); final HttpPatch updateObjectGraphMethod = new HttpPatch(subjectURI); updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); updateObjectGraphMethod.setEntity(new StringEntity("INSERT {<" + subjectURI + "> <info:test#label> \"foo\" ; " + " <info:test#number> 42 ; " + " <info:test#date> \"1953?\"^^<http://id.loc.gov/datatypes/edtf/EDTF> }" + " WHERE {}")); executeAndClose(updateObjectGraphMethod); try (CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertTrue("Didn't find a triple we thought we added.", graph.contains(ANY, createURI(subjectURI), createURI("info:test#label"), createLiteral("foo"))); assertTrue("Didn't find a triple we thought we added.", graph.contains(ANY, createURI(subjectURI), createURI("info:test#number"), createLiteral("42", XSDinteger))); assertTrue("Didn't find a triple we thought we added.", graph.contains(ANY, createURI(subjectURI), createURI("info:test#date"), createLiteral("1953?", getInstance().getSafeTypeByName("http://id.loc.gov/datatypes/edtf/EDTF")))); } updateObjectGraphMethod.setEntity(new StringEntity("DELETE WHERE { " + "<" + subjectURI + "> <info:test#label> \"foo\"." + "<" + subjectURI + "> <info:test#number> 42 . " + "<" + subjectURI + "> <info:test#date> \"1953?\"^^<http://id.loc.gov/datatypes/edtf/EDTF> . }; \n" + "INSERT {<" + subjectURI + "> <info:test#label> \"qwerty\" ; " + "<info:test#number> 43 ; " + "<info:test#date> \"1953\"^^<http://id.loc.gov/datatypes/edtf/EDTF> . } \n" + "WHERE { }")); try (final CloseableHttpResponse response = execute(updateObjectGraphMethod)) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); assertTrue("Didn't find ETag header!", response.containsHeader("ETag")); } try (CloseableDataset dataset = getDataset(new HttpGet(subjectURI))) { final DatasetGraph graph = dataset.asDatasetGraph(); assertFalse("Found a triple we thought we deleted.", graph.contains(ANY, createURI(subjectURI), createURI("info:test#label"), createLiteral("foo"))); assertFalse("Found a triple we thought we deleted.", graph.contains(ANY, createURI(subjectURI), createURI("info:test#number"), createLiteral("42", XSDinteger))); assertFalse("Found a triple we thought we deleted.", graph.contains(ANY, createURI(subjectURI), createURI("info:test#date"), createLiteral("1953?", getInstance().getSafeTypeByName("http://id.loc.gov/datatypes/edtf/EDTF")))); } } @Test // TODO there is no actual use of the JCR namespace in this test-- what is it testing? public void testUpdateWithSparqlQueryJcrNS() throws IOException { final String subjectURI = getLocation(postObjMethod()); final HttpPatch updateObjectGraphMethod = new HttpPatch(subjectURI); updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); updateObjectGraphMethod.setEntity(new StringEntity("PREFIX fcr: <http://xmlns.com/my-fcr/> " + "INSERT { <" + subjectURI + "> <info:test#label> \"asdfg\" } WHERE {}")); assertNotEquals("Got updated response with jcr namspace prefix!\n", NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod)); } @Test public void testUpdateObjectGraphWithProblems() throws IOException { final String subjectURI = getLocation(postObjMethod()); final Link ex = fromUri(URI.create(serverAddress + "static/constraints/ServerManagedPropertyException.rdf")) .rel(CONSTRAINED_BY.getURI()).build(); final HttpPatch patchObjMethod = new HttpPatch(subjectURI); patchObjMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); patchObjMethod.setEntity(new StringEntity("INSERT { <" + subjectURI + "> <" + REPOSITORY_NAMESPACE + "uuid> \"value-doesn't-matter\" } WHERE {}\n")); try (final CloseableHttpResponse response = execute(patchObjMethod)) { assertEquals(CONFLICT.getStatusCode(), getStatus(response)); assertEquals(ex.toString(), response.getFirstHeader(LINK).getValue().toString()); } } @Test public void testPutResourceBadRdf() throws IOException { final HttpPut httpPut = new HttpPut(serverAddress + getRandomUniqueId()); httpPut.setHeader(CONTENT_TYPE, "text/turtle"); httpPut.setEntity(new StringEntity("<> a \"still image\".")); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(httpPut)); } @Test public void testRepeatedPut() { final String id = getRandomUniqueId(); assertEquals(CREATED.getStatusCode(), getStatus(new HttpPut(serverAddress + id))); final HttpPut secondPut = new HttpPut(serverAddress + id); secondPut.setHeader(CONTENT_TYPE, "text/turtle"); assertEquals(CONFLICT.getStatusCode(), getStatus(secondPut)); } @Test public void testCreateResourceWithoutContentType() { assertEquals(CREATED.getStatusCode(), getStatus(new HttpPut(serverAddress + getRandomUniqueId()))); } @Test public void testUpdateObjectWithoutContentType() throws IOException { final HttpPut httpPut = new HttpPut(getLocation(postObjMethod())); // use a bytestream-based entity to avoid settin a content type httpPut.setEntity(new ByteArrayEntity("bogus content".getBytes(UTF_8))); assertEquals(UNSUPPORTED_MEDIA_TYPE.getStatusCode(), getStatus(httpPut)); } @Test public void testUpdateBinaryWithoutContentType() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "xyz"); final HttpPut httpPut = new HttpPut(serverAddress + id + "/x"); assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPut)); } @Test public void testRoundTripReplaceGraphForDatastreamDescription() throws IOException { final String id = getRandomUniqueId(); final String subjectURI = serverAddress + id + "/ds1"; createDatastream(id, "ds1", "some-content"); final HttpGet getObjMethod = new HttpGet(subjectURI + "/" + FCR_METADATA); getObjMethod.addHeader(ACCEPT, "text/turtle"); final Model model = createDefaultModel(); try (final CloseableHttpResponse getResponse = execute(getObjMethod)) { final String graph = EntityUtils.toString(getResponse.getEntity()); logger.trace("Got serialized object graph for testRoundTripReplaceGraphForDatastream():\n {}", graph); try (final StringReader r = new StringReader(graph)) { model.read(r, subjectURI, "TURTLE"); } } final HttpPut replaceMethod = new HttpPut(subjectURI + "/" + FCR_METADATA); try (final StringWriter w = new StringWriter()) { model.write(w, "N-TRIPLE"); final String updatedMetadata = w.toString() + "<" + subjectURI + "> <http://www.w3.org/2000/01/rdf-schema#label> 'foo' ."; replaceMethod.setEntity(new StringEntity(updatedMetadata)); logger.trace("Transmitting object graph for testRoundTripReplaceGraphForDatastream():\n {}", w); } replaceMethod.addHeader(CONTENT_TYPE, "application/n-triples"); assertEquals(NO_CONTENT.getStatusCode(), getStatus(replaceMethod)); } @Test public void testResponseContentTypes() throws Exception { final String id = getRandomUniqueId(); createObjectAndClose(id); for (final String type : POSSIBLE_RDF_RESPONSE_VARIANTS_STRING) { final HttpGet method = new HttpGet(serverAddress + id); method.addHeader(ACCEPT, type); assertEquals(type, getContentType(method)); } } @Test public void testDescribeRdfCached() throws IOException { try (final CloseableHttpClient cachClient = CachingHttpClientBuilder.create().setCacheConfig(DEFAULT).build()) { final String location = getLocation(postObjMethod()); try (final CloseableHttpResponse response = cachClient.execute(new HttpGet(location))) { assertEquals("Client didn't return a OK!", OK.getStatusCode(), getStatus(response)); logger.debug("Found HTTP headers:\n{}", asList(response.getAllHeaders())); assertTrue("Didn't find Last-Modified header!", response.containsHeader("Last-Modified")); final String lastModed = response.getFirstHeader("Last-Modified").getValue(); final String etag = response.getFirstHeader("ETag").getValue(); final HttpGet getObjMethod2 = new HttpGet(location); getObjMethod2.setHeader("If-Modified-Since", lastModed); getObjMethod2.setHeader("If-None-Match", etag); assertEquals("Client didn't get a NOT_MODIFIED!", NOT_MODIFIED.getStatusCode(), getStatus(getObjMethod2)); } } } @Test public void testValidHTMLForRepo() throws IOException, SAXException { validateHTML(""); } @Test public void testValidHTMLForObject() throws IOException, SAXException { final String id = getRandomUniqueId(); createObjectAndClose(id); validateHTML(id); } @Test public void testValidHTMLForDS() throws IOException, SAXException { final String id = getRandomUniqueId(); createDatastream(id, "ds", "content"); validateHTML(id + "/ds/" + FCR_METADATA); } private static void validateHTML(final String path) throws IOException, SAXException { final HttpGet getMethod = getObjMethod(path); getMethod.addHeader(ACCEPT, "text/html"); try (final CloseableHttpResponse response = execute(getMethod)) { assertEquals(OK.getStatusCode(), getStatus(response)); final String content = EntityUtils.toString(response.getEntity()); logger.trace("Retrieved HTML view:\n" + content); final HtmlParser htmlParser = new HtmlParser(ALLOW); htmlParser.setDoctypeExpectation(NO_DOCTYPE_ERRORS); htmlParser.setErrorHandler(new HTMLErrorHandler()); htmlParser.setContentHandler(new TreeBuilder()); try (final InputStream htmlStream = new ByteArrayInputStream(content.getBytes(UTF_8))) { htmlParser.parse(new InputSource(htmlStream)); } logger.debug("HTML found to be valid."); } } private static class HTMLErrorHandler implements ErrorHandler { @Override public void warning(final SAXParseException e) { fail(e.toString()); } @Override public void error(final SAXParseException e) { fail(e.toString()); } @Override public void fatalError(final SAXParseException e) { fail(e.toString()); } } @Test public void testLinkedDeletion() { final String linkedFrom = getRandomUniqueId(); final String linkedTo = getRandomUniqueId(); createObjectAndClose(linkedFrom); createObjectAndClose(linkedTo); final String sparql = "INSERT DATA { <" + serverAddress + linkedFrom + "> " + "<http://some-vocabulary#isMemberOfCollection> <" + serverAddress + linkedTo + "> . }"; final HttpPatch patch = patchObjMethod(linkedFrom); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new ByteArrayEntity(sparql.getBytes(UTF_8))); assertEquals("Couldn't link resources!", NO_CONTENT.getStatusCode(), getStatus(patch)); assertEquals("Error deleting linked-to!", NO_CONTENT.getStatusCode(), getStatus(deleteObjMethod(linkedTo))); assertEquals("Linked to should still exist!", OK.getStatusCode(), getStatus(getObjMethod(linkedFrom))); } @Test public void testUpdateObjectWithSpaces() throws IOException { final String id = getRandomUniqueId() + " 2"; try (final CloseableHttpResponse createResponse = createObject(id)) { final String subjectURI = getLocation(createResponse); final HttpPatch updateObjectGraphMethod = new HttpPatch(subjectURI); updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); updateObjectGraphMethod.setEntity(new StringEntity( "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"test\" } WHERE {}")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod)); } } @Test public void testCreatedAndModifiedDates() throws IOException, ParseException { final String location = getLocation(postObjMethod()); final HttpGet getObjMethod = new HttpGet(location); try (final CloseableHttpResponse response = execute(getObjMethod)) { try (final CloseableDataset dataset = getDataset(response)) { final DatasetGraph graph = dataset.asDatasetGraph(); final Model model = createModelForGraph(graph.getDefaultGraph()); final Resource nodeUri = createResource(location); final String lastmodString = response.getFirstHeader("Last-Modified").getValue(); final Optional<Instant> createdDateTriples = getDateFromModel(model, nodeUri, createProperty(REPOSITORY_NAMESPACE + "created")); final Optional<Instant> lastmodDateTriples = getDateFromModel(model, nodeUri, createProperty(REPOSITORY_NAMESPACE + "lastModified")); assertTrue(createdDateTriples.isPresent()); assertTrue(lastmodDateTriples.isPresent()); assertEquals(lastmodString, headerFormat.format(createdDateTriples.get())); assertEquals(lastmodString, headerFormat.format(lastmodDateTriples.get())); } } } @Test public void testLdpContainerInteraction() throws IOException { final String id = getRandomUniqueId(); final String location; try (final CloseableHttpResponse createResponse = createObject(id)) { location = getLocation(createResponse); } createObjectAndClose(id + "/t"); addMixin(id + "/t", DIRECT_CONTAINER.getURI()); final HttpPatch patch = patchObjMethod(id + "/t"); final String sparql = "INSERT DATA { " + "<> <" + MEMBERSHIP_RESOURCE + "> <" + location + "> .\n" + "<> <" + HAS_MEMBER_RELATION + "> <info:some/relation> .\n" + " }"; patch.setEntity(new StringEntity(sparql)); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); assertEquals("Expected patch to succeed", NO_CONTENT.getStatusCode(), getStatus(patch)); createObjectAndClose(id + "/b"); addMixin(id + "/b", DIRECT_CONTAINER.getURI()); final HttpPatch bPatch = patchObjMethod(id + "/b"); bPatch.addHeader(CONTENT_TYPE, "application/sparql-update"); final String bSparql = "INSERT DATA { " + "<> <" + MEMBERSHIP_RESOURCE + "> <" + location + "> .\n" + "<> <" + HAS_MEMBER_RELATION + "> <info:some/another-relation> .\n" + " }"; bPatch.setEntity(new StringEntity(bSparql)); assertEquals("Expected patch to succeed", NO_CONTENT.getStatusCode(), getStatus(bPatch)); createObject(id + "/t/1"); createObject(id + "/b/1"); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); final Node resource = createURI(location); assertTrue("Expected to have container t", graphStore.contains(ANY, resource, createURI(LDP_NAMESPACE + "contains"), createURI(location + "/t"))); assertTrue("Expected to have container b", graphStore.contains(ANY, resource, createURI(LDP_NAMESPACE + "contains"), createURI(location + "/b"))); assertTrue("Expected member relation", graphStore.contains(ANY, resource, createURI("info:some/relation"), createURI(location + "/t/1"))); assertTrue("Expected other member relation", graphStore.contains(ANY, resource, createURI("info:some/another-relation"), createURI(location + "/b/1"))); } } @Test public void testLdpIndirectContainerInteraction() throws IOException { // Create resource (object) final String resourceId = getRandomUniqueId(); final String resource; try (final CloseableHttpResponse createResponse = createObject(resourceId)) { resource = getLocation(createResponse); } // Create container (c0) final String containerId = getRandomUniqueId(); final String container; try (final CloseableHttpResponse createResponse = createObject(containerId)) { container = getLocation(createResponse); } // Create indirect container (c0/members) final String indirectContainerId = containerId + "/t"; final String indirectContainer; try (final CloseableHttpResponse createResponse = createObject(indirectContainerId)) { indirectContainer = getLocation(createResponse); } addMixin(indirectContainerId, INDIRECT_CONTAINER.getURI()); // Add LDP properties to indirect container final HttpPatch patch = patchObjMethod(indirectContainerId); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); final String sparql = "INSERT DATA { " + "<> <" + MEMBERSHIP_RESOURCE + "> <" + container + "> .\n" + "<> <" + HAS_MEMBER_RELATION + "> <info:some/relation> .\n" + "<> <" + LDP_NAMESPACE + "insertedContentRelation> <info:proxy/for> .\n" + " }"; patch.setEntity(new StringEntity(sparql)); assertEquals("Expected patch to succeed", NO_CONTENT.getStatusCode(), getStatus(patch)); // Add indirect resource to indirect container final HttpPost postIndirectResource = postObjMethod(indirectContainerId); final String irRdf = "<> <info:proxy/in> <" + container + "> ;\n" + " <info:proxy/for> <" + resource + "> ."; postIndirectResource.setEntity(new StringEntity(irRdf)); postIndirectResource.setHeader(CONTENT_TYPE, "text/turtle"); final String indirectResource; try (final CloseableHttpResponse postResponse = execute(postIndirectResource)) { indirectResource = getLocation(postResponse); assertEquals("Expected post to succeed", CREATED.getStatusCode(), getStatus(postResponse)); } // Ensure container has been updated with relationship... indirectly try (final CloseableHttpResponse getResponse = execute(new HttpGet(container)); final CloseableDataset dataset = getDataset(getResponse)) { final DatasetGraph graphStore = dataset.asDatasetGraph(); assertTrue("Expected to have indirect container", graphStore.contains(ANY, createURI(container), createURI(LDP_NAMESPACE + "contains"), createURI(indirectContainer))); assertTrue("Expected to have resource: " + graphStore.toString(), graphStore.contains(ANY, createURI(container), createURI("info:some/relation"), createURI(resource))); } // Remove indirect resource assertEquals("Expected delete to succeed", NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(indirectResource))); // Ensure container has been updated with relationship... indirectly try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(container)); final CloseableDataset dataset = getDataset(getResponse1);) { final DatasetGraph graph = dataset.asDatasetGraph(); assertFalse("Expected NOT to have resource: " + graph, graph.contains(ANY, createURI(container), createURI("info:some/relation"), createURI(resource))); } } @Test public void testWithHashUris() throws IOException { final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "text/turtle"); method.setEntity(new StringEntity("<> <info:some-predicate> <#abc> .\n" + "<#abc> <info:test#label> \"asdfg\" .")); try (final CloseableHttpResponse response = execute(method)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); final String location = getLocation(response); try (final CloseableDataset dataset = getDataset(new HttpGet(location))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); assertTrue(graphStore.contains(ANY, createURI(location), createURI("info:some-predicate"), createURI(location + "#abc"))); assertTrue(graphStore.contains(ANY, createURI(location + "#abc"), createURI("info:test#label"), createLiteral("asdfg"))); assertFalse(graphStore.contains(ANY, createURI(location + "#abc"), createURI(REPOSITORY_NAMESPACE + "lastModified"), ANY)); assertFalse(graphStore.contains(ANY, createURI(location + "#abc"), rdfType, createURI(REPOSITORY_NAMESPACE + "Resource"))); } } } @Test public void testCreateAndReplaceGraphMinimal() throws IOException { LOGGER.trace("Entering testCreateAndReplaceGraphMinimal()..."); final HttpPost httpPost = postObjMethod("/"); httpPost.addHeader("Slug", getRandomUniqueId()); httpPost.addHeader(CONTENT_TYPE, "text/turtle"); httpPost.setEntity(new StringEntity("<> <" + DCTITLE.getURI() + "> \"abc\"")); final String subjectURI; try (final CloseableHttpResponse response = execute(httpPost)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); subjectURI = getLocation(response); } final HttpPut replaceMethod = new HttpPut(subjectURI); replaceMethod.addHeader(CONTENT_TYPE, "text/turtle"); replaceMethod.addHeader("Prefer", "handling=lenient; received=\"minimal\""); replaceMethod.setEntity(new StringEntity("<> <" + DCTITLE.getURI() + "> \"xyz\"")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(replaceMethod)); final HttpGet get = new HttpGet(subjectURI); get.addHeader("Prefer", "return=minimal"); try (final CloseableDataset dataset = getDataset(get)) { assertTrue(dataset.asDatasetGraph().contains(ANY, ANY, DCTITLE, createLiteral("xyz"))); } LOGGER.trace("Done with testCreateAndReplaceGraphMinimal()."); } @Test @Ignore("This test needs manual intervention to decide how \"good\" the graph looks") // TODO Do we have any way to proceed with this kind of aesthetic goal? public void testGraphShouldNotBeTooLumpy() throws IOException { final HttpPut httpPut = putObjMethod(getRandomUniqueId()); httpPut.addHeader(CONTENT_TYPE, "text/turtle"); httpPut.setEntity(new StringEntity("<> a <" + DIRECT_CONTAINER.getURI() + ">;" + " <" + MEMBERSHIP_RESOURCE.getURI() + "> <> ;" + " <" + HAS_MEMBER_RELATION.getURI() + "> <" + LDP_NAMESPACE + "member> ;" + " <info:x> <#hash-uri> ;" + " <info:x> [ <" + DCTITLE.getURI() + "> \"xyz\" ] . " + "<#hash-uri> <" + DCTITLE.getURI() + "> \"some-hash-uri\" .")); /* * final HttpResponse response = execute(httpPut); final int status = * response.getStatusLine().getStatusCode(); assertEquals("Didn't get a CREATED response!", * CREATED.getStatusCode(), status); final String subjectURI = response.getFirstHeader("Location").getValue(); * final HttpGet get = new HttpGet(subjectURI); final HttpResponse getResponse = execute(get); final String s * = EntityUtils.toString(getResponse.getEntity()); */ } @Test public void testEmbeddedChildResources() throws IOException { final String id = getRandomUniqueId(); final String binaryId = "binary0"; assertEquals(CREATED.getStatusCode(), getStatus(putObjMethod(id))); assertEquals(CREATED.getStatusCode(), getStatus(putDSMethod(id, binaryId, "some test content"))); final HttpPatch httpPatch = patchObjMethod(id + "/" + binaryId + "/fcr:metadata"); httpPatch.addHeader(CONTENT_TYPE, "application/sparql-update"); httpPatch.setEntity(new StringEntity( "INSERT { <> <http://purl.org/dc/elements/1.1/title> 'this is a title' } WHERE {}")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPatch)); final HttpGet httpGet = getObjMethod(id); httpGet.setHeader("Prefer", "return=representation; include=\"http://fedora.info/definitions/v4/repository#EmbedResources\""); try (final CloseableDataset dataset = getDataset(httpGet)) { final DatasetGraph graphStore = dataset.asDatasetGraph(); assertTrue("Property on child binary should be found!" + graphStore, graphStore.contains(ANY, createURI(serverAddress + id + "/" + binaryId), createURI("http://purl.org/dc/elements/1.1/title"), createLiteral("this is a title"))); } } @Test public void testExternalMessageBody() throws IOException { // we need a client that won't automatically follow redirects try (final CloseableHttpClient noFollowClient = HttpClientBuilder.create().disableRedirectHandling().build()) { final String id = getRandomUniqueId(); final HttpPut httpPut = putObjMethod(id); httpPut.addHeader(CONTENT_TYPE, "message/external-body; access-type=URL; " + "URL=\"http://www.example.com/file\""); try (final CloseableHttpResponse response = execute(httpPut)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); final HttpGet get = new HttpGet(getLocation(response)); try (final CloseableHttpResponse getResponse = noFollowClient.execute(get)) { assertEquals(TEMPORARY_REDIRECT.getStatusCode(), getStatus(getResponse)); assertEquals("http://www.example.com/file", getLocation(getResponse)); } } } } @Test public void testJsonLdProfileCompacted() throws IOException { // Create a resource final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/n3"); final BasicHttpEntity entity = new BasicHttpEntity(); final String rdf = "<> <http://purl.org/dc/elements/1.1/title> \"ceci n'est pas un titre français\"@fr ." + "<> <http://purl.org/dc/elements/1.1/title> \"this is an english title\"@en ."; entity.setContent(new ByteArrayInputStream(rdf.getBytes(UTF_8))); method.setEntity(entity); final String location; try (final CloseableHttpResponse response = execute(method)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); location = response.getFirstHeader("Location").getValue(); } // GET the resource with a JSON profile final HttpGet httpGet = new HttpGet(location); httpGet.setHeader(ACCEPT, "application/ld+json; profile=\"http://www.w3.org/ns/json-ld#compacted\""); final JsonNode json; try (final CloseableHttpResponse responseGET = execute(httpGet)) { // Inspect the response final ObjectMapper mapper = new ObjectMapper(); json = mapper.readTree(responseGET.getEntity().getContent()); } final JsonNode titles = json.get("title"); assertNotNull(titles); assertTrue("Should be a list", titles.isArray()); assertEquals("Should be two langs!", 2, titles.findValues("@language").size()); assertEquals("Should be two values!", 2, titles.findValues("@value").size()); } @Test public void testJsonLdProfileExpanded() throws IOException { // Create a resource final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/n3"); final BasicHttpEntity entity = new BasicHttpEntity(); final String rdf = "<> <http://purl.org/dc/elements/1.1/title> \"ceci n'est pas un titre français\"@fr ." + "<> <http://purl.org/dc/elements/1.1/title> \"this is an english title\"@en ."; entity.setContent(new ByteArrayInputStream(rdf.getBytes(UTF_8))); method.setEntity(entity); final String location; try (final CloseableHttpResponse response = execute(method)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); location = response.getFirstHeader("Location").getValue(); } // GET the resource with a JSON profile final HttpGet httpGet = new HttpGet(location); httpGet.setHeader(ACCEPT, "application/ld+json; profile=\"http://www.w3.org/ns/json-ld#expanded\""); final JsonNode json; try (final CloseableHttpResponse responseGET = execute(httpGet)) { // Inspect the response final ObjectMapper mapper = new ObjectMapper(); json = mapper.readTree(responseGET.getEntity().getContent()); } final List<JsonNode> titlesList = json.findValues("http://purl.org/dc/elements/1.1/title"); assertNotNull(titlesList); assertEquals("Should be list of lists", 1, titlesList.size()); final JsonNode titles = titlesList.get(0); assertEquals("Should be two langs!", 2, titles.findValues("@language").size()); assertEquals("Should be two values!", 2, titles.findValues("@value").size()); } @Test public void testJsonLdProfileFlattened() throws IOException { // Create a resource final HttpPost method = postObjMethod(); method.addHeader(CONTENT_TYPE, "application/n3"); final BasicHttpEntity entity = new BasicHttpEntity(); final String rdf = "<> <http://purl.org/dc/elements/1.1/title> \"ceci n'est pas un titre français\"@fr ." + "<> <http://purl.org/dc/elements/1.1/title> \"this is an english title\"@en ."; entity.setContent(new ByteArrayInputStream(rdf.getBytes(UTF_8))); method.setEntity(entity); final String location; try (final CloseableHttpResponse response = execute(method)) { assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); location = response.getFirstHeader("Location").getValue(); } // GET the resource with a JSON profile final HttpGet httpGet = new HttpGet(location); httpGet.setHeader(ACCEPT, "application/ld+json; profile=\"http://www.w3.org/ns/json-ld#flattened\""); final JsonNode json; try (final CloseableHttpResponse responseGET = execute(httpGet)) { // Inspect the response final ObjectMapper mapper = new ObjectMapper(); json = mapper.readTree(responseGET.getEntity().getContent()); } final List<JsonNode> titlesList = json.get("@graph").findValues("title"); assertNotNull(titlesList); assertEquals("Should be list of lists", 1, titlesList.size()); final JsonNode titles = titlesList.get(0); assertEquals("Should be two langs!", 2, titles.findValues("@language").size()); assertEquals("Should be two values!", 2, titles.findValues("@value").size()); } @Test public void testPathWithEmptySegment() { final String badLocation = "test/me/mb/er/s//members/9528a300-22da-40f2-bf3c-5b345d71affb"; assertEquals(BAD_REQUEST.getStatusCode(), getStatus(headObjMethod(badLocation))); } private static Optional<Instant> getDateFromModel(final Model model, final Resource subj, final Property pred) throws NoSuchElementException, ParseException { final StmtIterator stmts = model.listStatements(subj, pred, (String) null); return Optional.ofNullable( stmts.hasNext() ? Instant.from(tripleFormat.parse(stmts.nextStatement().getString())) : null); } @Test public void testUpdateObjectGraphWithNonLocalTriples() throws IOException { final String pid = getRandomUniqueId(); createObject(pid); final String otherPid = getRandomUniqueId(); createObject(otherPid); final String location = serverAddress + pid; final String otherLocation = serverAddress + otherPid; final HttpPatch updateObjectGraphMethod = new HttpPatch(location); updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); updateObjectGraphMethod.setEntity(new StringEntity("INSERT { <" + location + "> <http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\". " + "<" + otherLocation + "> <http://purl.org/dc/elements/1.1/identifier> \"this is an identifier\"" + " } WHERE {}")); assertEquals("It ought not be possible to use PATCH to create non-local triples!", FORBIDDEN.getStatusCode(),getStatus(updateObjectGraphMethod)); } @Test public void testPutMalformedHeader() throws IOException { // Create a resource final String id = getRandomUniqueId(); executeAndClose(putObjMethod(id)); // Get the resource's etag String etag; final HttpHead httpHead = headObjMethod(id); try (final CloseableHttpResponse response = execute(httpHead)) { etag = response.getFirstHeader("ETag").getValue(); assertNotNull("ETag was missing!?", etag); } // PUT properly formatted etag final HttpPut httpPut = putObjMethod(id); httpPut.addHeader("If-Match", etag); try (final CloseableHttpResponse response = execute(httpPut)) { assertEquals("Should be a 412 Precondition Failed!", PRECONDITION_FAILED.getStatusCode(), getStatus(response)); } // PUT improperly formatted etag ... not quoted. final HttpPut httpPut2 = putObjMethod(id); httpPut2.addHeader("If-Match", etag.replace("\"", "")); try (final CloseableHttpResponse response = execute(httpPut2)) { assertEquals("Should be a 400 BAD REQUEST!", BAD_REQUEST.getStatusCode(), getStatus(response)); } } @Test public void testPutEmptyBody() throws IOException { final HttpPut httpPut = putObjMethod(getRandomUniqueId()); httpPut.addHeader(CONTENT_TYPE, "application/ld+json"); try (final CloseableHttpResponse response = execute(httpPut)) { assertEquals("Should be a client error", BAD_REQUEST.getStatusCode(), getStatus(response)); } } @Test public void testPutOgg() throws IOException { final String id = getRandomUniqueId(); createDatastream(id, "x", "OggS"); } @Test public void testPutReferenceRoot() throws Exception { final HttpPut httpPut = putObjMethod(getRandomUniqueId()); httpPut.addHeader(CONTENT_TYPE, "text/turtle"); httpPut.setEntity(new StringEntity("@prefix acl: <http://www.w3.org/ns/auth/acl#> . " + "<> a acl:Authorization ; " + "acl:agent \"smith123\" ; " + "acl:mode acl:Read ;" + "acl:accessTo <" + serverAddress + "> .")); try (final CloseableHttpResponse response = execute(httpPut)) { assertEquals(CREATED.getStatusCode(), getStatus(response)); } } @Test public void testDeleteLargeLiteralStoredAsBinary() throws IOException { final String pid = getRandomUniqueId(); createObject(pid); final Node DC_TITLE = title.asNode(); final String location = serverAddress + pid; final HttpPatch patch = new HttpPatch(location); final HttpPatch delPatch = new HttpPatch(location); final String longLiteral = // minimumBinaryInByteSize is currently 40 bytes "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"; LOGGER.info("BINARY LITERAL TEST"); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new StringEntity( "INSERT { <> <" + DC_TITLE + "> \"\"\"" + longLiteral + "\"\"\" } WHERE {}")); assertEquals("Unable to add property value", NO_CONTENT.getStatusCode(), getStatus(patch)); delPatch.addHeader(CONTENT_TYPE, "application/sparql-update"); delPatch.setEntity(new StringEntity( "DELETE WHERE { <> <" + DC_TITLE + "> \"\"\"" + longLiteral + "\"\"\"}")); // delete that triple, or at least try to. assertEquals("Unable to complete delete property HTTP request", NO_CONTENT.getStatusCode(), getStatus(delPatch)); // now test if property exists anymore (it shouldn't). try (final CloseableDataset dataset = getDataset(getObjMethod(pid))) { assertFalse("Found the literal we tried to delete!", dataset.asDatasetGraph().contains(ANY, createURI(location), DC_TITLE, createLiteral(longLiteral))); } } @Test public void testInboundLinksDoNotUpdateEtag() throws IOException { final String id1 = getRandomUniqueId(); final HttpPut httpPut = putObjMethod(id1); final String oldETag; final String oldMod; try (final CloseableHttpResponse response = execute(httpPut)) { assertEquals(CREATED.getStatusCode(), getStatus(response)); oldETag = response.getFirstHeader("ETag").getValue(); oldMod = response.getFirstHeader("Last-Modified").getValue(); } final String id2 = getRandomUniqueId(); createObject(id2).close(); final HttpPatch patch = patchObjMethod(id2); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new StringEntity( "INSERT { <> <http://purl.org/dc/elements/1.1/relation> <" + serverAddress + id1 + "> } WHERE {}")); assertEquals(NO_CONTENT.getStatusCode(), getStatus(execute(patch))); try (final CloseableHttpResponse response = execute(getObjMethod(id1))) { final String etag = response.getFirstHeader("ETag").getValue(); final String lastmod = response.getFirstHeader("Last-Modified").getValue(); assertEquals(oldMod, lastmod); assertEquals(oldETag, etag); } } @Test public void testCreationResponseDefault() throws Exception { testCreationResponse(null, null, CREATED, "text/plain"); testCreationResponse(null, "application/ld+json", NOT_ACCEPTABLE, "text/html"); } @Test public void testCreationResponseMinimal() { testCreationResponse("minimal", null, CREATED, null); testCreationResponse("minimal", "application/ld+json", CREATED, null); } @Test public void testCreationResponseRepresentation() { testCreationResponse("representation", null, CREATED, "text/turtle"); testCreationResponse("representation", "application/ld+json", CREATED, "application/ld+json"); } private static void testCreationResponse(final String prefer, final String accept, final Status expectedStatus, final String expectedType) { final HttpPost createMethod = new HttpPost(serverAddress); if (prefer != null) { createMethod.addHeader("Prefer", "return=" + prefer); } if (accept != null) { createMethod.addHeader(ACCEPT, accept); } try (final CloseableHttpResponse createResponse = execute(createMethod)) { assertEquals(expectedStatus.getStatusCode(), createResponse.getStatusLine().getStatusCode()); if (expectedType == null) { assertNull(createResponse.getFirstHeader(CONTENT_TYPE)); } else { assertTrue(createResponse.getFirstHeader(CONTENT_TYPE).getValue().startsWith(expectedType)); } } catch (final IOException e) { throw new AssertionError(e); } } @Test public void testPathsWithBrackets() { assertEquals(BAD_REQUEST.getStatusCode(), getStatus(deleteObjMethod("%5bfoo"))); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(getObjMethod("%5bfoo"))); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(patchObjMethod("%5bfoo"))); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(postObjMethod("%5bfoo"))); assertEquals(BAD_REQUEST.getStatusCode(), getStatus(putObjMethod("%5bfoo"))); } @Test public void testConcurrentPuts() throws InterruptedException, IOException { final String parent = getRandomUniqueId(); executeAndClose(putObjMethod(parent)); final String newResource = parent + "/test"; final Runnable updateRunnable = () -> { executeAndClose(putObjMethod(newResource)); }; final Thread t1 = new Thread(updateRunnable); final Thread t2 = new Thread(updateRunnable); final Thread t3 = new Thread(updateRunnable); final Thread t4 = new Thread(updateRunnable); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); try (final CloseableDataset dataset = getDataset(getObjMethod(parent))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); final Iterator<Quad> children = graphStore.find(ANY, ANY, CONTAINS.asNode(), ANY); assertTrue("One of the PUTs should have resulted in a child.", children.hasNext()); children.next(); if (children.hasNext()) { fail("Only one of the PUTs should have resulted in a child (unexpected child " + children.next().getObject().getURI() + ")!"); } } } @Test public void testConcurrentPutsWithPairtrees() throws InterruptedException, IOException { final String parent = getRandomUniqueId(); executeAndClose(putObjMethod(parent)); final String first = parent + "/00/1"; final String second = parent + "/00/2"; final String third = parent + "/00/3"; final String fourth = parent + "/00/4"; final Thread t1 = new Thread(() -> { executeAndClose(putObjMethod(first));}); final Thread t2 = new Thread(() -> { executeAndClose(putObjMethod(second)); }); final Thread t3 = new Thread(() -> { executeAndClose(putObjMethod(third)); }); final Thread t4 = new Thread(() -> { executeAndClose(putObjMethod(fourth)); }); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); try (final CloseableDataset dataset = getDataset(getObjMethod(parent))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); final List<String> childPaths = new ArrayList<String>(); final Iterator<Quad> children = graphStore.find(ANY, ANY, CONTAINS.asNode(), ANY); assertTrue("Four children should have been created (none found).", children.hasNext()); childPaths.add(children.next().getObject().getURI()); LOGGER.info("Found child: {}", childPaths.get(0)); assertTrue("Four children should have been created (only one found).", children.hasNext()); childPaths.add(children.next().getObject().getURI()); LOGGER.info("Found child: {}", childPaths.get(1)); assertTrue("Four children should have been created (only two found).", children.hasNext()); childPaths.add(children.next().getObject().getURI()); LOGGER.info("Found child: {}", childPaths.get(2)); assertTrue("Four children should have been created. (only three found)", children.hasNext()); childPaths.add(children.next().getObject().getURI()); LOGGER.info("Found child: {}", childPaths.get(3)); assertFalse("Only four children should have been created.", children.hasNext()); assertTrue(childPaths.contains(serverAddress + first)); assertTrue(childPaths.contains(serverAddress + second)); assertTrue(childPaths.contains(serverAddress + third)); assertTrue(childPaths.contains(serverAddress + fourth)); } } @Test public void testConcurrentUpdatesToBinary() throws IOException, InterruptedException { // create a binary final String path = getRandomUniqueId(); final HttpPut method = new HttpPut(serverAddress + path); method.setHeader(CONTENT_TYPE, "text/plain"); method.setEntity(new StringEntity("initial value")); final String binaryEtag; try (final CloseableHttpResponse response = execute(method)) { binaryEtag = response.getFirstHeader("ETag").getValue(); } final PutThread[] threads = new PutThread[] { new PutThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 1")), new PutThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 2")), new PutThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 3")), new PutThread(putBinaryObjMethodIfMatch(path, binaryEtag, "thread 4")) }; for (PutThread t : threads) { t.start(); } final List<PutThread> successfulThreads = new ArrayList<>(); for (PutThread t : threads) { t.join(1000); assertFalse("Thread " + t.getId() + " could not perform its operation in time!", t.isAlive()); final int status = t.response.getStatusLine().getStatusCode(); LOGGER.info("{} received a {} status code.", t.getId(), status); if (status == 204) { successfulThreads.add(t); } } assertEquals("Only one PUT request should have been successful!", 1, successfulThreads.size()); } private class PutThread extends Thread { private HttpPut put; private HttpResponse response; public PutThread(final HttpPut put) { this.put = put; } @Override public void run() { try { response = execute(put); } catch (IOException e) { LOGGER.error("Thread " + Thread.currentThread().getId() + ", failed to PUT resource!", e); } } } private HttpPut putBinaryObjMethodIfMatch(final String location, final String etag, final String content) { final HttpPut put = putObjMethod(location); put.setHeader(CONTENT_TYPE, "text/plain"); put.setHeader("If-Match", etag); try { put.setEntity(new StringEntity(content)); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } return put; } @Test public void testConcurrentPatches() throws IOException, InterruptedException { // create a resource final String path = getRandomUniqueId(); executeAndClose(putObjMethod(path)); final int[] responseCodes = new int[4]; final Thread t1 = new Thread(() -> { responseCodes[0] = patchWithSparql(path, "PREFIX dc: <http://purl.org/dc/elements/1.1/>\nINSERT DATA { <> dc:identifier 'one' . }");}); final Thread t2 = new Thread(() -> { responseCodes[1] = patchWithSparql(path, "PREFIX dc: <http://purl.org/dc/elements/1.1/>\nINSERT DATA { <> dc:identifier 'two' . }");}); final Thread t3 = new Thread(() -> { responseCodes[2] = patchWithSparql(path, "PREFIX dc: <http://purl.org/dc/elements/1.1/>\nINSERT DATA { <> dc:identifier 'three' . }");}); final Thread t4 = new Thread(() -> { responseCodes[3] = patchWithSparql(path, "PREFIX dc: <http://purl.org/dc/elements/1.1/>\nINSERT DATA { <> dc:identifier 'four' . }");}); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); assertEquals("Patch should have succeeded!", 204, responseCodes[0]); assertEquals("Patch should have succeeded!", 204, responseCodes[1]); assertEquals("Patch should have succeeded!", 204, responseCodes[2]); assertEquals("Patch should have succeeded!", 204, responseCodes[3]); try (final CloseableDataset dataset = getDataset(getObjMethod(path))) { final DatasetGraph graphStore = dataset.asDatasetGraph(); final List<String> missingDcIdentifiers = new ArrayList<>(Arrays.asList(new String[] { "one", "two", "three", "four" })); final Iterator<Quad> dcIdentifierIt = graphStore.find(ANY, createURI(serverAddress + path), createProperty("http://purl.org/dc/elements/1.1/identifier").asNode(), ANY); while (dcIdentifierIt.hasNext()) { final String value = dcIdentifierIt.next().getObject().getLiteralValue().toString(); assertTrue("Unexpected dc:identifier found: " + value, missingDcIdentifiers.remove(value)); } assertTrue("All of the dc:identifiers should have been applied! (missing " + Arrays.toString(missingDcIdentifiers.toArray()) + ")", missingDcIdentifiers.isEmpty()); assertTrue("Added property must exist!", graphStore.contains(ANY, createURI(serverAddress + path), createProperty("http://purl.org/dc/elements/1.1/identifier").asNode(), createLiteral("one"))); assertTrue("Added property must exist!", graphStore.contains(ANY, createURI(serverAddress + path), createProperty("http://purl.org/dc/elements/1.1/identifier").asNode(), createLiteral("two"))); assertTrue("Added property must exist!", graphStore.contains(ANY, createURI(serverAddress + path), createProperty("http://purl.org/dc/elements/1.1/identifier").asNode(), createLiteral("three"))); assertTrue("Added property must exist!", graphStore.contains(ANY, createURI(serverAddress + path), createProperty("http://purl.org/dc/elements/1.1/identifier").asNode(), createLiteral("four"))); } } private int patchWithSparql(final String path, final String sparqlUpdate) { try { final HttpPatch patch = new HttpPatch(serverAddress + path); patch.addHeader(CONTENT_TYPE, "application/sparql-update"); patch.setEntity(new StringEntity(sparqlUpdate)); final CloseableHttpResponse r = client.execute(patch); final int code = r.getStatusLine().getStatusCode(); r.close(); return code; } catch (IOException e) { throw new RuntimeException(e); } } @Test public void testBinaryLastModified() throws Exception { final String objid = getRandomUniqueId(); final String objURI = serverAddress + objid; final String binURI = objURI + "/binary1"; final Instant lastmod1; try (final CloseableHttpResponse response = execute(putDSMethod(objid, "binary1", "some test content"))) { assertEquals(CREATED.getStatusCode(), getStatus(response)); lastmod1 = Instant.from(headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())); } sleep(1000); // wait a second to make sure last-modified value will be different try (final CloseableDataset dataset = getDataset(new HttpGet(binURI + "/fcr:metadata"))) { verifyModifiedMatchesCreated(dataset); } final HttpPatch patchBinary = new HttpPatch(binURI + "/fcr:metadata"); patchBinary.addHeader(CONTENT_TYPE, "application/sparql-update"); patchBinary.setEntity(new StringEntity("INSERT { <" + binURI + "> " + "<http://www.w3.org/TR/rdf-schema/label> \"this is a label\" } WHERE {}")); final Instant lastmod2; try (final CloseableHttpResponse response = execute(patchBinary)) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); lastmod2 = Instant.from(headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())); assertTrue(lastmod2.isAfter(lastmod1)); } sleep(1000); // wait a second to make sure last-modified value will be different final Instant lastmod3; try (final CloseableHttpResponse response = execute(putDSMethod(objid, "binary1", "new test content"))) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); lastmod3 = Instant.from(headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())); assertTrue(lastmod3.isAfter(lastmod2)); } } @Test public void testContainerLastModified() throws Exception { final String objid = getRandomUniqueId(); final String objURI = serverAddress + objid; // create an object final long lastmod1; try (final CloseableHttpResponse response = execute(putObjMethod(objid))) { assertEquals(CREATED.getStatusCode(), getStatus(response)); lastmod1 = Instant.from( headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())).toEpochMilli(); } sleep(1000); // wait a second to make sure last-modified value will be different // initial created and last-modified properties should match try (final CloseableDataset dataset = getDataset(getObjMethod(objid))) { verifyModifiedMatchesCreated(dataset); } // update the object properties (last-modified should be updated) final HttpPatch patchObject = new HttpPatch(objURI); patchObject.addHeader(CONTENT_TYPE, "application/sparql-update"); patchObject.setEntity(new StringEntity("INSERT { <> " + "<http://www.w3.org/TR/rdf-schema/label> \"this is a label\" } WHERE {}")); final long lastmod2; try (final CloseableHttpResponse response = execute(patchObject)) { assertEquals(NO_CONTENT.getStatusCode(), getStatus(response)); lastmod2 = Instant.from( headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())).toEpochMilli(); assertTrue(lastmod2 > lastmod1); } sleep(1000); // wait a second to make sure last-modified value will be different // create a direct container (last-modified should be updated) final long lastmod3; final HttpPut createContainer = new HttpPut(objURI + "/members"); createContainer.addHeader(CONTENT_TYPE, "text/turtle"); final String membersRDF = "<> a <http://www.w3.org/ns/ldp#DirectContainer>; " + "<http://www.w3.org/ns/ldp#hasMemberRelation> <http://pcdm.org/models#hasMember>; " + "<http://www.w3.org/ns/ldp#membershipResource> <" + objURI + "> . "; createContainer.setEntity(new StringEntity(membersRDF)); try (final CloseableHttpResponse response = execute(createContainer)) { assertEquals(CREATED.getStatusCode(), getStatus(response)); lastmod3 = Instant.from( headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())).toEpochMilli(); assertTrue(lastmod3 > lastmod2); } sleep(1000); // wait a second to make sure last-modified value will be different // create child in the container final long lastmod4; assertEquals(CREATED.getStatusCode(), getStatus(new HttpPut(objURI + "/members/member1"))); // last-modified should be updated try (final CloseableHttpResponse response = execute(headObjMethod(objid))) { assertEquals(OK.getStatusCode(), getStatus(response)); lastmod4 = Instant.from( headerFormat.parse(response.getFirstHeader("Last-Modified").getValue())).toEpochMilli(); assertTrue(lastmod4 > lastmod3); } } private static void verifyModifiedMatchesCreated(final Dataset dataset) { final DatasetGraph graph = dataset.asDatasetGraph(); final Node cre = graph.find(ANY, ANY, CREATED_DATE.asNode(), ANY).next().getObject(); final Node mod = graph.find(ANY, ANY, LAST_MODIFIED_DATE.asNode(), ANY).next().getObject(); assertEquals(cre.getLiteralValue(), mod.getLiteralValue()); } }