Invoking Sling Servlet from OSGi service or Sling Model using Sling Servlet Helpers

Problem Statement:

How can I invoke the Sling servlet from the OSGI service or from Sling Model?

Introduction:

We are aware of invoking a service by using @Reference @OSGiService if are referring to any other sling model inside the Sling servlet you can also use adaptTo({class-name}.class) to invoke a sling model within a servlet. But is there any way we can invoke servlet from the Sling model? Or OSGi service?

Yes, we can use 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 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 that happen under the hood which leads to much simpler code.

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.

Step 1: Add the following Dependency to your core POM.XML

<dependency>
    <groupId>org.apache.sling</groupId>
    <artifactId>org.apache.sling.servlet-helpers</artifactId>
    <version>1.4.6</version>
</dependency>

The org.apache.sling.servlet-helpers has dependency on the older version of the org.apache.sling.api version. However, you can request AMS to install the bundle manually on Felix console if your Maven build fails

Step 2: Create a simple Servlet as shown below:

package com.chatgpt.core.servlets;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.servlets.post.JSONResponse;
import org.osgi.service.component.annotations.Component;
import com.adobe.granite.rest.Constants;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

@Component(service = { Servlet.class }, property = { "sling.servlet.paths=" + SamplePathServlet.RESOURCE_PATH,
        "sling.servlet.methods=POST" })
public class SamplePathServlet extends SlingAllMethodsServlet {

    private static final long serialVersionUID = 1L;
    public static final String RESOURCE_PATH = "/bin/sampleServletPath";

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType(JSONResponse.RESPONSE_CONTENT_TYPE);
        response.setCharacterEncoding(Constants.DEFAULT_CHARSET);

        JsonObject jsonResponse = new JsonObject();
        jsonResponse.addProperty("mesasge", "I am in Path based Servlet ");
        try (PrintWriter out = response.getWriter()) {
            out.print(new Gson().toJson(jsonResponse));
        }
    }
}

Step 3: Create a Sling model as shown below.

As you can see it internally uses SlingRequestProcessor API to mock internal request

package com.chatgpt.core.models;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.engine.SlingRequestProcessor;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.servlethelpers.internalrequests.SlingInternalRequest;
import org.apache.sling.servlets.post.JSONResponse;
import javax.annotation.PostConstruct;
import java.io.IOException;

@Model(adaptables = SlingHttpServletRequest.class)
public class ExampleModel {
    @OSGiService
    private SlingRequestProcessor slingProcessor;

    @SlingObject
    private ResourceResolver resourceResolver;

    @OSGiService
    private SlingRequestProcessor slingRequestProcessor;

