1. Overview

Learn how to set up a Microservice that can fill the role of an interceptor of any of the available interceptor types. Java code examples of such microservices can also be found here.

2. Basic Microservice Setup

In this tutorial, we will implement a Spring Boot microservice for the interceptors' obligations that can easily be integrated into the yuuvis® Momentum infrastructure (kubernetes). Learn more about Spring Boot Applications in their official documentation. To integrate with kubernetes, some additional application files need to be implemented in the microservice.

One nice thing about microservices is that you can use any programming language you like—as long as it supports REST which ensures communication with the other services.

2.1. Necessary Microservice Application Files

In this tutorials' code project, we implement some of our suggestions for the architecture of yuuvis® Momentum-integrated microservice. The following classes provide no logic specific to interceptors, but are still essential for the microservice to operate.

3. Functional Implementation of each Interceptor Type

In the following sections, an arbitrary practical example for each of the three interceptor types is provided for demonstrational purposes. At the end of the article, you will find a link to the Git Repository that houses the complete code project for your inspiration.

3.1. Type 'getContent'

Imagine the following situation: A large PDF file consisting of several sub-documents is stored in the system. When one specific document is requested, the respective pages should be extracted and returned as a separate PDF file. A good way to achieve this is using an interceptor, i.e., a service that runs in the background listening for its cue. When it occurs, the interceptor is called by the yuuvis® API system and performs its specific task, thereby rerouting the standard flow of the application.

3.1.1. Creating the 'getContent' REST Controller

Finally, we will need to create a REST endpoint to the service. A REST controller class will handle HTTP requests, producing the REST endpoint.

A traditional MVC controller and a RESTful web service controller in Spring differ significantly regarding the creation of the HTTP response body: In the traditional Model View Controller (MVC) paradigm, the controller would use a view technology in order to render an HTML version of the data and return a view object to be displayed. In Spring, however, the controller creates and returns a new instance of the resource representation class instead which will be written to the HTTP response as JSON.

Create the class PdfPageSelectorRestController and annotate it as @RestController. This will turn the class into a REST controller whose methods return domain objects instead of views (short for @Controller and @ResponseBody).

Rest Controller for the PDF Page Selector getContent Interceptor
@RestController
@RequestMapping("/api")
public class PdfPageSelectorRestController
{
    @Autowired
    private PdfPageSelectorService pdfPageSelectorService;

    @PostMapping(value = "/dms/objects/{objectId}", headers =   "content-type=application/json")
    public void getContentByPostRequest(@RequestBody Map<String, Object> dmsApiObjectList, @PathVariable("objectId") String objectId,
                                        @RequestHeader(value = "Authorization", required =false) String authorization,
                                        HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException
    {
        servletResponse.setContentType(MediaType.APPLICATION_PDF_VALUE);
        pdfPageSelectorService.extract(servletResponse.getOutputStream(), dmsApiObjectList, objectId, authorization);
    }
}

The controller sets the media type of response to PDF and calls the extract method of the PDFPageSelectorService.

3.1.2. Modelling a Sub-Document

As a document is identified by its start and end page, the domain object simply represents these two page numbers.

Pages.java Class Describing a Sub-Document of a PDF Document public class Pages
{

    private int startPage;
    private int endPage;

    public Pages(int startPage, int endPage)
    {
        this.startPage = startPage;
        this.endPage = endPage;
    }
    //omitted getter/setter methods
}

3.1.3. Creating the Page Extractor

This is the core of the service. Create the class PdfPageSelectorService and annotate it as @Service. This indicates that it holds the business logic and will communicate with the repository layer.

The extract method first gets the page boundaries, i.e., start and end page numbers, from the input by simply removing the page: prefix and splitting the string at the - character:

Creating Sub-Document Instances from Compatible Documents
if (range.startsWith("page:"))
{
    ((Map<String, Object>)contentSreamObject.get(0)).remove("range");
    String[] bounds = range.substring("page:".length()).split("-");
    int startPage = Integer.parseInt(bounds[0]);
    int endPage = Integer.parseInt(bounds[1]);

    return new Pages(startPage, endPage);
}

Next, it calls the repository via the REST template and sends a POST request with all object data.

Retrieving the Document from the yuuvis® Repository
restTemplate.execute(repositoryUrl + "/" + objectId, HttpMethod.POST, (ClientHttpRequest requestCallback) ->
  {
    if (StringUtils.hasLength(authorization))
    {
        // delegate auth header
        requestCallback.getHeaders().add(HttpHeaders.AUTHORIZATION, authorization);
    }
    requestCallback.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_PDF));
    requestCallback.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    requestCallback.getBody().write(this.objectMapper.writeValueAsString(requestObjects).getBytes("UTF-8"));
  },
