> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# AEM Servlets

Sling Servlets are Java classes that handle HTTP requests in AEM. They are the standard way to build custom endpoints - REST APIs, form handlers, AJAX callbacks, data sources for dialog dropdowns, and more. Sling's servlet resolution mechanism routes requests to servlets based on **resource type** or **path**, making them a natural extension of the content-driven architecture.

## How Sling Servlet Resolution Works

When a request arrives, Sling determines which servlet to invoke based on the request's resource type, selectors, extension, and method:

<svg id="mermaid-svg-757994" width="100%" xmlns="http://www.w3.org/2000/svg" style="max-width: 487.54168701171875px;" viewBox="0 0 487.54168701171875 1038.125" role="graphics-document document" aria-roledescription="flowchart-v2"><g><marker id="mermaid-svg-757994_flowchart-v2-pointEnd" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-757994_flowchart-v2-pointStart" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-757994_flowchart-v2-pointEnd-margin" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" style="stroke-width: 0; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-757994_flowchart-v2-pointStart-margin" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" style="stroke-width: 0; stroke-dasharray: 1, 0;"></polygon></marker><marker id="mermaid-svg-757994_flowchart-v2-circleEnd" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-757994_flowchart-v2-circleStart" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-757994_flowchart-v2-circleEnd-margin" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-757994_flowchart-v2-circleStart-margin" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-757994_flowchart-v2-crossEnd" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-757994_flowchart-v2-crossStart" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-757994_flowchart-v2-crossEnd-margin" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" style="stroke-width: 2.5;"></path></marker><marker id="mermaid-svg-757994_flowchart-v2-crossStart-margin" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"></path></marker><g><g></g><g><path d="M226.135,82L226.135,86.167C226.135,90.333,226.135,98.667,226.135,106.333C226.135,114,226.135,121,226.135,124.5L226.135,128" id="mermaid-svg-757994-L_Request_Decompose_0" style=";" data-edge="true" data-et="edge" data-id="L_Request_Decompose_0" data-points="W3sieCI6MjI2LjEzNTQxNzkzODIzMjQyLCJ5Ijo4Mn0seyJ4IjoyMjYuMTM1NDE3OTM4MjMyNDIsInkiOjEwN30seyJ4IjoyMjYuMTM1NDE3OTM4MjMyNDIsInkiOjEzMn1d" data-look="classic" marker-end="url(#mermaid-svg-757994_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M226.135,206L226.135,210.167C226.135,214.333,226.135,222.667,226.135,230.333C226.135,238,226.135,245,226.135,248.5L226.135,252" id="mermaid-svg-757994-L_Decompose_ResourceType_0" style=";" data-edge="true" data-et="edge" data-id="L_Decompose_ResourceType_0" data-points="W3sieCI6MjI2LjEzNTQxNzkzODIzMjQyLCJ5IjoyMDZ9LHsieCI6MjI2LjEzNTQxNzkzODIzMjQyLCJ5IjoyMzF9LHsieCI6MjI2LjEzNTQxNzkzODIzMjQyLCJ5IjoyNTZ9XQ==" data-look="classic" marker-end="url(#mermaid-svg-757994_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M162.515,490.379L148.777,508.816C135.039,527.253,107.562,564.126,93.824,608.24C80.086,652.354,80.086,703.708,80.086,755.063C80.086,806.417,80.086,857.771,81.424,890.626C82.763,923.481,85.44,937.837,86.779,945.015L88.117,952.193" id="mermaid-svg-757994-L_ResourceType_Servlet_0" style=";" data-edge="true" data-et="edge" data-id="L_ResourceType_Servlet_0" data-points="W3sieCI6MTYyLjUxNDg3ODEzMzgyNzM4LCJ5Ijo0OTAuMzc5NDYwMTk1NTk0OTZ9LHsieCI6ODAuMDg1OTM3NSwieSI6NjAxfSx7IngiOjgwLjA4NTkzNzUsInkiOjc1NS4wNjI1fSx7IngiOjgwLjA4NTkzNzUsInkiOjkwOS4xMjV9LHsieCI6ODguODUwMzUzNDIyNjE5MDUsInkiOjk1Ni4xMjV9XQ==" data-look="classic" marker-end="url(#mermaid-svg-757994_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M279.704,500.431L289.113,517.193C298.521,533.954,317.339,567.477,326.748,591.405C336.156,615.333,336.156,629.667,336.156,636.833L336.156,644" id="mermaid-svg-757994-L_ResourceType_PathBased_0" style=";" data-edge="true" data-et="edge" data-id="L_ResourceType_PathBased_0" data-points="W3sieCI6Mjc5LjcwNDAwNzAyNTA3MjEsInkiOjUwMC40MzE0MTA5MTMxNjA0fSx7IngiOjMzNi4xNTYyNTIzODQxODU4LCJ5Ijo2MDF9LHsieCI6MzM2LjE1NjI1MjM4NDE4NTgsInkiOjY0OH1d" data-look="classic" marker-end="url(#mermaid-svg-757994_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M287.081,813.05L273.529,829.062C259.978,845.075,232.874,877.1,209.593,900.541C186.311,923.983,166.851,938.84,157.121,946.269L147.391,953.698" id="mermaid-svg-757994-L_PathBased_Servlet_0" style=";" data-edge="true" data-et="edge" data-id="L_PathBased_Servlet_0" data-points="W3sieCI6Mjg3LjA4MDg3NTM5MDA2MzEsInkiOjgxMy4wNDk2MjMwMDU4Nzc0fSx7IngiOjIwNS43NzA4MzQ0NDU5NTMzNywieSI6OTA5LjEyNX0seyJ4IjoxNDQuMjExNTU4MDI5NzY1MTcsInkiOjk1Ni4xMjV9XQ==" data-look="classic" marker-end="url(#mermaid-svg-757994_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M348.656,849.625L349.967,859.542C351.278,869.459,353.899,889.292,355.21,906.375C356.521,923.458,356.521,937.792,356.521,944.958L356.521,952.125" id="mermaid-svg-757994-L_PathBased_Default_0" style=";" data-edge="true" data-et="edge" data-id="L_PathBased_Default_0" data-points="W3sieCI6MzQ4LjY1NTkzNTIxMTQ3MjQsInkiOjg0OS42MjUzMTcxNzI3MTMzfSx7IngiOjM1Ni41MjA4MzU4NzY0NjQ4NCwieSI6OTA5LjEyNX0seyJ4IjozNTYuNTIwODM1ODc2NDY0ODQsInkiOjk1Ni4xMjV9XQ==" data-look="classic" marker-end="url(#mermaid-svg-757994_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path></g><g><g><g data-id="L_Request_Decompose_0" transform="translate(0, 0)"></g></g><g><g data-id="L_Decompose_ResourceType_0" transform="translate(0, 0)"></g></g><g transform="translate(80.0859375, 755.0625)"><g data-id="L_ResourceType_Servlet_0" transform="translate(-11.328125, -22)"><foreignObject width="22.65625" height="44"><p>Yes</p></foreignObject></g></g><g transform="translate(336.1562523841858, 601)"><g data-id="L_ResourceType_PathBased_0" transform="translate(-9.401041984558105, -22)"><foreignObject width="18.80208396911621" height="44"><p>No</p></foreignObject></g></g><g transform="translate(221.40888, 890.6472)"><g data-id="L_PathBased_Servlet_0" transform="translate(-11.328125, -22)"><foreignObject width="22.65625" height="44"><p>Yes</p></foreignObject></g></g><g transform="translate(356.52083587646484, 909.125)"><g data-id="L_PathBased_Default_0" transform="translate(-9.401041984558105, -22)"><foreignObject width="18.80208396911621" height="44"><p>No</p></foreignObject></g></g></g><g><g id="mermaid-svg-757994-flowchart-Request-0" data-look="classic" transform="translate(226.13541793823242, 45)"><rect style="" x="-92.86979293823242" y="-37" width="185.73958587646484" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-62.86979293823242, -22)"><rect></rect><foreignObject width="125.73958587646484" height="44"><p>Incoming Request</p></foreignObject></g></g><g id="mermaid-svg-757994-flowchart-Decompose-1" data-look="classic" transform="translate(226.13541793823242, 169)"><rect style="" x="-98.5625" y="-37" width="197.125" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-68.5625, -22)"><rect></rect><foreignObject width="137.125" height="44"><p>URL Decomposition</p></foreignObject></g></g><g id="mermaid-svg-757994-flowchart-ResourceType-2" data-look="classic" transform="translate(226.13541793823242, 405)"><polygon points="149,0 298,-149 149,-298 0,-149" transform="translate(-148.5, 149)" fill="none" stroke="currentColor"></polygon><g style="" transform="translate(-100, -34)"><rect></rect><foreignObject width="200" height="68"><p>Registered by resource type?</p></foreignObject></g></g><g id="mermaid-svg-757994-flowchart-PathBased-3" data-look="classic" transform="translate(336.1562523841858, 755.0625)"><polygon points="107.0625,0 214.125,-107.0625 107.0625,-214.125 0,-107.0625" transform="translate(-106.5625, 107.0625)" fill="none" stroke="currentColor"></polygon><g style="" transform="translate(-70.0625, -22)"><rect></rect><foreignObject width="140.125" height="44"><p>Registered by path?</p></foreignObject></g></g><g id="mermaid-svg-757994-flowchart-Servlet-4" data-look="classic" transform="translate(95.75, 993.125)"><rect style="" x="-87.75" y="-37" width="175.5" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-57.75, -22)"><rect></rect><foreignObject width="115.5" height="44"><p>Matched Servlet</p></foreignObject></g></g><g id="mermaid-svg-757994-flowchart-Default-5" data-look="classic" transform="translate(356.52083587646484, 993.125)"><rect style="" x="-123.02083587646484" y="-37" width="246.0416717529297" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-93.02083587646484, -22)"><rect></rect><foreignObject width="186.0416717529297" height="44"><p>Default GET/POST Servlet</p></foreignObject></g></g></g></g></g><defs></defs><defs></defs><linearGradient id="mermaid-svg-757994-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#cccccc" stop-opacity="1"></stop><stop offset="100%" stop-color="hsl(180, 0%, 18.3529411765%)" stop-opacity="1"></stop></linearGradient></svg>

