Page tree
Skip to end of metadata
Go to start of metadata

This page provides guidelines and best practice recommendations on how to create a custom Java-based REST endpoint with Magnolia. 

Java-based custom REST endpoints give access to the full power of Java and provide a high level of flexibility and configurability. REST endpoints in Magnolia also allow fine-grained security. If you so require, Magnolia can integrate Swagger tools directly within the Admin UI to facilitate the development and testing of your endpoints. 

Endpoints can be used for many purposes, from triggering a third party service to implementing a custom JSON provider. However, there are also other possibilities to get Magnolia content as JSON.

In this page, we assume that you know Java and the basics of Maven or another similar dependency management tool.

What is a Magnolia REST endpoint?

A REST endpoint is a resource located on a server, in our case on a Magnolia instance, which can be accessed with a RESTful URL.

A Java endpoint makes use of the Java API for RESTful Web Services (JAX-RS). The endpoint exposes public methods accessible via distinct URLs. A public method is accessed with a request and produces a response.

Magnolia REST endpoints are JAX-RS endpoints.

REST base path and RestDispatcherServlet

The base path for every Magnolia REST resource is /<context>/.rest, where <context> is either the name of the webapp or  / if the webapp is served from the root context. For details about specific paths of specific REST resources, see Understanding @Path  below.

All requests to /<context>/.rest* are handled by the  RestDispatcherServlet , which is installed and configured by the  magnolia-rest-integration module.  

What do endpoints do?

An endpoint can have several methods accomplishing completely different tasks. Basically endpoints can do anything that is possible with Java.

Each publicly exposed method representing a REST resource must produce a response that contains at least some response headers (the minimum is a proper HTTP response code). Both the request and response may or may not contain a "payload" (JSON or XML) wrapped in the request response body.

An endpoint should implement at least one method that can be accessed by its specific path with one of the standard HTTP methods: GETPUTPOST or DELETE.

  

Data API endpoints

A typical use case for an endpoint is a data API, which can read, create, update or delete content. A data API endpoint must implement at least one method, such as a GET method to read data. 

Typically, methods for data interaction are called with these HTTP methods:

Data interaction functionRead dataCreate dataUpdate dataDelete data
HTTP MethodGETPUTPOSTDELETE
Request has payloadnoyesyesno
Response has payloadyesmaybemaybeno

Magnolia provides default endpoints to read, create, update and delete nodes and properties of JCR workspaces. For details about the Magnolia default JCR content API, read the section about the magnolia-rest-services module below or have a look at the REST API page.

Java, Maven and package names

Java classes typically are wrapped in modules. When the Java classes of your endpoint are part of a (Maven) module, you can add the module to your bundle or webapp as a .jar file or manage the dependency with Maven in the .pom file of the webapp. 

Best practice

Wrap your Java REST endpoint within a Magnolia Maven module.

Creating a Magnolia Maven module with Maven archetype

Use the Magnolia Maven archetype to create a new Magnolia Maven module to host your Java REST endpoint:

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -DarchetypeCatalog=https://nexus.magnolia-cms.com/content/groups/public/ 

Notes:

  • Choose the archetype magnolia-project-archetype - described as An archetype to create basic Magnolia modules.
  • Always use the latest version of the archeytpe.
  • Provide meaningful values for the Maven groupId and artifactId.
  • Provide the magnolia-bundle-version which fits your existing Magnolia bundle(s) best. 
    If you are unsure, use the latest released Magnolia version: 5.7.1
    (info) The archetype creates a pom file which imports the magnolia-bundle-parent which manages the versions of Magnolia modules on which you rely on.

 Click here to expand to see the dialog while using the Maven archetype
