AEM is built using Apache Sling – a powerful REST based web framework. While working on an exciting project, we discovered one of its exiting features – Apache Sling Filters. They are extremely useful tool for filtering request processing. They allow developers to apply filter chain to the requests before they are actually dispatched to the default processing destination.

Restoring Deleted Assets in AEM problem and Sling Filters to the rescue.

We found an issue in AEM with the out of the box assets deletion functionality. Particularly once the asset is deleted, there is no way to restore it. One can catch “Deleted” event and raise a custom “asset save” workflow, for example, just to find out that the asset has already been deleted.

Applying a custom Sling filter can help us access the asset or for that matter another resource that’s about to be deleted. Then perform some action with it like doing additional checks, sending notifications or emails or even create a copy somewhere.

Adding Trash Bin to AEM Assets!

Let’s review example implementation that intercepts requests for deleting assets made from the Touch UI and the Legacy UI. Then copies them to a destination of our choice and sends the request down the filter chain to complete its task of deleting the resource. Such feature can be useful if you want to have someone in our organization review deleted assets. Our example works both for deleted folders and deleted assets.

Note: This example is tested on AEM 6.3.3.6

package com.msi.aem.dam.core.filters;

import com.adobe.granite.asset.api.AssetManager;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.dam.commons.util.DamUtil;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Property;
import javax.jcr.Session;
import javax.servlet.*;
import java.io.IOException;
import java.util.Iterator;

import static org.apache.sling.engine.EngineConstants.*;

/**
 * A Sling Filter implementation that will catch deleted
 * assets from Touch UI and copy them to folder "/content/dam/deleted"
 * before they are permanently deleted from their original location
 *
 * @author Bobby Mavrov (bobby.mavrov@kbwebconsult.com)
 * */
@Component(
        configurationPid = "my.project.filters.ExampleFilter",
        service = { Filter.class },
        property = {
                SLING_FILTER_SCOPE + "=" + FILTER_SCOPE_REQUEST,
                SLING_FILTER_PATTERN + "=/bin/wcmcommand",
                Constants.SERVICE_RANKING + ":Integer=-1",
                "sling.filter.methods=POST"
        }
)
public class ExampleFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(ExampleFilter.class);
    private static final String DELETE_PAGE = "deletePage"; // the appropriate value stored under 'cmd' parameter in the request
    private static final String CMD = "cmd";
    private static final String PATH = "path";
    private static final String DAM_PATH = "/content/dam/"; //used to confirm the resources are part of DAM
    private static final String DESTINATION_FOLDER = "/content/dam/deleted/"; // the destination where assets will be copied to

    AssetManager assetManager = null;

    @Override
    public void init(FilterConfig filterConfig) {}

    /**
     * Retrieves the paths of all resource paths. Performs additional checks on the request to verify that
     * the it is a POST request for deleting resources in DAM, checks if each resource is an asset or folder
     * and calls copyAsset() or copyFolder() respectively. At the end calls chain.doFilter().
     * */
    @Override
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        ResourceResolver resolver = null;
        try {
            SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
            resolver = slingRequest.getResourceResolver();
            //AssetManager instance used to copy the assets
            assetManager = resolver.adaptTo(AssetManager.class);

            //Verifies the 'cmd' parameter in the request
            if(!verifyCommand(slingRequest))
                return;

            RequestParameter[] paths = slingRequest.getRequestParameters(PATH);
            if (paths != null) {
                // We need to create the destination folder before using the AssetManager API to move the asset
                JcrUtils.getOrCreateByPath(DESTINATION_FOLDER, false, JcrResourceConstants.NT_SLING_FOLDER,
                        JcrResourceConstants.NT_SLING_FOLDER, resolver.adaptTo(Session.class), true);
                for (RequestParameter eachPath : paths) {

                    // Verifying that the resource path is part of DAM and not somewhere else
                    if(!verifyPath(eachPath.getString()))
                        return;

                    Resource resource = resolver.getResource(eachPath.getString());
                    if(resource != null){
                        String primaryType = resource.getChild(JcrConstants.JCR_PRIMARYTYPE).adaptTo(Property.class)
                                .getValue().getString();
                        //Verifying that the resource is an asset
                        if(primaryType.equals(DamConstants.NT_DAM_ASSET)){
                            copyAsset(resource.adaptTo(Asset.class));
                            //Verifying that the resource is a folder
                        }else if(primaryType.matches(String.format("%s|%s|%s", JcrConstants.NT_FOLDER,
                                JcrResourceConstants.NT_SLING_FOLDER, JcrResourceConstants.NT_SLING_ORDERED_FOLDER))){
                            copyFolder(resource);
                        }
                    }
                }
            }
        }catch (Exception e){
            logger.error("Error while moving resource", e);
        }finally {
            //We must call this method so the request can be processed down the filter chain. Not doing so will crash
            //basically all requests to '/bin/wcmcommand'
            chain.doFilter(request, response);
        }
    }

    /**
     * Verifies that the parameter 'cmd' in the request is indeed 'deletePage'.
     * */
    private boolean verifyCommand(SlingHttpServletRequest slingRequest){
        String cmd = slingRequest.getParameter(CMD);
        if(cmd == null || !cmd.equals(DELETE_PAGE)){
            return false;
        }
        return true;
    }

    /**
     * Verifies that the path of the resource is inside /content/dam/ indeed
     * since our filter catches requests for deleting other resources like
     * pages
     * */
    private boolean verifyPath(String path){
        if (path.contains(DAM_PATH) && !path.contains(DESTINATION_FOLDER))
            return true;
        return false;
    }

    /**
     * Retrieves all assets in the resource using DamUtil.getAssets()
     * and then calls copyAsset() for each one.
     * */
    private void copyFolder(Resource resource){
        Iterator assets = DamUtil.getAssets(resource);
        while (assets.hasNext()){
            copyAsset(assets.next());
        }
    }

    /**
     * Retrieves the destination path of the asset and
     * calls AssetManager.copyAsset() to create a copy
     * there.
     * */
    private void copyAsset(Asset asset){
        String newPath = String.format("%s%s", DESTINATION_FOLDER, asset.getName());
        if(assetManager != null)
            assetManager.copyAsset(asset.getPath(), newPath);
    }

    /**
     * We don't need to implement this method for our purposes
     * */
    @Override
    public void destroy() {}
}