```markdown
Request URL:
/content/myproject/en/contact/jcr:content/root/container/form.submit.json

Decomposed:
  Resource path:  /content/myproject/en/contact/jcr:content/root/container/form
  Resource type:  myproject/components/contactform (from the node's sling:resourceType)
  Selector:       submit
  Extension:      json
  Method:         POST
```

Sling matches this against registered servlets using:

1. `sling.servlet.resourceTypes` + `sling.servlet.selectors` + `sling.servlet.extensions` + `sling.servlet.methods`
2. Or `sling.servlet.paths` for path-based servlets

## Registration: Resource Type vs Path

There are two ways to register a servlet. **Resource-type registration is always preferred.**

| Approach          | Pros                                                      | Cons                                                                                 |
| ----------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| **Resource type** | Respects Sling security, ACLs apply, follows AEM patterns | Requires a resource with the matching type to exist                                  |
| **Path**          | Works without a resource, simple URL                      | Bypasses Sling ACLs, must be explicitly allowed in the Sling Servlet Resolver config |

> \[!-warning] -warning
> warning
>
> Path-based servlets bypass JCR access controls. On AEMaaCS, only specific path prefixes are allowed (`/bin/`). Always prefer resource-type registration unless you have a specific reason to use paths.

## Servlet Registration Annotations