~/dev/repo/magnolia/documentation $ mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -DarchetypeCatalog=https://nexus.magnolia-cms.com/content/groups/public/
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:2.4:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:2.4:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO]
[INFO] --- maven-archetype-plugin:2.4:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: https://nexus.magnolia-cms.com/content/groups/public/ -> info.magnolia.maven.archetypes:magnolia-theme-archetype (An archetype to create STK Theme modules)
2: https://nexus.magnolia-cms.com/content/groups/public/ -> info.magnolia.maven.archetypes:magnolia-project-archetype (An archetype to create a Magnolia project (a parent pom and a webapp))
3: https://nexus.magnolia-cms.com/content/groups/public/ -> info.magnolia.maven.archetypes:magnolia-module-archetype (An archetype to create basic Magnolia modules)
4: https://nexus.magnolia-cms.com/content/groups/public/ -> info.magnolia.maven.archetypes:magnolia-forge-module-archetype (An archetype to create a Magnolia module to be hosted on the Magnolia Forge)
5: https://nexus.magnolia-cms.com/content/groups/public/ -> info.magnolia.maven.archetypes:magnolia-blossom-module-archetype (An archetype to create Magnolia modules using Blossom)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 3
Choose info.magnolia.maven.archetypes:magnolia-module-archetype version:
1: 1.1.0
2: 1.2.0
3: 1.2.1
4: 1.2.2
5: 1.2.3
6: 1.2.4-SNAPSHOT
7: 1.2.4
8: 1.2.5-SNAPSHOT
Choose a number: 8:
Downloading: https://nexus.magnolia-cms.com/content/groups/staff/info/magnolia/maven/archetypes/magnolia-module-archetype/1.2.5-SNAPSHOT/maven-metadata.xml
Downloaded: https://nexus.magnolia-cms.com/content/groups/staff/info/magnolia/maven/archetypes/magnolia-module-archetype/1.2.5-SNAPSHOT/maven-metadata.xml (1019 B at 0.6 KB/sec)
Define value for property 'groupId': : com.example.rest
Define value for property 'artifactId': : demo-rest-services
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  com.example.rest: :
Define value for property 'magnolia-bundle-version': : 5.6.6
Define value for property 'module-class-name': : DemoRestServicesModule
Define value for property 'module-name':  demo-rest-services: :
Confirm properties configuration:
groupId: com.example.rest
artifactId: demo-rest-services
version: 1.0-SNAPSHOT
package: com.example.rest
magnolia-bundle-version: 5.6.6
module-class-name: DemoRestServicesModule
module-name: demo-rest-services
 Y: : Y
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: magnolia-module-archetype:1.2.5-SNAPSHOT
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.example.rest
[INFO] Parameter: artifactId, Value: demo-rest-services
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.example.rest
[INFO] Parameter: packageInPathFormat, Value: com/example/rest
[INFO] Parameter: module-class-name, Value: DemoRestServicesModule
[INFO] Parameter: magnolia-bundle-version, Value: 5.6.6
[INFO] Parameter: package, Value: com.example.rest
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: com.example.rest
[INFO] Parameter: module-name, Value: demo-rest-services
[INFO] Parameter: artifactId, Value: demo-rest-services
[WARNING] CP Don't override file ~/dev/repo/magnolia/documentation/demo-rest-services/src/main/resources/demo-rest-services/i18n/module-demo-rest-services-messages_en.properties
[WARNING] CP Don't override file ~/dev/repo/magnolia/documentation/demo-rest-services/src/main/resources/demo-rest-services/dialogs/pages/dummy-page-properties.yaml
[WARNING] CP Don't override file ~/dev/repo/magnolia/documentation/demo-rest-services/src/main/resources/demo-rest-services/templates/components/dummy-component.yaml
[WARNING] CP Don't override file ~/dev/repo/magnolia/documentation/demo-rest-services/src/main/resources/demo-rest-services/templates/components/dummy-component.ftl
[WARNING] CP Don't override file ~/dev/repo/magnolia/documentation/demo-rest-services/src/main/resources/demo-rest-services/templates/pages/dummy-page.yaml
[WARNING] CP Don't override file ~/dev/repo/magnolia/documentation/demo-rest-services/src/main/resources/demo-rest-services/templates/pages/dummy-page.ftl
[WARNING] CP Don't override file ~/dev/repo/magnolia/documentation/demo-rest-services/src/main/resources/META-INF/magnolia/demo-rest-services.xml
[INFO] project created from Archetype in dir: /Users/cmeier/dev/repo/magnolia/documentation/demo-rest-services
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 59.369 s
[INFO] Finished at: 2018-05-09T19:00:25+07:00
[INFO] Final Memory: 12M/148M
[INFO] ------------------------------------------------------------------------

Java package name

Best practice