new StreamResponseExtractor(outputStream, pages.getStartPage(), pages.getEndPage()));

3.1.4. Extracting Content from a PDF

The StreamResponseExtractor then extracts the pages of the sub-document from the streamed PDF (using PDFBox) and writes them to a new PDF file. The PDFBox functionality is wrapped in the PdfTools helper class.

The advantage of streaming is that large files do not need to be copied first but can be handled on the fly. This is not required though: You could just as well get the file from the repository first and store it locally before processing.

Creating the Sub-Document Using PdfTools
public static void extractPageFromStream(InputStream inputStream, int startPage, int endPage, OutputStream outputStream)
    {
        try
        {
            Splitter splitter = new Splitter();
            splitter.setStartPage(startPage);
            splitter.setEndPage(endPage);
            splitter.setSplitAtPage(endPage - startPage + 1);

            try (PDDocument document = PDDocument.load(inputStream))
            {
                List<PDDocument> documents = splitter.split(document);

                if (documents.size() != 1)
                {
                    throw new IllegalArgumentException("cannot split document, wrong number of split parts");
                }
                try (PDDocument doc = documents.get(0))
                {
                    PdfTools.writeDocument(doc, outputStream);
                }
            }
        }
        catch (Exception e)
        {
            LOGGER.info(ExceptionUtils.getMessage(e));
            throw new IllegalArgumentException(ExceptionUtils.getMessage(e));
        }
    }

    private static void writeDocument(PDDocument doc, OutputStream outputStream) throws IOException
    {
        try (COSWriter writer = new COSWriter(outputStream))
        {
            writer.write(doc);
        }
    }

3.1.5. Interceptor 'getContent' Configuration

Create the interceptorConfiguration.json file, add the following configuration (either JavaScript or SpEL) and save it to the configuration server, either by updating the git repository with the new state, or, in systems running the 'native' profile on the config service, simply changing the configuration file in the file-system of the config service itself.

You only need one configuration—whether you prefer JavaScript or SpEL is up to you.

When configuring a getContent interceptor, we can infer the objects' metadata from the predicate.

Condition in JS
{
  "interceptors" : [
    {
      "type" : "getContent",
      "predicate" : "js:function process(dmsApiObject){return dmsApiObject[\"contentStreams\"][0][\"range\"]!=null && dmsApiObject[\"contentStreams\"][0][\"range\"].startsWith(\"page:\")}",
      "url" : "http://examplewebhook/api/dms/objects/{system:objectId}",
      "useDiscovery" : false
    }
  ]
}
Condition in SpEL
{
  "interceptors" : [
    {
      "type" : "getContent",
      "predicate" : "spel:contentStreams[0]['range'] != null ? contentStreams[0]['range'] matches '(?i)^page:.*' : false",
      "url" : "http://examplewebhook/api/dms/objects/{system:objectId}",
      "useDiscovery" : false
    }
  ]
}

Finally, restart the API Service to apply the new configuration to the system.

Whenever we need to handle documents containing critical information during their development within a content management system, we need to ensure that the accessibility of such documents is restricted for all users without a specific authorization for those documents. To implement such a mechanism in our yuuvis® system, we can make use of the search interceptor type together with a system tag that will need to be set on all documents.

3.2.1. Creating the search REST Controller

The REST controller offering the endpoint for the interceptor mechanism follows in the footsteps of the PdfPageSelectorRestController, barring the absence of the objectId in the URL and the changed response content type.

Search Interceptor Rest Controller Class
@RestController
@RequestMapping("/api")
public class QueryByTagFilterRestController {
@Autowired
private QueryFilterService queryFilterService;

    @PostMapping(value = "/dms/objects/search", headers = "content-type=application/json")
    public void searchByPostedQuery(@RequestBody Map<String, Object> incomingQuery,
                                    @RequestHeader(value = "Authorization", required = false) String auth,
                                    HttpServletRequest servletRequest,
                                    HttpServletResponse servletResponse) throws IOException {
        servletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
        queryFilterService.filterQueryByTag(servletResponse.getOutputStream(), incomingQuery, auth);
    }
}

3.2.2. Enriching an Incoming Query Object

