Creating a Custom Predicate for Querying by Version

Creating a Custom Predicate for Querying by Version

Recently I found myself implementing a custom parametric search UI within CQ. The intent of this view was to allow a user to query for Resources based on any combination of parameters. For all but one of these parameters, I was able to leverage the OOTB predicates but for querying on version, however, I had to think outside the box a bit. While the other parameters could all, in some form or another, be evaluated against a property within a Resource's JCR "branch", version data is stored elsewhere in the repository: specifically jcr:root/jcr:system/versionStorage. Further, no predicate exists to do the legwork here. After first querying for version separately, sorting both result sets, and merging them by path, I came to the grim realization that I was going to have to wire up custom pagination with this approach as it was not acceptable to arbitrarily limit the number of results and scrolling was not an elegant solution either. QueryBuilder provides both pagination and result limiting OOTB. With this knowledge happily in mind, I began my journey into custom predicates. With some help from this post by CoE, I had things up and running in no time. My general approach was as follows:

1. Extend AbstractPredicateEvaluator

com.day.cq.search.eval.AbstractPredicateEvaluator provides a base class for any PredicateEvaluator. All of the existing predicates (such as fulltext) extend it. Further, it's an OSGi component whose deployment can be verified in the Felix components console (located at [server]:[port]/system/console/components). For predicates, they should be declared as factories by including a factory attribute within the component annotation. Deploying the component in this way ensures a new instance of each component is instantiated each time it's referenced (what we'd expect since any given query is its own thing). Here's what the class signature looks like:

	
@Component(factory="com.day.cq.search.eval.PredicateEvaluator/version")
public class VersionPredicate extends AbstractPredicateEvaluator 
	

2. Override the appropriate methods

Note the full path to the PredicateEvaluator class, followed by /version. This essentially becomes the string by which you leverage this predicate within QueryBuilder. From there, there are four methods of interest to us within a PredicateEvaluator:


public String getXPathExpression(Predicate predicate, EvaluationContext context)
public boolean includes(Predicate predicate, Row row, EvaluationContext context)
public boolean canXpath(Predicate predicate, EvaluationContext context)
public boolean canFilter(Predicate predicate, EvaluationContext context)
	
These four methods combined provide the special sawce (yes, sawce - just roll with it) we need. getXpathExpression takes your query and returns (if it can), a valid XPath expression that'll get you the data you need. This expression then gets run and a result set is returned. Notice, however, that the getXpathExpression method doesn't provide any individual results whereas the includes method provides a row. You guessed it. The prior doesn't allow you to do any additional filtering while the latter does. So in my case, I needed to make sure that the includes method was being invoked for this predciate. To accomplish this, I updated the canXpath and canFilter methods accordingly. The result (pun intended) was that, for each hit (row), I could further investigate to see if that Resource has a version that matches the version query parameter.

3. Use PageManager to access a Resource's revision history

But wait! If versions are stored somewhere else in the JCR, then how do you know by looking at the actual resource for whose version's you're querying, that it actually has versions? The answer is that, when you create a create a version of something in AEM, it applies a jcr:baseVersion property on that Resource's jcr:content. So from this, we can query for the existence of any Resources with jcr:content/@jcr:baseVersion, use adaptation to get a PageManager, and then get the Resource's revisions. From there we can check the name of each version to see if we have a match. If the current Resource has the version you're looking for, return true and it'll be included in the results. Here's the full implementation for VersionPredicate:


package com.sixdimensions.olm.cq.internal.servlets.helpers;

import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;

import javax.jcr.RepositoryException;
import javax.jcr.query.Row;
import org.apache.felix.scr.annotations.Component;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.search.Predicate;
import com.day.cq.search.eval.AbstractPredicateEvaluator;
import com.day.cq.search.eval.EvaluationContext;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.Revision;
import com.day.cq.wcm.api.WCMException;
import com.sixdimensions.olm.cq.api.SharedConstants;
import javax.jcr.version.Version;

@Component(factory="com.day.cq.search.eval.PredicateEvaluator/version")
public class VersionPredicate extends AbstractPredicateEvaluator 
{
	private static final Logger log = LoggerFactory.getLogger(VersionPredicate.class);
	
	@Override
	public boolean includes(Predicate predicate, Row row, EvaluationContext context) 
	{
		Resource resource = context.getResource(row);
		String queryVersion = predicate.get("version");
		
		if(resource.getResourceType().equals("sling:OrderedFolder"))
			return false;
		
		Resource jcrContent = resource.getChild("jcr:content");
		if(null != jcrContent)
		{
			ValueMap properties = jcrContent.adaptTo(ValueMap.class);
			if(resource.getResourceType().equals("cq:Page")  && null != properties.get("jcr:baseVersion"))
			{
				try
				{
					PageManager pm = resource.adaptTo(Page.class).getPageManager();
					Collection<Revision> revisions = pm.getRevisions(resource.getPath(), null);
					Iterator<Revision> it = revisions.iterator();
					while(it.hasNext())
					{
						if(it.next().getVersion().getName().equals(queryVersion))
						{
							return true;
						}
					}
				}
				catch(WCMException e)
				{
					log.error("WCMException caught in VersionPredicate: ", e);
					e.printStackTrace();
				}
				catch(RepositoryException e)
				{
					log.error("RepositoryException caught in VersionPredicate: ", e);
					e.printStackTrace();
				}
			}
		}
			
		return false;
	}
	
	@Override
	public boolean canFilter(Predicate predicate, EvaluationContext context)
	{
		return true;
	}
	
	@Override
	public boolean canXpath(Predicate predicate, EvaluationContext context)
	{
		return false;
	}
}

After installing this component, you should now be able to test it through the Query Debugger located at [server]:[port]/libs/cq/search/content/querydebug.html. To test, create some versions of, say, a few pages under the main Geometrixx site and then run the following through QueryBuilder:


path=/content/geometrixx
version=[the version you want to search for]

It should be noted that the above code purposely has some additional, use case-specific checks in order to make things a bit more efficient. Filtering in this way is substantially slower than XPath so the quicker the method returns from each Resource, the better. Adjust this to your own needs. I hope this has been helpful! If you have any questions, don't hesitate to let me know.

Share this post

0 Comments

comments powered by Disqus