Add the Java class, which represents the REST resource, to a package with a name that allows versioning of the REST resource. Typically, the last part of the package name should indicate the version.
Example:
com.example.rest.service.v1.DemoEndpoint
package com.example.rest.service.v1;

@Path("/demo/v1")
public class DemoEndpoint {
  /* more code to be added here later on */
}
In this example:

  • The package name is com.example.rest.service.v1 .
  • The class name is DemoEndpoint.
  • The fully qualified class name (fqcn) is com.example.rest.service.v1.DemoEndpoint.

If you want to raise the version of the endpoint later on, without disabling the original version, create a package with a higher version such as com.example.rest.service.v2 .

Magnolia REST modules and recommended dependencies

We assume that you know the basics about how to manage dependencies, and that you manage these dependencies with Maven.

The Java API for RESTful Web Services - JAX-RS is defined in the packages javax.ws.rs and  javax.xml.bind. These are interfaces and sufficient for endpoint classes during compilation. However, on runtime, when the REST resources are used, a webapp also requires implementations of the these two mentioned packages. Magnolia uses RESTeasy for this purpose.

The dependencies (for both the interfaces and the implementations) are managed by the magnolia-rest-integration module.

Best practice

Depend on the  Magnolia REST modules. This ensures that you use the same versions of JAX-RS interfaces and implementation libraries as Magnolia.

You should at least depend on the magnolia-rest-integration module.

Your module, which depends on Magnolia REST module(s), automatically (transiently) inherits the dependencies from the Magnolia modules.

The Magnolia REST module contains three submodules.

Magnolia REST modules

magnolia-rest-integration

The REST Integration module installs the integration part of REST. The module:

  • Manages the dependencies for the required JAX-RS libraries.
  • Monitors /config/<module-name>/rest-endpoints for any custom endpoints you want to register. The monitoring mechanism is the same as used for observing registered dialogs, templates and apps. 
  • Installs a special servlet RestDispatcherServlet which dispatches requests to the individual endpoints registered in configuration. 
  • Lets you define additional providers or marshallers (called MessageBodyWorkers  in RESTeasy) you might need. The providers are responsible for translating the return object into JSON/XML and vice-versa.
  • Installs the default rest role that initially prevents access to unauthorized requests.

 Click here to expand to get the Maven dependency snippet

<dependency>
  <groupId>info.magnolia.rest</groupId>
  <artifactId>magnolia-rest-integration</artifactId>
</dependency>

magnolia-rest-services

The magnolia-rest-services module depends on the magnolia-rest-integration module. Therefore it provides everything explained in the preceding section.

In addition, it contains the Magnolia default REST endpoints  NodeEndpoint  and PropertyEndpoint  and the  CommandEndpoint , which are described on the REST API page.

This module also provides the  RepositoryMarshaller , which you can use within your custom endpoint.

 Click here to expand to get the Maven dependency snippet

<dependency>
  <groupId>info.magnolia.rest</groupId>
  <artifactId>magnolia-rest-services</artifactId>
</dependency>

magnolia-rest-content-delivery

This module provides the Delivery endpoint.

 Click here to expand to get the Maven dependency snippet

<dependency>
  <groupId>info.magnolia.rest</groupId>
  <artifactId>magnolia-rest-content-delivery</artifactId>
</dependency>

magnolia-rest-tools

The REST Tools module integrates the swagger tools into the Admin UI. These tools ease the development and testing of REST endpoints.

The module extends the RestDispatcherServlet with a custom, API-aware servlet that can read API annotations from all available REST endpoints. The servlet enables the endpoints in the Swagger API explorer. If you write your own endpoint you need to add annotations in the code yourself. 

The module is used for development and testing purposes only.

 Click here to expand to get the Maven dependency snippet

<dependency>
  <groupId>info.magnolia.rest</groupId>
  <artifactId>magnolia-rest-tools</artifactId>
</dependency>

Magnolia REST module versions

Make sure you use the same version for all Magnolia REST modules on which you depend.

The version of the Magnolia REST modules depends on the version of Magnolia you use to run your custom endpoint. We recommend you check the .pom file (or the parent .pom file) of your Magnolia webapp. The .pom file manages the version of the Magnolia REST modules using the property restVersion. If you are unsure, use the latest released version, which is currently: 2.1.2-SNAPSHOT.

