Lars Fischer
Lars Fischer
Aerophonist with an IT hobby.
Nov 14, 2017 6 min read

Creating custom REST endpoints in Magnolia

thumbnail for this post

Recently I had to provide several web services for creating application data in a Magnolia CMS app by using REST interfaces.

If you haven’t already, I would suggest you first read the excellent official documentation about REST based web services available from Magnolia (see end of article for resources).

You can find the code on GitHub, everything is contained in a Magnolia module based on Maven.

Use cases provided

In this article the following use cases will be covered:

  • uploading a file
  • posting data inline

In addtion to that, you’ll find some information about how to test those services with SoapUI - a tool that I normally don’t use but that was required in this context.

Adding needed dependencies

Magnolia CMS by default uses RESTEasy as implementation for the JAX-RS specification.

Depending on your bundle configuration you might need to add the following Maven dependencies to your Magnolia module:

If you also want to use the Swagger UI from within AdminCentral for convenient testing (recommended), you might also need to install the magnolia-rest-tools module (also see the official documentation).

<!-- module dependencies -->
<dependency>
  <groupId>info.magnolia.rest</groupId>
  <artifactId>magnolia-rest-services</artifactId>
  <version>${restVersion}</version>
</dependency>
<dependency>
  <groupId>info.magnolia.rest</groupId>
  <artifactId>magnolia-rest-tools</artifactId>
  <version>${restVersion}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jboss.resteasy/resteasy-multipart-provider -->
<dependency>
  <groupId>org.jboss.resteasy</groupId>
  <artifactId>resteasy-multipart-provider</artifactId>
  <version>${restEasyMultipartVersion}</version>
</dependency>

(Adjust module versions as needed).

And of course we adhere to Magnolia best practices 😃 so we also add dependencies on other Magnolia modules to the Magnolia module descriptor (this guarantees that needed modules are loaded before our own module).

Creating an endpoint for file uploads

Now we create our basic endpoint Java class, here called “UploadServicesEndpoint.java”.

@Path("/services/v1")
public class UploadServicesEndpoint<D extends EndpointDefinition> extends AbstractEndpoint<D> {
    public UploadServicesEndpoint(D endpointDefinition) {
      super(endpointDefinition);
    }

    @POST
    @Path("/uploadFile")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces({MediaType.TEXT_PLAIN})
    public Response uploadFile(MultipartFormDataInput uploadedFile) throws Exception {
        return Response.ok().entity("File successfully uploaded.\n").build();
    }
}

This is a very basic definition just providing the @Path and other JAX-RS annotations needed for uploading a file.

Please make sure access to the class constructor is public, otherwise the endpoint class cannot be registered by Magnolia!

To be able to use the Swagger UI, we also add annotations for that (see @Api):

@Api(value = "/services/v1")
@Path("/services/v1")
public class UploadServicesEndpoint<D extends EndpointDefinition> extends AbstractEndpoint<D> {
    private final Logger log = LoggerFactory.getLogger(UploadServicesEndpoint.class);

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

    @POST
    @Path("/uploadFile")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces({MediaType.TEXT_PLAIN})
    @ApiOperation(value = "Upload file to Magnolia.", notes = "Upload file.")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "OK"),
            @ApiResponse(code = 400, message = "BAD_REQUEST")
    })
    public Response uploadFile(MultipartFormDataInput uploadedFile) throws Exception {
        if (uploadedFile != null) {
            log.info("Receceived file...");
            try {
                // do something useful, eg parse a CSV file and import data into Magnolia
                // uploadedFile.getFormDataMap()...
            } catch (Exception e) {
                log.error("Problem while uploading file.", e);
                return Response.status(Response.Status.BAD_REQUEST).entity("ERROR: Problem while uploading file: " + e.getMessage() + ".\n").build();
            }
        }
        return Response.ok().entity("File successfully uploaded.\n").build();
    }

Creating an endpoint for posting data inline

In our custom REST endpoint Java class we add another method:

@POST
@Path("/uploadFileData")
@Consumes({MediaType.TEXT_PLAIN})
@Produces({MediaType.TEXT_PLAIN})
@ApiOperation(value = "Post file data inline.", notes = "Upload file by providing inline data.")
@ApiResponses(value = {
        @ApiResponse(code = 200, message = "OK"),
        @ApiResponse(code = 400, message = "BAD_REQUEST")
})
public Response uploadFileData(String data) throws Exception {
    log.info("uploadFileData - received data:\n" + data + "\n");

    try {
        // do something useful with the data...
    } catch (Exception e) {
        log.error("Problem while parsing inline data.", e);
        return Response.status(Response.Status.BAD_REQUEST).entity("ERROR: Problem while parsing inline data: " + e.getMessage() + ".\n").build();
    }

    return Response.ok().entity("Inline data successfully created.\n").build();
}

Now it’s time to build the wepapp (WAR) with Maven and startup Magnolia.

Magnolia configuration

REST tools base path setting

For using the REST tools app (under the “DEV” tile in AdminCentral) we first configure the correct context for the property apiBasePath in the rest-tools module. If you don’t use the Magnolia REST tools app this configuration is not needed.

Rest endpoint configuration

In Magnolia configuration, we configure in our module the rest endpoint for the Java class we just created as follows:

After applying these configurations, RESTART Magnolia to make the change a reality… !

Creating a role

By default, the newly created endpoint is not accessible from the outside because we did not create permissions yet.

In the Magnolia Security app, create a new role called “rest-services-uploads” and apply the following settings in the tab “WEB ACCESS”:

For “outside” testing without providing credentials within the REST call, you can assign the role to the anonymous system user. This make the service easier to test but is less secure. When assigning the role to a different user you will have to provide the user credentials with your call if you are not using the Magnolia REST tools app logged in with a user that has the appropriate rights assigned for testing. Also consider that certain Java methods might have to operate in system context depending on what you want to do with your code.

Filter configuration

By default it’s not allowed to upload arbitrary files to Magnolia so we also need to create a configuration for the Multipart request filter.

In the Magnolia Configuration app, navigate to

server => filters => multipartRequest => bypasses

and create a new bypass configuration:

Testing the endpoints

Now everything should be in place to test our web service endpoints.

Testing with curl

On the command line, we can use curl for testing. Adjust the commands below with your host address and web application context, open a terminal and execute:

curl -i -F fileData=@"/path/to/testFile.txt" http://localhost:8080/your_webapp_context/.rest/services/v1/uploadFile

If everything went well, you should see a response like

HTTP/1.1 100 

HTTP/1.1 200 
Set-Cookie: JSESSIONID=296FBB7F31E6E81550528EBAF89E8FFC; Path=/your_webapp_context; HttpOnly
Set-Cookie: NEW_VISITOR=new; Max-Age=86400; Expires=Wed, 08-Nov-2017 20:20:55 GMT; HttpOnly
Set-Cookie: VISITOR=returning; Path=/.rest/services/v1/uploadFile; HttpOnly
X-Magnolia-Registration: Registered
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: 28
Date: Tue, 07 Nov 2017 20:20:57 GMT

File successfully uploaded.

For testing the inline data version use:

curl -X POST --header 'Content-Type: text/plain' --header 'Accept: text/plain' -d 'here comes the content of your plain text data' 'http://localhost:8080/your_webapp_context/.rest/services/v1/uploadFileData'

If the anonymous user does not have the necessary roles assigned, you might need to add the parameters for the Magnolia user to be used, eg:

curl --user superuser:superuser -X POST --header 'Content-Type: text/plain' --header 'Accept: text/plain' -d 'here comes the content of your plain text data' 'http://localhost:8080/your_webapp_context/.rest/services/v1/uploadFileData'

If the response contains a HTML form, your security settings in Magnolia most likely are not correct.

Testing with SoapUI

Despite it’s age and dated UI, SoapUI still seems to be used in quite some organizations. After downloading the latest (free) version, I first had to fix the package content on macOS to make the program actually work (which is strange, because this bug seems to be known since a few years already…).

See this article on how to fix SoapUI on macOS.

File upload

  • change the method to POST
  • change the media Type to multipart/form-data
  • select RAW for the format of the output on the right side

  • select the attachments tab
  • click on the plus button and select your file from the disk

Depending on the method you created you might need to

  • adjust the content type for the file
  • set the correct parameter name in the ContentID column

If your method uses a specific paramter name to receive the file content, eg

formData.get("data")

then your method will fail if you don’t set the correct name in the ContentID column because the “get” above will receive a NULL value.

If everything went well, you will receive a result like:

Resources

Sample source code: