Best Practices for Committing and Saving Nodes Using AEM Resource Resolver

Problem Statement:

When should you commit or save nodes using resource resolver, and is it advisable to save nodes inside a loop?

Requirement:

This article discusses best practices for committing and saving nodes using AEM (Adobe Experience Manager) Resource Resolver. It highlights the importance of proper synchronization when working with resources and the different methods that can be used to manage them. The article also provides examples of saving nodes inside and outside for loops and explains how to avoid updating nodes unnecessarily by validating property existence and values.

Save 50 nodes with some properties

Introduction:

The ResourceResolver defines the API which may be used to resolve Resource objects and work with such resources as creating, editing or updating them. The resource resolver is available to the request processing servlet through the SlingHttpServletRequest.getResourceResolver() method. A resource resolver can also be created through the ResourceResolverFactory service.

A ResourceResolver is generally not thread safe! As a consequence, an application that uses the resolver, its returned resources, and/or objects resulting from adapting either the resolver or a resource, must provide proper synchronization to ensure no more than one thread concurrently operate against a single resolver, resource, or resulting objects.

An algorithm is used to resolve and getResource and provide various methods to manage resources like:

OperationDescription
Create(Resource, String, Map)for creating a new resource.
Delete(Resource)to delete a resource.
Adaptable.adaptTo(Class)allows to adapt a resource to a ModifiableValueMap to update a resource.
Move(String, String)to move resources.
Copy(String, String)to copy resources.
Commit()commits all staged changes.
Revert()reverts all staged changes.

All changes are transient and require committing them at the end

Hence as per API documentation, it’s better to stage all the changes before calling commit or revert.

But please make sure we are not trying to save millions of nodes at a time and also updating nodes takes more time compared to creating a new one as per the adapto conference showcase.

Hence check whether the node already has the property and value before you save it.

Resolution:

Saving resolver inside for loop

For our use case, I am using ResourceUtil.getOrCreateResource() for creating or getting the exiting node, and if it creating then it will be saving the node with default properties like jcr:primaryType = un:unstructured

Using ResourceUtil increases code readability and maintainability

Parameters:
resolver – The resource resolver to use for the creation
path – The full path to be created
resourceProperties – The optional resource properties of the final resource to create
intermediateResourceType – THe optional resource type of all intermediate resources
autoCommit – If set to true, a commit is performed after each resource creation.

In the below example, I am creating a for loop, I am creating the node (resource) with default properties, and I am setting auto-commit as true. After creating the resource, I am adapting it to ModifiableValueMap and I will be adding a new property name and value as “property+index” and committing the resolver.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, true);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      map.put("name", "property" + index);
      resourceResolver.commit();
    }
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

Saving resolver outside for loop

In the below example, I have to remove auto save as false and the rest of the code remains the same, but I am committing resolver outside for loop. By doing so, I can stage the resource resolver and commit it at last.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, false);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      map.put("name", "property" + index);
    }
    resourceResolver.commit();
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

What will happen if I rerun the same code?

ResourceUtil would handle getting the existing resource instead of recreating it, but I would still be updating the resource and committing the changes, which is a costly process.

Better implementation with Validation

In order to avoid the updating of the node we could validate that the property exists and check the value and if and only if the resolver has changed, will commit.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, false);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      if (!map.containsKey("name") || !StringUtils.equals(map.get("name", StringUtils.EMPTY), "property" + index)) {
        map.put("name", index);
      }
    }
    if (resourceResolver.hasChanges()) {
      resourceResolver.commit();
    }
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

Efficiently Iterating Child Nodes in Adobe Experience Manager (AEM)

Problem statement:

How can I iterate child nodes and get certain properties? Specifically, the requirement is to get child resources of the current resource and get all image component file reference properties into a list.

Requirement:

Get child resources of the current resource and get all image component file reference properties into a list

Can I use Java 8 Streams?

Introduction: Using while or for loop:

@PostConstruct
private void initModel() {
  List < String > imagePath = new ArrayList < > ();
  Iterator < Resource > children = resource.listChildren();
  while (children.hasNext()) {
    final Resource child = children.next();
    if (StringUtils.equalsIgnoreCase(child.getResourceType(), "wknd/components/image")) {
      Image image = modelFactory.getModelFromWrappedRequest(request, child, Image.class);
      imagePath.add(image.getFileReference());
    }
  }
}

Introduction Abstract Resource Visitor:

Sling provides AbstractResourceVisitor API, which performs traversal through a resource tree, which helps in getting child properties.

Create the class which extends AbstractResourceVisitor abstract class

Override accept, traverseChildren and visit methods as shown below