How to create a Java class that acts as a REST resource

To create a Java class that acts as a REST resource, write a Java class — your service — and register this service in the Magnolia configuration workspace. Registering the service makes sure that the REST resource can be accessed by a default Magnolia servlet.

In this section we create a simple endpoint implementation and explain some important details for every custom Java REST endpoint in the context of Magnolia.

Creating the Java class

Create the class com.example.rest.service.v1.DemoEndpoint. This is the code for a simple working example:

package com.example.rest.service.v1;

import info.magnolia.rest.AbstractEndpoint;
import info.magnolia.rest.EndpointDefinition;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/demo/v1")
public class DemoEndpoint<D extends EndpointDefinition> extends AbstractEndpoint<D> {

    public DemoEndpoint(D endpointDefinition) {
        super(endpointDefinition);
    }

    @Path("/hello")
    @GET
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response hello() {
        return Response.ok().build();
    }
}
In the example above, note that the class:

  • Extends  AbstractEndpoint  (line 13).
  • Has at least one method, which (here) has the return type  javax.ws.rs.core.Response .
  • Declares its path on both the class and the method level (lines 12, 19).
  • Declares its response type(s) (line 21).
  • Declares the HTTP methods through which it can be accessed (line 20).

Let's have a closer look at some of these details below.

Extending info.magnolia.rest.AbstractEndpoint

See line 13 in the example above. By extending AbstractEndpoint , the endpoint can be configured. We will see later on what the configuration and registration looks like. When a request comes to Magnolia mapped to this endpoint, an instance is created via Inversion of Control (IoC). During this process, the IoC framework also creates an instance of  EndpointDefinition , which is injected into the constructor of our endpoint class.

Using javax.ws.rs.* annotations

If you are not yet familiar with the javax.ws.rs.*  package, we recommend you take some time to learn the basics. This package contains many annotations that can be used to declare the details of Java-based REST resources. We will see how to use these annotations below.

Returning javax.ws.rs.core.Response

javax.ws.rs.core.Response is the standard response type for a Java REST endpoint.

On line 22 we declare the return type.

On line 23 we use the static method #ok (without an argument), which ensures that:

  • The HTTP response code is 200 (if everything else goes well). 
  • Since we use the method #ok with no argument, no payload is created and the response does not have a body, just headers.

Understanding @Producing

On line 21, by using the @Produces annotation, we can declare a list of possible HTTP response content types.

Note that, in our example, there is no need to declare the HTTP response content type because the method only returns response headers but no response body. However, if the method creates a payload (for instance JSON or XML) to be returned as an HTTP response body, then you should declare the response content type with @Produces.

Understanding @Path

A method on a REST endpoint has a distinct path to access it with a RESTful URL. A path generally consists of several parts:

  1. Magnolia REST base path/<context>/.rest.
    If you are running a local development bundle, the base REST path is typically /magnoliaAuthor/.rest or /magnoliaPublic/.rest.
    On a productive environment, where you serve the webapp from the root context, the base REST path is /.rest.
    This path is mapped to the Magnolia REST servlet.
  2. REST endpoint base path. This path is declared on the class level with the @Path annotation; see line 12 on the example above. 
    This path in our example is /demo/v1.
    The value of this path can be chosen arbitrarily. Note that it typically contains the version of the endpoint (which is /v1 here).
    If your package name reflects the version, you can use the same version identifier on both the package name and the endpoint base path.
  3. REST endpoint method path. This path is declared on the method level with the  @Path  annotation; see line 19 on the example above. 
    The example shown is static with the value /hello. However, the method path can be dynamic in combination with @PathParam, we will see an example later on.

To summarize, the path to a REST "resource" is a combination of:

Magnolia REST base path+REST endpoint base path+REST endpoint method path
/<context>/.rest
/demo/v1
/hello

/<context>/.rest/demo/v1/hello

Declaring the HTTP method

On line 20 we use the annotation @GET. This declares that the REST resource defined by this method can be accessed with the HTTP method GET only.

If you try to access the resources using another method (for example with POST), the resource returns an error HTTP response code. We will test this later on with cURL.

