Unlocking Efficiency: A Comprehensive Guide to AEM Job Scheduling for Asset Expiration

Hithardh Kota
Activate AEM
Published in
9 min readMar 24, 2024

Introduction

In the fast-paced world of asset management, ensuring that content remains up-to-date and relevant is paramount. Over time, some assets become obsolete or no longer serve their intended purpose, leading to clutter and potential confusion. This is where the power of automation in Adobe Experience Manager (AEM) shines — specifically through the implementation of a scheduled job to manage asset expiration.

Aim

The goal of this guide is straightforward: to demonstrate how to automatically move an asset from its existing folder to an Archive folder once it has reached its expiration date. This process not only helps in maintaining a cleaner and more efficient asset management system but also ensures that your content remains fresh and relevant.

Context

Imagine having thousands of assets stored in AEM, each serving a specific purpose at a certain time. As time progresses, certain assets lose their relevance, either due to the passage of time or the completion of the campaigns they were part of. Manually tracking and moving these expired assets can be a tedious and error-prone task.

To address this challenge, we will create and configure a job that executes at a given time interval. This job will scrutinise our custom expiration date field of each asset. If it finds that the asset’s expiration date is less than the current date, it will automatically move the asset to an Archive folder. This automation not only saves time but also significantly reduces the margin for error, ensuring that your asset library remains organised and up-to-date.

Through this custom approach we are extending the AEM’s digital rights management feature of asset expiry into a more organised way.

The Journey Ahead

By the end of this blog, you will learn how to harness the power of AEM’s scheduling capabilities to automate the tedious task of managing expired assets. We’ll walk through the entire process of setting up this job, from configuring the scheduler to writing the Java code that checks for asset expiration and moves expired assets accordingly.

Whether you’re a seasoned AEM developer or just starting out, this guide aims to provide you with a clear understanding and practical approach to automating asset expiration management in AEM. So, let’s dive in and start automating!

Setting Up Configuration

First off, we need to define the configuration for our scheduler. This is where we specify things like the scheduler name, whether it’s enabled, and the Cron expression for scheduling the job. We use annotations provided by OSGi to define this configuration interface.

This section defines an interface AssetExpirationSchedulerConfiguration using annotations provided by OSGi (Open Service Gateway Initiative) to specify configuration properties for the Asset Expiration Scheduler.

We are actually defining the name of the scheduler, whether it is enabled or not and the Cron expression.

We can write Cron expressions according to the use case. Here I am making my job run once a day.
Refer this link to generate a Cron expression — Cron Maker

AssetExpirationSchedulerConfiguration

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name = "Asset Expiration Scheduler Configuration", description = "Digital Asset Purge Scheduler Configuration")
public @interface AssetExpirationSchedulerConfiguration {
@AttributeDefinition(name = "Scheduler name", description = "Name of the scheduler", type = AttributeType.STRING)
public String updateSchedulerName() default "Asset Expiration Scheduler Configuration";

@AttributeDefinition(name = "Enabled", description = "True, if scheduler service is enabled", type = AttributeType.BOOLEAN)
public boolean enabled() default false;

@AttributeDefinition(name = "Cron Expression Associates With English folder Assets", description = "Cron expression used by the scheduler on English folder Assets", type = AttributeType.STRING)
public String cronExpression() default "0 0 12 1/1 * ? *";

}

Implementing the Scheduler

Next, we dive into the implementation of the scheduler itself. We create a Java class named AssetExpirationScheduler, which implements the Job interface provided by the Apache Sling Commons Scheduler. This interface ensures that our class can be scheduled by the AEM scheduler.