Call visit inside accepts method instead of super. visit, I have observed it was traversing twice if I use super hence keep this in mind

package utils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.AbstractResourceVisitor;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import com.day.cq.wcm.foundation.Image;
import com.drew.lang.annotations.NotNull;

public class ExampleResourceVisitor extends AbstractResourceVisitor {
	
	private static final String IMAGE_RESOURCE_TYPE = "wknd/components/image";
	private static final String TEXT_RESOURCE_TYPE = "wknd/components/text";
	
	private static final ArrayList<String> ACCEPTED_PRIMARY_TYPES = new ArrayList<>();
	static {
		ACCEPTED_PRIMARY_TYPES.add(IMAGE_RESOURCE_TYPE);
		ACCEPTED_PRIMARY_TYPES.add(TEXT_RESOURCE_TYPE);
	}
	
	private final List<String> imagepaths = new ArrayList<>();	
	
	public List<String> getImagepaths() {
		return imagepaths;
	}

	@Override
	public final void accept(final Resource resource) {
		if (null != resource) {
			final ValueMap properties = resource.adaptTo(ValueMap.class);
			final String primaryType = properties.get(ResourceResolver.PROPERTY_RESOURCE_TYPE, StringUtils.EMPTY);
			if(ACCEPTED_PRIMARY_TYPES.contains(primaryType)){
				visit(resource);
			}
			this.traverseChildren(resource.listChildren());
		}
	}

	@Override
	protected void traverseChildren(final @NotNull Iterator<Resource> children) {
		while (children.hasNext()) {
			final Resource child = children.next();
			accept(child);
		}
	}

	@Override
	protected void visit(@NotNull Resource resource) {
		final ValueMap properties = resource.adaptTo(ValueMap.class);
		final String primaryType = properties.get(ResourceResolver.PROPERTY_RESOURCE_TYPE, StringUtils.EMPTY);
		if (StringUtils.equalsIgnoreCase(primaryType, IMAGE_RESOURCE_TYPE)) {
			imagepaths.add(properties.get(Image.PN_REFERENCE, StringUtils.EMPTY));
		}
	}
}

Call the ExampleResourceVisitor and pass the resource and call the getImagepaths() to get the list of image paths

@PostConstruct
private void initModel() {
  ExampleResourceVisitor exampleResourceVisitor = new ExampleResourceVisitor();
  exampleResourceVisitor.accept(resource);
  List < String > imageVisitorPaths = exampleResourceVisitor.getImagepaths();
}

Introduction Resource Filter Stream:

Resource Filter bundle provides a number of services and utilities to identify and filter resources in a resource tree.

Resource Filter Stream:

ResourceFilterStream combines the ResourceStream functionality with the ResourcePredicates service to provide an ability to define a Stream<Resource> that follows specific child pages and looks for specific Resources as defined by the resources filter script. The ResourceStreamFilter is accessed by adaptation.

ResourceFilterStream rfs = resource.adaptTo(ResourceFilterStream.class);
rfs.stream().collect(Collectors.toList());

Example code for our problem statement would be like this:

@PostConstruct
private void initModel() {
  ResourceFilterStream rfs = resource.adaptTo(ResourceFilterStream.class);
  List < String > imagePaths = rfs.stream()
    .filter(r -> StringUtils.equalsIgnoreCase(r.getResourceType(), "wknd/components/image"))
    .map(r -> modelFactory.getModelFromWrappedRequest(request, r, Image.class).getFileReference())
    .collect(Collectors.toList());
}

Optimizing Traversals

Similar to indexing in a query there are strategies that you can do within a tree traversal so that traversals can be done in an efficient manner across a large number of resources. The following strategies will assist in traversal optimization.


Limit traversal paths

In a naive implementation of a tree traversal, the traversal occurs across all nodes in the tree regardless of the ability of the tree structure to support the nodes that are being looked for. An example of this is a tree of Page resources that has a child node of jcr:content which contains a subtree of data to define the page structure. If the jcr:content node is not capable of having a child resource of type Page and the goal of the traversal is to identify Page resources that match specific criteria then the traversal of the jcr:content node can not lead to additional matches. Using this knowledge of the resource structure, you can improve performance by adding a branch selector that prevents the traversal from proceeding down a nonproductive path


Limit memory consumption

The instantiation of a Resource object from the underlying ResourceResolver is a nontrivial consumption of memory. When the focus of a tree traversal is obtaining information from thousands of Resources, an effective method is to extract the information as part of the stream processing or utilize the forEach method of the ResourceStream object which allows the resource to be garbage collected in an efficient manner.