    @PostConstruct
    private void init() {
        try {
            String responString = new SlingInternalRequest(resourceResolver, slingRequestProcessor, "/bin/sampleServletPath")
                    .withRequestMethod("GET")
                    .execute()
                    .checkStatus(200)
                    .checkResponseContentType(JSONResponse.RESPONSE_CONTENT_TYPE+";charset=UTF-8")
                    .getResponseAsString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Step 4: Create an OSGi service as shown below:

package com.chatgpt.core.services.impl;

import java.io.IOException;
import com.chatgpt.core.services.InternalRequestService;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.engine.SlingRequestProcessor;
import org.apache.sling.servlethelpers.internalrequests.SlingInternalRequest;
import org.apache.sling.servlets.post.JSONResponse;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component(service = InternalRequestService.class, immediate = true, name = "Sample Internal Request service")
public class InternalRequestServiceImpl implements InternalRequestService {

    @Reference
    private SlingRequestProcessor slingRequestProcessor;

    @Override
    public String getInternalPathBasedRespone(ResourceResolver resourceResolver) {
        try {
            return new SlingInternalRequest(resourceResolver, slingRequestProcessor, "/bin/sampleServletPath")
                    .withRequestMethod("GET")
                    .execute()
                    .checkStatus(200)
                    .checkResponseContentType(JSONResponse.RESPONSE_CONTENT_TYPE+";charset=UTF-8")
                    .getResponseAsString();
        } catch (IOException e) {
            log.error("An error occurred while proccessing the request {} ", e.getMessage());
        }
        return StringUtils.EMPTY;
    }
}

Exporting AEM Experience Fragment/Page Content for A/B Testing and External Systems without HTML Tags

Problem Statement:

How to export experience fragment or page content from author to:

  1. Adobe Target or any other application similar to Target without HTML Head/Body tags just component content for A/B Testing or targeting etc.
  2. Salesforce Marketing Cloud or Adobe Campaign system without HTML Head/Body tags
  3. Non-AEM sister sites are to be used as an iframe content.

Introduction:

If you are using an A/B Testing tool or when you are using SFMC for an email campaigning system in your project and you want to export Experience fragment or Page content to the external system using HTTP Post request.

Adding the entire HTML content (which includes the head, body etc.) on the existing page would negatively impact the accessibility score and page performance.

Usually with the above use case people follow exporting HTML content from AEM involves sending an HTTP request to the AEM server and receiving a response that contains the HTML content. This can be achieved using various tools and techniques, but in this article, we will focus on using the SlingRequestProcessor OSGi service.

Advantages of using are:

  1. Process an HTTP request through the Sling request processing engine for example passing any selectors or params or suffixes etc.
  2. Request parameter – Usually a “synthetic” request, i.e., not supplied by the servlet container.
  3. Response parameter – Usually a “synthetic” response, i.e., not supplied by the servlet container.
  4. ResourceResolver parameter – The ResourceResolver is used for the Sling request processing.

Step 1: Add the following maven dependency into your Core POM.XML

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <scope>provided</scope>
</dependency>
Include JSOUP jar from lib in BND plugin

Step 2: Create an Export service as shown below:

package com.chatgpt.core.services;
public interface ExportHtmlService {
    public String getExportHTMLContent(String xfPath);
}

Step 3: Create an Implementation class as shown below:

package com.chatgpt.core.services.impl;

import com.chatgpt.core.services.ExportHtmlService;
import com.day.cq.contentsync.handler.util.RequestResponseFactory;
import com.day.cq.wcm.api.WCMMode;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.engine.SlingRequestProcessor;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component(service = ExportHtmlService.class, immediate = true, name = "HTML Content Export service")
public class ExportHtmlServiceImpl implements ExportHtmlService{

    @Reference
    ResourceResolverFactory resolverFactory;

    @Reference
    private SlingRequestProcessor slingProcessor;

    @Reference
    private RequestResponseFactory requestResponseFactory;

    Map<String, Object> wgServiceParam = Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "service-user-name");

    @Override
    public String getExportHTMLContent(String xfPath) {
        if (StringUtils.isNotEmpty(xfPath)) {
            try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(wgServiceParam)) {
                if (StringUtils.isNotEmpty(xfPath)) {
                    retrieveContent(xfPath, resolver);
                    //End point details : you can also use http client builder factory
                    String url = "https://example.com/api";
                    Map<String, String> headers = new HashMap<>();
                    headers.put("Content-Type", "application/json");
                    Document document = Jsoup.parse(retrieveContent(xfPath, resolver));
                    JSONObject jsonObject = new JSONObject();
                    Gson gson = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
                    jsonObject.put("from", "aem");
                    jsonObject.put("html", gson.toJson(document.body().html()));
                    return sendPost(url, headers, gson.toJson(jsonObject));
                }
            } catch (LoginException | ServletException | IOException | JSONException e) {
                log.error("An error occurred while proccessing the request {} ", e.getMessage());
            }
        }
        return StringUtils.EMPTY;
    }

    protected String retrieveContent(String requestUri, ResourceResolver resourceResolver)
            throws ServletException, IOException {
        HttpServletRequest req = requestResponseFactory.createRequest("GET", requestUri + ".nocloudconfigs.html");
        WCMMode.DISABLED.toRequest(req);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        HttpServletResponse res = requestResponseFactory.createResponse(out);
        slingProcessor.processRequest(req, res, resourceResolver);
        return out.toString();
    }

    public static String sendPost(String urlString, Map<String, String> headers, String body) throws IOException {
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setDoInput(true);

        // Set headers
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            conn.setRequestProperty(entry.getKey(), entry.getValue());
        }

        // Write body to output stream
        try (OutputStream os = conn.getOutputStream()) {
            byte[] input = body.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        // Read response from input stream
        try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
            StringBuilder response = new StringBuilder();
            String responseLine = null;
            while ((responseLine = br.readLine()) != null) {
                response.append(responseLine.trim());
            }
            return response.toString();
        }
    }
}

As you can see in the above code WCMMode API to set the request mode has been disabled as we are making the request from the author to avoid all the OOTB editor tags.

Here we are using JSOUP to remove unnecessary spaces and new line characters and also, we are using GsonBuilder for escaping double quotes.

The OOTB experience fragment page component comes with a nocloudconfigs custom selector to support Adobe target export functionality, which removes all the unnecessary HTML tags like head, body, etc.

Adobe Target export using nocloudconfigs selector

Step 4: Copy {selector}.html to your custom page component

You can also copy the same HTML files from the experience fragment html tag to your page component if your exporting complete page has iframe content to external systems or sister sites.

Step 5: Copy {selector}.html to your custom component

You can also hide or change component behaviour before exporting by creating {selector}.html (nocloudconfigs.html) inside the component.

Component level selector HTML file

For future enhancements:

Use HTTP Client factory which uses Apache Fluent with optimum performance and OSGi configurable service, please refer to the link for more details:

  1. https://kiransg.com/2021/11/08/aem-rest-service-using-http-client-factory/
  2. https://kiransg.com/2021/11/08/aem-invoke-api-how-to-use-http-client-factory/

You can also enhance special character escaping using the GSON Type registry.

Please refer the below link for working code: https://github.com/kiransg89/chatgpt