Custom PredicateEvaluators or: How I Learned to Stop Worrying and Love QueryBuilder

Custom PredicateEvaluators or: How I Learned to Stop Worrying and Love QueryBuilder

One client's AssetShare page contains a checkbox: Needs Meta-data. Checking this box is supposed to return all assets coming from the DAM that do not have metadata. Specifically in this case we check for a particular attribute (@dc:title) that's a child of the jcr:content/metadata node.

Our story begins with the following problem: checking the box wouldn't return certain assets. Upon investigation, I discovered that the one asset not returning in the query HAD an @dc:title attribute, but that this attribute was blank. It had no value.

 

 

Out comes the toolbox:

Chrome's Javascript debugger, to pull querybuilder http requests

http://localhost:4502/libs/cq/search/content/querydebug.html

and http://localhost:4502/crx/explorer/ui/search.jsp

 

Through the course of investigating I discovered that the following xpath is the correct one:

/jcr:root/content/dam//element(*, dam:Asset)[(not(jcr:content/metadata/@dc:title) or jcr:like(jcr:content/metadata/@dc:title, ''))]

Translation: I want all elements that are of type dam:Asset, that are children of /jcr:root/content/dam, that either do not have a jcr:content/metadata/@dc:title, or have a @dc:title property that is blank ('').

Being able to write out what you need in XPath will make it easier to figure out how to write the QueryBuilder syntax. The trouble with this is, you can't express it in QueryBuilder syntax, try as you might.

I eventually ended up with a setup that looked like this:

10_group.p.or=true
10_group.1_property=jcr:content/metadata/dc:title
10_group.1_property.operation=not
10_group.2_property=jcr:content/metadata/dc:title
10_group.2_property.operation=like
10_group.2_property.value='' 

 

Many combinations of value later, and the search still wouldn't work. The reason?

When QueryBuilder encounters a value of blank, meaning that you do not give a value, it drops the whole constraint.

There is no way, using the standard predicates, to express a null value. And for this example, you absolutely have to be able to pass null as a value. Thus, there is no way to use QueryBuilder through the JSON servlet and express this kind of constraint.

 

Enter the custom predicate. Following this post by Yogesh Upadhyay, I created my own custom predicate, which gets installed as part of an existing OSGi bundle.

package com.sixdimensions.cq.search.eval;

import javax.jcr.query.Row;import com.day.cq.search.Predicate;

import com.day.cq.search.eval.AbstractPredicateEvaluator;
import com.day.cq.search.eval.EvaluationContext;
import org.apache.felix.scr.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Custom predicate
* emptyproperty.property=
* @author Center of Excellence
*
*/
@Component(
metatype = false,
factory = "com.day.cq.search.eval.PredicateEvaluator/emptyproperty"
)
public class EmptyPropertyPredicateEvaluator extends AbstractPredicateEvaluator
{
  private static final Logger logger = LoggerFactory.getLogger(EmptyPropertyPredicateEvaluator.class);
 
  /** Field description */
  public static final String PROPERTY = "property";

/**
* Method description
*
*
* @param predicate
* @param row
* @param context
*
* @return
*/
@Override
public boolean includes(Predicate predicate, Row row, EvaluationContext context)
{
  if (predicate.hasNonEmptyValue(PROPERTY))
  {
    return true;
  }
  return super.includes(predicate, row, context);
}

/**
* Create custom Xpath expression based on property
*
* @param predicate
* @param context
*
* @return
*/
@Override
public String getXPathExpression(Predicate predicate, EvaluationContext context)
{
  logger.info("-> Get me my XPath");
  if (!predicate.hasNonEmptyValue(PROPERTY))
  {
    return null;
  }
  if (predicate.hasNonEmptyValue(PROPERTY))
  {
    logger.info("jcr:like(" + predicate.get(PROPERTY) + ", ''");
    return "jcr:like(" + predicate.get(PROPERTY) + ", '')";
  }
  return null;
}
}

****

What this class does, when installed, is create a new custom predicate. That predicate is accessible through the standard QueryBuilder interfaces (Java, JSON servlet, etc) and offers a new predicate called emptyproperty. The only attribute for emptyproperty is property, which must be specified as the full path relative to the present location. Thus, for our example, the property is specified as jcr:content/metadata/@dc:title (where the @ is the only difference in syntax between this and other predicates).

Rendered in the QueryBuilder syntax it rather looks like this now:

10_group.p.or=true
10_group.1_property=jcr:content/metadata/dc:title
10_group.1_property.operation=not
10_group.2_emptyproperty.property=jcr:content/metadata/@dc:title

The advantages of being able to do this include being able to write very specific, very targeted query predicates you might not be able to express conventionally using QueryBuilder's standard predicates. To install, define a class similar to the one above, and either place it in its own OSGi bundle, or add it to an existing one. Make sure you do NOT change the component factor line above: you must specifically reference that this is a component factory for com.day.cq.search.eval.PredicateEvaluator to handle your custom property, which is the bit that comes after the /.

 

I hope this is helpful to you as you work with QueryBuilder. 

Share this post

0 Comments

comments powered by Disqus