Monday 9 April 2018

Bookman HAALA add-on in Java

In this article I will build the “Bookman” add-on, which saves bookmarks to the database. I will write it in Java and then deploy onto the open source Scala platform - HAALA. The platform lives on Bitbucket and the source code for this add-on can be found in “Download” section.

Users registered with the Platform have add-ons (packs) they could use and the “Bookman” is going to be one of them. I will hook it into the Control Panel for managing and make a simple website to see users’ bookmarks.

I will go through the steps to build a complete web application and highlight its vital points. I will only paste bits of the source code, so please download “bookman-java-example-2.54.zip”, unzip and refer to it for further details. The “Bookman” add-on is the Maven project. I will not go over the project structure and its “pom.xml” as it is self-explanatory.

Content

Domain model
Services
Facades
Controllers
Directives
Feeds
Control Panel and Website
Deploy

Why HAALA?

It is fast and flexible, easy to install on a virtual machine and has the Control Panel I can reuse for add-ons management. Although the Platform backend is in Scala with XML based configuration, I want to see if it can run my annotation based Java add-on.

To keep it simple, I will reuse some of the Scala classes provided by the Platform, drop validation and hardcode UI text. I will not use advanced features such as a search engine and code just enough to get basic functionality going.

Let’s get started with the database schema and Hibernate entities.

Domain model

This chapter is about the database: SQL schema and Hibernate entities.

SQL schema

I will start by creating the database schema. “schema.sql” can be found in the resources folder.

The naming used in the SQL may be confusing but it follows the pattern: starts with the uppercased add-on name “BOOKMAN” followed by the underscore with either a table name or foreign key or whatever. “BOOKMAN_bookmarks” tells that this table belongs to the “BOOKMAN” add-on. As there could be many add-ons sharing the same database schema, it is a good idea to prefix them, this also helps to prevent name collisions with the generic tables used by the Platform.

Every add-on should have a table extending the generic “PACKS” table: mappings for users and add-ons. This table has just one column “ID” and its Hibernate entity takes care of sub-classing.

Sample for PostgreSQL database:
CREATE TABLE BOOKMAN_packs
(
  id bigint NOT NULL,
  CONSTRAINT BOOKMAN_packs_pkey PRIMARY KEY (id)
)
WITHOUT OIDS;

There is also the foreign key “BOOKMAN_pack_sub_fk” to reference this ID and the ID in the “PACKS” table.

| PACKS |           | BOOKMAN_packs |
|-------|  same as  |---------------|
| ID    |-----------| ID            |
| User  |    FK     |               |

Now, a user can be assigned to the “Bookman” add-on by inserting a mapping into the “PACKS” table. However, to save bookmarks we need to create yet another table linked to the add-on. Take a look at “BOOKMAN_bookmarks” table and note the “pack_id” column. Hibernate mappings will allow us to reach saved bookmarks collection as follows: User → Packs → lookup Bookman Pack → Bookmark entities. And vice versa: Bookmark entity → Bookman Pack → User.

That covers the database schema. The rest of “schema.sql” creates indices and sequences.

Hibernate entities

Although the Platform uses XML for Hibernate descriptors, those should mix nicely with annotations. There are two entities in the “Bookman” add-on:
  • “PackEntity” is the subclass of generic “Pack”: mappings for users and add-ons. It has the static “PACK” field to define the add-on name and the collection of bookmarks.
  • “BookmarkEntity” is a saved bookmark. It has fields mapped to the database and the link to “PackEntity”. “BookmarkDAO” helps to load and save this entity.
Generic Pack entity <--------- Bookman Pack entity
        |             extends           |
        |                               |
        |      maps to DB table         |
    | PACKS |                   | BOOKMAN_packs |
    |-------|      same as      |---------------|
    | ID    |-------------------| ID            |
    | User  |         FK        |               |

Note that Hibernate “nullable” and “unique” constraints are only used when creating a new schema from entities. Since the schema is already created with constraints, there is no need to put those into entities.

I used persistence annotations to configure the entities. Now it is time to write a service to operate on these entities.

Services

This chapter covers a simple service, which manages bookmarks.

“BookmarkService” is the Spring annotated service with auto-wired beans doing CRUD operations on bookmark entities. Here is the stub of the service with no methods implemented:
@Service
@Transactional
public class BookmarkService {

  @Autowired
  BookmarkDao bookmarkDao;

  @Autowired
  ServiceContainer sc;

  public BookmarkEntity create(long userId, String url, String description);

  @Transactional (readOnly = true)
  public BookmarkEntity read(long bookmarkId);

  public BookmarkEntity update(long bookmarkId, String url, String description);

  public void delete(long bookmarkId);

  @Transactional (readOnly = true)
  public List<bookmarkentity> findByUser(long userId);

  @Transactional (readOnly = true)
  public List<bookmarkentity> findAll();

}

It does not extend the generic “BaseService” because I want to keep it simple with no search engine nor internal caches. This functionality is included into “BaseService” along with “ServiceContainer” to access generic services such as Labels, Settings etc.