You can only declare one HTTP method per method. The Java compiler shows an error if you try to declare two methods (by using two annotations).

Registering the REST resource in Magnolia

REST resources in Magnolia should be registered in the configuration workspace. This ensures that the REST resource and its configuration are known in the Magnolia  EndpointDefinitionRegistry

You can configure REST endpoints on every module. This is what the configuration looks like:

Node nameValue

 modules


 <your-module>


 rest-endpoints


 demo


 class

info.magnolia.rest.service.node.definition.ConfiguredNodeEndpointDefinition

 implementationClass

com.example.rest.service.v1.DemoEndpoint

Properties:

rest-endpoints

required

The folder which contains a list of endpoint definitions. The name of the folder must be rest-endpoints.

<endpoint-name>

required

The name of the endpoint. The name is arbitrary but must be unique.

class

required

The fully qualified class name of the definition class. Typically info.magnolia.rest.EndpointDefinition or a subclass.

implementationClass

required

The fully qualified class name of the REST endpoint java class.

Granting access to the REST resource in the Security app

The last step before you can access the resource with a client is to grant access to the REST resource in the Security app. 

Permissions to issue REST requests are controlled using the standard Magnolia  role-based security mechanism .

The magnolia-rest-intagration module installs a  rest  role which has the permission to issue requests to the  nodes  and  properties  endpoints by default.

You can extend the existing rest role to provide web access to the path of your custom REST resource, or you can duplicate the existing role to create a new one just for your use case. In either case, make sure that you assign this role to the users who need to access this resource.

For our example we create a new role named demo-rest-role and assign it to superuser on the magnoliaAuthor instance. You also can download this role from the GIT repository.

Details of the role

Role info:

Access Control Lists (ACL):

Web access:

Access typePathValue
Deny/.rest*Deny all to begin.
Get&Post/.rest/swagger*

Allow access to the Swagger tools. This is only required if you have the magnolia-rest-tools module installed, which is used for testing and development purposes only.

Get&Post/.rest/demo/v1*

Allow access to our custom endpoint whose access path is .rest/demo/v1, as explained in Understanding @Path.

Note that Get would be sufficient, but we use Get&Post since we will create more methods later on.

Access type meanings:

Access typeMeaning
DenyEverything is denied.
GetOnly the HTTP method GET is allowed.
Get&PostAll HTTP methods available are allowed: GET, POST, PUT and DELETE.


This is the list of roles assigned to superuser:

Depending on your bundle, you may have more roles assigned to superuser.

Accessing the resource

Now everything is ready to access the custom endpoint.

Using cURL

cURL is a useful tool to test REST resources. To check if you have it installed, open a command shell and type:

curl
When it is installed, the shell displays something like this:
curl: try 'curl --help' or 'curl --manual' for more information
Once you have confirmed that cURL works, try the following request:
curl -i --user superuser:superuser -X GET --header 'Accept: application/json' 'http://localhost:8080/magnoliaAuthor/.rest/demo/v1/hello'
Since we provide the -i option, cURL displays the response header. Remember that the requested resource does not produce a payload, so there is no response body.
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=624D8A0283867F9FA07D02F72B9B72EA; Path=/magnoliaAuthor/; HttpOnly
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 0
Date: Tue, 04 Jul 2017 14:36:35 GMT
Now try to access the resource via the POST method:
curl -i --user superuser:superuser -X POST --header 'Accept: application/json' 'http://localhost:8080/magnoliaAuthor/.rest/demo/v1/hello'
Here are the response headers:
HTTP/1.1 500 Internal Server Error
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=C4A7CB1D1AA09967FD4E413E61D9AA4C; Path=/magnoliaAuthor/; HttpOnly
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: text/plain;charset=UTF-8
Content-Length: 79
Date: Tue, 04 Jul 2017 14:40:17 GMT
Connection: close

RESTEASY003650: No resource method found for POST, return 405 with Allow header
This is expected. The method #hello is annotated with @GET declaring it can only be accessed with the HTTP method GET. See the section declaring the HTTP method.

Note that you pass the username and password in clear text in this cURL call. This is acceptable when testing on a local machine, however, we advise against doing this to access a production environment server. 

In addition to cURL, there are other ways of testing your custom endpoint, such as Swagger tools.

