diff --git a/li.strolch.website/www.strolch.li/tutorial-configuration.html b/li.strolch.website/www.strolch.li/tutorial-configuration.html index 014c9b273..a6405c547 100644 --- a/li.strolch.website/www.strolch.li/tutorial-configuration.html +++ b/li.strolch.website/www.strolch.li/tutorial-configuration.html @@ -276,21 +276,6 @@ <strolch.env>dev.eitchpc</strolch.env> </properties> </profile> - <profile> - <id>m2e.eitchmac</id> - <activation> - <property> - <name>user.name</name> - <value>eitch</value> - </property> - <os> - <family>mac</family> - </os> - </activation> - <properties> - <strolch.env>dev.eitchmac</strolch.env> - </properties> - </profile> </profiles> </project> @@ -344,10 +329,6 @@ <root>/home/eitch/src/git/strolch-bookshop/runtime</root> <environment>dev</environment> </env> - <env id="dev.eitchmac" default="true"> - <root>/Users/eitch/src/git/strolch-bookshop/runtime</root> - <environment>dev</environment> - </env> </StrolchBootstrap> @@ -408,8 +389,47 @@ <Role name="User"> <Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"> </Privilege> - <Privilege name="li.strolch.model.query.StrolchQuery" policy="DefaultPrivilege"> - <Allow>internal</Allow> + + <Privilege name="li.strolch.search.StrolchSearch" policy="DefaultPrivilege"> + <AllAllowed>true</AllAllowed> + <Allow>li.strolch.bookshop.search.BookSearch</Allow> + </Privilege> + + <Privilege name="GetResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="GetOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="GetActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> </Privilege> </Role> <Role name="UserPrivileges"> @@ -424,9 +444,48 @@ <Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"> <AllAllowed>true</AllAllowed> </Privilege> - <Privilege name="li.strolch.model.query.StrolchQuery" policy="DefaultPrivilege"> - <AllAllowed>true</AllAllowed> + + <Privilege name="li.strolch.search.StrolchSearch" policy="DefaultPrivilege"> + <AllAllowed>true</AllAllowed> </Privilege> + + <Privilege name="GetResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="GetOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="GetActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="PrivilegeAddUser" policy="UserAccessPrivilege"> <AllAllowed>true</AllAllowed> </Privilege> @@ -441,12 +500,55 @@ <Allow>li.strolch.runtime.privilege.StrolchSystemActionWithResult</Allow> <Allow>li.strolch.persistence.postgresql.PostgreSqlSchemaInitializer</Allow> </Privilege> + <Privilege name="li.strolch.service.api.Service" policy="DefaultPrivilege"> <AllAllowed>true</AllAllowed> </Privilege> + <Privilege name="li.strolch.model.query.StrolchQuery" policy="DefaultPrivilege"> <AllAllowed>true</AllAllowed> </Privilege> + <Privilege name="li.strolch.search.StrolchSearch" policy="DefaultPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + + <Privilege name="GetResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="GetOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="GetActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="AddActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="UpdateActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveResource" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveOrder" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="RemoveActivity" policy="ModelPrivilege"> + <AllAllowed>true</AllAllowed> + </Privilege> + <Privilege name="PrivilegeAction" policy="DefaultPrivilege"> <Allow>Persist</Allow> <Allow>PersistSessions</Allow> @@ -657,6 +759,11 @@ unlimited JCE libraries for your JVM and when you restart the server, you don't need to log back in, if your session is still alive. +
  • In 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. +
  • Your project is now ready to be imported into your favourite IDE. We have used both IntelliJ and Eclipse so @@ -669,55 +776,59 @@

     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 {

    Now configure your IDE to start the web project, and then once it has started, you should see the following in the logs:

    -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 =))
     

    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 @@ - + @@ -57,7 +57,7 @@

    Since Books are central to the bookshop, we'll first create the CRUD - REST API for them. The API will be as follows:

    + REST API for them. The API will be as follows:

     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 {
     }
     
    -

    Query

    +

    Search

    -

    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:

    + class name to the 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. -

    + 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.

    + 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.

    +

    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.

    Get

    @@ -270,11 +275,11 @@ public Response get(@Context HttpServletRequest request, @PathParam("id") String

    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.

    + 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.

    + authenticated, which is enough for the moment.

    Create

    @@ -314,7 +319,7 @@ public Response create(@Context HttpServletRequest request, String data) { The service is implemented as follows:
    -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); })(); - +