Skip to content

Commit

Permalink
Implement Prefer: HTTP headers to control LDPC hierarchy serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
cbeer committed Apr 15, 2014
1 parent 6c9cdca commit aab525c
Show file tree
Hide file tree
Showing 14 changed files with 598 additions and 129 deletions.
109 changes: 79 additions & 30 deletions fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraNodes.java
Expand Up @@ -31,6 +31,7 @@
import static javax.ws.rs.core.Response.noContent;
import static javax.ws.rs.core.Response.status;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
import static org.apache.commons.lang.ArrayUtils.contains;
import static org.apache.http.HttpStatus.SC_BAD_GATEWAY;
import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
import static org.apache.http.HttpStatus.SC_CONFLICT;
Expand Down Expand Up @@ -59,6 +60,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

Expand Down Expand Up @@ -100,12 +102,16 @@
import org.fcrepo.http.commons.domain.MOVE;
import org.fcrepo.http.commons.domain.PATCH;
import org.fcrepo.http.commons.domain.COPY;
import org.fcrepo.http.commons.domain.Prefer;
import org.fcrepo.http.commons.domain.PreferTag;
import org.fcrepo.http.commons.session.InjectedSession;
import org.fcrepo.kernel.Datastream;
import org.fcrepo.kernel.FedoraResource;
import org.fcrepo.kernel.exception.InvalidChecksumException;
import org.fcrepo.kernel.rdf.IdentifierTranslator;
import org.fcrepo.kernel.rdf.HierarchyRdfContextOptions;
import org.fcrepo.kernel.utils.iterators.RdfStream;
import org.openrdf.util.iterators.Iterators;
import org.slf4j.Logger;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
Expand All @@ -123,9 +129,6 @@
@Path("/{path: .*}")
public class FedoraNodes extends AbstractResource {

public static final int NO_LIMIT = -1;
public static final int NO_MEMBER_PROPERTIES = -2;

@InjectedSession
protected Session session;

Expand All @@ -149,8 +152,8 @@ public class FedoraNodes extends AbstractResource {
TEXT_HTML, APPLICATION_XHTML_XML})
public RdfStream describe(@PathParam("path") final List<PathSegment> pathList,
@QueryParam("offset") @DefaultValue("0") final int offset,
@QueryParam("limit") @DefaultValue("-1") final int limit,
@QueryParam("non-member-properties") final String nonMemberProperties,
@QueryParam("limit") @DefaultValue("-1") final int limit,
@HeaderParam("Prefer") final Prefer prefer,
@Context final Request request,
@Context final HttpServletResponse servletResponse,
@Context final UriInfo uriInfo) throws RepositoryException {
Expand Down Expand Up @@ -180,37 +183,83 @@ public RdfStream describe(@PathParam("path") final List<PathSegment> pathList,
final HttpIdentifierTranslator subjects =
new HttpIdentifierTranslator(session, this.getClass(), uriInfo);

final int realLimit;
if (nonMemberProperties != null && limit == NO_LIMIT) {
realLimit = NO_MEMBER_PROPERTIES;
} else {
realLimit = limit;
}

final RdfStream rdfStream =
resource.getTriples(subjects).concat(
resource.getHierarchyTriples(subjects)).session(session)
resource.getTriples(subjects).session(session)
.topic(subjects.getSubject(resource.getNode().getPath())
.asNode());
if (realLimit != NO_MEMBER_PROPERTIES) {
final Node firstPage =
createURI(uriInfo.getRequestUriBuilder().replaceQueryParam(
"offset", 0).replaceQueryParam("limit", limit).build()
.toString().replace("&", "&amp;"));
final Node nextPage =
createURI(uriInfo.getRequestUriBuilder().replaceQueryParam(
"offset", offset + limit).replaceQueryParam("limit",
limit).build().toString().replace("&", "&amp;"));
rdfStream.concat(
create(subjects.getContext().asNode(), NEXT_PAGE.asNode(),
nextPage),
create(subjects.getContext().asNode(), FIRST_PAGE.asNode(),
firstPage)).limit(realLimit).skip(offset);

servletResponse.addHeader("Link", firstPage + ";rel=\"first\"");

final PreferTag returnPreference;

if (prefer != null && prefer.hasReturn()) {
returnPreference = prefer.getReturn();
} else {
returnPreference = new PreferTag("");
}

if (!returnPreference.getValue().equals("minimal")) {
String include = returnPreference.getParams().get("include");
if (include == null) {
include = "";
}

String omit = returnPreference.getParams().get("omit");
if (omit == null) {
omit = "";
}

final String[] includes = include.split(" ");
final String[] omits = omit.split(" ");

if (limit >= 0) {
final Node firstPage =
createURI(uriInfo.getRequestUriBuilder().replaceQueryParam("offset", 0)
.replaceQueryParam("limit", limit).build()
.toString().replace("&", "&amp;"));
final Node nextPage =
createURI(uriInfo.getRequestUriBuilder().replaceQueryParam("offset", offset + limit)
.replaceQueryParam("limit", limit).build()
.toString().replace("&", "&amp;"));
rdfStream.concat(create(subjects.getContext().asNode(), NEXT_PAGE.asNode(), nextPage),
create(subjects.getContext().asNode(), FIRST_PAGE.asNode(), firstPage));

servletResponse.addHeader("Link", firstPage + ";rel=\"first\"");
}

List<String> appliedIncludes = new ArrayList<>();

final HierarchyRdfContextOptions hierarchyRdfContextOptions = new HierarchyRdfContextOptions(limit, offset);
hierarchyRdfContextOptions.containment =
(!contains(includes, LDP_NAMESPACE + "PreferEmptyContainer") ||
contains(includes, LDP_NAMESPACE + "PreferContainment"))
&& !contains(omits, LDP_NAMESPACE + "PreferContainment");

hierarchyRdfContextOptions.membership =
(!contains(includes, LDP_NAMESPACE + "PreferEmptyContainer") ||
contains(includes, LDP_NAMESPACE + "PreferMembership"))
&& !contains(omits, LDP_NAMESPACE + "PreferMembership");

if (hierarchyRdfContextOptions.membershipEnabled()) {
appliedIncludes.add(LDP_NAMESPACE + "PreferMembership");
}

if (hierarchyRdfContextOptions.containmentEnabled()) {
appliedIncludes.add(LDP_NAMESPACE + "PreferContainment");
}

rdfStream.concat(resource.getHierarchyTriples(subjects, hierarchyRdfContextOptions));

final String preferences = "return=representation; include=\""
+ Iterators.toString(appliedIncludes.iterator(), " ") + "\"";
servletResponse.addHeader("Preference-Applied", preferences);
servletResponse.addHeader("Vary", preferences);

} else {
servletResponse.addHeader("Preference-Applied", "return=minimal");
servletResponse.addHeader("Vary", "return=minimal");
}



if (!etag.getValue().isEmpty()) {
servletResponse.addHeader("ETag", etag.toString());
}
Expand Down
Expand Up @@ -36,6 +36,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyMapOf;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
Expand All @@ -50,6 +51,7 @@
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;

Expand All @@ -70,10 +72,12 @@
import javax.ws.rs.core.UriInfo;

import org.apache.commons.io.IOUtils;
import org.fcrepo.http.commons.domain.Prefer;
import org.fcrepo.kernel.Datastream;
import org.fcrepo.kernel.FedoraObject;
import org.fcrepo.kernel.FedoraResourceImpl;
import org.fcrepo.kernel.identifiers.PidMinter;
import org.fcrepo.kernel.rdf.HierarchyRdfContextOptions;
import org.fcrepo.kernel.rdf.IdentifierTranslator;
import org.fcrepo.kernel.services.DatastreamService;
import org.fcrepo.kernel.services.NodeService;
Expand Down Expand Up @@ -337,7 +341,8 @@ public void testDescribeObject() throws RepositoryException {
when(mockObject.getEtagValue()).thenReturn("");
when(mockObject.getTriples(any(IdentifierTranslator.class))).thenReturn(
mockRdfStream);
when(mockObject.getHierarchyTriples(any(IdentifierTranslator.class))).thenReturn(
when(mockObject.getHierarchyTriples(any(IdentifierTranslator.class),
any(HierarchyRdfContextOptions.class))).thenReturn(
mockRdfStream2);
when(mockNodes.getObject(isA(Session.class), isA(String.class)))
.thenReturn(mockObject);
Expand All @@ -353,7 +358,7 @@ public void testDescribeObject() throws RepositoryException {
}

@Test
public void testDescribeObjectNoInlining() throws RepositoryException {
public void testDescribeObjectNoInlining() throws RepositoryException, ParseException {
final String pid = "FedoraObjectsRdfTest1";
final String path = "/" + pid;

Expand All @@ -364,14 +369,15 @@ public void testDescribeObjectNoInlining() throws RepositoryException {
when(mockObject.getLastModifiedDate()).thenReturn(mockDate);
when(mockObject.getTriples(any(IdentifierTranslator.class))).thenReturn(
mockRdfStream);
when(mockObject.getHierarchyTriples(any(IdentifierTranslator.class))).thenReturn(
mockRdfStream2);
when(mockObject.getHierarchyTriples(any(IdentifierTranslator.class),
any(HierarchyRdfContextOptions.class))).thenReturn(mockRdfStream2);
when(mockNodes.getObject(isA(Session.class), isA(String.class)))
.thenReturn(mockObject);
final Request mockRequest = mock(Request.class);
final Prefer prefer = new Prefer("return=representation;"
+ "include=\"http://www.w3.org/ns/ldp#PreferEmptyContainer\"");
final RdfStream rdfStream =
testObj.describe(createPathList(path), 0, -1, "", mockRequest, mockResponse,
mockUriInfo);
testObj.describe(createPathList(path), 0, -1, prefer, mockRequest, mockResponse, mockUriInfo);
assertEquals("Got wrong RDF!", mockRdfStream.concat(mockRdfStream2),
rdfStream);

Expand Down
Expand Up @@ -39,6 +39,7 @@
import static org.fcrepo.jcr.FedoraJcrTypes.ROOT;
import static org.fcrepo.kernel.RdfLexicon.DC_NAMESPACE;
import static org.fcrepo.kernel.RdfLexicon.DC_TITLE;
import static org.fcrepo.kernel.RdfLexicon.HAS_CHILD;
import static org.fcrepo.kernel.RdfLexicon.HAS_OBJECT_COUNT;
import static org.fcrepo.kernel.RdfLexicon.HAS_OBJECT_SIZE;
import static org.fcrepo.kernel.RdfLexicon.HAS_PRIMARY_IDENTIFIER;
Expand All @@ -65,6 +66,7 @@
import java.util.Collection;
import java.util.Iterator;
import java.util.UUID;
import java.util.regex.Matcher;

import javax.ws.rs.core.Variant;

Expand All @@ -85,6 +87,7 @@
import org.apache.http.impl.client.cache.CachingHttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.fcrepo.http.commons.domain.RDFMediaType;
import org.fcrepo.kernel.RdfLexicon;
import org.junit.Ignore;
import org.junit.Test;
import org.xml.sax.ErrorHandler;
Expand Down Expand Up @@ -357,8 +360,6 @@ public String apply(final Header h) {
final Model model = createModelForGraph(results.getDefaultGraph());

final Resource nodeUri = createResource(serverAddress + pid);
assertTrue("Didn't find inlined resources!", model.contains(nodeUri,
createProperty(LDP_NAMESPACE + "inlinedResource")));

assertTrue("Didn't find an expected triple!", model.contains(nodeUri,
createProperty(REPOSITORY_NAMESPACE + "mixinTypes"),
Expand Down Expand Up @@ -407,7 +408,6 @@ public void verifyFullSetOfRdfTypes() throws Exception {
verifyResource(model, nodeUri, rdfType, RESTAPI_NAMESPACE, "relations");
verifyResource(model, nodeUri, rdfType, RESTAPI_NAMESPACE, "resource");
verifyResource(model, nodeUri, rdfType, LDP_NAMESPACE, "Container");
verifyResource(model, nodeUri, rdfType, LDP_NAMESPACE, "Page");
verifyResource(model, nodeUri, rdfType, DC_NAMESPACE, "describable");
verifyResource(model, nodeUri, rdfType, MIX_NAMESPACE, "created");
verifyResource(model, nodeUri, rdfType, MIX_NAMESPACE, "lastModified");
Expand Down Expand Up @@ -465,10 +465,13 @@ public String apply(final Header h) {
}

@Test
public void testGetObjectGraphNonMemberProperties() throws Exception {
createObject("FedoraDescribeTestGraph");
public void testGetObjectGraphMinimal() throws Exception {
final String pid = UUID.randomUUID().toString();
createObject(pid);
createObject(pid + "/a");
final HttpGet getObjMethod =
new HttpGet(serverAddress + "FedoraDescribeTestGraph?non-member-properties");
new HttpGet(serverAddress + pid);
getObjMethod.addHeader("Prefer", "return=minimal");
getObjMethod.addHeader("Accept", "application/n-triples");
final HttpResponse response = client.execute(getObjMethod);
assertEquals(OK.getStatusCode(), response.getStatusLine()
Expand All @@ -478,15 +481,76 @@ public void testGetObjectGraphNonMemberProperties() throws Exception {
logger.debug("Retrieved object graph:\n" + content);

assertFalse(
"Didn't expect inlined resources",
"Didn't expect member resources",
compile(
"<"
+ serverAddress
+ "FedoraDescribeTestGraph> <" + LDP_NAMESPACE + "inlinedResource>",
+ pid + "> <" + HAS_CHILD + ">",
DOTALL).matcher(content).find());

}

@Test
public void testGetObjectOmitMembership() throws Exception {
final String pid = UUID.randomUUID().toString();
createObject(pid);
createObject(pid + "/a");
final HttpGet getObjMethod =
new HttpGet(serverAddress + pid);
getObjMethod.addHeader("Prefer", "return=representation; omit=\"http://www.w3.org/ns/ldp#PreferContainment http://www.w3.org/ns/ldp#PreferMembership\"");
getObjMethod.addHeader("Accept", "application/n-triples");
final HttpResponse response = client.execute(getObjMethod);
assertEquals(OK.getStatusCode(), response.getStatusLine()
.getStatusCode());
final String content = EntityUtils.toString(response.getEntity());

logger.debug("Retrieved object graph:\n" + content);

assertFalse(
"Didn't expect inlined member resources",
compile(
"<"
+ serverAddress
+ pid + "> <" + HAS_CHILD + ">",
DOTALL).matcher(content).find());

}

@Test
public void testGetObjectOmitContainment() throws Exception {
final String pid = UUID.randomUUID().toString();
createObject(pid);
createObject(pid + "/a");
final HttpGet getObjMethod =
new HttpGet(serverAddress + pid);
getObjMethod.addHeader("Prefer", "return=representation; omit=\"http://www.w3.org/ns/ldp#PreferContainment\"");
getObjMethod.addHeader("Accept", "application/n-triples");
final HttpResponse response = client.execute(getObjMethod);
assertEquals(OK.getStatusCode(), response.getStatusLine()
.getStatusCode());
final String content = EntityUtils.toString(response.getEntity());

logger.debug("Retrieved object graph:\n" + content);

assertTrue("Didn't find member resources",
compile(
"<"
+ serverAddress
+ pid + "> <" + HAS_CHILD + ">",
DOTALL).matcher(content).find());

int count = 0;

final Matcher matcher = compile("<http://fedora.info/definitions/v4/repository#uuid>",
DOTALL).matcher(content);

while (matcher.find()) {
count++;
}

assertEquals("Didn't expect to find inlined resources", 1, count);
}

@Test
public void testGetObjectGraphByUUID() throws Exception {
createObject("FedoraDescribeTestGraphByUuid");
Expand Down

0 comments on commit aab525c

Please sign in to comment.