N
Naveenr.dev
Chapter 06
4 min read2026-06-17

Real-World Scenarios in AEM

A practical AEM servlet-based cleanup approach for removing empty DAM and Experience Fragment nodes in production.

Problem Statement

We received a request from the business indicating that a lot of empty folders or nodes have been created inside the DAM and Experience Fragment folders in the production environment. This makes it difficult for authors to find the content they need, as they have to scroll past all these empty folders.

Approach to Deleting Empty Folders or Nodes

Why a Servlet-Based Approach?

We chose a servlet-based approach because it’s dynamic. This means we can specify paths for deletion. For example, we can delete empty folders or nodes from the DAM by providing the DAM URL, and similarly, we can delete from the Experience Fragment folders by providing the respective URL.

Steps to Follow

Backup Production Environment

Since we’re working in the production environment, the first step is to take a complete backup. This ensures we can restore everything if something goes wrong.

Freeze Content Authoring

We need to temporarily stop all content authoring activities. This prevents any changes while we’re cleaning up the empty folders.

Take Screenshots

Before we start deleting, take screenshots of the DAM and Experience Fragment folders. This helps us document the state before and after the cleanup.

Delete Empty Folders

We’ll use a servlet to delete the empty folders or nodes. The servlet will be dynamic, meaning we can change the URL to target different paths (e.g., DAM or Experience Fragment).

Proof of Concept (POC)

Before starting the implementation, I want to explain the POC that I have done.

In the POC, I found that there are some empty properties available in the JCR (Java Content Repository). Based on this, we wrote the logic to check these nodes. If a node is found to be empty, the logic will delete the node.

Implementation Steps

Let’s start with writing the servlet code that can delete empty folders or nodes dynamically based on specified paths.

Here is the working Code


import java.io.IOException;
import lombok.NonNull;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.resource.PersistenceException;
@Component(service = Servlet.class, property = {
        Constants.SERVICE_DESCRIPTION + "=Delete Nodes Servlet",
        ServletResolverConstants.SLING_SERVLET_PATHS + "=/bin/deletenodes",
        ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_GET
})
public class DeletePagesServlet extends SlingSafeMethodsServlet {
    @Override
    protected void doGet(SlingHttpServletRequest request, @NonNull SlingHttpServletResponse response) throws IOException {
        String directoryPath = request.getParameter("directoryPath");
        if (isInvalidDirectoryPath(directoryPath, response)) return;
        ResourceResolver resourceResolver = request.getResourceResolver();
        Resource directoryResource = resourceResolver.getResource(directoryPath);
        if (isDirectoryNotFound(directoryResource, response)) return;
        try {
            assert directoryResource != null;
            boolean hasDeleted = deleteNodes(resourceResolver, directoryResource);
            if (hasDeleted) {
                resourceResolver.commit();
            }
            sendSuccessResponse(response);
        } catch (PersistenceException | RuntimeException e) {
            sendErrorResponse(response, e);
        }
    }
    private boolean isInvalidDirectoryPath(String directoryPath, SlingHttpServletResponse response) throws IOException {
        if (directoryPath == null || directoryPath.isEmpty()) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write("Directory path parameter is missing");
            return true;
        }
        return false;
    }
    private boolean isDirectoryNotFound(Resource directoryResource, SlingHttpServletResponse response) throws IOException {
        if (directoryResource == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            response.getWriter().write("Directory not found");
            return true;
        }
        return false;
    }
    private boolean deleteNodes(ResourceResolver resourceResolver, Resource directoryResource) throws PersistenceException {
        boolean hasDeleted = false;
        for (Resource child : directoryResource.getChildren()) {
            ValueMap properties = child.getValueMap();
            if (shouldDeleteNode(properties)) {
                resourceResolver.delete(child);
                hasDeleted = true;
            }
        }
        return hasDeleted;
    }
    private boolean shouldDeleteNode(ValueMap properties) {
        String accountID = properties.get("accountID", String.class);
        String mediaId = properties.get("mediaId", String.class);
        String playerId = properties.get("playerId", String.class);
        String videoId = properties.get("videoId", String.class);
        String videoType = properties.get("videoType", String.class);
        return accountID != null && isEmpty(mediaId) && isEmpty(playerId) && isEmpty(videoId) && isEmpty(videoType);
    }
    private boolean isEmpty(String value) {
        return value == null || value.isEmpty();
    }
    private void sendSuccessResponse(SlingHttpServletResponse response) throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().write("Nodes deleted successfully");
    }
    private void sendErrorResponse(SlingHttpServletResponse response, Exception e) throws IOException {
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        response.getWriter().write("Commit failed: " + e.getMessage());
    }
}

Let’s Understand the code:

Imports and Annotations

  • Import necessary classes and annotations.
  • Register the servlet as an OSGi service with specific properties.

Servlet Class Definition

  • Extend SlingSafeMethodsServlet for safe HTTP methods like GET.
  • Override the doGet method to handle GET requests.