Using Swagger tools

The Swagger framework is supported by a set of core tools for designing, building, and documenting RESTful APIs.

Source: https://swagger.io/tools/

Magnolia provides integration with Swagger tools directly in the Admin UI.

Swagger tools are for development and testing purposes only.

To test your endpoints with Swagger:

  • Add the magnolia-rest-tools module to your webapp.
  • Set the Swagger API base path.
  • Use the Swagger annotations in your endpoint class.
  • Use the app within the Magnolia Admin UI to test your endpoints.

Installing the module

Make sure the magnolia-rest-tools module is part of your webapp. See recommended dependencies above about how to manage Swagger integration with Maven.

Setting the Swagger API base path

The Swagger API explorer tool searches for the API at a path set in /modules/rest-tools/config/apiBasepath. The default value is  http://localhost:8080/.rest . The value for this property must match the following pattern:

<protocol>://<hostname>:<port>/<context>/.rest

When using one of Magnolia's preconfigured bundles running on localhost, set the property to  http://localhost:8080/magnoliaAuthor/.rest .

Set the path to where REST services reside on your system. If you run the standard Magnolia bundle and work on the author instance, set the path to  http://localhost:8080/magnoliaAuthor/.rest .

Node nameValue

 modules


 rest-tools


 config


 apiBasepath

http://localhost:8080/magnoliaAuthor/.rest

After setting the base path, restart Magnolia.

Swagger is in Dev > REST Tools.

Using Swagger annotations

    MultiExcerpt named 'swagger-start' was not found
The page: Getting started with REST was found, but the multiexcerpt named 'swagger-start' was not found. Please check/update the page name used in the 'multiexcerpt-include macro.

To have our custom endpoint appear in the Swagger UI, we must add some annotations to the class of our custom endpoint.

This is the modified class of the example above — now enriched with annotations:

com.example.rest.service.v1.DemoEndpoint
package com.example.rest.service.v1;

import info.magnolia.rest.AbstractEndpoint;
import info.magnolia.rest.EndpointDefinition;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Api(value = "/demo/v1", description = "The demo endpoint")
@Path("/demo/v1")
public class DemoEndpoint<D extends EndpointDefinition> extends AbstractEndpoint<D> {

    private static final String STATUS_MESSAGE_OK = "OK";
    private static final String STATUS_MESSAGE_METHOD_NOT_ALLOWED = "Method Not Allowed";


    public DemoEndpoint(D endpointDefinition) {
        super(endpointDefinition);
    }

    @Path("/hello")
    @GET
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    @ApiOperation(value = "Say hello to the endpoint.")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = STATUS_MESSAGE_OK),
            @ApiResponse(code = 405, message = STATUS_MESSAGE_METHOD_NOT_ALLOWED),
    })
    public Response hello() {
        return Response.ok().build();
    }
}           
Notes:

  • Line 16: @Api  — Marks a class as a Swagger resource. This is required to see your endpoint on the Swagger UI.
  • Line 31: @ApiOperation  —  A description of the endpoint. 
  • Lines 32-35:  @ApiResponses @ApiResponse  —  Lists the possible response (codes).

After you add the annotations, restart the server (hot deployment will not work).

When you open the REST Tools app again, you should see your custom endpoint in Swagger UI list:

The Swagger UI: 

If the REST resource can accept parameters, the UI reflects that with distinct fields for each parameter.

About marshalling and data conversion

An endpoint dealing with data must manage two types of data conversion:

  • Transforming data into a simple Java bean or POJO (Plain Old Java Object) and vice versa.
  • Transforming the POJO into JSON or XML and vice versa.

If the endpoint handles both read (GET) and write (PUT to create and POST to update) actions, marshalling and data conversion must also work in both directions.

In this section we focus on some aspects of this kind of data conversion.

Converting a data object into a POJO and vice versa

Regardless of the type of data you deal with, in the context of a REST endpoint, at some point you typically transform "data" into a POJO (and vice versa).

There is no "one size fits all" for transforming a data object into a POJO and vice versa. The best solution for you depends on your use case.

JCR to POJO

The magnolia-rest-services module provides classes to transform JCR nodes and properties into POJOs and vice versa.

  • RepositoryMarshaller  is the transformer.
  • RepositoryNode  is the type of the POJO.

