Sling Servlet Helpers and Internal Requests

The Sling Servlet Helpers bundle provides mock implementations of the SlingHttpServletRequest, SlingHttpServletResponse and related classes, along with fluent SlingInternalRequest and ServletInternalRequest helpers for internal requests.

The mock request/response implementations are meant to be used in tests and also with services like the SlingRequestProcessor when making requests to that service outside of an HTTP request processing context.

They are used under the hood by the SlingInternalRequest and ServletInternalRequest helpers to provide a simple and foolproof way of executing internal Sling requests.

The GraphQL Core module, for example, uses them for internal requests that retrieve a GraphQL schema dynamically, taking into account the current Resource and request selectors.

See the automated tests of the servlet-helpers module for more info, besides the general descriptions found below.

InternalRequest helpers

The internal request helpers use either a SlingRequestProcessor to execute internal requests using the full Sling request processing pipeline, or a ServletResolver to resolve and call a Servlet or Script directly. The necessary "mocking" of requests are responses happens under the hood which leads to much simpler code than using the mock request/response classes directly.

The latter direct-to-servlet (or script) mode is more efficient but less faithful to the way HTTP requests are processed, as it bypasses all Servlet Filters, in particular.

Here's an example using the SlingInternalRequest helper - see the test code for more. The ServletInternalRequest API is very similar but takes a ServletResolver and an actual Resource as its starting points.

OutputStream os = new SlingInternalRequest(resourceResolver, slingRequestProcessor, path)
  .withResourceType("website/article/news")
  .withResourceSuperType("website/article")
  .withSelectors("print", "a4")
  .withExtension("pdf")
  .execute()
  .checkStatus(200)
  .checkResponseContentType("application/pdf")
  .getResponse()
  .getOutputStream()

Not all servlets and scripts are suitable to be called by the ServletInternalRequest, depending on their "environmental" requirements like Request attributes for example.

In case of doubt you can start with the SlingInternalRequest helper which uses the SlingRequestProcessor so that servlets or scripts should see no difference compared to HTTP requests. And once that works you can try the more efficient ServletInternalRequest helper to check if your scripts and servlets support that mode.

In both cases, the standard Sling Servlet/Script resolution mechanism is used, which can be useful to execute scripts that are resolved based on the current resource type, for non-HTTP operations. Inventing HTTP method names for this is fine and allows for reusing this powerful resolution mechanism in other contexts.

Troubleshooting internal requests

To help map log messages to internal requests, as several of those might be used to handle a single HTTP request, the InternalRequest parent class of the helpers discussed above sets a log4j Mapped Diagnostic Context (MDC) value with the sling.InternalRequestkey.

The value of that key provides the essential attributes of the current request, so that using a log formatting pattern that displays it, like:

%-5level [%-50logger{50}] %message ## %mdc{sling.InternalRequest} %n

Causes the internal request information to be logged, like in this example (lines folded for readability):

DEBUG [o.a.s.s.internalrequests.SlingInternalRequest     ]
   Executing request using the SlingRequestProcessor
   ## GET P=/content/tags/monitor+array S=null EXT=json RT=samples/tag(null)
WARN  [org.apache.sling.engine.impl.request.RequestData  ]
  SlingRequestProgressTracker not found in request attributes
  ## GET P=/content/tags/monitor+array S=null EXT=json RT=samples/tag(null)
DEBUG [o.a.s.s.resolver.internal.SlingServletResolver    ]
  Using cached servlet /apps/samples/tag/json.gql
  ## GET P=/content/tags/monitor+array S=null EXT=json RT=samples/tag(null)

In these log messages, GET P=/content/tags/monitor+array S=null EXT=json RT=samples/tag(null) points to the current internal request, showing its method, path, selectors, extension, resource type and resource supertype.

Mock Request/Response classes

These are useful for testing or if you need to do something that the internal request helpers do not support.

SlingHttpServletRequest

Example for preparing a sling request with custom request data:

// prepare sling request
MockSlingHttpServletRequest request = new MockSlingHttpServletRequest(resourceResolver);

// simulate query string
request.setQueryString("param1=aaa&param2=bbb");

// alternative - set query parameters as map
request.setParameterMap(ImmutableMap.<String,Object>builder()
    .put("param1", "aaa")
    .put("param2", "bbb")
    .build());

// set current resource
request.setResource(resourceResolver.getResource("/content/sample"));

// set sling request path info properties
MockRequestPathInfo requestPathInfo = (MockRequestPathInfo)request.getRequestPathInfo();
requestPathInfo.setSelectorString("selector1.selector2");
requestPathInfo.setExtension("html");

// set method
request.setMethod(HttpConstants.METHOD_POST);

// set attributes
request.setAttribute("attr1", "value1");

// set headers
request.addHeader("header1", "value1");

// set cookies
request.addCookie(new Cookie("cookie1", "value1"));

SlingHttpServletResponse

Example for preparing a sling response which can collect the data that was written to it:

// prepare sling response
MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();

// execute the code that writes to the response...

// validate status code
assertEquals(HttpServletResponse.SC_OK, response.getStatus());

// validate content type and content length
assertEquals("text/plain;charset=UTF-8", response.getContentType());
assertEquals(CharEncoding.UTF_8, response.getCharacterEncoding());
assertEquals(55, response.getContentLength());

// validate headers
assertTrue(response.containsHeader("header1"));
assertEquals("5", response.getHeader("header2"));

// validate response body as string
assertEquals(TEST_CONTENT, response.getOutputAsString());

// validate response body as binary data
assertArrayEquals(TEST_DATA, response.getOutput());