Content Fragments in Depth. CF Deactivation. Part 3.5

Exadel Digital Experience Team Tech Tips June 7, 2023 8 min read

Having looked at replication, we’ll now explore content fragment deactivation. If we go to the first article in this series, we’ll see there are 2 types of pages: Webinar and Event Page. An author starts by creating an Event Content Fragment, then it’s picked up by the process and no more manual involvement is needed. Once the Event has taken place and the webinar video has been uploaded, there’s no need for this in this Event Page as it points at an expired event. So, as not to unpublish it manually, we set up a mechanism to automate this process.

Content Fragment Setup

First of all, we decided to let an author decide whether a Content Fragment should be deactivated automatically by adding a new property into the Event CF Model

<deactivateonexpire jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/checkbox" fielddescription="If checked, the event (and the pages, which refers this CF) will be deactivated after it expires" listorder="4" metatype="boolean" name="deactivateOnExpire" renderreadonly="false" showemptyinreadonly="true" text="Deactivate when expired" valuetype="boolean"></deactivateonexpire>
See more See less

Deactivation Job

A new job is introduced to handle this process. com.wcm.site.jobs.deactivation.impl.EventCFDeactivationScheduler:

package com.wcm.site.jobs.deactivation.impl;
 
import static com.wcm.pursiteestorage.util.ServiceUtils.killActiveSlingJobsIfFound;
import java.util.HashMap;
import org.apache.sling.event.jobs.JobManager;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 
@Component(immediate = true, service = Runnable.class, configurationPolicy = ConfigurationPolicy.REQUIRE)
@ServiceDescription("Deactivate expired pages")
@Designate(ocd = EventCFDeactivationScheduler.Configuration.class)
public class EventCFDeactivationScheduler implements Runnable {
 
    static final String EVENT_CF_DEACTIVATE_TOPIC = "site/event-cf-deactivate";
 
    @ObjectClassDefinition(name="Event CF Deactivation Scheduler", description = "Deactivate expired pages")
    public @interface Configuration {
 
        @AttributeDefinition(
                name = "Concurrent",
                description = "Schedule task concurrently",
                type = AttributeType.BOOLEAN
        )
        boolean scheduler_concurrent() default false;
 
        @AttributeDefinition(
                name = "Expression",
                description = "Cron-job expression",
                type = AttributeType.STRING
        )
        String scheduler_expression();
 
        @AttributeDefinition(
                name = "Scheduler runOn",
                description = "Scheduler runOn",
                type = AttributeType.STRING
        )
        String scheduler_runOn();
    }
 
    @Reference
    private JobManager jobManager;
 
 
    @Override
    public void run() {
        killActiveSlingJobsIfFound(EVENT_CF_DEACTIVATE_TOPIC, jobManager);
        jobManager.addJob(EVENT_CF_DEACTIVATE_TOPIC, new HashMap<>());
    }
}
See more See less

And a json config for it:

{
  "scheduler.concurrent": false,
  "scheduler.expression": "0 */10 * ? * *",
  "scheduler.runOn": "SINGLE"
}
See more See less

And the consumer itself. com.wcm.site.jobs.deactivation.impl.EventCFDeactivationJobConsumer:

package com.wcm.site.jobs.deactivation.impl;
 
import com.wcm.site.localization.Locale;
import com.wcm.site.services.CFProxyPageService;
import com.wcm.site.services.FlushExternalCacheService;
import com.wcm.site.services.ReplicationService;
import com.wcm.site.services.indexgrid.service.impl.IndexSearchService;
import com.wcm.site.util.PdfUrlRewriteUtils;
import com.wcm.site.util.ServiceUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
 
import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import static com.wcm.site.jobs.deactivation.impl.EventCFDeactivationScheduler.EVENT_CF_DEACTIVATE_TOPIC;
 