The Magnolia endpoints  PropertyEndpoint  and  NodeEndpoint  use these classes. You also can use the classes for your custom endpoints. However, note that the structure of the POJO and of the resulting payload after marshalling may not cover your requirements.

Marshalling and unmarshalling - creating and receiving payload

Transforming a POJO into JSON or XML is known as marshalling. Transforming JSON or XML into a POJO is known as unmarshalling. Both processes are sometimes referred to as marshalling.

Marshalling is handled by the REST framework by the JAX-RS API implementation.

However, when dealing with payload (XML or JSON) in your endpoint, note the following:

  • To create payload, you must PUT a POJO into the response of the endpoint method.
  • When receiving payload from a request, the responsible method must have an appropriate signature in order to initiate unmarshalling.

Adding POJOs to the REST response to deliver payload

Imagine you have a POJO with the name Lunch and two properties: food and beverage.

com.example.rest.pojos.Lunch
public class Lunch extends BasicStorable {
 
    public static String BADFOOD = "Bad food";
 
    private String food;
    private String beverage;
 
    /**
     * In the context of REST it is necessary to have a default constructor written in the class.
     * (Otherwise object mappers may fail when trying to create an object from a giving payload.)
     */
    public Lunch() {
 
    }
 
    public Lunch(String food, String beverage) {
        this.food = food;
        this.beverage = beverage;
    }
 
    public String getFood() {
        return food;
    }
 
    public void setFood(String food) {
        this.food = food;
    }
 
    public String getBeverage() {
        return beverage;
    }
 
    public void setBeverage(String beverage) {
        this.beverage = beverage;
    }
}
Now you require a method which can deliver a lunch payload. Endpoint method(s) providing a Lunch object would look like this:
    @Path("/lunch2")
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    public Response lunch2() {
        Lunch pojo = new Lunch("Rösti mit Geschnetzeltem", "Ueli Weizen");
        return Response.ok(pojo).build();
    }


    @Path("/lunch")
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    public Lunch lunch() {
        Lunch pojo = new Lunch("Svíčková na smetaně ", "Gambrinus Plzen");
        return pojo;
    }   	
Note that the snippet shows two methods: #lunch and #lunch2. Both produce the same payload when called from outside, however, their implementation is slightly different. 

#lunch2

  • Uses the generic return type javax.ws.rs.core.Response.
  • Puts the POJO (of the type Lunch) into this response; see line 6.
  • This approach provides high flexibility concerning the response code, for instance you can do things like this:

    return Response.status(Response.Status.NOT_ACCEPTABLE).build();
    //...
    return Response.status(Response.Status.BAD_REQUEST).build();

#lunch

  • Uses the specific POJO as return type. Here it is com.example.rest.pojos.Lunch.
  • Returns the POJO directly on the method.

Note that both method produce the same response headers and identical payload (structure, in this example only the values of the food and beverage properties differ). 

 Click to see the result of calling the above examples with cURL

#lunch2

curl -i --user superuser:superuser -X GET --header 'Accept: application/json' 'http://localhost:8080/magnoliaAuthor/.rest/demo/v1/lunch2'
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=427FFB4FD60492BDD7D8C76E28F91222; Path=/magnoliaAuthor/; HttpOnly
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 61
Date: Wed, 05 Jul 2017 16:48:53 GMT

{"id":"b8a3da44-35cf-48a5-9f69-dc38bff881da","food":"Rösti mit Geschnetzeltem","beverage":"Ueli Weizen"}
#lunch
curl -i --user superuser:superuser -X GET --header 'Accept: application/json' 'http://localhost:8080/magnoliaAuthor/.rest/demo/v1/lunch'
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=8842AE5F355D4E4E188587483B6B0B52; Path=/magnoliaAuthor/; HttpOnly
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 64
Date: Wed, 05 Jul 2017 16:51:29 GMT

{"id":"17f552a0-11e8-4d5d-898b-ecf17f05b1ba","food":"Svíčková na smetaně ","beverage":"Gambrinus Plzen"}
Note that the payload also contains the property id. This property comes from the super class com.example.rest.pojos.BasicStorable of Lunch; we will need this id later on.

