January 7, 2014

Building JSON API with Katharsis for Hippo LAB

Woonsan KoWoonsan Ko is a Lead Solution Architect for Professional Services in Hippo USA. He's a committer in Apache Portals and Apache Commons and was involved in the development of HST-2, other Hippo CMS products and several forge projects. He blogs and shares his code here and there.

 

Code for this lab can be found in https://github.com/woonsanko/katharsis-examples-hippo.

 

Introduction

Have you ever argued in your team or got complaints from your API consumers about how the REST API should be designed or formatted? Sometimes it’s useful (but still time-consuming) if the best practices for better RESTful API designing and implementations are discussed and followed at least. But sometimes it may end up unnecessary “bikeshedding” issues while wasting time and neglecting more important problems to solve.

JSON API is designed to avoid this kind of bikeshedding issues based on RESTful API best practices. Furthermore, JSON API specification enforces a standard JSON format of request/response messages, and so modern applications can consume the JSON API services transparently without worrying about the data format across the network. This would lead to increased productivity and adoptability. Please browse some JSON API payload examples in http://jsonapi.org.

In this article, I’d like to show how to build JSON API services with KATHARSIS, an elegant and powerful HATEOAS framework, for Hippo Delivery tier (HST-2)

 

Demo Project

I created a demo project at https://github.com/woonsanko/katharsis-examples-hippo. It uses katharsis-servlet to integrate with Hippo Delivery tier (HST-2).

You can build and run it with the following commands:

$ mvn clean package
$ mvn -Pcargo.run

OK. I already created some example content in the demo project: project documents and task documents. A project document may have zero or more links to task documents. Each document is quite simple for demonstration purpose.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-docs.png

Browse projects folder and tasks folder to see how the examples look like. Please note that if you edit and publish those documents, it will affect the JSON API request results, of course. ;-)

Now, you're ready to test the JSON API services which were already implemented in the demo project.

This demo project implemented two JSON API types: projects (for project documents) and tasks (for task documents).

So, let's try to get the list of projects like the following example. (I used RESTClient on FireFox here, but you must have your favorite tool for this. You can use cURL commands like shown in README.md of the demo project, too.)

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-req1.png

In the example above, http://localhost:8080/site/api is the base URL for the mapped JSON API services in the demo project. I'll explain more about the configuation later.

The type of the resource objects comes after the base URL: /projects. So, the request URL fetches a collection of projects, according to the JSON API specification. You can see the result above, showing a collection of projects in the "data" top level object.

By the way, you will probably prefer looking at the result in the "Response Body (Highlight)" tab in pretty JSON format. I just had to select the "Response Body (Raw)" tab to reduce the screenshot image size.

In the same way, you can retrieve a collection of tasks like the following example:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-req2.png

As JSON API specification standardizes Filtering with "filter" query parameter, the following example shows how you can filter the project resource data by a full text search query term in filter parameter.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-req3-2.png

Also, you can retrieve a specific project resource by a path consisting of resource type ("projects") and resource ID ("75625baf-b47c-4ec5-b3e1-58889a1a8110") in the following example:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-req4.png

As you may have already imagine, I simply used the document handle UUIDs as identifiers of JSON API resource objects, which is the simplest one, right?

In the same way, you can also retrieve a specific task resource by specificying the resource type ("tasks") and resource ID ("d52877d9-36da-45c9-96f8-8dbf7946a394"):

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-req5.png

As JSON API specification standardizes Inclusion of Related Resources, you can add "include" query parameter to include the related project resource data of the specific task resource object:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-req6-2.png

Also, the JSON API relationship object itself ("projects" in this example) can be retrieved like the following example:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-integ-demo-req7.png

You've run the demo JSON API services based on Katharsis for Hippo successfully! Congrats! :-)

 

Design/Implementation Details

