From f51d5ae81f2423b733023369685fe93cd277d110 Mon Sep 17 00:00:00 2001
From: Robert von Burg Your project is now ready to be imported into your favourite IDE. We have used both IntelliJ and Eclipse so
@@ -669,55 +776,59 @@
Now configure your IDE to start the web project, and then once it has started, you should see the following
in the logs: This log tells us the name of the app as defined in the StrolchConfiguration.xml file as well as which
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 89978c1a6..e8b257fcf 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 @@
Since Books are central to the bookshop, we'll first create the CRUD
- REST API for them. The API will be as follows:PrivilegeRoles.xml
there seems to be a lot of boilerplate. One thing about a highly
+ configurable system is that sometimes the configuration is bigger. In this case we have opted to have
+ the configuration shown and not use default values which you don't see, so that privilege acces is
+ clearly seen.
+
package li.strolch.bookshop.web;
-import java.io.InputStream;
-
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.io.InputStream;
import li.strolch.agent.api.StrolchAgent;
import li.strolch.agent.api.StrolchBootstrapper;
+import li.strolch.utils.helper.StringHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
@WebListener
public class StartupListener implements ServletContextListener {
private static final Logger logger = LoggerFactory.getLogger(StartupListener.class);
+ private static final String APP_NAME = "Bookshop";
private StrolchAgent agent;
@Override
public void contextInitialized(ServletContextEvent sce) {
- logger.info("Starting Bookshop...");
+ logger.info("Starting " + APP_NAME + "...");
+ long start = System.currentTimeMillis();
try {
- // we load the configuration by reading the boot strap file:
String boostrapFileName = "/WEB-INF/" + StrolchBootstrapper.FILE_BOOTSTRAP;
InputStream bootstrapFile = sce.getServletContext().getResourceAsStream(boostrapFileName);
StrolchBootstrapper bootstrapper = new StrolchBootstrapper(StartupListener.class);
-
- // now setup, initialize and start Strolch:
this.agent = bootstrapper.setupByBoostrapFile(StartupListener.class, bootstrapFile);
this.agent.initialize();
this.agent.start();
-
- } catch (Exception e) {
- logger.error("Failed to start Bookshop due to: " + e.getMessage(), e);
+ } catch (Throwable e) {
+ logger.error("Failed to start " + APP_NAME + " due to: " + e.getMessage(), e);
throw e;
}
- logger.info("Started Bookshop.");
+ long took = System.currentTimeMillis() - start;
+ logger.info("Started " + APP_NAME + " in " + (StringHelper.formatMillisecondsDuration(took)));
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (this.agent != null) {
- this.agent.stop();
- this.agent.destroy();
+ logger.info("Destroying " + APP_NAME + "...");
+ try {
+ this.agent.stop();
+ this.agent.destroy();
+ } catch (Throwable e) {
+ logger.error("Failed to stop " + APP_NAME + " due to: " + e.getMessage(), e);
+ throw e;
+ }
}
- logger.info("Destroyed Bookshop.");
+ logger.info("Destroyed " + APP_NAME);
}
}
@@ -725,7 +836,7 @@ public class StartupListener implements ServletContextListener {
-Bookshop:dev All 8 Strolch Components started. Strolch is now ready to be used. Have fun =))
+Bookshop:dev All 8 Strolch Components started. Took 44ms. Strolch is now ready to be used. Have fun =))
GET ../rest/books?query=,offset=,limit= @@ -68,10 +68,10 @@ 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.
+ 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:
+ configure JAX-RS. Thus create the following class:@ApplicationPath("rest") public class RestfulApplication extends ResourceConfig { @@ -119,10 +119,10 @@ public class BooksResource { }-
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:
+The first service we'll add is to query, or search for the existing books. The API defines three parameters, + with which the result can be controlled. The method can be defined as follows:
@GET @@ -135,7 +135,7 @@ public Response query(@Context HttpServletRequest request, @QueryParam("query")
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:
+ which we used in the model file:public class BookShopConstants { @@ -144,44 +144,64 @@ public class BookShopConstants { }-
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.
+As this tutorial progresses, 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:
In Strolch there are multiple way to access objects. The old way was using Queries, the new search API is
+ much more fluent and easier to read and write. The search API, as well as the deprecated query API allows us
+ to implement privilege validation and thus one should create corresponding classes for each type of search.
+ Book entities are Resources, thus we will be creating a ResourceSearch
. The search is for
+ Resources of type Book thus the resulting search looks as follows:
-public class BooksQuery<U> extends ResourceQuery<U> { - public BooksQuery() { - super(new StrolchTypeNavigation(BookShopConstants.TYPE_BOOK)); +public class BooksSearch<U> extends ResourceSearch<U> { + public BookSearch() { + types(TYPE_BOOK); + } + + public BookSearch stringQuery(String value) { + if (isEmpty(value)) + return this; + + // split by spaces + value = value.trim(); + String[] values = value.split(" "); + + // add where clauses for id, name and description + where(id().containsIgnoreCase(values) // + .or(name().containsIgnoreCase(values)) // + .or(param(BAG_PARAMETERS, PARAM_DESCRIPTION).containsIgnoreCase(values))); + + return this; } }+
Note how we added a special method stringQuery(String)
- this method defines where a search
+ string entered by the user will be used to match a book. In this case for ID
, name
+ and the description
parameter.
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:
PrivilegeRoles.xml
file as follows:
... <Role name="User"> - <Privilege name="li.strolch.model.query.StrolchQuery" policy="DefaultPrivilege"> + <Privilege name="li.strolch.search.StrolchSearch" policy="DefaultPrivilege"> <Allow>internal</Allow> - <Allow>li.strolch.bookshop.query.BooksQuery</Allow> + <Allow>li.strolch.bookshop.search.BookSearch</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:
+Now we have all parts we need to implement the query method. The method will include opening a transaction, + instantiating the search, executing the search, and returning the result:
@Path("books") public class BooksResource { @@ -198,46 +218,31 @@ public class BooksResource { int limit = StringHelper.isNotEmpty(limitS) ? Integer.valueOf(limitS) : 0; // open the TX with the certificate, using this class as context + Paging<Resource> paging; try (StrolchTransaction tx = RestfulStrolchComponent.getInstance().openTx(cert, getClass())) { - // prepare the query - ResourceQuery<Resource> query = new BooksQuery<JsonObject>(); - - // 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<Resource> books = tx.doQuery(query); - - // perform paging - Paging<Resource> paging = Paging.asPage(books, offset, limit); - List<Resource> page = paging.getPage(); - - // return result - ResourceVisitor<JsonObject> visitor = new StrolchRootElementToJsonVisitor().flat().asResourceVisitor(); - return ResponseUtil.listToResponse(StrolchRestfulConstants.DATA, page, a -> a.accept(visitor)); + // perform a book search + paging = new BookSearch() // + .stringQuery(queryS) // + .search(tx) // + .orderByName(false) // + .toPaging(offset, limit); } + + ResourceVisitor<JsonObject> visitor = new StrolchRootElementToJsonVisitor().flat().asResourceVisitor(); + return ResponseUtil.toResponse(paging, e -> e.accept(visitor)); } }
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.
.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.
The helper class ResponseUtil
takes care of creating the JsonObject and the proper page. 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's own taste.
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.
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.
+ authenticated, which is enough for the moment.-public class CreateBookService extends AbstractService{ +public class CreateBookService extends AbstractService<JsonServiceArgument, JsonServiceResult> { private static final long serialVersionUID = 1L; @Override @@ -358,7 +363,7 @@ public class CreateBookService extends AbstractService Note: For the authenticated user to be able to perform this service, we must add it to their - privileges:
+ privileges:... <Role name="User"> @@ -374,7 +379,7 @@ public class CreateBookService extends AbstractServiceUpdate 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.
+ give the user the privilege.PUT Method:
@@ -556,21 +561,22 @@ public class RemoveBookService extends AbstractService<StringServiceArgument,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 +
- The REST API delegates to the Services, or Searches, 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 +
- Searches 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.
+ 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 (i.e. NOT in the REST + API).This concludes the CRUD of books.
@@ -614,7 +620,7 @@ public class RemoveBookService extends AbstractService<StringServiceArgument, s.parentNode.insertBefore(g, s); })(); - +