Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poke object graph through the REST API #54

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
153 changes: 153 additions & 0 deletions fcrepo-http-api/src/main/java/org/fcrepo/api/FedoraGraph.java
@@ -0,0 +1,153 @@
package org.fcrepo.api;

import com.codahale.metrics.annotation.Timed;
import com.hp.hpl.jena.update.GraphStore;
import com.hp.hpl.jena.update.UpdateAction;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpStatus;
import org.apache.jena.riot.WebContent;
import org.fcrepo.AbstractResource;
import org.fcrepo.Datastream;
import org.fcrepo.FedoraObject;
import org.fcrepo.exception.InvalidChecksumException;
import org.fcrepo.utils.FedoraJcrTypes;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.Variant;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.ok;
import static org.slf4j.LoggerFactory.getLogger;

@Component
@Path("/rest/{path: .*}/fcr:graph")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajs6f I couldn't get this to play nice as a part of the FedoraNodes routing.. it seemed like some of the IT requests were hitting the rdf-producing endpoints instead of the object profile one..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea what FedoraNodes is/does, so I'll start there. More soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, FedoraNodes seems to be the endpoint formerly known as "fcr:describe". It's not clear to me why there's any difficulty-- you're not doing anything that plenty of other working endpoints aren't doing. What exactly are the paths that are failing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is a question for @barmintor then..

When I tried to put this method in FedoraNodes, several of the integration tests (perhaps all of them?) that tried to get the ObjectProfile or DatastreamProfile XML/JSON responses were getting this response instead. I assume in the absence of an Accept header, JAX-RS magically chooses a response, and ended up choosing wrong.

Maybe this isn't a problem in the long term (if we kill of those profile responses), but was more than I wanted to bite off..