Let’s review some of the more interesting points in our example:

Our example uses the newer OSGi annotations instead of the Apache Felix Scr annotations. We use an implementation of javax.servlet.Filter and create and OSGi component using @Component

@Component(
        configurationPid = "my.project.filters.ExampleFilter",
        service = { Filter.class },
        property = {
                SLING_FILTER_SCOPE + "=" + FILTER_SCOPE_REQUEST,
                SLING_FILTER_PATTERN + "=/bin/wcmcommand",
                Constants.SERVICE_RANKING + ":Integer=-1",
                "sling.filter.methods=POST"
        }
)

The constants used here belong to org.apache.sling.engine.EngineConstants package. We are using a filter pattern that equals “/bin/wcmcommand” which will actually filter in multiple other requests not only for deleting assets. For that purpose, we’ll need to implement additional checks later on to confirm that the request is indeed for deleting assets. We’ll also need to set the service ranking to some negative value to make sure this is executed before the actual deleting of the asset. For our example filter we can pick “-1”.

In the implementation of doFilter() we are going to obtain the “cmd” and “path” parameters and verify their values as mentioned before. We’ll need to confirm that the ‘cmd’ equals “deletePage” and that each value in “path” is inside "/content/dam/” since we want to avoid resource that are not assets like site pages for example. We are going to retrieve ResourceResolver and AssetManager instances and cast the request to SlingHttpServlerRequest.

We’ll also make sure our destination folder under the const DESTINATION_FOLDER exists as well as going to check the primary type of the resources under “path”. If they are dam:Asset we are going to call copyAsset(), if they are nt:folder, sling:Folder or sling:OrderedFolder we’ll call copyFolder() to retrieve all their assets and call copyAsset() on each one.

Let’s review in details some interesting points this method

  • For our purposes we use SlingHttpServletRequest and we need to cast the ServletRequest
SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
  • We retrieve the request parameters under “path” and then iterate through them
RequestParameter[] paths = slingRequest.getRequestParameters(PATH);
  • Using JcrUtils to create the destination folder where the assets will be copied to. We could also implement additional check here to verify the returned Node by this method is not null. 
JcrUtils.getOrCreateByPath(DESTINATION_FOLDER, false, JcrResourceConstants.NT_SLING_FOLDER, JcrResourceConstants.NT_SLING_FOLDER, resolver.adaptTo(Session.class), true);
  • After obtaining the resource under each path we verify it’s not null and then retrieve its primary type using
String primaryType = resource.getChild(JcrConstants.JCR_PRIMARYTYPE).adaptTo(Property.class)
        .getValue().getString();

And respectively call copyAsset() or copyFolder() depending on it

In our example we also use several helper functions

  • With verifyCommand() we verify that the “cmd” parameter exists and equals “deletePage”. This is necessary since our filter will intercepts other requests to “/bin/wcmcommand” not meant to delete assets.
if(cmd == null || !cmd.equals(DELETE_PAGE)){return false;}
  • With verifyPath() we confirm that the resource paths are inside “/content/dam/” since we’ll also catch requests for deleting resources like pages, and also outside of “/content/dam/deleted/” since it is our destination folder and we don’t want assets deleted from there being copied back to the same folder.
if (path.contains(DAM_PATH) && !path.contains(DESTINATION_FOLDER))
    return true;
  • With copyFolder() we retrieve all assets in the folder resource
Iterator<Asset> assets = DamUtil.getAssets(resource);

 and then call copyAsset() for each one.

  • With copyAsset() we obtain the new appropriate path for our asset and use AssetManager to copy it
String newPath = String.format("%s%s", DESTINATION_FOLDER, asset.getName());
if(assetManager != null)
    assetManager.copyAsset(asset.getPath(), newPath);

Thank you very much for looking into our blog. We hope you have a lot of fun with Sling Filters and you all stay safe and healthy during the COVID-19 pandemic!


KBWEB Consult specializes in customizing and integrating the Adobe Experience Manager (AEM) Platform. Contact us for a free consultation.