Exposing Hippo Sitemenus over REST 

Minos is a lead architect for Hippo Professional Services. Having worked with CMS systems for the last 12 years, he's an expert in the field, with a strong focus on consulting, coaching, establishing best practices and solving challenges.

When he's not coding or guiding partners, he likes to go to parks, build stuff and... code!
All code for this lab and more useful information can be found in Github here

Recently we’ve seen a trend towards headless implementations and the general paradigm of consuming content as a service. I rarely encounter questions on how to use REST, since in Hippo we expose content as a service in an out-of-the-box fashion. But Hippo is much more than content alone; our delivery tier framework is playing a major role and is a primary citizen in most project implementations, be them headless, non-headless or a mixture of both. In that last case we, in Hippo, identify the solution as ‘hybrid’.

The delivery tier in Hippo provides a complete, publishable configuration model, has built-in searching, built-in previewing and templating, built-in content and URL mapping, while easily exposing content via REST and allowing for configuring all these via user- and developer-friendly interfaces. Using it to drive headless sites couldn’t be more convenient; such implementations (that use both Hippo’s delivery tier and client-side frameworks) fall under the hybrid family of architectures. On the other hand, it’s also possible to run full headless sites, that only consume content from Hippo, but then you either have to sacrifice or re-implement functionality that HST gives you out-of-the-box. A major limitation of headless-only architectures, in my opinion, is the inability to provide users and developers with a means to easily manage a site. In this lab post I’d like to focus on hybrid solutions and show how Hippo’s delivery tier can help, by exposing its configuration model via a service.

Use case

About a year ago I had the chance to work on a project where we attempted to couple a Single Page Application (SPA) frontend with the HST configuration, thus taking hybrid to a next level. The goal was not just for the SPA to consume content from Hippo but furthermore drive and configure it via HST, just like you configure your HST site. Of course, this meant that the SPA needed to parse, consume and use the HST configuration. The implementation we built was exposing the hierarchies of the hst:sitemaps, the hst:pages and the hst:sitemenus as JSON feeds. The client application would use the sitemenus feed to render its menus, the sitemap feed for content and hst:page mapping and the pages feed to build a model of all participating components in a webpage. Then it generally had two options: either get the content from Hippo’s REST service and render it client-side or call the HST components one by one (we made them all asynchronous), get server-rendered HTML and aggregate all these responses into one output. For the record, the complexity of the client app was substantial since it effectively re-implemented a lot of functionality of the HST. Management of the client app however, was now possible via HST configuration and the Channel manager.

In this lab post I’d like to present parts of the solution, specifically the setup needed to expose sitemenus via a REST service. Exposing the hst:pages is a more advanced topic with less use cases. Perhaps I’ll cover this in another lab post if the Hippo community is interested.

Implementation

We start with creating a clean new project from Essentials. For this lab post, Hippo 11 is being used. You don’t need to configure anything with Essentials, just generate a new project and build it. In case you want to build this solution on an existing project that already has REST services configured via Essentials, then you need to take some extra care. In each paragraph below, we’ll be mentioning what must be done differently if using the Essentials REST services.

Configure mounts

The first step is registering a new mount that will handle the REST requests. We will use the plain rest services of HST [1]. We add the following configuration under /hst:hst/hst:hosts/dev-localhost/localhost/hst:root:

<?xml version="1.0" encoding="UTF-8"?> 
<sv:node sv:name="rest" xmlns:sv="http://www.jcp.org/jcr/sv/1.0"> 
  <sv:property sv:name="jcr:primaryType" sv:type="Name"> 
    <sv:value>hst:mount</sv:value> 
  </sv:property> 
  <sv:property sv:name="hst:alias" sv:type="String"> 
    <sv:value>rest</sv:value> 
  </sv:property> 
  <sv:property sv:name="hst:isSite" sv:type="Boolean"> 
    <sv:value>false</sv:value> </sv:property> 
  <sv:property sv:name="hst:ismapped" sv:type="Boolean"> 
    <sv:value>false</sv:value> 
  </sv:property> 
  <sv:property sv:name="hst:namedpipeline" sv:type="String">
    <sv:value>JaxrsRestPlainPipeline</sv:value> 
  </sv:property> 
  <sv:property sv:name="hst:types" sv:type="String" sv:multiple="true"> 
    <sv:value>rest</sv:value> 
  </sv:property> 
