RFC - API for composing pages

Version 8

    This is the first draft for a proposal on how the "Compose pages API" could look like.

     

    Introduction

     

    The purpose of the API is to provide an easy, supported way for portlet developers to build pages on demand. Currently, there's no API for composing pages and adding content to it, and administrators have to use workarounds to offer pages to their users, mostly by dealing with XML files and interacting with GateIn via the import/export tool. Portlet developers might workaround this limitation by using the non-public API, this is subject to sudden and non-supported changes and/or might involve more complex steps than would be required for this specific purpose.

     

    The support for a public API is something that is going to be under more in-depth discussion for GateIn 4.x. With that said, it's worth mentioning that this proposal is not linked in any way to the bigger efforts related to the API for the version 4.x. While it would certainly be interesting to provide a "forward compatible" version of the API, this is not the main purpose at this point, though it might be in the future, before the completion of the specification.

     

    While the main purpose of this API extension is to provide a way to compose pages, other changes to the API might be required. On such cases, the extension will allow only for read-only operations first, with "write-operations" coming when required.

     

    Current state

     

    The current supported GateIn API resides in the module identified by the Maven artifact "org.gatein.api:gatein-api". The module provides already a simple way to create a Page object, but not a way to specify its contents. The current API also provides a way to query pages, as well as other objects that might be of interest of a portlet developer, like Site. The following wiki page contains more details about the current API: https://docs.jboss.org/author/display/GTNPORTAL36/Portal+API

     

    Approach

     

    As there's already a well-defined API for GateIn, the initial idea is to extend the current version of the API by adding Builder classes and extending the current models with methods that would allow the accomplishment of the goal. The main idea is to have only backward compatible changes, bumping the version of the current API by +1 on the minor version number. Should a change be required that would be backwards incompatible, we might consider then bumping the version on the major version number.

     

    Goal

     

    The following code is the main goal of what should be possible to do with the Page Composition API. This would use a Builder pattern, to allow for a fluid definition of a page.

    Page page = new PageBuilder()
      .beginLayoutContainer()
    
        .addContainer()
          .setContent(gadgetRss)
        .buildContainer()
    
        .addContainer()
          .addColumn()
            .setContent(gadgetCalculator)
          .addColumn()
            .setContent(wsrpCompanyNews)
        .buildContainer()
    
        .addContainer()
          .addRow()
            .setContent(wsrpCompanyRoster)
          .addRow()
            .setContent(portletCompanyCalendar)
        .buildContainer()
    
        .addContainer()
          .addRow()
            .addContainer()
              .addColumn()
                .setContent(portletUsefulLinks)
              .addColumn()
                .setContent(portletUsefulLinks)
            .buildContainer()
          .addColumn()
            .setContent(gadgetCalculator)
        .buildContainer()
    
        .addContainer()
          .addColumn()
            .setContent(portletCommonFooterLinks)
        .buildContainer()
    
      .finishLayoutContainer()
    
      .withName("New awesome page")
      .withExtendedLabelMode(true)
      .withDisplayName("New awesome page")
      .withVisibility(true)
      .withPublicationDateStarting(new Date())
      .withLocale("en_US")
      .buildPage();
    
    

     

     

     

    At the end of the presented code, the Page could be persisted the same way as it currently is, with no changes to how portlet developers currently do.

     

    The simplest possible usage of the API would look like this:

     

    Page page = PageBuilder.addContent(rssReader).buildPage();
    
    
    
    

     

    Additionally, the following should also be possible, once a Page has been created and/or retrieved from the persistent storage (based on the first scenario, with a complex page):

    page.getContainers()
      -> List<Container> (size=4)
    
    container = page.getContainers().get(0)
      -> Container{columns=1, rows=0, containers=0, content=[]}
    container.getColumns().get(0)
      -> Container{columns=0, rows=0, containers=0, content=[gadgetRss]}
    
    container = page.getContainers().get(1)
      -> Container{columns=2, rows=0, containers=0, content=[]}
    container.getColumns().get(0)
      -> Container{columns=0, rows=0, containers=0, content=[gadgetCalculator]}
    container.getColumns().get(1)
      -> Container{columns=0, rows=0, containers=0, content=[wsrpCompanyNews]}
    
    container = page.getContainers().get(2)
      -> Container{columns=0, rows=2, containers=0, content=[]}
    container.getRows().get(0)
      -> Container{columns=0, rows=0, containers=0, content=[wsrpCompanyRoster]}
    container.getRows().get(1)
      -> Container{columns=0, rows=0, containers=0, content=[portletCompanyCalendar]}
    
    container = page.getContainers().get(3)
      -> Container{columns=0, rows=1, containers=0, content=[]}
    container.getRows().get(0)
      -> Container{columns=0, rows=0, containers=2, content=[]}
    containers.getRows().get(0).getContainers().get(0).getColumns().get(0)
      -> Container{columns=0, rows=0, containers=0, content=[portletUsefulLinks]}
    containers.getRows().get(0).getContainers().get(1).getColumns().get(0)
      -> Container{columns=0, rows=0, containers=0, content=[portletCommonFooterLinks]}
    
    
    Container newHeaderOptions = ContainerBuilder.addContainer().addContent(newHeaderOptionsPortlet)
    page.getContainers().add(0, newHeaderOptions)
    
    page.getContainers().remove(1)
    
    containers.getRows().get(0).getContainers().get(1).getColumns().get(0).setContent(portletNewCommonFooterLinks)
    
    
    
    
    
    
    
    
    

     

    The structure for the builder would look like the following. The idea behind splitting the LayoutBuilder and the PageBuilder is that a possible future SiteBuilder could also inherit the behavior for building containers.

     

    abstract LayoutBuilder<T> {
        T addContainer();
        T addContent();
        T addRow();
        T addColumn();
        T buildContainer();
        T withName();
    }
    
    PageBuilder extends LayoutBuilder<PageBuilder> {
        PageBuilder withExtendedLabelMode();
        PageBuilder withDisplayName();
        PageBuilder withVisibility();
        PageBuilder withPublicationDate();
        PageBuilder withLanguage();
        Page build();
    }
    
    
    
    
    
    

     

    Data structure

     

    The data structure for the Page object will look similar to the one currently in use org.exoplatform.portal.webui.page.UIPage, in that a Page is an extension of a Container, with a Container able to hold other Containers.

    class Container {
        public List<Container> rows;
        public List<Container> columns;
        public List<Container> containers;
        public Content content; // similar to org.exoplatform.portal.config.model.Application
        private List<Container> children; // references to all the containers, in the order they were added
    }
    
    class Page extends Container {
        public String name;
        ...
    }
    
    
    
    
    
    
    
    
    

     

    Similarly to the current approach on the composition of pages via the UI, a DataMapper will be built to translate between the Page from the API into a Page that is understandable by the MOP components (particularly, org.exoplatform.portal.mop.page.PageState)

     

    Related tasks

     

    In order to accomplish the goals for this task, a few other parts might need some work. The first ones that comes to mind are:

     

    1. Navigation API - Composing a page in itself is one step in getting the page ready to be displayed to an end-user. Adding it as a navigation node is the final step. There's already a Navigation API, and it seems that it's ready to be used (org.gatein.api.navigation.Navigation and org.gatein.api.navigation.Node) . In concrete terms, the consumer of the API would need to deal with the Node/Navigation API to get one (or more) paths to a Page. If this is not possible, then it is in the scope of this task to add means to make it possible.
    2. Application Registry - A page is ultimatelly a set of organized containers, with Portlets/Gadgets as content. As such, a consumer of the Page API would need a way to query and get references to the Portlets/Gadgets that need to be included in the container. There doesn't seem to be any way to do that, currently, so, it is in the scope of this task to bring a read-only API for the application registry. A read-write API for the Application Registry is not in the scope, but might be added as part of this task, if it proves to be simple enough and if time allows.
    3. Documentation - During the development of this task, special care needs to be placed on documenting this API via Javadoc, as it will be part of the public API. Consumers of the API without knowledge of GateIn internals are the target, and it should be possible to perform the basic operations with minimal support from external documentation. Despite that, a review of the current user guide and other reference material is needed, to adjust and amend the information related to the changes.
    4. Quick start - At the final stage of the task, a quick start needs to be developed to demonstrate how a page can be programatically added to a portal. The focus will not be on this API, but on the task "Adding a new node to a portal", which should show all the steps, from the composition of the page to the final step in getting the URL of the newly created node.

    Risks

     

    At this point, the following points can be considered a risk for this task:

     

    1. The most critical point is the application registry: without an easy way to get the available portlets/gadgets, it would not be possible to get a Page completely composed. In other words: a page with containers but without content would be of no use.
    2. Compatibility with existing pages: while the initial focus of the task is to allow the composition of new pages, it should be perfectly possible to change existing pages, created via the API or not. The risk is that an existing page would be formatted in a way that is not correctly understood by the API, causing the page to be corrupted, malformed or defaced. Similarly, a page created via the API might get problems when changed via the GUI, on the "Page editor" feature.
    3. While the goal is to have a fluent API as stated under the "Goals" section, during the actual work it might prove that it wouldn't be possible, as the data structure would need to be converted internally into something that the MOP Page understands. If that's the case, then time might be lost and the ease to use of the API might get compromised.