@Component(immediate = true, service = JobConsumer.class, property = {
       JobConsumer.PROPERTY_TOPICS + ="" +="" event_cf_deactivate_topic="" },="" configurationpolicy="ConfigurationPolicy.REQUIRE)" @designate(ocd="EventCFDeactivationJobConsumer.Configuration.class)" public="" class="" eventcfdeactivationjobconsumer="" implements="" jobconsumer="" {="" private="" static="" final="" logger="" log="LoggerFactory.getLogger(EventCFDeactivationJobConsumer.class);" string="" locale_pattern="{LOCALE}" ;="" @objectclassdefinition(name="EventCFDeactivationJobConsumer" )="" @interface="" configuration="" @attributedefinition(name="CF root path patterns" string[]="" root_path_patterns();="" }="" list<string=""> rootPaths = new ArrayList<>();
 
   @Reference
   private IndexSearchService indexSearchService;
 
   @Reference
   private ResourceResolverFactory resourceResolverFactory;
 
   @Reference
   private ReplicationService replicationService;
 
   @Reference
   private FlushExternalCacheService flushExternalCacheService;
 
   @Reference
   private CFProxyPageService cfProxyPageService;
 
   @Activate
   protected void activate(Configuration configuration) {
       this.rootPaths = Stream.concat(
               Stream.of(Locale.values()).map(locale -> String.format("%s_%s", locale.getLanguageCode(), locale.getIsoCode()))
                       .flatMap(localeStr -> prepareRootPaths(configuration, localeStr).stream())
               , prepareRootPaths(configuration, PdfUrlRewriteUtils.EN).stream()
       ).collect(Collectors.toList());
   }
 
   private List<string> prepareRootPaths(Configuration configuration, String localeStr) {
       return Stream.of(configuration.root_path_patterns())
               .map(pattern -> StringUtils.replace(pattern, LOCALE_PATTERN, localeStr))
               .collect(Collectors.toList());
   }
 
   @Override
   public JobResult process(Job job) {
 
       ServiceUtils.withResourceResolver(resourceResolverFactory, "any_appropriate_user", resolver -> {
           LOG.info("Deactivation started, userId: {}", resolver.getUserID());
 
           Set<string> pathsToDeactivate = new HashSet<>();
           Set<string> pathsToFlush = new HashSet<>();
 
           for (Resource resource : indexSearchService.getExpiredWebinarCFs(resolver, rootPaths)) {
               List<resource> pageResources = cfProxyPageService.findProxyPageResources(resolver, resource.getPath());
               pageResources.stream()
                       .filter(Objects::nonNull)
                       .map(res -> StringUtils.contains(res.getPath(), JCR_CONTENT) ? res.getParent() : res)
                       .filter(Objects::nonNull)
                       .map(Resource::getPath)
                       .forEach(pagePath -> processItem(pagePath, pathsToDeactivate, pathsToFlush));
               pathsToDeactivate.add(resource.getPath());
               pathsToFlush.add(resource.getPath());
           }
 
           if (!pathsToDeactivate.isEmpty()) {
               LOG.info("Deactivating paths: [{}]", String.join(",", pathsToDeactivate));
               replicationService.deactivate(resolver, pathsToDeactivate.toArray(new String[0]));
           }
           if (!pathsToFlush.isEmpty()) {
               LOG.info("Flushing paths: [{}]", String.join(",", pathsToFlush));
               flushExternalCacheService.flushCDN(pathsToFlush.toArray(new String[0]), resolver);
           }
       });
 
       return JobConsumer.JobResult.OK;
   }
 
   private void processItem(String pagePath, Set<string> pathsToDeactivate, Set<string> pathsToFlush) {
       pathsToDeactivate.add(pagePath);
       pathsToFlush.add(pagePath);
   }
 
}
</string></string></resource></string></string></string></string="">
See more See less

And its config:

{
"root.path.patterns": [
"/content/dam/site-com/content-fragments/${LOCALE}/webinars", "/content/dam/site-com/content-fragments/${LOCALE}/events"],
}
See more See less

And a simple method to query expired CFs. com.wcm.site.services.indexgrid.service.impl.IndexSearchService:

..
@Reference
private QueryBuilder queryBuilder;
 
public List<resource> getExpiredWebinarCFs(ResourceResolver resourceResolver, List<string> rootPaths) {
   return getResults(prepareQuery(resourceResolver, expiredWebinarsCFPredicate(rootPaths)), null);
}
 
private Query prepareQuery(ResourceResolver resourceResolver, PredicateGroup predicates) {
   if (predicates == null) {
       return null;
   }
 
   return queryBuilder.createQuery(predicates, resourceResolver.adaptTo(Session.class));
}
 
 
private PredicateGroup expiredWebinarsCFPredicate(List<string> paths) {
   ImmutableMap.Builder<string, object=""> builder = ImmutableMap.<string, object="">builder()
           .put("type", "dam:Asset");
 
   for (int i = 0; i < paths.size(); i++) {
       builder.put(String.format("group.0_group.%d_path", i), paths.get(i));
   }
   builder.put("group.0_group.p.or", "true");
 
   builder.put("1_property", "jcr:content/cq:lastReplicationAction");
   builder.put("1_property.value", "Activate");
  
   builder.put("2_property", "jcr:content/data/master/deactivateOnExpire")
   builder.put("2_property.value", "true");
 
   builder.put("3_daterange.property", "jcr:content/data/master/endDate");
   builder.put("3_daterange.upperOperation", "<=");
   builder.put("3_daterange.upperBound", DateUtil.getISO8601Date(Calendar.getInstance().getTime(), TimeZone.getTimeZone("GMT")));
  
 
   return PredicateGroup.create(builder
           .put("p.limit", "-1")
           .build());
}
…..
</string,></string,></string></string></resource>
See more See less

As we’re searching for the following properties “cq:lastReplicationAction”“deactivateOnExpire”“endDate” – make sure to add them to the corresponding indexes.

That’s pretty much it. This is just a CF real usage example. You don’t have to, and wouldn’t need to, copy it completely. But we hope that this series of articles will add some ideas on how to incorporate CFs into a project and automate a lot of manual processes. Need a hand with coding? Look no further! We’re AEM developers who know this CMS inside out. We’re here to help!

Author: Iryna Ason

Was this article useful for you?

Get in the know with our publications, including the latest expert blogs