@Component(immediate = true, service = Job.class)
@Designate(ocd = AssetExpirationSchedulerConfiguration.class)
public class AssetExpirationScheduler implements Job {

The class is annotated with @Component, marking it as an OSGi component

@Component(immediate = true, service = Job.class)

@Designate annotation associates the configuration interface

AssetExpirationSchedulerConfiguration (this we created already) with this component.

@Designate(ocd = AssetExpirationSchedulerConfiguration.class)

Activating and Deactivating the Scheduler

In the activate() method, we initialize our scheduler and set it up based on the provided configuration. We log a message to indicate that our scheduler is active. On deactivation, we gracefully remove our scheduler to prevent any lingering processes.

@Activate
private void activate(AssetExpirationSchedulerConfiguration configuration) {

this.schedulerName = configuration.updateSchdulerName();

logger.info("**** Asset Update Scheduler ****");
// This scheduler will continue to run automatically even after the server
// reboot, otherwise the scheduled tasks will stop running after the server
// reboot.
addScheduler(configuration);
}

@Deactivate
protected void deactivated(AssetExpirationSchedulerConfiguration configuration) {
logger.info(">>>>> Removing Scheduler Successfully on deactivation >>>>>");
removeScheduler(configuration);
}

The @Activate annotation marks the method activate() to be invoked when the component is activated.

In the activate() method, it retrieves the scheduler name from the configuration and initialises the scheduler.

In the method deactivated() which is invoked when the component is deactivated.

Adding and Removing the Scheduler

The addScheduler() method handles the addition of our scheduler. It checks if the scheduler is enabled and if so, creates the necessary scheduling options using the provided Cron expression. We then schedule the job accordingly. If the scheduler is disabled, we log a message indicating that it’s turned off.

protected void addScheduler(AssetExpirationSchedulerConfiguration config) {
boolean enabled = config.enabled();

if (enabled) {
ScheduleOptions scheduleOptions = scheduler.EXPR(config.cronExpression());
scheduleOptions.name(this.schedulerName);
//scheduleOptions.canRunConcurrently(true);
scheduler.schedule(this, scheduleOptions);
logger.info(">>>>>>SCHEDULER ADDED>>>>>");
} else {
logger.info(">>>>>disabled>>>>>>");
}
}

private void removeScheduler(AssetExpirationSchedulerConfiguration configuration) {
logger.info(">>>>> Removing Scheduler Successfully >>>>> {}", schedulerName);
scheduler.unschedule(schedulerName);

}

We defined the addScheduler() method to schedule the job.

It checks if the scheduler is enabled, then creates schedule options using cron expression from configuration.

It schedules the job with provided options.

We can call removeScheduler() to unschedule the job.

Executing the Job

Now comes the exciting part — executing the job! In the execute() method, we perform the actual logic of our scheduler. We initialise resources such as the ResourceResolver and JCR Session, construct a JCR query to find assets with expiration dates in the past, process the results by moving expired assets to the archive folder, and finally, save the session changes.

@Override
public void execute(JobContext context) {
logger.info(">>>>>>EXECUTE METHOD RUNNING>>>>>");


ResourceResolver resolver = null;
Session session = null;
String currentDateString = getCurrentDateString();
try {
Map<String, Object> serviceUserProps = new HashMap<>();
serviceUserProps.put(ResourceResolverFactory.SUBSERVICE, SUBSERVICE_NAME);


resolver = resolverFactory.getServiceResourceResolver(serviceUserProps);
logger.info(">>>>>>RESOLVER CREATED>>>>>");

session = resolver.adaptTo(Session.class);
QueryManager queryManager = session.getWorkspace().getQueryManager();


// Specify the query to retrieve assets with an expiry date in the past

String queryStatement = "SELECT * FROM [dam:Asset] WHERE ISDESCENDANTNODE('/content/dam/my-project') AND [jcr:content/metadata/expirationdate] < '" + currentDateString + "'";
Query query = null;


query = queryManager.createQuery(queryStatement, Query.JCR_SQL2);

QueryResult result = null;


result = query.execute();
logger.info(">>>>>>QUERY RESULT EXECUTED>>>>>");

// Iterate through the result set and move expired assets to the 'Archive' folder
if (result != null) {
NodeIterator nodesIterator = result.getNodes();

while (nodesIterator.hasNext()) {
Node hit = (Node) nodesIterator.next();
logger.info(hit.getName());

// Get the 'Archive' folder
Resource archiveFolder resolver.getResource("/content/dam/Archive");
logger.info(archiveFolder.getPath());

if (archiveFolder != null) {
// Move the asset to the 'Archive' folder
resolver.move(hit.getPath(), archiveFolder.getPath());
}

}
}

logger.info(">>>>>>MOVING ASSET>>>>>>>");

// Save the session changes

session.save();
} catch (RepositoryException e) {
throw new RuntimeException(e);
} catch (PersistenceException e) {
throw new RuntimeException(e);
} catch (LoginException e) {
throw new RuntimeException(e);
} finally {
// Always close the ResourceResolver in a finally block
if (resolver != null && resolver.isLive()) {
resolver.close();
}
}
}

Utility Method for Date Formatting

To ensure consistency in date formatting, we define a utility method named getCurrentDateString(). This method generates the current date in a specific format required for comparison with expiration dates in the JCR query.

private String getCurrentDateString() {
Date currentDate = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
String currentDateString = dateFormat.format(currentDate);
return currentDateString;
}

AssetExpirationScheduler.java

import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.commons.scheduler.Job;
import org.apache.sling.commons.scheduler.JobContext;
import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.PersistenceException;

import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.Node;

import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import com.day.cq.search.QueryBuilder;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.osgi.service.component.annotations.Activate;
import org.apache.sling.commons.scheduler.Scheduler;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component(immediate = true, service = Job.class)
@Designate(ocd = AssetExpirationSchedulerConfiguration.class)
public class AssetExpirationScheduler implements Job {

@Reference
private ResourceResolverFactory resolverFactory;
@Reference
private QueryBuilder queryBuilder;
private final Logger logger = LoggerFactory.getLogger(AssetExpirationScheduler.class);
private static final String SUBSERVICE_NAME = "aem-asset-expiration";
@Reference
Scheduler scheduler;

String schedulerName;

@Activate
private void activate(AssetExpirationSchedulerConfiguration configuration) {

this.schedulerName = configuration.updateSchdulerName();

logger.info("**** Asset Update Scheduler ****");
// This scheduler will continue to run automatically even after the server
// reboot, otherwise the scheduled tasks will stop running after the server
// reboot.
addScheduler(configuration);
}

@Deactivate
protected void deactivated(AssetExpirationSchedulerConfiguration configuration) {
logger.info(">>>>> Removing Scheduler Successfully on deactivation >>>>>");
removeScheduler(configuration);
}

private void removeScheduler(AssetExpirationSchedulerConfiguration configuration) {
logger.info(">>>>> Removing Scheduler Successfully >>>>> {}", schedulerName);
scheduler.unschedule(schedulerName);

}

protected void addScheduler(AssetExpirationSchedulerConfiguration config) {
boolean enabled = config.enabled();

if (enabled) {
ScheduleOptions scheduleOptions = scheduler.EXPR(config.cronExpression());
scheduleOptions.name(this.schedulerName);
//scheduleOptions.canRunConcurrently(true);
scheduler.schedule(this, scheduleOptions);
logger.info(">>>>>>SCHEDULER ADDED>>>>>");
} else {
logger.info(">>>>>disabled>>>>>>");
}
}

@Override
public void execute(JobContext context) {
logger.info(">>>>>>EXECUTE METHOD RUNNING>>>>>");


ResourceResolver resolver = null;
Session session = null;
String currentDateString = getCurrentDateString();
try {
Map<String, Object> serviceUserProps = new HashMap<>();
serviceUserProps.put(ResourceResolverFactory.SUBSERVICE, SUBSERVICE_NAME);


resolver = resolverFactory.getServiceResourceResolver(serviceUserProps);
logger.info(">>>>>>RESOLVER CREATED>>>>>");

session = resolver.adaptTo(Session.class);
QueryManager queryManager = session.getWorkspace().getQueryManager();


// Specify the query to retrieve assets with an expiry date in the past

String queryStatement = "SELECT * FROM [dam:Asset] WHERE ISDESCENDANTNODE('/content/dam/my-project') AND [jcr:content/metadata/expirationdate] < '" + currentDateString + "'";
Query query = null;


query = queryManager.createQuery(queryStatement, Query.JCR_SQL2);

QueryResult result = null;


result = query.execute();
logger.info(">>>>>>QUERY RESULT EXECUTED>>>>>");

// Iterate through the result set and move expired assets to the 'Archive' folder
if (result != null) {
NodeIterator nodesIterator = result.getNodes();

while (nodesIterator.hasNext()) {
Node hit = (Node) nodesIterator.next();
logger.info(hit.getName());

// Get the 'Archive' folder
Resource archiveFolder = resolver.getResource("/content/dam/Archive");
logger.info(archiveFolder.getPath());

if (archiveFolder != null) {
// Move the asset to the 'Archive' folder
resolver.move(hit.getPath(), archiveFolder.getPath());
}

}
}

logger.info(">>>>>>MOVING ASSET>>>>>>>");

// Save the session changes

session.save();
} catch (RepositoryException e) {
throw new RuntimeException(e);
} catch (PersistenceException e) {
throw new RuntimeException(e);
} catch (LoginException e) {
throw new RuntimeException(e);
} finally {
// Always close the ResourceResolver in a finally block
if (resolver != null && resolver.isLive()) {
resolver.close();
}
}
}
private String getCurrentDateString() {
Date currentDate = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
String currentDateString = dateFormat.format(currentDate);
return currentDateString;
}
}

DEMO

AEM provides an ‘Expires’ field in the Advanced section of assets.

If you want to use that field please make changes in the code accordingly.

I have done this by creating a custom metadata field that the scheduled job will look for. Refer to AEM Assets: Custom Metadata and Search Facets blog to create a Custom Metadata Field called ‘Expiration date’ of Asset.

Refer to this blog to create system user. Following this blog, create a User Mapper which provides required permissions to the user.

Here are some screenshots of the implementation

Initially, your Archive folder is empty. Here we use ‘sample.pdf’ as our asset which is about to expire.

Select the pdf file and open its properties. There you can find Expiration Date.

Note:- Only after creating a new Metadata field and applying that schema to your folder you can get that field. (Please refer above Custom Metadata blog)

After creating Metadata Field for Expiration date(by following the above mentioned blog), author Expiration date for the assets.

While authoring date and time on my system

Start the server and Build your code.

Check the Archive folder for your asset after the expiration date and time. It would have moved already.

And there you have it! With this guide, you should have a solid understanding of how to set up an Asset Expiration Scheduler in AEM. Feel free to customize and extend it further based on your specific requirements. Happy coding!

--

--