Both approaches to produce payload with POJOs shown here work in more complex scenarios. For example: you could return a list of POJOs; the properties of the POJO could also be POJOs; and so on.

Receiving payload and getting the POJO

Endpoint methods that receive a payload (JSON or XML) are typically used to create or update an object. 

Let's imagine an example where the endpoint allows you add a lunch object to a store. In the context of a data store, adding is like creating. To create an object, a REST service is called with the the HTTP method PUT.

This code snippet shows the method to add a lunch:

com.example.rest.service.v1.DemoEndpoint#storeLunch
    @Path("/store-lunch")
    @Consumes({MediaType.APPLICATION_JSON})
    @PUT
    @Produces({MediaType.APPLICATION_JSON})
    public Response storeLunch(Lunch lunch){
        if(lunch != null){
            try {
                store.add(lunch);
                return Response.ok(lunch).build();
            } catch (Exception e) {
                log.error("Failed to store the lunch");
                    return Response.status(Response.Status.NOT_ACCEPTABLE).build();
            }
        }else{
            return Response.status(Response.Status.BAD_REQUEST).build();
        }
    }// eof: storeLunch

  • Line 140: It is generally considered good practice to tell the service what type of data it consumes. However, it is not required.

  • Line 143: The most important point in the current context is the method signature.
    The method must contain a parameter of the type to which the payload should be mapped to.

  • Line 146: If the mapping was successful, we add the lunch POJO to the store. The store actually is a mock store, it does not really store data in the example. (See  BlackboxDatastore on Git.). However, store#add may throw an exception if it fails.
  • Line 147: Build the response with the response code 200 (ok), and also put the lunch object into the payload, it now carries the object id too. Giving back the just created object could be used on a client to render an update UI or to summarize the successful add action.
  • Line 148-150: These lines handle the exception thrown by the store. You can provoke this by sending a payload to the method where the property food has the value Bad food.

You can test the new method using cURL or the Swagger tools.

 Click to see the result of calling the storeLunch method with cURL

curl -i http://localhost:8080/magnoliaAuthor/.rest/demo/v1/store-lunch \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-X PUT \
--user superuser:superuser \
--data \
'{"food": "Pizza Napoli", "beverage": "Birra Moretti"}'


HTTP/1.1 200 OK
^Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=7E99151D9036CDA5DC200BA8FE70EACB; Path=/magnoliaAuthor/; HttpOnly
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 94
Date: Thu, 06 Jul 2017 16:33:04 GMT

{"id":"b64633b7-4555-4571-86b6-8e45bdab61b3","food":"Pizza Napoli","beverage":"Birra Moretti"}
The first attempt was successful! (smile)

Now provoke an error with "Bad food":

curl -i http://localhost:8080/magnoliaAuthor/.rest/demo/v1/store-lunch \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-X PUT \
--user superuser:superuser \
--data \
'{ "food": "Bad food", "beverage": "Bud Light"}'


HTTP/1.1 406 Not Acceptable
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=FEA3475D150D8106F8A265E97060BC2F; Path=/magnoliaAuthor/; HttpOnly
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 0
Date: Thu, 06 Jul 2017 16:36:15 GMT
The "Bad food" was rejected. No payload was returned and the HTTP response code is 406 (Not Acceptable).

Implementing a method to update an object (and using the HTTP method POST) works in a similar way. In such a case, the payload sent to the REST endpoint must contain the object id in order to update the correct item on the data store. 

The same-origin policy problem

Let's assume you want to use a data API endpoint to serve content managed with Magnolia apps to independent applications located "outside" of Magnolia. For example, you want to use an AngularJS application to display your content.
We also assume Magnolia and the Angular application are hosted on completely different systems.

According to the same-origin policy:

(...) a web browser permits scripts contained in a first web page to access data in a second web page, but only if both web pages have the same origin.

Wikipedia

Therefore, if your application resides within a domain that is different from the domain that hosts your data, the web browser will actively prevent accessing content stored under the host domain.

An elegant solution to solve the same-origin policy without changing Apache configuration is the Magnolia Add HTTP Headers filter. This fiter is available out-of-the-box; you only need to configure it.

Sample code on GIT

The sample code for the examples on this page is available on the Magnolia GIT repository