### Modern approach (recommended)

Use `@SlingServletResourceTypes` or `@SlingServletPaths` from the `org.apache.sling.servlets.annotations` package. These are type-safe and self-documenting:

```java
@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "myproject/components/contactform",
    methods = HttpConstants.METHOD_POST,
    selectors = "submit",
    extensions = "json"
)
public class ContactFormServlet extends SlingAllMethodsServlet { ... }
```

```java
@Component(service = Servlet.class)
@SlingServletPaths("/bin/myproject/api/search")
public class SearchApiServlet extends SlingSafeMethodsServlet { ... }
```

### Legacy approach (avoid)

The older `@SlingServlet` annotation is deprecated. The even older `@Component` + `@Property` pattern should never be used:

```java
// ❌ Old @SlingServlet annotation (deprecated since Sling 2.5)
@SlingServlet(resourceTypes = "...", methods = "GET")
public class OldServlet extends SlingSafeMethodsServlet { ... }

// ❌ Even older @Property pattern (Sling SCR annotations, fully deprecated)
@Component
@Service(value = Servlet.class)
@Properties({
    @Property(name = "sling.servlet.resourceTypes", value = "..."),
    @Property(name = "sling.servlet.methods", value = "GET")
})
public class VeryOldServlet extends SlingSafeMethodsServlet { ... }
```