To integrate with Katharsis for Hippo, you will need to do the following:

  • Add a KatharsisFilter in your web application
  • Configure a mount for the KatharsisFilter as a HST Container Integration
  • Implement JSON API Resource POJO model classes
  • Implement JSON API ResourceRepository classes
  • (Optional) Implement JsonApiExceptionMapper classes for custom JSON API error responses on Exception
  • (Optional) Implement JSON API RelationshipRepository classes for relationship fetching service URL handling
  • (Optional) ...

 

Add a KatharsisFilter in your web application

First of all, we need to add a dependency on katharsis-servlet, which already explains very well about how to use in its project README.md. Anyway, you will need to add the following in a pom.xml:

    <dependency>
      <groupId>io.katharsis</groupId>
      <artifactId>katharsis-servlet</artifactId>
      <version>${katharsis-servlet.version}</version>
    </dependency>

katharsis-servlet project also explains very well in README.md about how you can extend the default servlet filter (AbstractKatharsisServlet or SampleKatharsisFilter) to integrate with your container or environment.

In this demo project, because I want to define all the ResourceRepository classes in Spring bean assembly (by annotating respository beans with @Component this time in this demo project, not by XML bean configuration) and I want to load the singleton ResourceRepository components from HST Container's ComponentManager, I extended SampleKatharsisFilter in HstEnabledKatharsisFilter.java like the following:

public class HstEnabledKatharsisFilter extends SampleKatharsisFilter {

    /**
     * Returns the currently resolved domain by HST host/site mapping.
     * @return the currently resolved domain by HST host/site mapping
     */
    @Override
    public String getResourceDefaultDomain() {
        final HstRequestContext requestContext = RequestContextProvider.get();
        // ...
        // For convenience, simply create a link for the root path, '/', on the currently resolved mount
        // and remove the trailing slash to find the base URL.
        HstLinkCreator linkCreator = HstServices.getComponentManager().getComponent(HstLinkCreator.class.getName());
        HstLink link = linkCreator.create("/", requestContext.getResolvedMount().getMount());
        URI uri = URI.create(link.toUrlForm(requestContext, true));
        return StringUtils.removeEnd(uri.toString(), "/");
    }

    /**
     * Replaces {@link JsonServiceLocator} with {@link HstContainerJsonServiceLocator}
     * in order to load resource repository beans from HST Container (through {@link ComponentManager}).
     */
    @Override
    protected KatharsisInvokerBuilder createKatharsisInvokerBuilder() {
        JsonServiceLocator jsonServiceLocator = new HstContainerJsonServiceLocator();
        return super.createKatharsisInvokerBuilder().jsonServiceLocator(jsonServiceLocator);
    }
}

HstEnabledKatharsisFilter in this demo project simply overrides two methods. #getResourceDefaultDomain() method is overriden to resolve the base URL dynamically based on HST host/mount configuration, and #createKatharsisInvokerBuilder() method is overriden to replace the default JsonServiceLocator by HstContainerJsonServiceLocator which is defined in this demo project like the following:

public class HstContainerJsonServiceLocator implements JsonServiceLocator {

    /**
     * Looks up {@link ResourceRepository} bean by the {@code clazz} bean type
     * from HST Container (through {@link ComponentManager})
     */
    @Override
    public <T> T getInstance(Class<T> clazz) {
        Map<String, T> components = HstServices.getComponentManager().getComponentsOfType(clazz);

        if (!components.isEmpty()) {
            if (components.size() > 1) {
                log.warn("There are multiple components of type '{}' in HST Container!", clazz);
            }

            return components.values().iterator().next();
        }

        return null;
    }
}

So, when it builds JSON API ResourceRepository registry, katharsis-core will invoke the JsonServiceLocator implementation. In our custom implemnetation (HstContainerJsonServiceLocator), it tries to find component(s) defined in Spring assembly of HST Container ComponentManager. If found, it returns the singleton bean to katharsis-core.

