[Project] Added CRUD Book tutorial part

This commit is contained in:
Robert von Burg 2017-06-01 12:41:55 +02:00
parent 8cec401ec0
commit 8ef9e3ac92
1 changed files with 526 additions and 3 deletions

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="google-site-verification" content="CPhbjooaiTdROm7Vs4E7kuHZvBfkeLUtonGgcVUbTL8" />
<meta name="google-site-verification" content="CPhbjooaiTdROm7Vs4E7kuHZvBfkeLUtonGgcVUbTL8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
@ -53,7 +53,530 @@
<div class="content">
<a href="tutorial-model.html" class="pull-left">Previous: Model</a> <br><br>
<p>Coming soon...</p>
<h3>Preparation</h3>
<p>Since Books are central to the bookshop, we'll first create the <a target="_blank"
href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a>
REST API for them. The API will be as follows:</p>
<pre>
GET ../rest/books?query=,offset=,limit=
GET ../rest/books/{id}
POST ../rest/books
PUT ../rest/books/{id}
DELETE ../rest/books/{id}
</pre>
<p>Thus corresponding with querying, getting, creating, updating and removing of books. So let's go ahead and
add these REST APIs to our project.</p>
<p>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:</p>
<pre class="pre-scrollable">
@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");
}
}
}
</pre>
<p>As we add new resources they will be automatically since we register the entire package.</p>
<p>Now add the books resource class:</p>
<pre>
@Path("books")
public class BooksResource {
}
</pre>
<h3>Query</h3>
<p>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:</p>
<pre>
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response query(@Context HttpServletRequest request, @QueryParam("query") String queryS,
@QueryParam("offset") String offsetS, @QueryParam("limit") String limitS) {
// TODO
}
</pre>
<p>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:</p>
<pre>
public class BookShopConstants {
public static final String TYPE_BOOK = "Book";
}
</pre>
<p>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.</p>
<p>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
<code>ResourceQuery</code>. Since the query is for Resources of type Book, we will define this using a
navigation. Thus the resulting query looks as follows:</p>
<pre>
public class BooksQuery&lt;U&gt; extends ResourceQuery&lt;U&gt; {
public BooksQuery() {
super(new StrolchTypeNavigation(BookShopConstants.TYPE_BOOK));
}
}
</pre>
<p>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 <code>PrivilegeRoles.xml</code> file as follows:</p>
<pre>
...
&lt;Role name="User"&gt;
&lt;Privilege name="li.strolch.model.query.StrolchQuery" policy="DefaultPrivilege"&gt;
&lt;Allow&gt;internal&lt;/Allow&gt;
&lt;Allow&gt;li.strolch.bookshop.query.BooksQuery&lt;/Allow&gt;
&lt;/Privilege&gt;
&lt;/Role&gt;
...
</pre>
<p><b>Note:</b> The <code>internal</code> 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.
</p>
<p>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:</p>
<pre class="pre-scrollable">
@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&lt;JsonObject&gt; query = new BooksQuery&lt;JsonObject&gt;() //
// 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&lt;JsonObject&gt; books = tx.doQuery(query);
// perform paging
Paging&lt;JsonObject&gt; page = Paging.asPage(books, offset, limit);
// return result
return ResponseUtil.toResponse(StrolchRestfulConstants.DATA, page.getPage());
}
}
}
</pre>
<p><b>Note:</b> We automatically transform the Resource objects to JSON using the <code>StrolchElementToJsonVisitor</code>.
By calling the method <code>.flat()</code> we have a more compact JSON format. Paging is handled by a util
class.</p>
<p>As a rule we use the format where we return two fields: <code>msg</code> is a dash if all is ok, otherwise an
error message will be present. Data is always in the <code>data</code> field. This is just a personal taste,
and can be changed to one't taste.</p>
<h3>Get</h3>
We have all we need now to implement the GET method:
<pre class="pre-scrollable">
@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);
}
}
</pre>
<p>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 <code>StrolchTransaction</code>. There are methods to retrieve elements, and also
perform queries. We will use more of these methods later.</p>
<p>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.</p>
<h3>Create</h3>
To create a new book we need to implement a <code>Service</code>. This service will be called <code>CreateBookService</code>.
A Service always has a <code>ServiceArgument</code> and a <code>ServiceResult</code>. Our service will use the
<code>JsonServiceArgument</code> and the <code>JsonServiceResult</code>. The implementation of the POST method
is as follows:
<pre class="pre-scrollable">
@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);
}
</pre>
<p><b>Note:</b> We return the created object again as JSON in its own data field.</p>
The service is implemented as follows:
<pre class="pre-scrollable">
public class CreateBookService 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 {
// 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);
}
}
</pre>
<p><b>Note:</b> For the authenticated user to be able to perform this service, we must add it to their
privileges:</p>
<pre>
...
&lt;Role name="User"&gt;
...
&lt;Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"&gt;
&lt;Allow&gt;li.strolch.bookshop.service.CreateBookService&lt;/Allow&gt;
&lt;/Privilege&gt;
...
&lt;/Role&gt;
...
</pre>
<h3>Update</h3>
<p>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.</p>
<p><b>PUT Method:</b></p>
<pre class="pre-scrollable">
@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);
}
</pre>
<p><b>Update Service:</b></p>
<pre class="pre-scrollable">
public class UpdateBookService extends AbstractService&lt;JsonServiceArgument, JsonServiceResult&gt; {
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);
}
}
</pre>
<p><b>Privilege:</b></p>
<pre>
...
&lt;Role name="User"&gt;
...
&lt;Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"&gt;
...
&lt;Allow&gt;li.strolch.bookshop.service.UpdateBookService&lt;/Allow&gt;
...
&lt;/Privilege&gt;
...
&lt;/Role&gt;
...
</pre>
<h3>Remove</h3>
<p>To remove a book, we need a DELETE method, a remove service and the associated privilege.</p>
<p><b>DELETE Method:</b></p>
<pre class="pre-scrollable">
@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);
}
</pre>
<p><b>Remove Service:</b></p>
<pre class="pre-scrollable">
public class RemoveBookService extends AbstractService&lt;StringServiceArgument, ServiceResult&gt; {
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();
}
}
</pre>
<p><b>Privilege:</b></p>
<pre>
...
&lt;Role name="User"&gt;
...
&lt;Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"&gt;
...
&lt;Allow&gt;li.strolch.bookshop.service.RemoveBookService&lt;/Allow&gt;
...
&lt;/Privilege&gt;
...
&lt;/Role&gt;
...
</pre>
<h3>Notes:</h3>
<p>One should now see a pattern emerge:</p>
<ul>
<li>The REST API delegates to the Services, or queries, with the exception of the retrieval of a single
object by id.
</li>
<li>Services should do initial validation of the input. Not much validation was done here, but more could be
done.
</li>
<li>Commands are reusable objects to perform recurring work.</li>
<li>Queries and Services are privileged actions for which a user must have the privilege to perform the
action.
</li>
</ul>
<p>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.</p>
<p>This concludes the CRUD of books.</p>
<a href="tutorial-model.html" class="pull-left">Previous: Model</a>
<!-- content here -->
@ -95,7 +618,7 @@
s.parentNode.insertBefore(g, s);
})();
</script>
<noscript><p><img src="http://piwik.eitchnet.ch/piwik.php?idsite=2" style="border:0;" alt="" /></p></noscript>
<noscript><p><img src="http://piwik.eitchnet.ch/piwik.php?idsite=2" style="border:0;" alt=""/></p></noscript>
<!-- End Piwik Code -->
</body>