### Registration properties reference

| Property            | Purpose                                    | Example                       |
| ------------------- | ------------------------------------------ | ----------------------------- |
| `resourceTypes`     | Sling resource type(s) the servlet handles | `"myproject/components/form"` |
| `methods`           | HTTP methods (GET, POST, PUT, DELETE)      | `HttpConstants.METHOD_POST`   |
| `selectors`         | URL selector(s) that must be present       | `"submit"`, `"action"`        |
| `extensions`        | URL extension(s)                           | `"json"`, `"html"`            |
| `resourceSuperType` | Parent resource type for inheritance       | `"sling/servlet/default"`     |

## Base Classes

Sling provides two servlet base classes:

| Class                     | Methods                                                | Use when                                           |
| ------------------------- | ------------------------------------------------------ | -------------------------------------------------- |
| `SlingSafeMethodsServlet` | `doGet()`, `doHead()`, `doOptions()`                   | Read-only operations (GET endpoints, data sources) |
| `SlingAllMethodsServlet`  | All of the above + `doPost()`, `doPut()`, `doDelete()` | Write operations (form submissions, API mutations) |

> \[!-success] -success
> tip
>
> Always extend `SlingSafeMethodsServlet` if your servlet only handles GET requests. This makes the read-only intent explicit and prevents accidental POST handling.

***

## Practical Examples

### GET Servlet: JSON API endpoint

A servlet that returns JSON data for a search API:

```java
package com.myproject.core.servlets;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Iterator;

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "myproject/components/page",
    methods = HttpConstants.METHOD_GET,
    selectors = "search",
    extensions = "json"
)
public class SearchServlet extends SlingSafeMethodsServlet {

    private static final Gson GSON = new Gson();

    @Override
    protected void doGet(SlingHttpServletRequest request,
                         SlingHttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        String query = request.getParameter("q");
        int limit = getIntParam(request, "limit", 10);

        JsonObject result = new JsonObject();
        JsonArray items = new JsonArray();

        if (query != null && !query.trim().isEmpty()) {
            // Use QueryBuilder or JCR-SQL2 to search content
            Resource root = request.getResource();
            Iterator<Resource> children = root.listChildren();

            int count = 0;
            while (children.hasNext() && count < limit) {
                Resource child = children.next();
                String title = child.getValueMap().get("jcr:title", "");
                if (title.toLowerCase().contains(query.toLowerCase())) {
                    JsonObject item = new JsonObject();
                    item.addProperty("path", child.getPath());
                    item.addProperty("title", title);
                    items.add(item);
                    count++;
                }
            }
        }

        result.add("results", items);
        result.addProperty("total", items.size());

        response.getWriter().write(GSON.toJson(result));
    }

    private int getIntParam(SlingHttpServletRequest request, String name, int defaultValue) {
        String value = request.getParameter(name);
        if (value != null) {
            try {
                return Integer.parseInt(value);
            } catch (NumberFormatException e) {
                // fall through to default
            }
        }
        return defaultValue;
    }
}
```

**Request:**

```markdown
GET /content/myproject/en.search.json?q=news&limit=5
```

**Response:**

```json
{
    "results": [
        { "path": "/content/myproject/en/news", "title": "Latest News" },
        { "path": "/content/myproject/en/newsletter", "title": "Newsletter Signup" }
    ],
    "total": 2
}
```

### POST Servlet: Form submission