As you can see from the stub, “update” and “delete” methods do not take User ID: a user is already permissioned for these operations. The service is triggered by a facade, which has a validator set. If such a validator sees that the user is not the owner of an entity, it should not allow the request to go through.

The actual service implementation is straightforward. Please refer to “BookmarkService” for details. One thing worth mentioning is “UserMod”: it registers users with the add-on.

The service is ready and it can load and save entities to the database. But to trigger this service I need a facade.

Facades

This chapter covers a Facade for users to trigger services from.

A facade is a class exposed to end users. The Platform exposes facades as JavaScript methods for users to trigger. JavaScript calls the backend as AJAX and the Platform converts incoming JSON into Scala classes and vise versa.

JavaScript   Facade      Service
    |           |           |
    |   calls   |           |
    |---------->| validates |
    |           |-----+     |
    |           |<----+     |
    |           |   calls   |
    |           |---------->| executes
    |  result   |  result   |-----+     
    |<----------|<----------|<----+     

Here is how a facade stub looks like:
@AopFacade
public class BookmarkFacade {

  @Autowired
  @ValidatorClass
  @Qualifier("baseValidator")
  BaseValidator validator;

  @Autowired
  BookmarkService service;

  @SecuredRole("USER")
  public CallResult create(CreateBookmarkVO cmd, HttpServletRequest request);

  @SecuredRole("USER")
  @ErrorCR(on = ObjectNotFoundException.class, 
                 key = "fcall.nofnd", log = "Bookmark #{bookmarkId} not found")
  public CallResult update(UpdateBookmarkVO cmd, HttpServletRequest request);

  @SecuredRole("USER")
  public CallResult remove(LoadVO cmd, HttpServletRequest request);

  @SecuredRole("USER")
  public CallResult findMy(SearchVO cmd, HttpServletRequest request);

}

The “AopFacade” annotation is required as it applies quite a few aspects to this class. Aspects catch errors, set data into requests, perform security checks etc.

The “Validator” field requirement forced by aspects. Here I will use the default validator, which only supports generic validation. This validator does not know if a calling user owns a bookmark and has permissions to delete it, for that you need to implement your own validator, refer to the “Toyshop” add-on for details. Aspects validate non-string arguments, such as “CreateBookmarkVO”, based on field annotations defining restrictions.

The “SecuredRole” should be present on each exposed method, requirement forced by aspects. You can see the available roles in “ROLES” table and introduce your own roles.

The actual facade implementation is straightforward. Please refer to “BookmarkFacade” for details.

XML

The Platform uses DWR to expose facades and the configuration is done in XML. Here is a sample exposing “create” and “read” methods of “BookmarkFacade” as JavaScript methods:
<bean class="org.example.haala.bookman.BookmarkFacade">
  <dwr:remote javascript="BookmanBookmarkF">
    <dwr:include method="create"/>
    <dwr:include method="read"/>
  </dwr:remote>
</bean>

Although the facade itself uses annotations, I needed to write XML to expose its methods. Now I will dive into controllers and URL mappings: next post.

Controllers

This chapter is about controllers and how to map those to URLs.

The Platform invokes services via facades leaving controllers with no special action other than rendering page templates. But nothing prevents calling services from controllers.

The best part is that I do not have to write a controller. I will use the generic “DynamicController” to render a page based on URL mapping configuration.

Here is a sample of a mapping:
{ "pattern": "view/*", "handler": "dynamicCtrl",
  "handlerConf": {
    "pageName": "view",
    "options": "ftl-page",
    "facade": "BookmanBookmarkF",
    "mod": "bookmanBookmarkCtrlMod"
  }}

It maps “view.ftl” Freemarker template to a URL ending with “view”, for example “http://example.org/view/foobar”. “facade” specify facades to include into a page and “mod” is executed by a controller.

Here is a stub of a controller mod:
public class BookmarkCtrlMod extends BaseControllerMod {

  @Autowired
  BookmarkService service;

  @Override
  public scala.Option<String> process(BaseController ctrl, 
            HttpServletRequest request, HttpServletResponse response);
}

The “process” method returns Scala “None” to tell the Platform to use a page template configured in the mapping. And because a Java class cannot directly extend Scala trait, the adapter “BaseControllerMod” is used.

Please refer to “BookmarkCtrlMod” for details on how to retrieve a bookmark and then refer to FTL files for rendering details.

“BookmarkCtrlMod “ is annotation based but its bean is defined in XML so that the generic controller can find it. Now I will write Freemarker directives to use in page templates.

Directives

This chapter covers Freemarker directives and how to use those in page templates.

Please refer to Freemarker manual to learn about directives. You can write your own directive and register it with the Platform to get access to services and HTTP objects.

Here is a stub for a directive:
public class ListBookmarksDirective extends BaseDirective {

  @Autowired
  BookmarkService service;

  @Override
  public void execute(Environment env, Map params, 
                TemplateModel[] loopVars, TemplateDirectiveBody body);

  @Override
  public void execute(State state);

}