</sv:node>

If you’re using the Essentials REST services already, then you’ll need to use the existing REST mount instead of registering a new one. This mount needs to be using the JaxrsRestPlainPipeline.

We then need to add an alias to the parent mount, that is the one named hst:root. This is the mount that is coupled with the hst: configuration that we want to read sitemenus from. In our service class we’re going to retrieve the hst:root mount by its alias to query for sitemenus registered under that mount. To set the alias, add the following property on node /hst:hst/hst:hosts/dev-localhost/localhost/hst:root:

<sv:property sv:name="hst:alias" sv:type="String"> 
  <sv:value>main-site</sv:value>
</sv:property>

Add sample site menus

For testing purposes, we need to create an HST sitemenu with a few items. Import the following under /hst:hst/hst:configurations/myhippoproject/hst:sitemenus:

<?xml version="1.0" encoding="UTF-8"?> 
<sv:node sv:name="main" xmlns:sv="http://www.jcp.org/jcr/sv/1.0"> 
  <sv:property sv:name="jcr:primaryType" sv:type="Name"> 
    <sv:value>hst:sitemenu</sv:value> 
  </sv:property> 
  <sv:node sv:name="home"> 
    <sv:property sv:name="jcr:primaryType" sv:type="Name"> 
      <sv:value>hst:sitemenuitem</sv:value> 
    </sv:property> 
    <sv:property sv:name="hst:referencesitemapitem" sv:type="String"> 
      <sv:value>/home</sv:value> 
    </sv:property> 
  </sv:node> 
  <sv:node sv:name="news"> 
    <sv:property sv:name="jcr:primaryType" sv:type="Name"> 
      <sv:value>hst:sitemenuitem</sv:value> 
    </sv:property> 
    <sv:property sv:name="hst:referencesitemapitem" sv:type="String"> 
      <sv:value>/news</sv:value> 
    </sv:property> 
    <sv:node sv:name="latest"> 
      <sv:property sv:name="jcr:primaryType" sv:type="Name"> 
        <sv:value>hst:sitemenuitem</sv:value> 
      </sv:property> 
      <sv:property sv:name="hst:referencesitemapitem" sv:type="String"> 
        <sv:value>${parent}/latest</sv:value> 
      </sv:property> 
    </sv:node> 
  </sv:node> 
  <sv:node sv:name="events"> 
    <sv:property sv:name="jcr:primaryType" sv:type="Name"> 
      <sv:value>hst:sitemenuitem</sv:value> 
    </sv:property> 
    <sv:property sv:name="hst:referencesitemapitem" sv:type="String"> 
      <sv:value>/events</sv:value> 
    </sv:property> 
  </sv:node> 
</sv:node>

Create the REST Service class

Next step is to create the REST service class and register it via Spring. The registration is very simple, just create an xml file (give it any name you wish, e.g. “customRestPlainResourceProviders.xml”) at project path myhippoproject/site/src/main/resources/META-INF/hst-assembly/overrides and containing the following:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> 

  <import resource="classpath:/org/hippoecm/hst/site/optional/jaxrs/SpringComponentManager-rest-jackson.xml"/> 
  <import resource="classpath:/org/hippoecm/hst/site/optional/jaxrs/SpringComponentManager-rest-plain-pipeline.xml"/> 

  <bean id="customRestPlainResourceProviders" class="org.springframework.beans.factory.config.ListFactoryBean"> 
    <property name="sourceList"> 
      <list> 
        <bean class="org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider"> 
          <constructor-arg> 
            <bean class="org.example.rest.HstConfigurationRESTService"/> 
          </constructor-arg> 
        </bean> 
      </list> 
    </property> 
  </bean> 

  <bean id="jaxrsRestPlainServiceQueryStringReplacingInterceptor" class="org.hippoecm.hst.jaxrs.cxf.QueryStringReplacingInterceptor"> 
    <property name="additionalQueryString"> 
      <value>_type=json</value> 
    </property> 
  </bean> 