doGet Method

  • Retrieve the directory path from request parameters.
  • If the path is invalid, send a BAD_REQUEST response.
  • Use ResourceResolver to access the repository and get the directory resource.
  • If the directory is not found, send a NOT_FOUND response.
  • Call deleteNodes to delete nodes within the directory.
  • If nodes are deleted, commit the changes and send a success response.
  • Handle exceptions and send an error response if needed.

Methods in the Code

  • isInvalidDirectoryPath: Checks if the directory path is invalid.
  • isDirectoryNotFound: Checks if the directory resource is not found.
  • deleteNodes: Iterates through children of the directory resource and deletes nodes based on conditions.
  • shouldDeleteNode: Checks if a node should be deleted based on its properties.
  • isEmpty: Utility method to check if a string is null or empty.
  • sendSuccessResponse: Sends a success response.
  • sendErrorResponse: Sends an error response with the exception message.

Once your servlet code is ready, you need to deploy it in your local machine.

To build and deploy only the core module in AEM, you can use the following command:

mvn clean install -PautoInstallBundle

If you want to skip tests during the build, you can add the -DskipTests=true flag:

mvn clean install -PautoInstallBundle -DskipTests=true

Once deployed, go and hit this URL:

http://localhost:4502/bin/deletenodes?directoryPath=/content/dam

You will get the confirmation that Nodes deleted successfully.

JUnit Test Case Code

For this test case, I have covered more than 80% code coverage.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.IOException;
import java.util.Iterator;
import org.apache.sling.api.resource.ValueMap;
class DeletePagesServletTest {
    @Mock
    private SlingHttpServletRequest request;
    @Mock
    private SlingHttpServletResponse response;
    @Mock
    private ResourceResolver resourceResolver;
    @Mock
    private Resource directoryResource;
    @Mock
    private Resource childResource;
    @Mock
    private ValueMap valueMap;
    @InjectMocks
    private DeletePagesServlet deletePagesServlet;
    private StringWriter responseWriter;
    @BeforeEach
    void setUp() throws IOException {
        MockitoAnnotations.initMocks(this);
        responseWriter = new StringWriter();
        when(response.getWriter()).thenReturn(new PrintWriter(responseWriter));
    }
    @Test
    void testDoGet_MissingDirectoryPath() throws IOException {
        when(request.getParameter("directoryPath")).thenReturn(null); // Simulate missing parameter
        deletePagesServlet.doGet(request, response);
        verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST);
        assertEquals("Directory path parameter is missing", responseWriter.toString().trim());
    }
   
    @SuppressWarnings("unchecked")
    @Test
    void testDoGet_EmptyDirectory() throws IOException {
        when(request.getParameter("directoryPath")).thenReturn("/content/empty");
        when(request.getResourceResolver()).thenReturn(resourceResolver);
        when(resourceResolver.getResource("/content/empty")).thenReturn(directoryResource);
        Iterable<Resource> mockChildren = mock(Iterable.class, withSettings().defaultAnswer(CALLS_REAL_METHODS).lenient());
        Iterator<Resource> mockIterator = mock(Iterator.class, withSettings().defaultAnswer(CALLS_REAL_METHODS).lenient());
        when(directoryResource.getChildren()).thenReturn(mockChildren);
        when(mockChildren.iterator()).thenReturn(mockIterator);
        when(mockIterator.hasNext()).thenReturn(false);
        deletePagesServlet.doGet(request, response);
        verify(resourceResolver, never()).delete(any(Resource.class));
        verify(resourceResolver, never()).commit();
        verify(response).setStatus(HttpServletResponse.SC_OK);
        assertEquals("Nodes deleted successfully", responseWriter.toString().trim());
    }
    @SuppressWarnings("unchecked")
    @Test
    void testDoGet_CommitFailure() throws IOException {
        when(request.getParameter("directoryPath")).thenReturn("/content/path");
        when(request.getResourceResolver()).thenReturn(resourceResolver);
        when(resourceResolver.getResource("/content/path")).thenReturn(directoryResource);
        Iterable<Resource> mockChildren = mock(Iterable.class, withSettings().defaultAnswer(CALLS_REAL_METHODS).lenient());
        Iterator<Resource> mockIterator = mock(Iterator.class, withSettings().defaultAnswer(CALLS_REAL_METHODS).lenient());
        when(directoryResource.getChildren()).thenReturn(mockChildren);
        when(mockChildren.iterator()).thenReturn(mockIterator);
        when(mockIterator.hasNext()).thenReturn(true, false);
        when(mockIterator.next()).thenReturn(childResource);
        when(childResource.getValueMap()).thenReturn(valueMap);
        when(valueMap.get("accountID", String.class)).thenReturn("12345");
        doThrow(new RuntimeException("Commit failed")).when(resourceResolver).commit();
        deletePagesServlet.doGet(request, response);
        verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        assertEquals("Commit failed: Commit failed", responseWriter.toString().trim());
    }
}

I hope you found this interesting and informative. Please share it with your friends to spread the knowledge.

You can follow me for upcoming blogs follow.