Interceptor Example Tutorial
Table of Contents
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.
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.
Necessary Microservice Application Files
In this tutorials' code project, we implement some of our suggestions for the architecture of yuuvis®-integrated microservice. The following classes provide no logic specific to interceptors, but are still essential for the microservice to operate.
src/main/java/com/os/services/interceptor/ExampleInterceptorApplication.java
- Runs the application. The annotation
@SpringBootApplication
tells Spring Boot to run the application in an embedded Tomcat.
src/main/java/com/os/services/interceptor/config/DefaultServicesResponseEntityExceptionHandler.java
- Error Handling suggestion.
src/main/java/com/os/services/interceptor/config/InterceptorConfiguration.java
- Creates a REST template.
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.
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.
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
).
@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
.
Modelling a Sub-Document
As a document is identified by its start and end page, the domain object simply represents these two page numbers.
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 }
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:
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.
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()));
Extracting Content from a PDF Document Using PdfTools
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.
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); } }
Interceptor 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.
Note: 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.
{ "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 } ] }
{ "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.
Type "search"
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.
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.
@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); } }
Enriching an Incoming Query Object
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.
Interceptor Configuration
Search interceptors receive a JSON denoting the querying users' authorization details, including the granted permissions/roles of the user.
{ "type" : "search", "predicate" : "spel:!grantedAuthorities.contains('SOME_NEEDED_ROLE')", "url" : "http://exampleinterceptor/api/dms/objects/search", "useDiscovery" : true }
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.
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.
@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); } }
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.
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.
Interceptor 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.
{ "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 }
Summary
Your service is complete! Find the complete code project described in this tutorial in this GitHub Repository.