It extends “BaseDirective”, which has methods to render the output. More than that, “BaseDirective” has plenty of Freemarker utilities: output scope, wrapping objects etc. Since the backend is in Scala, there some Java to Scala conversions involved.

The actual directive implementation is very simple: print all bookmarks into a page template. Please refer to “ListBookmarksDirective” for details.

XML

To make this directive available to a page template, I need to update add-on XML.
<entry key="bookman">
  <list>
    <ref bean="bookmanListBookmarksFDir"/>
  </list>
</entry>

Using the directive

Here is a sample of FTL page using the directive:
<@bookman.listBookmarks ; bookmark >
  <div class=”bokmark”>
    <p>$(bookmark.url}</p>
    <p>$(bookmark.decription!}</p>
  </div>
</@>

Macros

Directives are very powerful and the Platform supplies quite a few of them.  But there is no need to write a directive if one can do it with a macro. Please refer to Freemarker manual to learn about macros.

How do FTL templates compare to JSP pages? FTL files are easier to read even with embedded macros and directives, in comparison to JSP with tag libs and scriplets. Another important point is that FTL could be updated with no redeployment needed: by feeding files to the Platform.

Feeds

This chapter covers updating resources such as files and settings on the fly through the automatic feed process and explains the “site” concept of the Platform.

The Platform looks for JSON or XML files in a configured directory. It then processes whatever found in there. Please look into the add-on “resources/feed” directory: settings, URL mappings, FTL templates etc. JSON descriptors such as “x-settings.json” apply the settings, “x-files.json” specify files to upload etc.

Sites

The Platform uses “site” to differentiate between website resources. It is a unique combination of lowercased characters followed by a number. This combination dictates which add-on resources belong to and language.

The “Bookman” website uses “boo” string as its site. All of its resources will be saved as “boo1” – the website in English. As I do not care about multilingual support, I am hardcoding English text in UI instead of saving it to the database. To hook into the Control Panel and use its features, some of the add-on resources need to be saved as “cp1” - the Control Panel in English.

Uploading the feeds will hook the add-on into the Control Panel and create resources for a standalone website. I will explain more about template structure.

Control Panel and Website

In this chapter I will hook the add-on into the Control Panel and create a website to view all the bookmarks.

Control Panel

Supplying FTL templates with macros will do the trick. Please refer to “pack-bookman.ftl” for details on how to render a link to the “Bookman” add-on main page. Templates uploaded as “cp1” will be used for the Control Panel in English. And to separate add-on resources from other add-ons, its page templates will be uploaded into “pages/cp/bookman” folder.

FTL structure

In the generic “DOMAINS” table I registered “bookman.example.org” with “style=modern”. The Platform then uses “modern.jsp” to render add-on pages. It requires top and bottom decorator FTL templates, with optional inserts to use custom web resources. Please refer to the FTL files in “resources/feed” folder.

|     main.jsp    |
|-----------------|
| head          <-+--- inserts (FTL or label)
| body            |
|                 |
| [  modern.jsp   |
| |---------------|
| |             <-+--- layout-top.ftl
| | page template |--- FTL or JSP      
| |             <-+--- layout-bottom.ftl
|                 |
|               <-+--- inserts (FTL or label)

Finally, it is time to see the add-on in action.

Deploy

This chapter is about deploying, configuring and running the new add-on.

As the Platform could change in the future and become incompatible with the add-on, I will use its version 2.54, which also has Vagrant support. Following the directions in “vagrant” folder , I got the virtual machine is up and running and can access “example.org” website.

These are the steps to install the Bookman add-on, which could be applied with small adjustments to any add-on:
  1. Login to the virtual machine and stop the Tomcat
  2. Copy add-on into “/usr/local/haala-project”
  3. Edit “cactus/pom.xml” and include the new module
    <module>../bookman-java-example</module>
    
  4. Edit “cactus/webapp/src/main/webapp/WEB-INF/pack-all.xml” and include the file with XML snippets to process during the build
    <!-- xml::import { file = pack-bookman.xml; node = root } -->
    
  5. Edit “cactus/webapp/pom.xml” and include the resources and add-on itself
    <resource>
      <directory>../../bookman-java-example/src/main/resources</directory>
      <targetPath>../${project.build.finalName}/WEB-INF</targetPath>
      <includes>
        <include>pack-bookman.xml</include>
      </includes>
    </resource>
    
    <dependency>
      <groupId>org.example.haala.bookman</groupId>
      <artifactId>bookman</artifactId>
      <version>${project.version}</version>
    </dependency>
    
  6. Update database with “schema.sql” found in the add-on directory.
  7. Copy “feed” folder from the add-on directory into “/usr/local/haala-cactus.fs/feed”
  8. Start the Tomcat, access the Control Panel and run the “AggignMissingPacksTask”
  9. After all the feeds were consumed, try to access: 
    • http://cpanel.example.org:8082/cp/bookman/main.htm
    • http://bookman.example.org:8080
I am now able to manage bookmarks through the Control Panel and see the list of all bookmarks in the standalone website.

No comments:

Post a Comment