API design prototype

134 views
Skip to first unread message

bill mckinney

unread,
May 29, 2013, 10:55:03 AM5/29/13
to dspac...@googlegroups.com

Hi all,

Reinhard and I have made some progress on a prototype that we hope follows some of the current best practices for REST API design (in particular, "intent driven design").

You can mess around with the prototype here: http://50.116.59.51:8080/api/docs/v2/index.html 

Hopefully the documentation should be pretty clear -- our hope was to make it very easy to try out the API through this interactive/self-documenting UI (courtesy of swagger).

Features:

  • produces xml, json and jsonp (if a callback parameter is included)
  • you can specify what fields you want it to return
  • paging support built in using start and limit parameters
  • sort results using sort and dir parameters
  • locate works using DSpace Item IDs or handles
  • add search constraints using Solr queries

Behind the scenes we're currently using Jersey + Solr for these read-only endpoints. While we're more focused on the interface of the API right now, using SOLR under the hood has the advantage of making it nice and fast!

Here's an example of a javascript widget using the API (on real DASH data): http://50.116.59.51:8080/api/widgets/paging.html

Below is our initial stab at a schema. Is it missing anything critical? Is it too verbose? Is it simple and clear enough to a developer who knows nothing about DSpace and its database/table terminology? If we can agree on a common schema and REST behavior, theoretically institutions could implement a DSpace REST API in any framework they see fit. In any case, it would be nice to have the desired API interface drive implementation decisions rather than the other way around. 

Very much looking forward to your feedback!

"models":{
        "Community":{
            "id":"Community",
            "properties":{
                "id":{
                    "type":"int"
                },
                "description":{
                    "type":"string"
                },
                "name":{
                    "type":"string"
                },
                "url":{
                    "type":"string"
                }
            }
        },
        "Collection":{
            "id":"Collection",
            "properties":{
                "id":{
                    "type":"int"
                },
                "owningCollection":{
                    "type":"boolean"
                },
                "description":{
                    "type":"string"
                },
                "name":{
                    "type":"string"
                },
                "url":{
                    "type":"string"
                }
            }
        },
        "File":{
            "id":"File",
            "properties":{
                "id":{
                    "type":"int"
                },
                "name":{
                    "type":"string"
                },
                "format":{
                    "type":"string"
                },
                "url":{
                    "type":"string"
                },
                "size":{
                    "type":"long"
                }
            }
        },
        "Author":{
            "id":"author",
            "properties":{
                "id":{
                    "type":"string"
                },
                "authority":{
                    "type":"string"
                },
                "orcid":{
                    "type":"string"
                },
                "givenNames":{
                    "type":"string"
                },
                "email":{
                    "type":"string"
                },
                "name":{
                    "type":"string"
                },
                "surname":{
                    "type":"string"
                },
                "affiliation":{
                    "type":"string"
                },
                "type":{
                    "type":"string"
                },
                "href":{
                    "type":"string"
                }
            }
        },
        "Works":{
            "id":"Works",
            "properties":{
                "communities":{
                    "items":{
                        "$ref":"Community"
                    },
                    "type":"Array"
                },
                "handle":{
                    "type":"string"
                },
                "depositingAuthor":{
                    "type":"string"
                },
                "issn":{
                    "items":{
                        "type":"string"
                    },
                    "type":"Array"
                },
                "subjects":{
                    "items":{
                        "type":"string"
                    },
                    "type":"Array"
                },
                "type":{
                    "type":"string"
                },
                "lang":{
                    "type":"string"
                },
                "version":{
                    "type":"string"
                },
                "publisher":{
                    "type":"string"
                },
                "journal":{
                    "type":"string"
                },
                "id":{
                    "type":"int"
                },
                "authors":{
                    "items":{
                        "$ref":"author"
                    },
                    "type":"Array"
                },
                "title":{
                    "type":"string"
                },
                "otherVersions":{
                    "items":{
                        "type":"string"
                    },
                    "type":"Array"
                },
                "isbn":{
                    "type":"string"
                },
                "pubYear":{
                    "type":"string"
                },
                "license":{
                    "type":"string"
                },
                "href":{
                    "type":"string"
                },
                "publisherUrl":{
                    "type":"string"
                },
                "collections":{
                    "items":{
                        "$ref":"Collection"
                    },
                    "type":"Array"
                },
                "abstract":{
                    "type":"string"
                },
                "citation":{
                    "type":"string"
                },
                "files":{
                    "items":{
                        "$ref":"File"
                    },
                    "type":"Array"
                },
                "lastModified":{
                    "type":"string"
                },
                "url":{
                    "type":"string"
                },
                "dateAccessioned":{
                    "type":"string"
                },
                "school":{
                    "type":"string"
                },
                "dateAvailable":{
                    "type":"string"
                },
                "doi":{
                    "type":"string"
                }
            }
        }
    }


 