</beans>

Here is the java class implementing the service. The name of the class is org.example.rest.HstConfigurationRESTService, just as we defined it in our Spring configuration above.

@Path("/hst/") 
public class HstConfigurationRESTService extends org.hippoecm.hst.jaxrs.services.AbstractResource {

  @GET 
  @Path("/menus/") 
  public HstMenusRepresentation getHstMenusConfiguration( 
    @Context HttpServletRequest servletRequest, 
    @Context HttpServletResponse servletResponse) { 
    return new HstMenusRepresentation( RequestContextProvider.get().getMount("main-site").getHstSite().getSiteMenusConfiguration()); 
  }

  @GET 
  @Path("/menus/{menuName}/") 
  public HstMenuRepresentation getHstMenusConfiguration( 
    @Context HttpServletRequest servletRequest, 
    @Context HttpServletResponse servletResponse, 
    @PathParam("menuName") String menuName) { 
    return new HstMenuRepresentation( RequestContextProvider.get().getMount("main-site").getHstSite().getSiteMenusConfiguration().getSiteMenuConfiguration(menuName)); 
  } 
}

As can be seen above, the service listens at path /hst and if we also take the REST mount into account, the final path becomes /rest/hst. We’ve defined 2 methods, the first one listens at /rest/hst/menus and lists all available menus. Using any of those menus, we can call the second service at path /rest/hst/menus/{menuName}; we only need the name of a menu as provided by the first call.

An important point is that we explicitly get the mount with alias “main-site” and then retrieve sitemenu configurations from it. You could retrieve sitemenus of any mount (any channel) in this way, though for the scope of this lab we kept the alias hard-coded in the service class. Additionally, you could have another service call, listening perhaps at the empty path (/), that lists all available mount aliases. If you then refactor the /hst/menus call so it accepts the alias as a path parameter, you can have a service that returns sitemenus of any mount you specify.

An alternative way to retrieve sitemenus is by adding the path your REST service uses (/hst/_any_), as a sitemapitem entry in your hst configuration. Then, either the registration of the REST mount will need to change, specifically an hst configuration will need to be coupled to the mount and the boolean property ‘isMapped’ will need to be set to true, or the sitemapitem could use the JAX-RS pipeline directly; in that case you don’t even need a new mount. Once these steps are done, then in our service constructor we can access the sitemenus from the request context, like so:

RequestContextProvider.get().getHstSiteMenus()

This call will give us the runtime model of the sitemenus, for this specific request and the mount that matched it.

Setting up all the above in case you’re already using the Essentials REST services, is quite different. Your registration needs to be merged with the Essentials’ customRestPlainResourceProviders registration, you need to add no-arg constructors to all the representation beans and then register them in Spring bean jaxrsHippoContextProvider. If you have configured REST with Essentials, then for more information please see file at path site/src/main/resources/META-INF/hst-assembly/overrides/spring-plain-rest- api.xml

Representation classes

The HstConfigurationRESTService depends on a number of classes that represent the site menus and children. Below is the menus collection representation, org.example.rest.HstMenusRepresentation:

@XmlRootElement(name = "hstMenus") 
public class HstMenusRepresentation { 

  @XmlElementWrapper(name = "sitemenus") 
  @XmlElements(@XmlElement(name = "sitemenu")) 
  private List<HstMenuRepresentation> siteMenus = new ArrayList<>(); 

  public HstMenusRepresentation(HstSiteMenusConfiguration hstMenusConfiguration) { 
    for(HstSiteMenuConfiguration hstSiteMenuConfiguration : hstMenusConfiguration.getSiteMenuConfigurations().values()){ 
      siteMenus.add(new HstMenuRepresentation(hstSiteMenuConfiguration)); 
    } 
  } 
}

Next class, a single menu representation, org.example.rest.HstMenuRepresentation:

@XmlRootElement(name = "sitemenu") 
public class HstMenuRepresentation { 

  @XmlElement 
  private String name; 

