RESTful JAX-RS Component Support
Introduction
Goal
Create custom JAX-RS-based RESTful web services in Hippo's delivery tier.
Background
In addition to the generic Content REST API, Hippo's delivery tier (a.k.a. HST) provides built-in support for JAX-RS to expose content over custom RESTful web services. Two types of services are supported:
- Plain JAX-RS services are used to expose preconfigured functionality within a site, not directly mapped to content, but can make use of it.
- Context-aware JAX-RS services are used to dynamically expose site content mapped on content type.
Both context-aware and plain JAX-RS services are provided through the HST with full access to its API.
Hippo's setup application (a.k.a. Essentials) provides a REST Services Setup tool which enables easy setup of plain JAX-RS services.
Information on implementing the custom services can be found on this page and the dedicated pages about plain and context-aware services.
How to Enable JAX-RS Services Support
If you used the Hippo Maven archetype to bootstrap your project then support for JAX-RS services is already enabled and you can skip this paragraph.
Otherwise JAX-RS support can be added to any Hippo project as follows:
-
Add the following dependencies to your site module's pom.xml:
<dependency> <groupId>org.apache.geronimo.specs</groupId> <artifactId>geronimo-annotation_1.1_spec</artifactId> <version>1.0.1</version> <!-- NOTE: You should use 'provided' instead of 'compile' when your application container provides javax.annotation.security package. --> <scope>compile</scope> </dependency> <dependency> <groupId>org.onehippo.cms7.hst.components</groupId> <artifactId>hst-jaxrs</artifactId> <version>${hippo.hst.version}</version> </dependency>
- Add the following Spring configuration file to your site module as /META-INF/hst-assembly/overrides/custom-jaxrs-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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- The following three imports will include pipeline configurations for both JaxrsRestPlainPipeline and JaxrsRestContentPipeline !!! --> <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" /> <import resource="classpath:/org/hippoecm/hst/site/optional/jaxrs /SpringComponentManager-rest-content-pipeline.xml" /> <!-- Your custom JAX-RS REST Plain Resource Providers will be added into the following list !!! --> <bean id="customRestPlainResourceProviders" class="org.springframework.beans.factory.config.ListFactoryBean"> <property name="sourceList"> <list> </list> </property> </bean> <!-- Your custom JAX-RS REST Content/Context Aware Resource Providers will be added into the following list !!! --> <bean id="customRestContentResourceProviders" class="org.springframework.beans.factory.config.ListFactoryBean"> <property name="sourceList"> <list> </list> </property> </bean> </beans>
In the configuration above, the three imports should be added first to use either the JaxrsRestPlainPipeline or the JaxrsRestContentPipeline.
Your custom Plain RESTful JAX-RS components will be added into the "sourceList" of "customRestPlainResourceProviders", which is merged and used by the JaxrsRestPlainPipeline later.
Your custom Content/Context-Aware RESTful JAX-RS components will be added into the "sourceList" of "customRestContentResourceProviders", which is merged and used by the JaxrsRestContentPipeline later.
NOTE: Spring Framework Beans assembly configuration under classpath:/META-INF/hst-assembly/overrides/*.xml resources will be automatically read and merged if you do not explicitly set the property assembly.overrides to an empty string in /WEB-INF/hst-config.properties.
Using XML or JSON Payloads
HST-2 uses Apache CXF as the JAX-RS runtime engine. Apache CXF JAX-RS can consume and produce XML or JSON formatted payloads automatically even though the JAX-RS Resource Beans do not specify the format. By default, Apache CXF JAX-RS will read "Accept" HTTP Request header to detect the most proper payload format first. For example, if "Accept" header value from the client is "text/xml" or "application/xml", then the runtime will decide to use XML format by default. If "Accept" header value is "application/json", then the runtime will decide to use JSON format.
In addition, Apache CXF JAX-RS supports a special request parameter "_type". For example, if the request contains a request parameter like " ?_type=xml" or " ...&_type=xml", then the runtime will decide to use XML format by default. If the request contains a request parameter like " ?_type=json" or " ...&_type=json", then the runtime will use JSON format.
How to customize the "_type" parameter name
HST-2 provides customization support for the default Apache CXF JAX-RS request parameter, " _type".
In order to customize this default request parameter, you need to add the following Spring Beans configuration fragments into your overriding spring assembly XML file(s):
<!-- Enabling to use '_format' parameter name instead of the CXF default _type parameter name for the plain JAX-RS pipeline. Also, if you set 'additionalQueryString' property, then all the JAX-RS requests will have the additional parameters forcefully. For example, if you set it to '_type=json', then you can force the output format to JSON format globally without considering 'Accept' HTTP request header. --> <bean id="jaxrsRestPlainServiceQueryStringReplacingInterceptor" class="org.hippoecm.hst.jaxrs.cxf.QueryStringReplacingInterceptor"> <property name="paramNameReplaces"> <map> <!-- The following will replace '_format' parameter name with '_type' parameter name before JAX-RS processing. --> <entry key="_format" value="_type" /> </map> </property> <property name="additionalQueryString"> <value></value> <!-- The following will append additional query string before JAX-RS processing <value>_type=json</value> --> </property> </bean> <!-- Enabling to use '_format' parameter name instead of the CXF default _type parameter name for the Content/Context-Aware JAX-RS pipeline. Also, if you set 'additionalQueryString' property, then all the JAX-RS requests will have the additional parameters forcefully. For example, if you set it to '_type=json', then you can force the output format to JSON format globally without considering 'Accept' HTTP request header. --> <bean id="jaxrsRestContentServiceQueryStringReplacingInterceptor" class="org.hippoecm.hst.jaxrs.cxf.QueryStringReplacingInterceptor"> <property name="paramNameReplaces"> <map> <!-- The following will replace '_format' parameter name with '_type' parameter name before JAX-RS processing. --> <entry key="_format" value="_type" /> </map> </property> <property name="additionalQueryString"> <value></value> <!-- The following will append additional query string before JAX-RS processing <value>_type=json</value> --> </property> </bean>
In the example above, the bean, " jaxrsRestPlainServiceQueryStringReplacingInterceptor", is for the default Plain JAX-RS Service Pipeline, and the bean, " jaxrsRestContentServiceQueryStringReplacingInterceptor", is for the default Content/Context-Aware JAX-RS Service Pipeline.
In any of those interceptor beans, you can configure " paramNameReplaces" property by parameter name replacement pairs. So, in the example above, if a request parameter named '_format' is provided, the parameter name will be replaced by '_type', which is known to the Apache CXF JAX-RS runtime as a special system parameter name. For example, if your request contains " ?_format=json" or " ...&_format=json", then it will be equivalent to " ?_type=json" or " ...&_type=json" with the configuration example above.
In this way, you can use a different parameter name for the Apache CXF JAX-RS internal "_type" system parameter name.
How to force to use JSON or XML format by default
If you want to use only one format either JSON or XML by default in your system without considering "Accept" HTTP Request header, then you can configure the " additionalQueryString" property value by " _type=json" or " _type=xml".
In the example Spring configuration fragment above (see 3.1), the " additionalQueryString" property value is an empty string by default. But if you set it to " _type=json" like the commented block for instance, then HST-2 Container will append the configured query parameter(s) string into the request message forcefully. So, even though the client prefers XML format by Accept header (e.g., "Accept: text/xml"), Apache CXF JAX-RS runtime will use JSON format only when you configure " _type=json" for " additionalQueryString" property by default.
However, if the client code requests JAX-RS URL by explicitly specifying the format parameter (e.g., " _type=json" or " _type=xml"), then this default configuration will not be applied. In that case, the client-specified request parameter will be applied.
How to configure JAX-RS Extension Providers
The JAX-RS Specification supports the javax.ws.rs.ext.Provider interface which is a marker interface for an implementation of an extension such as javax.ws.rs.ext.MessageBodyReader, javax.ws.rs.ext.MessageBodyWriter, javax.ws.rs.ext.ContextResolver, and javax.ws.rs.ext.ExceptionMapper.
For example, you can add an extension point to return a specific HTTP error on a Java exception by configuring an ExceptionMapper. A simple ExceptionMapper implementation could be like this:
package org.hippoecm.hst.demo.jaxrs.ext; /** * This extension provider can be configured to be invoked whenever * MyCustomSecurityException occurs. In that case, this provider simply * returns a forbidden access status code. This provider can make the * remaining JAX-RS Resource operation implementations much more simplified. * Refer to the JAX-RS Specification for more detail. */ @Provider public class MyCustomSecurityExceptionMapper implements ExceptionMapper<MyCustomSecurityException> { public Response toResponse(MyCustomSecurityException ex) { return Response.status(Response.Status.FORBIDDEN).build(); } }
You can configure your providers in an overriding Spring assembly file. For example, you may add the following block in classpath:/META-INF/hst-assembly/overrides/custom-jaxrs-resources.xml:
<bean id="customJaxrsRestEntityProviders" class="org.springframework.beans.factory.config.ListFactoryBean"> <property name="sourceList"> <list> <bean class= "org.hippoecm.hst.demo.jaxrs.ext.MyCustomSecurityExceptionMapper" /> </list> </property> </bean>
Your provider(s) will then be registered with the JAX-RS runtime engine (Apache CXF) to provide and use the extension(s).
Security Annotations Support
HST-2 provides security authentication/authorization configurations per site mount or site map item. With that configuration you can manage secured access control per site mount or site map item.
However, sometimes you might need to do secured access control per JAX-RS component operation invocation. For example, you can allow every read access from an operation, but you can allow some updating operations only to some specified roles.
To fulfill this requirement, HST-2 supports the Java EE Security Annotations.
So, you can annotate your methods with the following Java EE Security Annotations:
-
javax.annotation.security.DenyAll
-
javax.annotation.security.PermitAll
-
javax.annotation.security.RolesAllowed
For example, you can add Java EE Security Annotations to your JAX-RS component code like this:
package org.hippoecm.hst.demo.jaxrs.services; @Path("/demosite:productdocument/") public class ProductContentResource extends AbstractContentResource { @RolesAllowed(value={"everybody"}) @GET @Path("/") public ProductRepresentation getProductResource( @Context HttpServletRequest servletRequest, @Context HttpServletResponse servletResponse) { // do nothing for now... return null; } @RolesAllowed(value={"author", "editor"}) @GET @Path("/body/") public HippoHtmlRepresentation getHippoHtmlRepresentation( @Context HttpServletRequest servletRequest, @Context HttpServletResponse servletResponse) { return super.getHippoHtmlRepresentation(servletRequest, null, null); } }
With the example above, the #getProductResource() operation will be allowed for every user having the "everybody" role, while the #getHippoHtmlRepresentation() operation will be allowed only to users having the "author" or "editor" role.
@Persistable Annotation Support
HST-2 supports @Persistable ( org.hippoecm.hst.content.annotations.Persistable) annotation in the JAX-RS Service bean operations (whether it is a Context/Content-Aware JAX-RS Service or Plain JAX-RS Service bean).
If you annotate your JAX-RS Service operations by @Persistable ( org.hippoecm.hst.content.annotations.Persistable) annotation, the following invocation will always return a JCR session from the writable session pool:
-
HstRequestContext#getSession()
So, you don't have to try to use different JCR sessions in an operation context. (e.g., reading with a JCR session from the default session pool and persisting with another JCR session from the writable session)
Instead, you can use only one JCR session from the writable session pool when you read beans and/or persist your beans to the repository. Also, this will reduce possible error-prone codes because you don't have to refresh a JCR session to read some contents again after persisting contents.
Here is an example code, which can be found in the hippo-testsuite project (See the References below).
@Path("/products/")public class ProductPlainResource extends org.hippoecm.hst.jaxrs.services.AbstractResource { @GET @Path("/search/") public List<ProductRepresentation> searchProductResources( @Context HttpServletRequest servletRequest, @Context HttpServletResponse servletResponse, @Context UriInfo uriInfo) { List<ProductRepresentation> products = new ArrayList<ProductRepresentation>(); try { HstRequestContext requestContext = RequestContextProvider.get(); // NOTE : AbstractResource#getHstQueryManager(requestContext) // simply get the JCR session by calling // requestContext.getSession(). // If this operation is annotated by @Persistable, HST-2 JAX-RS // runtime engine returns a JCR session from the writable // session pool. However, this method is not annotated by // @Persistable, so it returns a JCR session from the default // session pool. HstQueryManager hstQueryManager = getHstQueryManager(requestContext); // NOTE: AbstractResource#getMountContentBaseBean(requestContext) // simply get the JCR session by calling // requestContext.getSession(). // If this operation is annotated by @Persistable, HST-2 // JAX-RS runtime engine returns a JCR session from the // writable session pool However, this method is not // annotated by @Persistable, so it returns a JCR session from // the default session pool. for plain jaxrs, we do not have a // requestContentBean because no resolved sitemapitem HippoBean scope = getMountContentBaseBean(requestContext); HstQuery hstQuery = hstQueryManager.createQuery(scope, ProductBean.class, true); HstQueryResult result = hstQuery.execute(); HippoBeanIterator iterator = result.getHippoBeans(); while (iterator.hasNext()) { ProductBean productBean = (ProductBean) iterator.nextHippoBean(); if (productBean != null) { ProductRepresentation productRep = new ProductRepresentation().represent(productBean); products.add(productRep); } } } catch (Exception e) { throw new WebApplicationException(e, ResponseUtils.buildServerErrorResponse(e)); } return products; } @Persistable @POST public ProductRepresentation createProductResources( @Context HttpServletRequest servletRequest, @Context HttpServletResponse servletResponse, @Context UriInfo uriInfo, ProductRepresentation productRepresentation) { HstRequestContext requestContext = getRequestContext(servletRequest); try { // NOTE: // AbstractResource#getPersistenceManager(requestContext) // simply get a JCR session by calling // requestContext.getSession(). // This method is annotated by @Persistable, so HST-2 JAX-RS // runtime engine returns a JCR session from the writable // session pool. WorkflowPersistenceManager wpm = (WorkflowPersistenceManager) getPersistenceManager(requestContext); HippoFolderBean contentBaseFolder = getMountContentBaseBean(requestContext); String productFolderPath = contentBaseFolder.getPath() + "/products"; String beanPath = wpm.createAndReturn(productFolderPath, "demosite:productdocument", productRepresentation.getBrand(), true); ProductBean productBean = (ProductBean) wpm.getObject(beanPath); productBean.setBrand(productRepresentation.getBrand()); productBean.setColor(productRepresentation.getColor()); productBean.setType(productRepresentation.getType()); productBean.setPrice(productRepresentation.getPrice()); wpm.update(productBean); wpm.save(); productBean = (ProductBean) wpm.getObject(productBean.getPath()); } catch (ObjectBeanManagerException e) { throw new WebApplicationException(e, ResponseUtils.buildServerErrorResponse(e)); } catch (RepositoryException e) { throw new WebApplicationException(e, ResponseUtils.buildServerErrorResponse(e)); } return productRepresentation; } }
In the example above, there is no code line to get a writable (persistable) JCR session manually. Instead, the #createProductResources() method is annotated by @Persistable. So, when HstRequestContext#getSession() is invoked, a JCR session will be retrieved from the writable session pool automatically. Every call on HstRequestContext#getSession() in the invoking context of the @Persistable annotated methods, will return a JCR session from the writable session pool automatically.
If your JAX-RS operation needs to persist beans into the repository, it is strongly recommended to use @Persistable annotation rather than to try to get a writable (persistable) JCR session manually.
The ProductBean class that is used in the above example to store a new document must implement the CodeBinderInterface to link the bean properties to node properties.