Richard Rodgers

unread,
May 29, 2013, 11:15:57 AM5/29/13
to bill mckinney, dspac...@googlegroups.com
Hi Bill:

Looks really cool - I want to give it a closer look. I'm just out the door for vacation, so it won't be for a week or two:
so don't interpret as lack of interest.

Thanks,

Richard
--
You received this message because you are subscribed to the Google Groups "DSpace REST" group.
To unsubscribe from this group and stop receiving emails from it, send an email to dspace-rest...@googlegroups.com.
Visit this group at http://groups.google.com/group/dspace-rest?hl=en.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Peter Dietz

unread,
May 29, 2013, 11:42:07 AM5/29/13
to dspac...@googlegroups.com

Hi Bill,

Very neat stuff here. I'm glad to see it. The swagger page looks useful. 

Here's some random notes I have from looking at your project.


I'm not sure how swagger gets generated, but from reading: https://github.com/wordnik/swagger-core/wiki/java-jax-rs

It looks like you add swagger to your pom.xml, add some configurations, it scans your resources in your API, and it generates this pretty UI for listing your resources, their properties/methods, and documentation?


I like the _metadata portion of the response. 

"_metadata": {
   
"offset": 0,
   
"limit": 10,
   
"totalCount": 1
 
}


I'm still wrapping my head around what HATEOAS means, and if its a buzz-word, or if it should actually be implemented. 


If we're talking being Intent-Driven, then I'm guessing thats the distillation of the use cases. 

  • I want to fetch a resource from /items or /works, to be able to display all information about that "work", including the metadata I want to display, the authors (formatted according to norms), and provide a URL to the bitstreams / "files".


Collection.properties.owningCollection is type Boolean, I don't know what that means. 

Do we want to expose a route to follow the hierarchy/relationships between objects?

I guess we don't want to get stuck with DSpace's logic, but Collection has owningCommunity, and recursively upwards, it can have many predecessor Communities. Would we want to expose Community ID's or CommunityObjects.


Bitstream has been renamed to File. Thats fine with me. Would we want to have an endpoint for /bitstreams? or to funnel that through /files?


We don't have "smart" Author objects within our DSpace. I wish we did something better. On campus we have "Research in View" (formerly OSU:PRO)  in which academics list their publications, and we have an API for that data, so perhaps we could marry the two together, but for now authors are text in either dc.creator… or dc.contributor… 


Works looks like the "Intended Use" of Item. It has a depositingAuthor String property. Would we want to stick with text such as depositingAuthorFormatted which displays the authors name in an expected format, and then to have depositingAuthor as an AuthorObject? Ohh, but then I notice that there is an authors array of AuthorObjects. 


I'm not sure what versions are, but I'm guessing if you can support multiple versions of the same Work/Item, then you can provide some alternates. 


I'm not sure what "school" is for a work (we don't use that metadata). Is school an attribute of an author, is it the University where this work was created, is it an academic department within the university. After trying out a request with swagger, one value says school="Anthropology", so that must be the Academic Department / Subject.