Now, it's time to define this custom KatharsisFilter in site/src/main/webapp/WEB-INF/web.xml like the following:

  <!-- SNIP -->

  <filter>
    <filter-name>HstEnabledKatharsisFilter</filter-name>
    <filter-class>com.github.woonsanko.katharsis.examples.hippo.katharsis.filter.HstEnabledKatharsisFilter</filter-class>
    <init-param>
      <!-- JSON API Resource model and repository classes scanning base package -->
      <param-name>katharsis.config.core.resource.package</param-name>
      <param-value>com.github.woonsanko.katharsis.examples.hippo.katharsis.resource</param-value>
    </init-param>
    <init-param>
      <!-- Base path info before JSON API resource path -->
      <param-name>katharsis.config.web.path.prefix</param-name>
      <param-value>/api</param-value>
    </init-param>
  </filter>

  <!-- SNIP -->

  <filter-mapping>
    <filter-name>HstFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>HstEnabledKatharsisFilter</filter-name>
    <url-pattern>/api/*</url-pattern>
  </filter-mapping>

  <!-- SNIP -->

Please note that HstEnabledKatharsisFilter is mapped by /api/* and located after HstFilter mapping. This is because we're leveraging HST Container Integration with Other Web Application Frameworks feature. So, in this case, HstEnabledKatharsisFilter plays a role of "Other Web Application Framework". Therefore, HstFilter should be executed first and then HstEnabledKatharsisFilter should be executed afterward in order to be able to access HstRequestContext and its resolved mapping data. Anyway, please keep in mind that if you make a request like /site/api/projects/* for instance, then the request processing is handled by HstFilter (/*) first and HstEnabledKatharsisFilter (/api/*) is executed afterward by a proper HST Container Integration with Other Web Application Frameworks configuration, which is going to be explained in the next section.

 

Configure a mount for the KatharsisFilter as a HST Container Integration

It is quite easy to configure the HstEnabledKatharsisFilter for /api mount as a HST Container Integration with Other Web Application Frameworks.

As shown below, you should add "api" mount under the hst:root mount and simply add "hst:namedpipeline" property with value, "WebApplicationInvokingPipeline". That's it!

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/blog/katharsis-filter-mount-config.png

Now, our custom HstEnabledKatharsisFilter is ready to serve requests on /api/**. Oh, wait! We didn't implement any JSON API Resource model POJOs and ResourceRepository classes yet. How can it serve my domain specific projects and tasks? Yes, you're right! But we've already established the most important backbone. The next sections will be understood much easier because those implementations are very straightforward based on katharsis-core annotations and API interfaces.

 

Implement JSON API Resource POJO model classes

It is very straightforward to implement a Resource POJO model beans. The POJO bean must be annotated with @JsonApiResource(type="...") in order to specify which resource type is repredented by the bean. And, the bean must have a field annotated by @JsonApiId which defines which field should be used as resource identifier. Here's the tasks resource example:

@JsonApiResource(type="tasks")
public class TaskResource extends AbstractResource {

    @JsonApiId
    private String id;

    private String name;

    private String description;

    @JsonApiToMany
    @JsonApiIncludeByDefault
    private List projects;

    @Override
    public  T represent(HippoBean hippoBean) {
        if (!(hippoBean instanceof Task)) {
            throw new IllegalArgumentException("Not a task document: " + hippoBean);
        }

        Task taskDoc = (Task) hippoBean;

        setId(taskDoc.getCanonicalHandleUUID());
        setName(taskDoc.getName());
        setDescription(taskDoc.getDescription());

        return (T) this;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List getProjects() {
        return projects;
    }

    public void setProjects(List projects) {
        this.projects = projects;
    }
}

Please note that you can specify a relationship object by using either @JsonApiToOne or @JsonApiToMany, too. I put @JsonApiToMany here because I suppose a task resource can be referred by multiple projects. However, this kind of relationships really depend on your business domain model. For projects field, I also added @JsonApiIncludeByDefault annotation in order to include the relationship objects in the JSON API "included" top level object by default.

Please browse other code in site/src/main/java/com/github/woonsanko/katharsis/examples/hippo/katharsis/resource/model/ as well.

 

Implement JSON API ResourceRepository classes

ResourceRepository is one of main interfaces of katharsis-core. By implementing resource repositories with resource type class and resource identifier field class in Java generic type parameters, katharsis-core scans them and builds a registry to serve JSON API requests.

The following example implements a ResourceRepository for "tasks" type (by TaskResource generic type parameter) with "String" ID field type.

Also, ResourceRepository#findOne(...) method should return one resource object to serve single resource requests. e.g, /projects/123 (projects type with ID=123) or /tasks/456 (tasks type with ID=456).

ResourceRepository#findAll(...) method should return an Iterable of resource bean objects to serve a collection of resource data. e.g, /projects/ (all of projects type resources) or /tasks/ (all tasks type resources).

@Component
public class TaskRepository extends AbstractRepository implements ResourceRepository {

    private static Logger log = LoggerFactory.getLogger(TaskRepository.class);

    @Override
    public TaskResource findOne(String id, QueryParams queryParams) {
        Task taskDoc = (Task) findHippoBeanByIdentifier(id);

        if (taskDoc == null) {
            throw new ResourceNotFoundException("Task not found by '" + id + "'.");
        }

        TaskResource taskRes = new TaskResource().represent(taskDoc);

        final Map typeRelationsParams = queryParams.getIncludedRelations().getParams();
        if (typeRelationsParams.containsKey("tasks")) {
            final IncludedRelationsParams tasksParams = typeRelationsParams.get("tasks");
            final Set inclusions = tasksParams.getParams();
            if (CollectionUtils.isNotEmpty(inclusions)
                    && StringUtils.equals("projects", inclusions.iterator().next().getPath())) {
                includeProjectResources(taskDoc, taskRes);
            }
        }

        return taskRes;
    }

    @Override
    public Iterable findAll(QueryParams queryParams) {
        List taskResList = new LinkedList<>();

        try {
            final HstRequestContext requestContext = RequestContextProvider.get();
            final HippoBean scope = requestContext.getSiteContentBaseBean();
            final HstQuery hstQuery = requestContext.getQueryManager().createQuery(scope, Task.class, true);

            String queryTerm = null;

            final Map typeFilterParams = queryParams.getFilters().getParams();
            if (typeFilterParams.containsKey("tasks")) {
                final FilterParams tasksParams = typeFilterParams.get("tasks");
                final Set filterValues = tasksParams.getParams().get("$contains");
                if (CollectionUtils.isNotEmpty(filterValues)) {
                    queryTerm = StringUtils.trim(filterValues.iterator().next());
                }
            }

            if (StringUtils.isNotEmpty(queryTerm)) {
                final Filter filter = hstQuery.createFilter();
                filter.addContains(".", queryTerm);
                hstQuery.setFilter(filter);
            }

            hstQuery.setOffset(getPaginationOffset(queryParams, 0));
            hstQuery.setLimit(this.getPaginationLimit(queryParams, 200));

            final HstQueryResult result = hstQuery.execute();

            Task taskDoc;
            TaskResource taskRes;

            for (HippoBeanIterator it = result.getHippoBeans(); it.hasNext(); ) {
                taskDoc = (Task) it.nextHippoBean();
                taskRes = new TaskResource().represent(taskDoc);
                taskResList.add(taskRes);
            }
        } catch (Exception e) {
            log.error("Failed to query tasks.", e);
        }

        return taskResList;
    }

    @Override
    public Iterable findAll(Iterable ids, QueryParams queryParams) {
        return findAll(queryParams);
    }

    @Override
    public <S extends TaskResource> save(S entity) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void delete(String id) {
        // TODO Auto-generated method stub
    }

    private void includeProjectResources(final Task taskDoc, final TaskResource taskRes) {
        final List referringProjectDocList = taskDoc.getReferringProjects();

        if (CollectionUtils.isNotEmpty(referringProjectDocList)) {
            List projectResList = new LinkedList<>();
            ProjectResource projectRes;

            for (Project projectDoc : referringProjectDocList) {
                projectRes = new ProjectResource().represent(projectDoc);
                projectResList.add(projectRes);
            }

            taskRes.setProjects(projectResList);
        }
    }
}

In the example shown above, each operation was implemented by using HST API and HST Content Beans. And finally each operation converts HST Content Beans to JSON API POJO Resource beans (by invoking #represent(hippoBean) conventionally defined in each bean.

Becuase I wanted to register all the repository beans automatically by annotation scanning instead of manual definitions in Spring assembly XML, I added the following in site/src/main/resources/META-INF/hst-assembly/overrides/katharsis-resources.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">

    <context:component-scan base-package="com.github.woonsanko.katharsis.examples.hippo.katharsis.resource" />

</beans>

 

(Optional) Implement JsonApiExceptionMapper classes to control error responses

Optionally, you can implement custom ExceptionMapper classes. If an exception is thrown in repository implementations, then katharsis-core will try to find a mapped ExceptionMapper and invoke your custom ExceptionMapper if a registered ExceptionMapper is found for that specific Exception.

@ExceptionMapperProvider
public class BadResourceRequestExceptionMapper implements JsonApiExceptionMapper<BadResourceRequestException> {

    @Override
    public ErrorResponse toErrorResponse(BadResourceRequestException exception) {
        ErrorData errorData = new ErrorDataBuilder()
            .setStatus(Integer.toString(HttpServletResponse.SC_BAD_REQUEST))
            .setTitle("Bad request")
            .setDetail(exception.getMessage())
            .build();

        return ErrorResponse.builder()
                .setStatus(HttpServletResponse.SC_BAD_REQUEST)
                .setSingleErrorData(errorData)
                .build();
    }
}

BadResourceRequestExceptionMapper registers a custom ExceptionMapper for BadResourceRequestException. Therefore, in your resource repository implementations, if you throw a BadResourceRequestException, then katharsis-core will invoke #toErrorResponse(exception) operation to write a custom error response.

 

Next Steps

The demo project didn't implement any content updating API operations (e.g, #save(entity) and #delete(id)). I'd like to leave it to you for your challenge. ;-)

Basically, katharsis will covert JSON payload to the proper entity object based on your JSON API Resource POJO bean definition. So, in each method, you can use HST API, HST Content Beans and HstComponent Persistable annotation and workflow to update content. However, please beware that @Persistable annotation is not supported in this katharsis integration (yet).

Anyway, if your repository implementation returns a JSON API Resource POJO bean in #save(entity) after processing the update/save request, then katharsis will do the remaining for you based on JSON API specification.

 

Summary

JSON API is designed to avoid “bikeshedding” issues based on RESTful API best practices, and by enforcing a standard JSON format of request/response messages, it allows modern applications to consume the JSON API services transparently. This would lead to increased productivity and adoptability. You can build JSON API services with katharsis, an elegant and powerful HATEOAS framework, for Hippo Delivery tier (HST-2). So you can introduce that transparency, productivity and adoptibility for your Hippo project, too!
HstEnabledKatharsisFilter in the demo project extends one of the default KatharsisFilter classes in katharsis-servlet to define and load JSON API ResourceRepository singleton components. HstEnabledKatharsisFilter is mapped to /api/** URL path in the demo project and is located after HstFilter filter mappings to leverage HST Container Integration with Other Web Application Frameworks feature. katharsis scans all the resource model classes and resource repository classes from the base package configured (katharsis.config.core.resource.package init parameter) in HstEnabledKatharsisFilter to build a JSON API resource registry.

JSON API Resource model POJO beans must be annotated with @JsonApiResource annotation and its identifier field must be annotated with @JsonApiId annotation. JSON API ResourceRepository classes must implement ResourceRepository with generic type parameters for serving resource type and resource ID type.

You can use HST API to retrieve HST Content Beans and convert them to JSON API Resource POJO objects to return in repository implementations. Then katharsis will do the remaining for you.

 

References

 

Stay up to date

 

Join our community to stay up to date with the latest Hippo CMS developments. Get the latest information on our CMS updates, ask questions and meet fellow Hippo developers.