-
Coming soon...
+Preparation
+ +Since Books are central to the bookshop, we'll first create the CRUD + REST API for them. The API will be as follows:
+ ++GET ../rest/books?query=,offset=,limit= +GET ../rest/books/{id} +POST ../rest/books +PUT ../rest/books/{id} +DELETE ../rest/books/{id} ++ +
Thus corresponding with querying, getting, creating, updating and removing of books. So let's go ahead and + add these REST APIs to our project.
+ +Our project is using JAX-RS 2.0 as the API and Jersey 2.x as the implementation, thus first we need to + configure JAX-RS. Thus create the following class:
++@ApplicationPath("rest") +public class RestfulApplication extends ResourceConfig { + + public RestfulApplication() { + + // add strolch resources + register(AuthenticationService.class); + + // add project resources by package name + packages(BooksResource.class.getPackage().getName()); + + // filters + register(AuthenticationRequestFilter.class, Priorities.AUTHENTICATION); + register(AccessControlResponseFilter.class); + register(AuthenticationResponseFilter.class); + register(HttpCacheResponseFilter.class); + + // log exceptions and return them as plain text to the caller + register(StrolchRestfulExceptionMapper.class); + + // the JSON generated is in UTF-8 + register(CharsetResponseFilter.class); + + RestfulStrolchComponent restfulComponent = RestfulStrolchComponent.getInstance(); + if (restfulComponent.isRestLogging()) { + register(new LoggingFeature(java.util.logging.Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), + Level.SEVERE, LoggingFeature.Verbosity.PAYLOAD_ANY, Integer.MAX_VALUE)); + + property(ServerProperties.TRACING, "ALL"); + property(ServerProperties.TRACING_THRESHOLD, "TRACE"); + } + } +} ++
As we add new resources they will be automatically since we register the entire package.
+ +Now add the books resource class:
++@Path("books") +public class BooksResource { + +} ++ +
Query
+ +The first service we'll add is to query the existing books. The API defines three parameters, with which the + result can be controlled. The method can be defined as follows:
+ ++@GET +@Produces(MediaType.APPLICATION_JSON) +public Response query(@Context HttpServletRequest request, @QueryParam("query") String queryS, + @QueryParam("offset") String offsetS, @QueryParam("limit") String limitS) { + + // TODO +} ++ +
To fill this method we need a few things. First let's define a constants class where we keep String constants + which we used in the model file:
++public class BookShopConstants { + + public static final String TYPE_BOOK = "Book"; + +} ++ +
As this tutorial progesses, more and more constants will be added here. This class helps with two issues: + Through the constants we can easily reason over where certain fields, and types are used and of course + String literals in code are a rather bad thing.
+ +Queries in Strolch are their own objects, which allows us to implement privilege validation and thus we need
+ to create this class as well. Book entities are Resources, thus we will be creating a
+ ResourceQuery
. Since the query is for Resources of type Book, we will define this using a
+ navigation. Thus the resulting query looks as follows:
+public class BooksQuery<U> extends ResourceQuery<U> { + public BooksQuery() { + super(new StrolchTypeNavigation(BookShopConstants.TYPE_BOOK)); + } +} ++ +
So that our users can call this query, we must give them this as a privilege. This is done by adding the full
+ class name to the PrivilegeRoles.xml
file as follows:
+... + <Role name="User"> + <Privilege name="li.strolch.model.query.StrolchQuery" policy="DefaultPrivilege"> + <Allow>internal</Allow> + <Allow>li.strolch.bookshop.query.BooksQuery</Allow> + </Privilege> + </Role> +... ++ +
Note: The internal
allow value is a special privilege which is used internally when a
+ service or something performs internal queries. This means that a service can perform a query for object to
+ which the user might not have access, but without which the service could not be completed. We will use this
+ in a later stage.
+
Now we all parts we need to implement the query method. The method will include opening a transaction, + instantiating the query, executing the query, and returning the result:
++@Path("books") +public class BooksResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response query(@Context HttpServletRequest request, @QueryParam("query") String queryS, + @QueryParam("offset") String offsetS, @QueryParam("limit") String limitS) { + + // this is an authenticated method call, thus we can get the certificate from the request: + Certificate cert = (Certificate) request.getAttribute(StrolchRestfulConstants.STROLCH_CERTIFICATE); + + int offset = StringHelper.isNotEmpty(offsetS) ? Integer.valueOf(offsetS) : 0; + int limit = StringHelper.isNotEmpty(limitS) ? Integer.valueOf(limitS) : 0; + + // open the TX with the certificate, using this class as context + try (StrolchTransaction tx = RestfulStrolchComponent.getInstance().openTx(cert, getClass())) { + + // prepare the query + ResourceQuery<JsonObject> query = new BooksQuery<JsonObject>() // + // set transformation to JSON + .setVisitor(new StrolchElementToJsonVisitor().flat()); + + // prepare selections + if (StringHelper.isEmpty(queryS)) { + query.withAny(); + } else { + OrSelection or = new OrSelection(); + or.with(ParameterSelection.stringSelection(BookShopConstants.BAG_PARAMETERS, + BookShopConstants.PARAM_DESCRIPTION, queryS, StringMatchMode.ci())); + or.with(new NameSelection(queryS, StringMatchMode.ci())); + + // add selections + query.with(or); + } + + // perform the query + List<JsonObject> books = tx.doQuery(query); + + // perform paging + Paging<JsonObject> page = Paging.asPage(books, offset, limit); + + // return result + return ResponseUtil.toResponse(StrolchRestfulConstants.DATA, page.getPage()); + } + } +} ++ +
Note: We automatically transform the Resource objects to JSON using the StrolchElementToJsonVisitor
.
+ By calling the method .flat()
we have a more compact JSON format. Paging is handled by a util
+ class.
As a rule we use the format where we return two fields: msg
is a dash if all is ok, otherwise an
+ error message will be present. Data is always in the data
field. This is just a personal taste,
+ and can be changed to one't taste.
Get
+ + We have all we need now to implement the GET method: + ++@GET +@Path("{id}") +@Produces(MediaType.APPLICATION_JSON) +public Response get(@Context HttpServletRequest request, @PathParam("id") String id) { + + // this is an authenticated method call, thus we can get the certificate from the request: + Certificate cert = (Certificate) request.getAttribute(StrolchRestfulConstants.STROLCH_CERTIFICATE); + + // open the TX with the certificate, using this class as context + try (StrolchTransaction tx = RestfulStrolchComponent.getInstance().openTx(cert, getClass())) { + + // get the book + Resource book = tx.getResourceBy(BookShopConstants.TYPE_BOOK, id); + if (book == null) + return ResponseUtil.toResponse(Status.NOT_FOUND, "Book " + id + " does not exist!"); + + // transform to JSON + JsonObject bookJ = book.accept(new StrolchElementToJsonVisitor().flat()); + + // return + return ResponseUtil.toResponse(StrolchRestfulConstants.DATA, bookJ); + } +} ++ +
Note how we simply retrieve the book as a Resource from the TX. This is a good moment to familiarize yourself
+ with the API of the StrolchTransaction
. There are methods to retrieve elements, and also
+ perform queries. We will use more of these methods later.
Further it can be noted that a simple retrieval isn't validated against the user's privileges, the user is + authenticated, which is enough for the moment.
+ +Create
+ + To create a new book we need to implement aService
. This service will be called CreateBookService
.
+ A Service always has a ServiceArgument
and a ServiceResult
. Our service will use the
+ JsonServiceArgument
and the JsonServiceResult
. The implementation of the POST method
+ is as follows:
+
+ +@POST +@Produces(MediaType.APPLICATION_JSON) +public Response create(@Context HttpServletRequest request, String data) { + + // this is an authenticated method call, thus we can get the certificate from the request: + Certificate cert = (Certificate) request.getAttribute(StrolchRestfulConstants.STROLCH_CERTIFICATE); + + // parse data to JSON + JsonObject jsonData = new JsonParser().parse(data).getAsJsonObject(); + + // instantiate the service with the argument + CreateBookService svc = new CreateBookService(); + JsonServiceArgument arg = svc.getArgumentInstance(); + arg.jsonElement = jsonData; + + // perform the service + ServiceHandler serviceHandler = RestfulStrolchComponent.getInstance().getServiceHandler(); + JsonServiceResult result = serviceHandler.doService(cert, svc, arg); + + // return depending on the result state + if (result.isOk()) + return ResponseUtil.toResponse(StrolchRestfulConstants.DATA, result.getResult()); + return ResponseUtil.toResponse(result); +} ++ +
Note: We return the created object again as JSON in its own data field.
+ + The service is implemented as follows: ++public class CreateBookService extends AbstractService+ +{ + private static final long serialVersionUID = 1L; + + @Override + protected JsonServiceResult getResultInstance() { + return new JsonServiceResult(); + } + + @Override + public JsonServiceArgument getArgumentInstance() { + return new JsonServiceArgument(); + } + + @Override + protected JsonServiceResult internalDoService(JsonServiceArgument arg) throws Exception { + + // open a new transaction, using the realm from the argument, or the certificate + Resource book; + try (StrolchTransaction tx = openArgOrUserTx(arg)) { + + // get a new book "instance" from the template + book = tx.getResourceTemplate(BookShopConstants.TYPE_BOOK); + + // map all values from the JSON object into the new book element + new FromFlatJsonVisitor().visit(book, arg.jsonElement.getAsJsonObject()); + + // add command to store the resource + AddResourceCommand cmd = new AddResourceCommand(getContainer(), tx); + cmd.setResource(book); + tx.addCommand(cmd); + + // notify the TX that it should commit on close + tx.commitOnClose(); + } + + // map the return value to JSON + JsonObject result = book.accept(new StrolchElementToJsonVisitor().flat()); + + // and return the result + return new JsonServiceResult(result); + } +} + +
Note: For the authenticated user to be able to perform this service, we must add it to their + privileges:
++... + <Role name="User"> + ... + <Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"> + <Allow>li.strolch.bookshop.service.CreateBookService</Allow> + </Privilege> + ... + </Role> +... ++ +
Update
+ +Updating of a book is basically the same as the creation, we just use PUT, verify that the book exists and + give the user the privilege.
+ +PUT Method:
++@PUT +@Path("{id}") +@Produces(MediaType.APPLICATION_JSON) +public Response update(@Context HttpServletRequest request, @PathParam("id") String id, String data) { + + // this is an authenticated method call, thus we can get the certificate from the request: + Certificate cert = (Certificate) request.getAttribute(StrolchRestfulConstants.STROLCH_CERTIFICATE); + + // parse data to JSON + JsonObject jsonData = new JsonParser().parse(data).getAsJsonObject(); + + // instantiate the service with the argument + UpdateBookService svc = new UpdateBookService(); + JsonServiceArgument arg = svc.getArgumentInstance(); + arg.objectId = id; + arg.jsonElement = jsonData; + + // perform the service + ServiceHandler serviceHandler = RestfulStrolchComponent.getInstance().getServiceHandler(); + JsonServiceResult result = serviceHandler.doService(cert, svc, arg); + + // return depending on the result state + if (result.isOk()) + return ResponseUtil.toResponse(StrolchRestfulConstants.DATA, result.getResult()); + return ResponseUtil.toResponse(result); +} ++ +
Update Service:
++public class UpdateBookService extends AbstractService<JsonServiceArgument, JsonServiceResult> { + + private static final long serialVersionUID = 1L; + + @Override + protected JsonServiceResult getResultInstance() { + return new JsonServiceResult(); + } + + @Override + public JsonServiceArgument getArgumentInstance() { + return new JsonServiceArgument(); + } + + @Override + protected JsonServiceResult internalDoService(JsonServiceArgument arg) throws Exception { + + // verify same book + DBC.PRE.assertEquals("ObjectId and given Id must be same!", arg.objectId, + arg.jsonElement.getAsJsonObject().get(Json.ID).getAsString()); + + // open a new transaction, using the realm from the argument, or the certificate + Resource book; + try (StrolchTransaction tx = openArgOrUserTx(arg)) { + + // get the existing book + book = tx.getResourceBy(BookShopConstants.TYPE_BOOK, arg.objectId, true); + + // map all values from the JSON object into the new book element + new FromFlatJsonVisitor().visit(book, arg.jsonElement.getAsJsonObject()); + + // add command to update the resource + UpdateResourceCommand cmd = new UpdateResourceCommand(getContainer(), tx); + cmd.setResource(book); + tx.addCommand(cmd); + + // notify the TX that it should commit on close + tx.commitOnClose(); + } + + // map the return value to JSON + JsonObject result = book.accept(new StrolchElementToJsonVisitor().flat()); + + // and return the result + return new JsonServiceResult(result); + } +} ++ +
Privilege:
++... + <Role name="User"> + ... + <Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"> + ... + <Allow>li.strolch.bookshop.service.UpdateBookService</Allow> + ... + </Privilege> + ... + </Role> +... ++ + +
Remove
+ +To remove a book, we need a DELETE method, a remove service and the associated privilege.
+ + +DELETE Method:
++@DELETE +@Path("{id}") +@Produces(MediaType.APPLICATION_JSON) +public Response update(@Context HttpServletRequest request, @PathParam("id") String id) { + + // this is an authenticated method call, thus we can get the certificate from the request: + Certificate cert = (Certificate) request.getAttribute(StrolchRestfulConstants.STROLCH_CERTIFICATE); + + // instantiate the service with the argument + RemoveBookService svc = new RemoveBookService(); + StringServiceArgument arg = svc.getArgumentInstance(); + arg.value = id; + + // perform the service + ServiceHandler serviceHandler = RestfulStrolchComponent.getInstance().getServiceHandler(); + ServiceResult result = serviceHandler.doService(cert, svc, arg); + + // return depending on the result state + return ResponseUtil.toResponse(result); +} ++ +
Remove Service:
++public class RemoveBookService extends AbstractService<StringServiceArgument, ServiceResult> { + + private static final long serialVersionUID = 1L; + + @Override + protected ServiceResult getResultInstance() { + return new ServiceResult(); + } + + @Override + public StringServiceArgument getArgumentInstance() { + return new StringServiceArgument(); + } + + @Override + protected ServiceResult internalDoService(StringServiceArgument arg) throws Exception { + + // open a new transaction, using the realm from the argument, or the certificate + try (StrolchTransaction tx = openArgOrUserTx(arg)) { + + // get the existing book + Resource book = tx.getResourceBy(BookShopConstants.TYPE_BOOK, arg.value, true); + + // add command to remove the resource + RemoveResourceCommand cmd = new RemoveResourceCommand(getContainer(), tx); + cmd.setResource(book); + tx.addCommand(cmd); + + // notify the TX that it should commit on close + tx.commitOnClose(); + } + + // and return the result + return ServiceResult.success(); + } +} ++ +
Privilege:
++... + <Role name="User"> + ... + <Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"> + ... + <Allow>li.strolch.bookshop.service.RemoveBookService</Allow> + ... + </Privilege> + ... + </Role> +... ++ +
Notes:
+One should now see a pattern emerge:
+-
+
- The REST API delegates to the Services, or queries, with the exception of the retrieval of a single + object by id. + +
- Services should do initial validation of the input. Not much validation was done here, but more could be + done. + +
- Commands are reusable objects to perform recurring work. +
- Queries and Services are privileged actions for which a user must have the privilege to perform the + action. + +
The book services are quite simple, but as more requirements arise, it should be easy to implement them in + the service layer. Thus should a service be required to be performed by an integration layer, then they can + simply call the services, since the input is defined and validation is done there.
+ +This concludes the CRUD of books.
Previous: Model @@ -95,7 +618,7 @@ s.parentNode.insertBefore(g, s); })(); - +