bill mckinney

unread,
May 29, 2013, 12:39:50 PM5/29/13
to dspac...@googlegroups.com
Thanks Peter, really useful feedback!

1. Terminology

One user persona I try to keep in mind is someone like a Drupal developer at the law school who wants to embed a DSpace listing on their website. He or she will probably not know or care about DSpace terminology so it could make a lot of sense to translate it into plain english (e.g., bitstream = file). I think some of my attempts at this failed (e.g., sponsorship = school). And maybe some properties I included are completely unnecessary (e.g., owningCollection). A typical API user probably has no need to know which of several collections is the owning collection for an item. Version was meant to indicate if the deposited file was a manuscript or published pdf, etc. Again, some more explicit label is probably needed.

2. Authors

I completely agree with you on this. We have additional author info for Harvard affiliated authors via authority control in DSpace. I really like your idea about being able to augment with external APIs. We have a "Faculty Finder" API that could help us. Other institutions probably have an equivalent.

3. Swagger Generator

You're right, annotations are used to generate this. I haven't found a really good standalone generator and have hacked this project for my purposes in the meantime: https://github.com/aharwood/swagger-standalone-generator

Here's what the works resource looks like, along with swagger annotations (I promise to get this up on github sometime soon):

package edu.harvard.dash.jersey.resources;

import com.sun.jersey.api.json.JSONWithPadding;
import com.wordnik.swagger.annotations.*;
import edu.harvard.dash.jersey.exceptions.RepoException;
import edu.harvard.dash.jersey.pojos.WorksWrapper;
import edu.harvard.dash.jersey.providers.SolrWorksProvider;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/works")
@Api(value = "/works", description = "Operations about works")

public class WorkResource {

