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
SlingSafeMethodsServletfor safe HTTP methods like GET. - Override the
doGetmethod to handle GET requests.
doGet Method
- Retrieve the directory path from request parameters.
- If the path is invalid, send a
BAD_REQUESTresponse. - Use
ResourceResolverto access the repository and get the directory resource. - If the directory is not found, send a
NOT_FOUNDresponse. - Call
deleteNodesto 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.