  @XmlElementWrapper(name = "menuitems") 
  @XmlElements(@XmlElement(name = "menuitem")) 
  private List<HstMenuItemRepresentation> menuItems = new ArrayList<>(); 

  public HstMenuRepresentation(HstSiteMenuConfiguration hstSiteMenuConfiguration) { 
    this.name = hstSiteMenuConfiguration.getName(); 
    for(HstSiteMenuItemConfiguration siteMenuItemConfiguration : hstSiteMenuConfiguration.getSiteMenuConfigurationItems()) { 
      menuItems.add(new HstMenuItemRepresentation(siteMenuItemConfiguration)); 
    } 
  } 
}

Last, a single menu item representation, org.example.rest.HstMenuItemRepresentation. Note that objects of this class have references to children objects of the same type, effectively mirroring the hierarchy of sitemenuitems in HST.

@XmlRootElement(name = "menuitem") 
public class HstMenuItemRepresentation { 

  @XmlElement 
  private String name; 

  @XmlElement 
  private String path; 

  @XmlElement 
  private String externalLink;
 
  @XmlElementWrapper(name = "parameters") 
  @XmlElements(@XmlElement(name = "parameter")) 
  private Map<String, String> parameters; 

  @XmlElementWrapper(name = "menuitems") 
  @XmlElements(@XmlElement(name = "menuitem")) 
  private List<HstMenuItemRepresentation> menuItems = new ArrayList<>(); 

  public HstMenuItemRepresentation(HstSiteMenuItemConfiguration hstSiteMenuItemConfiguration) { 
    this.name = hstSiteMenuItemConfiguration.getName(); 
    this.path = hstSiteMenuItemConfiguration.getSiteMapItemPath(); 
    this.externalLink = hstSiteMenuItemConfiguration.getExternalLink(); 
    this.parameters = hstSiteMenuItemConfiguration.getParameters(); 
    for (HstSiteMenuItemConfiguration child : hstSiteMenuItemConfiguration.getChildItemConfigurations()) { 
      menuItems.add(new HstMenuItemRepresentation(child)); 
    } 
  } 
}

Running the lab

To run this solution, simply do a clean install, run with cargo and visit URL http://localhost:8080/site/rest/hst/menus. You should see a list of all the available menus, including their children menu items. You can take the name of any HstMenuRepresentation (there’s only one here, named “main”) and call the second service, like so: http://localhost:8080/site/rest/hst/menus/main. This then lists all children items of the “main” site menu and all sub items they may have.

Here’s the expected output when calling the /hst/menus service:

​{
  "siteMenus": [
    {
      "HstMenuRepresentation": {
        "name": "main",
        "menuItems": [
          {
            "HstMenuItemRepresentation": {
              "name": "home",
              "path": "/home",
              "parameters": {},
              "menuItems": []
            }
          },
          {
            "HstMenuItemRepresentation": {
              "name": "news",
              "path": "/news",
              "parameters": {},
              "menuItems": [
                {
                  "HstMenuItemRepresentation": {
                    "name": "latest",
                    "path": "/news/latest",
                    "parameters": {},
                    "menuItems": []
                  }
                }
              ]
            }
          },
          {
            "HstMenuItemRepresentation": {
              "name": "events",
              "path": "/events",
              "parameters": {},
              "menuItems": []
            }
          }
        ]
      }
    }
  ]
}

Conclusion

Exposing HST configuration via REST is straightforward in Hippo by using plain REST services. The service is able to expose the sitemenus of a particular mount. If you prefer a more generic approach, you could setup the service to accept a specific mount as a parameter and expose that mount’s menus.

Next steps for this solution would be to make other parts of the HST configuration available over REST and perhaps provide an additional service for exposing preview HST configuration. In case this solution is used in a headless only system, then a nice addition is to create a very basic HST site, just for allowing the configuration of the menus via the Channel manager instead of the CMS Console. It’s worth mentioning also that the solution opens some topics for discussion, like making the REST service an authenticated one, investigating approaches for processing external and internal sitemenu links and exposing the translations of labels of sitemenus.

References

[1] RESTful API Support - Plain JAX-RS Services