A servlet that handles contact form submissions. See [CSRF Protection](#csrf-protection) below for the token handling AEM adds on top:

```java
package com.myproject.core.servlets;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.myproject.core.services.EmailService;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

import static org.apache.sling.api.servlets.HttpConstants.METHOD_POST;

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "myproject/components/contactform",
    methods = METHOD_POST,
    selectors = "submit",
    extensions = "json"
)
public class ContactFormServlet extends SlingAllMethodsServlet {

    private static final Logger LOG = LoggerFactory.getLogger(ContactFormServlet.class);
    private static final Gson GSON = new Gson();

    @Reference
    private EmailService emailService;

    @Override
    protected void doPost(SlingHttpServletRequest request,
                          SlingHttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        JsonObject result = new JsonObject();

        try {
            // Read form parameters
            String name = request.getParameter("name");
            String email = request.getParameter("email");
            String message = request.getParameter("message");

            // Validate input
            if (StringUtils.isAnyBlank(name, email, message)) {
                response.setStatus(SlingHttpServletResponse.SC_BAD_REQUEST);
                result.addProperty("success", false);
                result.addProperty("error", "All fields are required.");
                response.getWriter().write(GSON.toJson(result));
                return;
            }

            // Process the form (e.g. send email)
            emailService.sendContactEmail(name, email, message);

            response.setStatus(SlingHttpServletResponse.SC_OK);
            result.addProperty("success", true);
            result.addProperty("message", "Thank you for your message.");

        } catch (Exception e) {
            LOG.error("Error processing contact form", e);
            response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            result.addProperty("success", false);
            result.addProperty("error", "An internal error occurred.");
        }

        response.getWriter().write(GSON.toJson(result));
    }
}
```

**Request URL:**

```markdown
POST /content/myproject/en/contact/jcr:content/root/container/form.submit.json
```

The URL is derived from the page path + the component's position in the parsys + the selector and extension.

### DataSource Servlet: Dynamic dialog dropdowns

DataSource servlets populate dialog dropdowns, selects, and autocomplete fields dynamically from backend data. The result is set as a **request attribute** (not written to the response).

**Dialog field:**

```xml
<category
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/select"
    fieldLabel="Category"
    name="./category">
    <datasource
        jcr:primaryType="nt:unstructured"
        sling:resourceType="/apps/myproject/datasource/categories"/>
</category>
```

**Servlet:**

```java
package com.myproject.core.servlets;

import com.adobe.granite.ui.components.ds.DataSource;
import com.adobe.granite.ui.components.ds.SimpleDataSource;
import com.adobe.granite.ui.components.ds.ValueMapResource;
import com.day.cq.commons.jcr.JcrConstants;
import org.apache.commons.collections4.iterators.TransformIterator;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceMetadata;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;

import javax.servlet.Servlet;
import java.util.*;

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    methods = HttpConstants.METHOD_GET,
    resourceTypes = "/apps/myproject/datasource/categories"
)
public class CategoryDataSourceServlet extends SlingSafeMethodsServlet {

    @Override
    protected void doGet(SlingHttpServletRequest request,
                         SlingHttpServletResponse response) {

        ResourceResolver resolver = request.getResourceResolver();

        // Build the list of options (could come from JCR, an API, OSGi config, etc.)
        List<Map.Entry<String, String>> options = new ArrayList<>();
        options.add(Map.entry("news", "News"));
        options.add(Map.entry("blog", "Blog"));
        options.add(Map.entry("product", "Product"));
        options.add(Map.entry("event", "Event"));

        // Convert to a DataSource
        DataSource dataSource = new SimpleDataSource(
            new TransformIterator<>(options.iterator(), entry -> {
                ValueMap vm = new ValueMapDecorator(new HashMap<>());
                vm.put("value", entry.getKey());
                vm.put("text", entry.getValue());
                return new ValueMapResource(
                    resolver,
                    new ResourceMetadata(),
                    JcrConstants.NT_UNSTRUCTURED,
                    vm
                );
            })
        );

        // Set on the request - NOT the response
        request.setAttribute(DataSource.class.getName(), dataSource);
    }
}
```

> \[!-info] -info
> info
>
> The DataSource is set as a **request attribute**, not written to the response body. The Granite UI framework reads the attribute and renders the dropdown options automatically.

### DataSource with JCR-based values

A more realistic example that reads tag nodes from the JCR to populate a dropdown:

```java
@Override
protected void doGet(SlingHttpServletRequest request,
                     SlingHttpServletResponse response) {

    ResourceResolver resolver = request.getResourceResolver();
    List<Map.Entry<String, String>> options = new ArrayList<>();

    // Read tags from the JCR
    Resource tagRoot = resolver.getResource("/content/cq:tags/myproject/categories");
    if (tagRoot != null) {
        for (Resource child : tagRoot.getChildren()) {
            String value = child.getName();
            String text = child.getValueMap().get("jcr:title", child.getName());
            options.add(Map.entry(value, text));
        }
    }

    // ... convert to DataSource as above ...
}
```

***

## Testing POST Servlets

POST requests to AEM require authentication. When testing with tools like curl or Postman, you need to provide AEM's login cookie.

### Using curl

```bash
# Local SDK / dev only - never ship admin:admin credentials against a real environment.

# 1. Authenticate and capture the cookie
curl -u admin:admin -c cookies.txt http://localhost:4502/libs/granite/core/content/login.html

# 2. Use the cookie for the POST request
curl -b cookies.txt \
  -X POST \
  -d "name=John&email=john@example.com&message=Hello" \
  http://localhost:4502/content/myproject/en/contact/jcr:content/root/container/form.submit.json
```

### Using Postman

1. Open your browser's DevTools and find the `login-token` cookie for `localhost:4502`
2. In Postman, add the cookie manually via **Cookies** (below the Send button)
3. Set the cookie domain to `localhost` and copy the `login-token` key/value
4. Set the `Expires` timestamp to several hours in the future
5. Send the POST request

![Browser cookie view](/aem/backend/servlets/browser-cookie-view.png)

![Postman cookie settings](/aem/backend/servlets/postman-cookie-settings.png)

![Postman cookie entry](/aem/backend/servlets/postman-cookie-entry.png)

![Postman 200 response](/aem/backend/servlets/postman-200-response.png)

### CSRF Protection

AEM protects POST endpoints with CSRF tokens. If your POST request returns a `403 Forbidden`, you need to include the CSRF token:

```bash
# 1. Fetch the CSRF token
TOKEN=$(curl -s -b cookies.txt \
  http://localhost:4502/libs/granite/csrf/token.json | jq -r '.token')

# 2. Include it in your POST request
curl -b cookies.txt \
  -X POST \
  -H "CSRF-Token: $TOKEN" \
  -d "name=John&email=john@example.com" \
  http://localhost:4502/content/myproject/en/contact/jcr:content/root/container/form.submit.json
```

In frontend JavaScript, fetch the token before submitting:

```javascript
async function submitForm(formData) {
    // Fetch CSRF token
    const tokenResponse = await fetch('/libs/granite/csrf/token.json');
    const { token } = await tokenResponse.json();

    // Submit with token
    const response = await fetch(formEndpoint, {
        method: 'POST',
        headers: { 'CSRF-Token': token },
        body: formData,
    });

    return response.json();
}
```

***

## Servlet Response Helpers

### JSON responses

A reusable pattern for consistent JSON responses:

```java
private void writeJsonResponse(SlingHttpServletResponse response,
        int status, boolean success, String message) throws IOException {
    response.setStatus(status);
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");

    JsonObject json = new JsonObject();
    json.addProperty("success", success);
    json.addProperty("message", message);
    response.getWriter().write(new Gson().toJson(json));
}
```

### Streaming binary content

Serve files or generated PDFs:

```java
@Override
protected void doGet(SlingHttpServletRequest request,
                     SlingHttpServletResponse response) throws IOException {

    response.setContentType("application/pdf");
    response.setHeader("Content-Disposition", "attachment; filename=\"report.pdf\"");

    try (InputStream inputStream = generatePdf();
         OutputStream outputStream = response.getOutputStream()) {
        IOUtils.copy(inputStream, outputStream);
    }
}
```

### Redirects

```java
@Override
protected void doGet(SlingHttpServletRequest request,
                     SlingHttpServletResponse response) throws IOException {

    String targetPath = request.getParameter("target");
    if (StringUtils.isNotBlank(targetPath)) {
        // Use ResourceResolver.map() for proper URL mapping
        String mappedUrl = request.getResourceResolver().map(request, targetPath);
        response.sendRedirect(mappedUrl);
    } else {
        response.sendError(SlingHttpServletResponse.SC_BAD_REQUEST, "Missing target parameter");
    }
}
```

***

## Dispatcher and Servlet Caching

By default, Dispatcher caches GET responses. For servlets that return dynamic data, you need to either disable caching or configure appropriate cache rules.

### Preventing caching on dynamic endpoints

```java
@Override
protected void doGet(SlingHttpServletRequest request,
                     SlingHttpServletResponse response) throws IOException {

    // Prevent Dispatcher and browser caching
    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    response.setHeader("Pragma", "no-cache");
    response.setDateHeader("Expires", 0);
    response.setHeader("Dispatcher", "no-cache");

    // ... write response ...
}
```

### Allowing Dispatcher caching for stable data

For servlets that return relatively stable data (e.g. site navigation, tag lists), allow Dispatcher caching with a TTL:

```java
response.setHeader("Cache-Control", "public, max-age=300"); // 5 minutes
```

Configure a matching Dispatcher rule:

```markdown
/0100 { /glob "*.json" /type "allow" }
```

***

## Servlet vs Sling Model vs Filter

| Feature      | Servlet                                    | Sling Model                   | Filter                                       |
| ------------ | ------------------------------------------ | ----------------------------- | -------------------------------------------- |
| Purpose      | Handle HTTP requests directly              | Provide data to HTL templates | Intercept/modify request/response pipeline   |
| Triggered by | HTTP request to matching URL               | `data-sly-use` in HTL         | Every request matching filter scope          |
| Returns      | Full HTTP response (JSON, HTML, binary)    | Java object consumed by HTL   | Passes through (modifies request/response)   |
| Use case     | API endpoints, form handlers, data sources | Component rendering logic     | Security headers, logging, response wrapping |

### When to use a servlet

- You need a **custom HTTP endpoint** (API, webhook, data feed)
- You return **non-HTML** content (JSON, XML, PDF, CSV)
- You handle **POST/PUT/DELETE** operations
- You provide **DataSource** values for dialog dropdowns
- You need **full control** over the HTTP response

### When NOT to use a servlet

- You just need data for a component -> use a **Sling Model**
- You need to add headers or log requests -> use a **Filter**
- You need to transform responses -> use a **Filter** or **Transformer**

***

## Best Practices and Common Pitfalls

### Always set content type and encoding

```java
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
```

Omitting these causes encoding issues and incorrect MIME types in browser developer tools.

### Validate and sanitize all input

Servlet parameters come directly from the HTTP request. Never trust them:

```java
String path = request.getParameter("path");

// Validate: must start with /content/
if (path == null || !path.startsWith("/content/")) {
    response.sendError(SC_BAD_REQUEST, "Invalid path");
    return;
}

// Sanitize: prevent path traversal
path = ResourceUtil.normalize(path);
if (path == null) {
    response.sendError(SC_BAD_REQUEST, "Invalid path");
    return;
}
```

### Don't close the request's ResourceResolver

The request's `ResourceResolver` is managed by the Sling engine. Closing it in a servlet will break the rest of the request processing chain.

### Use SlingSafeMethodsServlet for GET-only servlets

It prevents accidental POST handling and signals intent. Only extend `SlingAllMethodsServlet` when you actually need to handle POST/PUT/DELETE.

### Handle errors gracefully

Never let exceptions bubble up unhandled. Always catch, log, and return a meaningful HTTP status:

```java
try {
    // ... business logic ...
} catch (Exception e) {
    LOG.error("Error in servlet", e);
    response.setStatus(SC_INTERNAL_SERVER_ERROR);
    // Write a JSON error body (don't expose internal details)
    writeJsonResponse(response, SC_INTERNAL_SERVER_ERROR, false, "An error occurred.");
}
```

### Limit path-based servlets

On AEMaaCS, path-based servlets are restricted to specific prefixes (`/bin/`). Even on AEM 6.5, prefer resource-type registration. If you must use a path, configure the Sling Servlet Resolver to allow it:

```json
{
    "servletresolver.servletRoot": "/bin",
    "servletresolver.paths": ["/bin/myproject"]
}
```