public class FedoraGraph extends AbstractResource {

private static final Logger logger = getLogger(FedoraGraph.class);

@GET
@Produces({WebContent.contentTypeN3,
WebContent.contentTypeN3Alt1,
WebContent.contentTypeN3Alt2,
WebContent.contentTypeTurtle,
WebContent.contentTypeRDFXML,
WebContent.contentTypeRDFJSON,
WebContent.contentTypeNTriples})
public StreamingOutput describeRdf(@PathParam("path") final List<PathSegment> pathList, @Context Request request) throws RepositoryException, IOException {

final String path = toPath(pathList);
logger.trace("getting profile for {}", path);



List<Variant> possibleResponseVariants =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yuck. Help?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you trying to avoid reproducing the array of contentType strings? I could push a kind of goofy bit of reflection that pulls the values out of the Produces annotation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto Ben's question, and if these two guys should always be the same, might we not define them somewhere else and just use the definition twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well:

  • is there a better way to just ask JAX-RS "what accept header made you choose this route anyway?"
  • is there some standard way to just say "just take this string and make a media type out of it?"

Maybe I've missed something in Jena too that'd make this less annoying..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can context-inject the request and inspect it. That's a clean two-liner.

And yes, there's MediaType.valueOf(type).

Variant.mediaTypes(new MediaType(WebContent.contentTypeN3.split("/")[0], WebContent.contentTypeN3.split("/")[1]),
new MediaType(WebContent.contentTypeN3Alt1.split("/")[0], WebContent.contentTypeN3Alt1.split("/")[1]),
new MediaType(WebContent.contentTypeN3Alt2.split("/")[0], WebContent.contentTypeN3Alt2.split("/")[1]),
new MediaType(WebContent.contentTypeTurtle.split("/")[0], WebContent.contentTypeTurtle.split("/")[1]),
new MediaType(WebContent.contentTypeRDFXML.split("/")[0], WebContent.contentTypeRDFXML.split("/")[1]),
new MediaType(WebContent.contentTypeRDFJSON.split("/")[0], WebContent.contentTypeRDFJSON.split("/")[1]),
new MediaType(WebContent.contentTypeNTriples.split("/")[0], WebContent.contentTypeNTriples.split("/")[1]),
new MediaType(WebContent.contentTypeTriG.split("/")[0], WebContent.contentTypeTriG.split("/")[1]),
new MediaType(WebContent.contentTypeNQuads.split("/")[0], WebContent.contentTypeNQuads.split("/")[1])
)
.add().build();
Variant bestPossibleResponse = request.selectVariant(possibleResponseVariants);


final String rdfWriterFormat = WebContent.contentTypeToLang(bestPossibleResponse.getMediaType().toString()).getName().toUpperCase();

return new StreamingOutput() {
@Override
public void write(final OutputStream out) throws IOException {

final Session session = getAuthenticatedSession();
try {
Node node = session.getNode(path);

final FedoraObject object = objectService.getObject(node.getSession(), path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to get the node first, and then pass the session in? I think you could just pass the session in and let the objectService find the node, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the pattern we're slowly coming to is "inject the Session as a field, then use it with the *Services".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not totally sure how JAX-RS context injection plays with anonymous classes... I wouldn't think there's any problem, tho'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cribbed this from FedoraExport. We probably don't need to stream the response, but it was convenient.

final GraphStore graphStore = object.getGraphStore();

graphStore.toDataset().getDefaultModel().write(out, rdfWriterFormat);
} catch (final RepositoryException e) {
throw new WebApplicationException(e);
} finally {
session.logout();
}
}

};

}

/**
* Creates a new object.
*
* @param pathList
* @return 201
* @throws RepositoryException
* @throws org.fcrepo.exception.InvalidChecksumException
* @throws IOException
*/
@POST
@Consumes({WebContent.contentTypeSPARQLUpdate})
@Timed
public Response updateSparql(
@PathParam("path") final List<PathSegment> pathList,
final InputStream requestBodyStream
) throws RepositoryException, IOException, InvalidChecksumException {

String path = toPath(pathList);
logger.debug("Attempting to ingest with path: {}", path);

final Session session = getAuthenticatedSession();

try {
if (objectService.exists(session, path)) {

if(requestBodyStream != null) {

final FedoraObject result = objectService.getObject(session, path);

UpdateAction.parseExecute(IOUtils.toString(requestBodyStream), result.getGraphStore());

session.save();

return ok().build();
} else {
return Response.status(HttpStatus.SC_CONFLICT).entity(path + " is an existing resource").build();
}
} else {
return Response.status(HttpStatus.SC_NOT_FOUND).entity(path + " must be an existing resource").build();
}

} finally {
session.logout();
}
}
}
58 changes: 46 additions & 12 deletions fcrepo-http-api/src/main/java/org/fcrepo/api/FedoraNodes.java
@@ -1,17 +1,13 @@

package org.fcrepo.api;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
import static javax.ws.rs.core.MediaType.TEXT_XML;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.noContent;
import static javax.ws.rs.core.Response.ok;
import static javax.ws.rs.core.MediaType.*;
import static javax.ws.rs.core.Response.*;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import javax.jcr.Node;
Expand All @@ -27,19 +23,29 @@
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.Variant;

import com.codahale.metrics.annotation.Timed;
import com.hp.hpl.jena.update.GraphStore;
import com.hp.hpl.jena.update.UpdateAction;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpStatus;
import org.apache.jena.riot.WebContent;
import org.fcrepo.AbstractResource;
import org.fcrepo.Datastream;
import org.fcrepo.FedoraObject;
import org.fcrepo.exception.InvalidChecksumException;
import org.fcrepo.jaxb.responses.access.DescribeRepository;
import org.fcrepo.jaxb.responses.access.ObjectProfile;
import org.fcrepo.jaxb.responses.management.DatastreamProfile;
import org.fcrepo.serialization.FedoraObjectSerializer;
import org.fcrepo.services.DatastreamService;
import org.fcrepo.services.LowLevelStorageService;
import org.fcrepo.services.ObjectService;
Expand All @@ -58,7 +64,7 @@ public class FedoraNodes extends AbstractResource {
private LowLevelStorageService llStoreService;

@GET
@Produces({TEXT_XML, APPLICATION_JSON})
@Produces({TEXT_XML, APPLICATION_JSON, TEXT_PLAIN})
public Response describe(@PathParam("path")
final List<PathSegment> pathList) throws RepositoryException, IOException {

Expand Down Expand Up @@ -170,12 +176,22 @@ public DescribeRepository getRepositoryProfile() throws RepositoryException {
@PUT
@Timed
public Response modifyObject(@PathParam("path")
final List<PathSegment> pathList) throws RepositoryException {
final List<PathSegment> pathList, final InputStream requestBodyStream) throws RepositoryException, IOException {
final Session session = getAuthenticatedSession();
String path = toPath(pathList);
logger.debug("Modifying object with path: {}", path);

try {
// TODO do something with awful mess of fcrepo3 query params

final FedoraObject result =
objectService.getObject(session, path);

if (requestBodyStream != null) {
UpdateAction.parseExecute(IOUtils.toString(requestBodyStream), result.getGraphStore());
}
session.save();
return created(uriInfo.getRequestUri()).build();

return temporaryRedirect(uriInfo.getRequestUri()).build();
} finally {
session.logout();
}
Expand Down Expand Up @@ -209,14 +225,32 @@ public Response createObject(

try {
if (objectService.exists(session, path)) {
return Response.status(HttpStatus.SC_CONFLICT).entity(path + " is an existing resource").build();

if(requestBodyStream != null && requestContentType != null && requestContentType.toString().equals(WebContent.contentTypeSPARQLUpdate)) {

final FedoraObject result = objectService.getObject(session, path);

UpdateAction.parseExecute(IOUtils.toString(requestBodyStream), result.getGraphStore());

session.save();

return ok().build();
} else {
return Response.status(HttpStatus.SC_CONFLICT).entity(path + " is an existing resource").build();
}
}

if (FedoraJcrTypes.FEDORA_OBJECT.equals(mixin)){
final FedoraObject result =
objectService.createObject(session, path);
if (label != null && !"".equals(label)) {
result.setLabel(label);
}

if(requestBodyStream != null && requestContentType != null && requestContentType.toString().equals(WebContent.contentTypeSPARQLUpdate)) {
UpdateAction.parseExecute(IOUtils.toString(requestBodyStream), result.getGraphStore());
}

}
if (FedoraJcrTypes.FEDORA_DATASTREAM.equals(mixin)){
final MediaType contentType =
Expand Down
Expand Up @@ -90,13 +90,14 @@ public void testIngestAndMint() throws RepositoryException {
}

@Test
public void testModify() throws RepositoryException {
public void testModify() throws RepositoryException, IOException {
final String pid = "testObject";
final Response actual = testObj.modifyObject(createPathList(pid));
final Response actual = testObj.modifyObject(createPathList(pid), null);
assertNotNull(actual);
assertEquals(Status.CREATED.getStatusCode(), actual.getStatus());
assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(), actual.getStatus());
// this verify will fail when modify is actually implemented, thus encouraging the unit test to be updated appropriately.
verifyNoMoreInteractions(mockObjects);
// HA!
// verifyNoMoreInteractions(mockObjects);
verify(mockSession).save();
}

Expand Down
Expand Up @@ -93,6 +93,21 @@ public void testGetDatastreamInXML() throws Exception {
logger.debug("Retrieved datastream profile:\n" + content);
}

@Test
public void testGetObjectGraph() throws Exception {
client.execute(postObjMethod("FedoraDescribeTestGraph"));
final HttpGet getObjMethod =
new HttpGet(serverAddress + "objects/FedoraDescribeTestGraph/fcr:graph");
getObjMethod.addHeader("Accept", "application/n-triples");
final HttpResponse response = client.execute(getObjMethod);
assertEquals(200, response.getStatusLine().getStatusCode());
final String content = EntityUtils.toString(response.getEntity());
logger.debug("Retrieved object graph:\n" + content);


}




}
20 changes: 20 additions & 0 deletions fcrepo-kernel/pom.xml
Expand Up @@ -49,6 +49,24 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.apache.jena</groupId>
<artifactId>apache-jena-libs</artifactId>
<type>pom</type>
<version>2.10.0</version>

<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Logging: we'll use LogBack (which implements the SLF4J API); ModeShape
knows what to do. -->
<dependency>
Expand All @@ -75,6 +93,8 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>


</dependencies>

<build>
Expand Down