    @GET
    @Path("/")
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, "application/x-javascript" })
    @ApiOperation(value = "Fetch or search all works",
            notes = "This implementation returns all published works in all collections.",
            responseClass = "edu.harvard.dash.jersey.pojos.Works")
    @ApiErrors(value = {
            @ApiError(code = 404, reason = "Works not found")
    })
    public Response findAllWorks(

            @ApiParam(value = "Search query using Solr syntax (e.g., 'natural language processing AND type:Article'). ",
                    required = false)
            @DefaultValue("*:*") @QueryParam("q") String q,

            @ApiParam(value = "Comma separated list of fields to display (e.g., 'id,title,authors'). " +
                    "Skip this parameter to get all fields in the response.", required = false)
            @DefaultValue("") @QueryParam("fields") String fields,

            @ApiParam(value = "Field to sort on.", required = false)
            @DefaultValue("") @QueryParam("sort") String sort,

            @ApiParam(value = "Direction to sort: asc or desc (default).", required = false)
            @DefaultValue("desc") @QueryParam("dir") String dir,

            @ApiParam(value = "For paging, the result to start at (zero-based).", required = false)
            @DefaultValue("0") @QueryParam("start") int start,

            @ApiParam(value = "For paging, the number of results to show per page.", required = false)
            @DefaultValue("10") @QueryParam("limit") int limit,

            @ApiParam(value = "For JSONP, the name of the callback function.", required = false)
            @QueryParam("callback") @DefaultValue("fn") String callback)

    {

        SolrWorksProvider provider = new SolrWorksProvider(fields,start,limit,sort,dir);
        WorksWrapper ww = provider.getAllWorks(q);
        if (ww.getStatus() != 200) {
            throw new RepoException(ww.getStatus(),
                    "{\"errorCode\": " + ww.getStatus() + ", \"errorMessage\": \"" + ww.getStatusMessage() +"\"}");
        }
        Response response = Response.status(404).build();
        if (ww != null) {
            if (!callback.equalsIgnoreCase("fn")) {
                response = Response.status(200).entity(new JSONWithPadding(ww, callback)).type("application/x-javascript").build();
            } else {
                response = Response.status(200).entity(new JSONWithPadding(ww, callback)).build();
            }
        }
        return response;
    }


    @GET
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, "application/x-javascript" })
    @ApiOperation(value = "Fetch works by id",
            notes = "This implementation uses DSpace's database ids as the work identifier.",
            responseClass = "edu.harvard.dash.jersey.pojos.Works")
    @ApiErrors(value = {
            @ApiError(code = 400, reason = "Invalid id supplied"),
            @ApiError(code = 404, reason = "Works not found")
    })
    public Response findById(

            @ApiParam(value = "The work's id. Use 2 for testing.", required = true)
            @PathParam("id") String id,

            @ApiParam(value = "Comma separated list of fields to display (e.g., 'id,title,authors'). " +
                    "Skip this parameter to get all fields in the response.", required = false)
            @DefaultValue("") @QueryParam("fields") String fields,

            @ApiParam(value = "For JSONP, the name of the callback function. Default = 'fn'", required = false)
            @QueryParam("callback") @DefaultValue("fn") String callback)


    {

        SolrWorksProvider provider = new SolrWorksProvider(fields);
        WorksWrapper ww = provider.getWorksById(id);
        if (ww.getStatus() != 200) {
            throw new RepoException(ww.getStatus(),
                    "{\"errorCode\": " + ww.getStatus() + ", \"errorMessage\": \"" + ww.getStatusMessage() +"\"}");
        }
        Response response = Response.status(404).build();
        if (ww != null) {
            if (!callback.equalsIgnoreCase("fn")) {
                response = Response.status(200).entity(new JSONWithPadding(ww, callback)).type("application/x-javascript").build();
            } else {
                response = Response.status(200).entity(new JSONWithPadding(ww, callback)).build();
            }
        }
        return response;

    }

    @GET
    @Path("/{prefix}/{handle}")
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @ApiOperation(value = "Fetch works by handle",
            notes = "This implementation uses DSpace handles as the work identifier.",
            responseClass = "edu.harvard.dash.jersey.pojos.Works")
    @ApiErrors(value = {
            @ApiError(code = 400, reason = "Invalid prefix and/or handle supplied"),
            @ApiError(code = 404, reason = "Works not found")
    })
    public Response findByHandle(

            @ApiParam(value = "The handle's prefix. Use 1 for testing. A wildcard '*' is allowed.", required = true)
            @PathParam("prefix") String prefix,

            @ApiParam(value = "The handle. Use 3 for testing. A wildcard '*' is allowed.", required = true)
            @PathParam("handle") String handle,

            @ApiParam(value = "Comma separated list of fields to display (e.g., 'id,title,authors'). " +
                    "Skip this parameter to get all fields in the response.", required = false)
            @DefaultValue("") @QueryParam("fields") String fields,

            @ApiParam(value = "For paging, the result to start at (zero-based).", required = false)
            @DefaultValue("0") @QueryParam("start") int start,

            @ApiParam(value = "For paging, the number of results to show per page.", required = false)
            @DefaultValue("10") @QueryParam("limit") int limit,

            @ApiParam(value = "For JSONP, the name of the callback function. Default = 'fn'", required = false)
            @QueryParam("callback") @DefaultValue("fn") String callback)

    {

        SolrWorksProvider provider = new SolrWorksProvider(fields,start,limit);
        WorksWrapper ww = provider.getWorksByHandle(prefix, handle);
        if (ww.getStatus() != 200) {
            throw new RepoException(ww.getStatus(),
                "{\"errorCode\": " + ww.getStatus() + ", \"errorMessage\": \"" + ww.getStatusMessage() +"\"}");
        }
        Response response = Response.status(404).build();
        if (ww != null) {
            if (!callback.equalsIgnoreCase("fn")) {
                response = Response.status(200).entity(new JSONWithPadding(ww, callback)).type("application/x-javascript").build();
            } else {
                response = Response.status(200).entity(new JSONWithPadding(ww, callback)).build();
            }
        }
        return response;

    }

}


Reply all
Reply to author
Forward
0 new messages