[Project] Added CRUD Book tutorial part
This commit is contained in:
parent
8cec401ec0
commit
8ef9e3ac92
|
@ -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<U> extends ResourceQuery<U> {
|
||||
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>
|
||||
...
|
||||
<Role name="User">
|
||||
<Privilege name="li.strolch.model.query.StrolchQuery" policy="DefaultPrivilege">
|
||||
<Allow>internal</Allow>
|
||||
<Allow>li.strolch.bookshop.query.BooksQuery</Allow>
|
||||
</Privilege>
|
||||
</Role>
|
||||
...
|
||||
</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<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());
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
...
|
||||
<Role name="User">
|
||||
...
|
||||
<Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege">
|
||||
<Allow>li.strolch.bookshop.service.CreateBookService</Allow>
|
||||
</Privilege>
|
||||
...
|
||||
</Role>
|
||||
...
|
||||
</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<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);
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
<p><b>Privilege:</b></p>
|
||||
<pre>
|
||||
...
|
||||
<Role name="User">
|
||||
...
|
||||
<Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege">
|
||||
...
|
||||
<Allow>li.strolch.bookshop.service.UpdateBookService</Allow>
|
||||
...
|
||||
</Privilege>
|
||||
...
|
||||
</Role>
|
||||
...
|
||||
</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<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();
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
<p><b>Privilege:</b></p>
|
||||
<pre>
|
||||
...
|
||||
<Role name="User">
|
||||
...
|
||||
<Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege">
|
||||
...
|
||||
<Allow>li.strolch.bookshop.service.RemoveBookService</Allow>
|
||||
...
|
||||
</Privilege>
|
||||
...
|
||||
</Role>
|
||||
...
|
||||
</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>
|
||||
|
|
Loading…
Reference in New Issue