> 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.

# Sling Models and Services

Sling Models are annotation-driven Java classes that automatically map AEM content (JCR properties, child resources, OSGi services) to Java objects. They are the **standard approach** for implementing component logic in AEM and replace the older `WCMUsePojo` and JavaScript Use-API patterns.

This page covers how to build, use, and test Sling Models, as well as the common AEM service APIs you'll use alongside them.

## How Sling Models Work

<svg id="mermaid-svg-6112996" width="100%" xmlns="http://www.w3.org/2000/svg" style="max-width: 1394.3958740234375px;" viewBox="0 0 1394.3958740234375 147" role="graphics-document document" aria-roledescription="flowchart-v2"><g><marker id="mermaid-svg-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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-6112996_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="M166.219,64.499L181.799,61.249C197.378,57.999,228.538,51.5,259.031,48.25C289.524,45,319.351,45,334.264,45L349.177,45" id="mermaid-svg-6112996-L_HTL_AdaptTo_0" style=";" data-edge="true" data-et="edge" data-id="L_HTL_AdaptTo_0" data-points="W3sieCI6MTY2LjIxODc1LCJ5Ijo2NC40OTg2ODc3NTQzMDk3N30seyJ4IjoyNTkuNjk3OTIxNzUyOTI5NywieSI6NDV9LHsieCI6MzUzLjE3NzA5MzUwNTg1OTQsInkiOjQ1fV0=" data-look="classic" marker-end="url(#mermaid-svg-6112996_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M583.208,45L591.615,45C600.021,45,616.833,45,632.979,45C649.125,45,664.604,45,672.344,45L680.083,45" id="mermaid-svg-6112996-L_AdaptTo_Injectors_0" style=";" data-edge="true" data-et="edge" data-id="L_AdaptTo_Injectors_0" data-points="W3sieCI6NTgzLjIwODM0MzUwNTg1OTQsInkiOjQ1fSx7IngiOjYzMy42NDU4NDM1MDU4NTk0LCJ5Ijo0NX0seyJ4Ijo2ODQuMDgzMzQzNTA1ODU5NCwieSI6NDV9XQ==" data-look="classic" marker-end="url(#mermaid-svg-6112996_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M893.115,45L913.948,45C934.781,45,976.448,45,1017.455,47.945C1058.462,50.889,1098.809,56.778,1118.983,59.723L1139.157,62.667" id="mermaid-svg-6112996-L_Injectors_Model_0" style=";" data-edge="true" data-et="edge" data-id="L_Injectors_Model_0" data-points="W3sieCI6ODkzLjExNDU5MzUwNTg1OTQsInkiOjQ1fSx7IngiOjEwMTguMTE0NTkzNTA1ODU5NCwieSI6NDV9LHsieCI6MTE0My4xMTQ1OTM1MDU4NTk0LCJ5Ijo2My4yNDUxNjk0NjQ2ODE2Nn1d" data-look="classic" marker-end="url(#mermaid-svg-6112996_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M1143.115,98.755L1122.281,101.796C1101.448,104.837,1059.781,110.918,1000.695,113.959C941.609,117,865.104,117,801.026,117C736.948,117,685.297,117,631.896,117C578.495,117,523.344,117,461.019,117C398.694,117,329.196,117,279.52,113.886C229.843,110.773,199.989,104.545,185.062,101.432L170.134,98.318" id="mermaid-svg-6112996-L_Model_HTL_0" style=";" data-edge="true" data-et="edge" data-id="L_Model_HTL_0" data-points="W3sieCI6MTE0My4xMTQ1OTM1MDU4NTk0LCJ5Ijo5OC43NTQ4MzA1MzUzMTgzNX0seyJ4IjoxMDE4LjExNDU5MzUwNTg1OTQsInkiOjExN30seyJ4Ijo3ODguNTk4OTY4NTA1ODU5NCwieSI6MTE3fSx7IngiOjYzMy42NDU4NDM1MDU4NTk0LCJ5IjoxMTd9LHsieCI6NDY4LjE5MjcxODUwNTg1OTQsInkiOjExN30seyJ4IjoyNTkuNjk3OTIxNzUyOTI5NywieSI6MTE3fSx7IngiOjE2Ni4yMTg3NSwieSI6OTcuNTAxMzEyMjQ1NjkwMjN9XQ==" data-look="classic" marker-end="url(#mermaid-svg-6112996_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path></g><g><g transform="translate(259.6979217529297, 45)"><g data-id="L_HTL_AdaptTo_0" transform="translate(-68.47917175292969, -22)"><foreignObject width="136.95834350585938" height="44"><p>data-sly-use.model</p></foreignObject></g></g><g><g data-id="L_AdaptTo_Injectors_0" transform="translate(0, 0)"></g></g><g transform="translate(1018.1145935058594, 45)"><g data-id="L_Injectors_Model_0" transform="translate(-100, -34)"><foreignObject width="200" height="68"><p>@ValueMapValue, @OSGiService,...</p></foreignObject></g></g><g transform="translate(633.6458435058594, 117)"><g data-id="L_Model_HTL_0" transform="translate(-25.4375, -22)"><foreignObject width="50.875" height="44"><p>getters</p></foreignObject></g></g></g><g><g id="mermaid-svg-6112996-flowchart-HTL-0" data-look="classic" transform="translate(87.109375, 81)"><rect style="" x="-79.109375" y="-37" width="158.21875" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-49.109375, -22)"><rect></rect><foreignObject width="98.21875" height="44"><p>HTL Template</p></foreignObject></g></g><g id="mermaid-svg-6112996-flowchart-AdaptTo-1" data-look="classic" transform="translate(468.1927185058594, 45)"><rect style="" x="-115.015625" y="-37" width="230.03125" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-85.015625, -22)"><rect></rect><foreignObject width="170.03125" height="44"><p>adaptTo(MyModel.class)</p></foreignObject></g></g><g id="mermaid-svg-6112996-flowchart-Injectors-2" data-look="classic" transform="translate(788.5989685058594, 45)"><rect style="" x="-104.515625" y="-37" width="209.03125" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-74.515625, -22)"><rect></rect><foreignObject width="149.03125" height="44"><p>Sling Model Injectors</p></foreignObject></g></g><g id="mermaid-svg-6112996-flowchart-Model-3" data-look="classic" transform="translate(1264.7552185058594, 81)"><rect style="" x="-121.640625" y="-37" width="243.28125" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-91.640625, -22)"><rect></rect><foreignObject width="183.28125" height="44"><p>Populated Model Instance</p></foreignObject></g></g></g></g></g><defs></defs><defs></defs><linearGradient id="mermaid-svg-6112996-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>

1. The HTL template triggers `adaptTo()` via `data-sly-use`
2. Sling locates the model class by matching the `resourceType` or class name
3. The injector framework populates all annotated fields
4. `@PostConstruct` runs any initialisation logic
5. The model instance is returned to HTL for rendering

## Defining a Sling Model

### Minimal model

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

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

@Model(
    adaptables = SlingHttpServletRequest.class,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class GreetingModel {

    @ValueMapValue
    private String title;

    public String getTitle() {
        return title != null ? title : "Hello, World!";
    }
}
```

```html
<sly data-sly-use.model="com.myproject.core.models.GreetingModel"/>
<h1>${model.title}</h1>
```

### Full model with all common features

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

import com.adobe.cq.export.json.ComponentExporter;
import com.day.cq.wcm.api.Page;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.myproject.core.services.DateFormatterService;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.*;

import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;

@Model(
    adaptables = SlingHttpServletRequest.class,
    adapters = {ArticleModel.class, ComponentExporter.class},
    resourceType = ArticleModel.RESOURCE_TYPE,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = "jackson", extensions = "json")
public class ArticleModel implements ComponentExporter {

    static final String RESOURCE_TYPE = "myproject/components/article";

    // --- JCR Properties ---
    @ValueMapValue
    private String title;

    @ValueMapValue
    private String description;

    @ValueMapValue
    private Calendar publishDate;

    @ValueMapValue
    @Default(values = "general")
    private String category;

    // --- Child Resources (multifield) ---
    @ChildResource
    private List<Resource> relatedLinks;

    // --- Sling Objects ---
    @SlingObject
    private Resource resource;

    // --- Script Variables ---
    @ScriptVariable
    private Page currentPage;

    // --- OSGi Services ---
    @OSGiService
    private DateFormatterService dateFormatter;

    // --- Computed fields ---
    private String formattedDate;

    @PostConstruct
    protected void init() {
        if (publishDate != null && dateFormatter != null) {
            formattedDate = dateFormatter.format(publishDate);
        }
    }

    // --- Getters ---

    @JsonProperty("title")
    public String getTitle() {
        return StringUtils.defaultIfBlank(title, currentPage != null
            ? currentPage.getTitle() : "Untitled");
    }

    public String getDescription() { return description; }
    public String getFormattedDate() { return formattedDate; }
    public String getCategory() { return category; }

    public List<Resource> getRelatedLinks() {
        return relatedLinks != null
            ? Collections.unmodifiableList(relatedLinks)
            : Collections.emptyList();
    }

    public boolean isEmpty() {
        return StringUtils.isBlank(title) && StringUtils.isBlank(description);
    }

    @Override
    public String getExportedType() {
        return RESOURCE_TYPE;
    }
}
```

### Adaptables: Request vs Resource

| Adaptable                                | When to use                                                   | Available injections                                                            |
| ---------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `SlingHttpServletRequest.class`          | **Default for components** - HTL rendering, servlets          | Everything: resource, request, session, script variables, WCM mode              |
| `Resource.class`                         | Utility models, child models for `@ChildResource`, unit tests | Resource properties, child resources, OSGi services                             |
| Both (`{Request.class, Resource.class}`) | Models used from both contexts                                | Use `@Via("resource")` when adapting from request to access resource properties |

> \[!-success] -success
> tip
>
> Always prefer `SlingHttpServletRequest.class` for component models. Use `Resource.class` only for child models (e.g. multifield items via `@ChildResource`) or utility models that don't need request context.

### Binding to a resource type

When you specify `resourceType` in the `@Model` annotation, Sling automatically selects this model when HTL uses `data-sly-use` without a fully qualified class name:

```java
@Model(adaptables = SlingHttpServletRequest.class,
       resourceType = "myproject/components/teaser")
public class TeaserModel { ... }
```

```html
<!-- Sling resolves the model automatically via the component's sling:resourceType -->
<sly data-sly-use.model="com.myproject.core.models.TeaserModel"/>

<!-- Or even shorter if resourceType is set: -->
<sly data-sly-use.model="myproject/components/teaser"/>
```

## Injection Annotations

For a complete reference of all Sling Model annotations (`@ValueMapValue`, `@OSGiService`, `@Self`, `@ChildResource`, `@ScriptVariable`, `@RequestAttribute`, `@Via`, `@Default`, etc.), see the dedicated Sling Model Annotations Reference.

### Quick cheat sheet

| Annotation          | Injects                                                        |
| ------------------- | -------------------------------------------------------------- |
| `@ValueMapValue`    | JCR property from the resource's ValueMap                      |
| `@ChildResource`    | Child JCR nodes as models or resources                         |
| `@OSGiService`      | OSGi services                                                  |
| `@SlingObject`      | Resource, ResourceResolver, Request, Response                  |
| `@ScriptVariable`   | HTL bindings: `currentPage`, `properties`, `pageManager`, etc. |
| `@Self`             | The adaptable itself or another model adapted from it          |
| `@RequestAttribute` | Request attributes (e.g. from `data-sly-use` parameters)       |
| `@PostConstruct`    | Runs initialisation after all injections                       |

***

## JSON Export (Sling Model Exporter)

The Sling Model Exporter framework lets you serialize a model as JSON via a `.model.json` URL. This is essential for headless AEM, SPA Editor, and Content as a Service scenarios.

### Setup

```java
@Model(
    adaptables = SlingHttpServletRequest.class,
    adapters = {TitleModel.class, ComponentExporter.class},
    resourceType = TitleModel.RESOURCE_TYPE,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = "jackson", extensions = "json")
public class TitleModel implements ComponentExporter {

    static final String RESOURCE_TYPE = "myproject/components/title";

    @JsonProperty("title")  // optional: customise the JSON key name
    @ValueMapValue
    private String title;

    public String getTitle() {
        return title;
    }

    @Override
    public String getExportedType() {
        return RESOURCE_TYPE;
    }
}
```

### JCR component definition

```xml
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
    cq:icon="textLeft"
    jcr:description="Displays a title."
    jcr:primaryType="cq:Component"
    jcr:title="Title"
    componentGroup="My Project"/>
```

### Usage

Request:

```markdown
GET /content/my-site/en/home/jcr:content/root/container/title.model.json
```

Response:

```json
{
    ":type": "myproject/components/title",
    "title": "Welcome to My Site"
}
```

### Controlling JSON output

| Annotation                 | Purpose                           |
| -------------------------- | --------------------------------- |
| `@JsonProperty("name")`    | Rename a field in JSON output     |
| `@JsonIgnore`              | Exclude a getter from JSON output |
| `@JsonInclude(NON_NULL)`   | Omit null values                  |
| `@JsonRootName("article")` | Wrap output in a root element     |

***

## Accessing Template Policies

Editable template policies let template authors configure component settings (e.g. allowed heading levels, default variants) via the template editor. These settings are stored as content policies and can be read in your Sling Model.

### Design dialog

Define the policy fields in a `_cq_design_dialog`:

```xml
<someSelectProperty
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/select"
    fieldLabel="Size"
    required="{Boolean}true"
    name="./size">
    <items jcr:primaryType="nt:unstructured">
        <small jcr:primaryType="nt:unstructured" text="Small" selected="{Boolean}true" value="small"/>
        <medium jcr:primaryType="nt:unstructured" text="Medium" value="medium"/>
        <large jcr:primaryType="nt:unstructured" text="Large" value="large"/>
    </items>
</someSelectProperty>
```

### Reading policy values

**Option 1: Utility methods** (reusable across models)

```java
public final class PolicyUtils {

    private PolicyUtils() {}

    /**
     * Retrieves the ContentPolicy for the given resource.
     */
    public static ContentPolicy getContentPolicy(Resource resource) {
        ContentPolicyManager cpm = resource.getResourceResolver()
            .adaptTo(ContentPolicyManager.class);
        return cpm != null ? cpm.getPolicy(resource) : null;
    }

    /**
     * Reads a typed property from a content policy.
     */
    public static <T> T getPolicyProperty(ContentPolicy policy,
            String property, Class<T> type) {
        if (policy != null) {
            return policy.getProperties().get(property, type);
        }
        return null;
    }

    /**
     * Reads a typed property with a default fallback.
     */
    public static <T> T getPolicyProperty(ContentPolicy policy,
            String property, T defaultValue) {
        if (policy != null) {
            return policy.getProperties().get(property, defaultValue);
        }
        return defaultValue;
    }
}
```

**Usage in a Sling Model:**

```java
@SlingObject
private Resource resource;

private String size;

@PostConstruct
protected void init() {
    ContentPolicy policy = PolicyUtils.getContentPolicy(resource);
    size = PolicyUtils.getPolicyProperty(policy, "size", "small");
}
```

**Option 2: Using `@ScriptVariable` with `currentStyle`**

For simpler cases, access policy values directly via the `currentStyle` binding:

```java
@ScriptVariable
private Style currentStyle;

public String getSize() {
    return currentStyle != null
        ? currentStyle.get("size", "small")
        : "small";
}
```

***

## Common AEM Service APIs

### WCM Mode (Author vs Publish)

Determine the current WCM mode to conditionally render content or enable/disable features:

```java
import com.day.cq.wcm.api.WCMMode;

public boolean isEditMode(SlingHttpServletRequest request) {
    WCMMode mode = WCMMode.fromRequest(request);
    return mode == WCMMode.EDIT || mode == WCMMode.DESIGN;
}

public boolean isAuthorEnvironment(SlingHttpServletRequest request) {
    WCMMode mode = WCMMode.fromRequest(request);
    switch (mode) {
        case EDIT:
        case DESIGN:
        case PREVIEW:
            return true;
        default:
            return false;
    }
}
```

| WCMMode     | Environment | Description                        |
| ----------- | ----------- | ---------------------------------- |
| `EDIT`      | Author      | Dialog editing is active           |
| `PREVIEW`   | Author      | Author preview mode                |
| `DESIGN`    | Author      | Template editor / design mode      |
| `ANALYTICS` | Author      | Analytics overlay mode             |
| `READ_ONLY` | Author      | Read-only view                     |
| `DISABLED`  | Publish     | WCM is disabled (normal rendering) |

> \[!-success] -success
> tip
>
> In HTL, you can use `wcmmode.edit`, `wcmmode.preview`, and `wcmmode.disabled` directly without a Sling Model:
>
> ```html
> <div data-sly-test="${wcmmode.edit}" class="cq-placeholder">
>     Click to configure this component.
> </div>
> ```

### TagManager

Resolve AEM tags to their titles and descriptions:

```java
@SlingObject
private SlingHttpServletRequest request;

@ValueMapValue(name = "cq:tags")
private String[] tagIds;

public List<TagInfo> getResolvedTags() {
    TagManager tagManager = request.getResourceResolver().adaptTo(TagManager.class);
    if (tagManager == null || tagIds == null) {
        return Collections.emptyList();
    }

    List<TagInfo> tags = new ArrayList<>();
    for (String tagId : tagIds) {
        Tag tag = tagManager.resolve(tagId);
        if (tag != null) {
            tags.add(new TagInfo(
                tag.getTagID(),
                tag.getTitle(),
                tag.getDescription()
            ));
        }
    }
    return tags;
}
```

Common `TagManager` operations:

| Method                                 | Purpose                             |
| -------------------------------------- | ----------------------------------- |
| `resolve(tagId)`                       | Resolves a tag ID to a `Tag` object |
| `createTag(tagId, title, description)` | Creates a new tag                   |
| `find(keyword)`                        | Searches tags by keyword            |
| `getTags(resource)`                    | Gets all tags for a resource        |
| `setTags(resource, tags)`              | Sets tags on a resource             |

### UserManager

Access user and group information via the JCR `UserManager`:

```java
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.Authorizable;

public String getUserDisplayName(ResourceResolver resolver, String userId) {
    try {
        Session session = resolver.adaptTo(Session.class);
        UserManager userManager = ((JackrabbitSession) session).getUserManager();
        Authorizable authorizable = userManager.getAuthorizable(userId);

        if (authorizable != null && !authorizable.isGroup()) {
            User user = (User) authorizable;
            Value[] givenName = user.getProperty("profile/givenName");
            Value[] familyName = user.getProperty("profile/familyName");

            String first = givenName != null && givenName.length > 0
                ? givenName[0].getString() : "";
            String last = familyName != null && familyName.length > 0
                ? familyName[0].getString() : "";

            return (first + " " + last).trim();
        }
    } catch (RepositoryException e) {
        LOG.error("Error reading user profile for {}", userId, e);
    }
    return userId; // fallback to userId
}
```

### PageManager

Navigate the page hierarchy and perform page operations:

```java
@ScriptVariable
private PageManager pageManager;

@ScriptVariable
private Page currentPage;

public List<Page> getChildPages() {
    List<Page> children = new ArrayList<>();
    Iterator<Page> childIterator = currentPage.listChildren();
    while (childIterator.hasNext()) {
        Page child = childIterator.next();
        if (!child.isHideInNav()) {
            children.add(child);
        }
    }
    return children;
}

public Page getParentPage(int level) {
    // Get the page at a specific depth (0 = root, 1 = site, 2 = language, etc.)
    return currentPage.getAbsoluteParent(level);
}
```

***

## ResourceResolver

The `ResourceResolver` is the central API for accessing JCR content in Sling. Understanding its lifecycle is critical to avoiding resource leaks and session issues.

### Implicit vs explicit resolvers

| Type         | Source                                                    | Who closes it?                              |
| ------------ | --------------------------------------------------------- | ------------------------------------------- |
| **Implicit** | `request.getResourceResolver()`, `@SlingObject`           | The framework - **never close it yourself** |
| **Explicit** | `resourceResolverFactory.getServiceResourceResolver(...)` | **You** - always close it                   |

### Using service resource resolvers

When you need elevated permissions (e.g. in workflows, scheduled jobs, or event listeners), use a service resource resolver:

```java
@Reference
private ResourceResolverFactory resolverFactory;

public void doPrivilegedOperation() {
    Map<String, Object> params = Map.of(
        ResourceResolverFactory.SUBSERVICE, "my-service"
    );

    try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(params)) {
        Resource resource = resolver.getResource("/content/my-site/en");
        // ... perform operations ...
        resolver.commit();
    } catch (LoginException e) {
        LOG.error("Could not obtain service resolver", e);
    } catch (PersistenceException e) {
        LOG.error("Could not persist changes", e);
    }
}
```

> \[!-warning] -warning
> warning
>
> **Never close an implicit ResourceResolver.** Closing the request's resolver (`request.getResourceResolver()`) kills the underlying JCR session for the entire request, breaking all subsequent processing.
>
> **Always close explicit ResourceResolvers.** Service resolvers opened via `ResourceResolverFactory` must be closed, ideally using `try-with-resources`.
>
> **Rule of thumb:** close the resolver where you opened it. If a resolver is passed as a parameter, the caller is responsible for its lifecycle.

### Common operations

```java
// Read a resource
Resource resource = resolver.getResource("/content/my-site/en/home");

// Check existence
boolean exists = resolver.getResource(path) != null;

// Create a resource
Map<String, Object> props = Map.of("jcr:primaryType", "nt:unstructured", "title", "New Page");
resolver.create(parentResource, "child-name", props);

// Delete a resource
resolver.delete(resource);

// Persist changes (required after create/delete/modify)
resolver.commit();

// Map an internal path to an external URL
String externalUrl = resolver.map(request, "/content/my-site/en/about");
// → "/en/about.html"

// Adapt to JCR Session (when you need low-level JCR API)
Session session = resolver.adaptTo(Session.class);
```

***

## OSGi Services

Business logic that is shared across components, servlets, or workflows should live in OSGi services. Sling Models should stay focused on component rendering; complex logic belongs in services.

### Defining a service

```java
public interface DateFormatterService {
    String format(Calendar date);
    String format(Calendar date, String pattern);
}
```

```java
@Component(service = DateFormatterService.class)
@Designate(ocd = DateFormatterServiceImpl.Config.class)
public class DateFormatterServiceImpl implements DateFormatterService {

    @ObjectClassDefinition(name = "Date Formatter Configuration")
    @interface Config {
        @AttributeDefinition(name = "Default Pattern")
        String defaultPattern() default "MMMM d, yyyy";
    }

    private String defaultPattern;

    @Activate
    @Modified
    protected void activate(Config config) {
        this.defaultPattern = config.defaultPattern();
    }

    @Override
    public String format(Calendar date) {
        return format(date, defaultPattern);
    }

    @Override
    public String format(Calendar date, String pattern) {
        if (date == null) return "";
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        return sdf.format(date.getTime());
    }
}
```

### Using services in Sling Models

```java
@OSGiService
private DateFormatterService dateFormatter;
```

For detailed OSGi service patterns (typed configs, factory configs, run modes, service users), see OSGi Configuration.

***

## Transport Handler

A `TransportHandler` intercepts the replication pipeline and lets you route content to external systems (CI/CD pipelines, CDNs, search indexes, etc.) during activation:

```java
@Component(
    service = TransportHandler.class,
    immediate = true,
    property = {
        "label=External CDN Purge Handler"
    }
)
public class ExternalCDNTransportHandler implements TransportHandler {

    private static final Logger LOG = LoggerFactory.getLogger(
        ExternalCDNTransportHandler.class);

    /**
     * Return true if this handler should process the replication event.
     * Match by agent configuration (e.g. a custom transport URI scheme).
     */
    @Override
    public boolean canHandle(AgentConfig agentConfig) {
        String transportURI = agentConfig.getTransportURI();
        return transportURI != null && transportURI.startsWith("cdn://");
    }

    /**
     * Called when content is replicated through a matching agent.
     * Use this to push content or purge caches on external systems.
     */
    @Override
    public ReplicationResult deliver(TransportContext ctx,
            ReplicationTransaction tx) throws ReplicationException {
        String path = tx.getAction().getPath();
        LOG.info("Purging CDN cache for: {}", path);

        try {
            // Call external CDN API to purge the path
            purgeExternalCDN(path);
            return ReplicationResult.OK;
        } catch (Exception e) {
            LOG.error("CDN purge failed for {}", path, e);
            return new ReplicationResult(false, 0, "CDN purge failed: " + e.getMessage());
        }
    }

    private void purgeExternalCDN(String path) {
        // HTTP call to CDN invalidation API
    }
}
```

***

## JavaScript Use-API (Legacy)

> \[!-warning] -warning
> warning
>
> The JavaScript Use-API (server-side Rhino, ES5 only) is **legacy**. Always prefer Sling Models for new components. It is slower, harder to test, and has limited API access. This section is included for reference when working with legacy codebases.

```markdown
mycomponent/
├── mycomponent.html
└── helper.js
```

```javascript
use(function () {
    'use strict';

    // 'this' contains variables passed from HTL via data-sly-use
    var inputValue = this.someVar;

    // ⚠️ Runs server-side (Rhino engine) - no console.log, no window, no DOM
    // ⚠️ Must use ES5 syntax - arrow functions, let/const, template literals will fail

    return {
        processedValue: inputValue ? inputValue.toUpperCase() : 'DEFAULT'
    };
});
```

```html
<div data-sly-use.helper="${'helper.js' @ someVar=properties.title}">
    ${helper.processedValue}
</div>
```

| Feature        | Sling Model (Java)               | JS Use-API                     |
| -------------- | -------------------------------- | ------------------------------ |
| Language       | Java (any version)               | ES5 only (Rhino)               |
| Performance    | Fast (compiled)                  | Slow (interpreted per request) |
| Unit testing   | Full (JUnit, AEM Mocks)          | Not testable                   |
| API access     | Full Sling/JCR/AEM API           | Limited (no OSGi services)     |
| IDE support    | Full (autocomplete, refactoring) | Minimal                        |
| Recommendation | **Always use**                   | Avoid in new code              |

***

## Unit Testing Sling Models

Use [wcm.io AEM Mocks](https://wcm.io/testing/aem-mock/) to test Sling Models without a running AEM instance:

```java
@ExtendWith(AemContextExtension.class)
class GreetingModelTest {

    private final AemContext context = new AemContext();

    @BeforeEach
    void setUp() {
        // Register the model class
        context.addModelsForClasses(GreetingModel.class);
    }

    @Test
    void shouldReturnAuthoredTitle() {
        // Create test content
        context.create().page("/content/test", "/conf/test/templates/page");
        context.create().resource("/content/test/jcr:content/mycomponent",
            Map.of("sling:resourceType", "myproject/components/greeting",
                   "title", "Welcome"));

        // Set the current resource
        context.currentResource("/content/test/jcr:content/mycomponent");

        // Adapt and assert
        GreetingModel model = context.request().adaptTo(GreetingModel.class);
        assertNotNull(model);
        assertEquals("Welcome", model.getTitle());
    }

    @Test
    void shouldReturnDefaultWhenTitleMissing() {
        context.create().page("/content/test", "/conf/test/templates/page");
        context.create().resource("/content/test/jcr:content/mycomponent",
            Map.of("sling:resourceType", "myproject/components/greeting"));

        context.currentResource("/content/test/jcr:content/mycomponent");

        GreetingModel model = context.request().adaptTo(GreetingModel.class);
        assertNotNull(model);
        assertEquals("Hello, World!", model.getTitle());
    }
}
```

### Testing with OSGi services

```java
@BeforeEach
void setUp() {
    context.addModelsForClasses(ArticleModel.class);

    // Register a mock or real service
    context.registerService(DateFormatterService.class, new DateFormatterServiceImpl());
}
```

***

## Best Practices

### Keep models thin

Sling Models should focus on **data access and simple transformations**. Complex business logic belongs in OSGi services that the model injects via `@OSGiService`.

### Use OPTIONAL as default strategy

Set `defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL` on every model. This prevents the entire model from returning `null` when a single optional field is missing. Mark truly required fields with `injectionStrategy = InjectionStrategy.REQUIRED`.

### Return empty collections, not null

```java
// Bad - callers must null-check
public List<LinkItem> getLinks() {
    return links;
}

// Good - always safe to iterate
public List<LinkItem> getLinks() {
    return links != null ? Collections.unmodifiableList(links) : Collections.emptyList();
}
```

### Avoid heavy computation in getters

Getters may be called multiple times by HTL. Compute values once in `@PostConstruct` and return the cached result:

```java
private String slug;  // computed once

@PostConstruct
protected void init() {
    slug = title != null
        ? title.toLowerCase().replaceAll("[^a-z0-9]+", "-")
        : "";
}

public String getSlug() {
    return slug;
}
```

### Always use specific injection annotations

Prefer `@ValueMapValue`, `@OSGiService`, `@SlingObject` over the generic `@Inject`. Specific annotations are self-documenting and avoid ambiguity.

### Don't store state between requests

Sling Models are **request-scoped** - a new instance is created for each request. Never store cross-request state in a model. Use OSGi services with thread-safe patterns for shared state.