Enriching an Incoming Query
public Map<String, Object> enrichQueryByTagFilter(Map<String, Object> incomingQuery){
        Map<String, Object> queryMap = (Map<String, Object>)incomingQuery.get("query");
        String statement = String.valueOf(queryMap.get("statement"));
        String filteredStatement = "";
        if (statement.contains("WHERE")){
            filteredStatement = statement + " AND system:tags[\"test\"].state > 1";
        } else {
            filteredStatement = statement + " WHERE system:tags[\"test\"].state > 1";
        }
        queryMap.replace("statement", filteredStatement);
        incomingQuery.replace("query", queryMap);

        return incomingQuery;
    }

Essentially, we will enrich each incoming query from less authorized users on the search service using the interceptor, adding a filtering statement excluding documents where the state of our test tag does not indicate the information can be released systemwide. This means the documents we would want to exclude from the users' visibility need to have the test tag with a state value lower than 3. Increasing the state beyond this number will result in the filter to no longer apply, thereby making the document public to all users affected by the interceptor predicate. Please be aware that this Interceptor configuration may break more complex queries, such as those ordering results at the end of the statement, due to out of place WHERE clauses.

3.2.3. Interceptor 'search' Configuration

Search interceptors receive a JSON denoting the querying users' authorization details, including the granted permissions/roles of the user.

SpEL search Interceptor Configuration
{
      "type" : "search",
      "predicate" : "spel:!grantedAuthorities.contains('SOME_NEEDED_ROLE')",
      "url" : "http://exampleinterceptor/api/dms/objects/search",
      "useDiscovery" : true
}

3.3. Type 'updateDmsObject'

The last of the available interceptor types enters the stage whenever anyone tries to update an object matching the predicate declared in the interceptor configuration. The Interceptor can modify the incoming update metadata, while also having access to the current version of the object’s metadata.

3.3.1. Creating the 'updateDmsObject' REST Controller

Again, the Rest Controller class will not diverge much from the previous two interceptor types, keeping the object ID path variable of the getContent type and the JSON Request Content Type parameter of the search interceptor type.

updateDmsObject REST Controller Class
@RestController
@RequestMapping("/api")
public class UpdateEnricherRestController {
@Autowired
private UpdateEnricherService updateEnricherService;

    @PostMapping(value = "/dms/objects/{objectId}/update", headers = "content-type=application/json")
    public void enrichedUpdate(@RequestBody Map<String, Object> dmsApiObjectList,
                               @PathVariable("objectId") String objectId,
                               @RequestHeader(value = "Authorization", required = false) String auth,
                               HttpServletRequest servletRequest,
                               HttpServletResponse servletResponse) throws IOException {
        servletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
        updateEnricherService.enrichMetadata(servletResponse.getOutputStream(), dmsApiObjectList, objectId, auth);
    }
}

3.3.2. Manipulating Incoming Metadata

We can use our interceptor, for instance, to increment a property tracking the amount of edits of a certain property commited on an object. We can modify the incoming update metadata within the interceptor service to implement this logic in a very flexible manner, allowing for complex interactions with tertiary systems for the metadata enrichment.

Method for Enriching Incoming Update Metadata
public Map<String, Object> enrichMetadataTag(Map<String, Object> incomingMetadata){
        List<Map<String, Object>> list = (List<Map<String, Object>>)incomingMetadata.get("objects");
        Map<String, Object> dmsApiObject = list.get(0);
        Map<String, Object> propertyMap = (Map<String, Object>)dmsApiObject.get("properties");
        Map<String, Object> testStringMap = (Map<String, Object>)propertyMap.get("appInterceptor:testString1");
        String oldValue = testStringMap.get("value").toString();
        testStringMap.replace("value", (oldValue+ " (enriched value)"));
        return incomingMetadata;
    }

In this demonstration, we opt to enrich a metadata property value present in the body of the update request. We assume that the update already tries to modify the value. That way we can obtain the proposed new value for the property from the propertyMap within the incomingMetadata object. If we wanted to modify this value even if it was not present in the update body, we simply need to inject the property into the same property map, as it will be treated as overwriting metadata when forwarding the enriched metadata to the repository service in the next step.

3.3.3. Interceptor 'updateDmsObject' Configuration

Similarly to the getContent interceptor type, the updateDmsObject interceptor predicates are based on the current version of the objects' metadata. We use the predicate to verify the document that is meant to be updated is of the specific object type handled by our interceptor.

SpEL 'updateDmsObject' Interceptor Configuration
{
      "type" : "updateDmsObject",
      "predicate" : spel:contentStreams != null && contentStreams.size() > 0 && properties['system:objectTypeId'] == "appInterceptor:exampleDoc",
      "url" : "http://exampleinterceptor/api/dms/objects/{system:objectId}/update",
      "useDiscovery" : true
}

4. Summary

Your service is complete! Find the complete code project described in this tutorial in this GitHub Repository.