diff --git a/li.strolch.website/www.strolch.li/tutorial-crud-book.html b/li.strolch.website/www.strolch.li/tutorial-crud-book.html index c3e69cc07..8dab72502 100644 --- a/li.strolch.website/www.strolch.li/tutorial-crud-book.html +++ b/li.strolch.website/www.strolch.li/tutorial-crud-book.html @@ -3,7 +3,7 @@ - + @@ -53,7 +53,530 @@
Previous: Model

-

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 a Service. 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 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); })(); - +