com.day.cq.search.eval.JcrPropertyPredicateEvaluator Maven / Gradle / Ivy
/*
* Copyright 1997-2008 Day Management AG
* Barfuesserplatz 6, 4001 Basel, Switzerland
* All Rights Reserved.
*
* This software is the confidential and proprietary information of
* Day Management AG, ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Day.
*/
package com.day.cq.search.eval;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.query.Row;
import com.day.cq.search.impl.util.GlobPatternUtil;
import org.apache.felix.scr.annotations.Component;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.search.Predicate;
import com.day.cq.search.facets.FacetExtractor;
import com.day.cq.search.facets.extractors.DistinctValuesFacetExtractor;
/**
* Matches on JCR properties and their values.
*
*
* Supports facet extraction. Will provide buckets for each unique property value in the results.
*
*
Name:
* property
*
* Properties:
*
* - property
* - relative path to property, for example
jcr:title
* - value
* - value to check property for; follows the JCR property type to string conversions
* - N_value
* - use 1_value, 2_value, ... to check for multiple values (combined with OR by default, with AND if and=true) (since 5.3)
* - and
* - set to true for combining multiple values (N_value) with AND (since 5.3)
* - operation
* - "equals" for exact match (default), "unequals" for unequality comparison, "like" for using the jcr:like xpath function (optional),
* "not" for no match (eg. "not(@prop)" in xpath, value param will be ignored), "exists" for
* existence check (value can be true - property must exist, the default - or false - same as "not") , "equalsIgnoreCase" for
* case insensitive match, "unequalsIgnoreCase" for case insensitive unequality comparison
* - depth
* - number of wildcard levels underneath which the property/relative path can exist
* (for instance, property=size depth=2 will check
* node/size, node/*/size and node/*/*/size)
*
*
* @since 5.2
*/
@Component(metatype = false, factory="com.day.cq.search.eval.PredicateEvaluator/property")
public class JcrPropertyPredicateEvaluator extends AbstractPredicateEvaluator {
private static final Logger log = LoggerFactory.getLogger(JcrPropertyPredicateEvaluator.class);
public static final String PROPERTY = "property";
public static final String VALUE = "value";
public static final String OPERATION = "operation";
public static final String OP_EQUALS = "equals";
public static final String OP_UNEQUALS = "unequals";
public static final String OP_LIKE = "like";
public static final String OP_NOT = "not";
public static final String OP_EXISTS = "exists";
public static final String OP_EQUALS_IGNORE_CASE = "equalsIgnoreCase";
public static final String OP_UNEQUALS_IGNORE_CASE = "unequalsIgnoreCase";
public static final String AND = "and";
public static final String DEPTH = "depth";
public static final String STEP = "*/";
public static final int MAX_NUMBER_OF_VALUES = Integer.MAX_VALUE;
@Override
public String getXPathExpression(Predicate p, EvaluationContext context) {
final String property = p.get(PROPERTY);
int depth = Integer.parseInt(p.get(DEPTH, "0"));
String operation = p.get(OPERATION);
// check for single value or NOT or EXISTS
// property = jcr:title
// property.value = foo
// => xpath:
// @jcr:title = 'foo'
if (OP_NOT.equals(operation) || OP_EXISTS.equals(operation) || p.hasNonEmptyValue(VALUE)) {
return getXPathExpression(property, p.get(VALUE), getOperation(p, operation), depth);
}
// multiple values:
// property = jcr:title
// property.1_value = foo
// property.2_value = bar
// property.3_value = test
// => xpath:
// (@jcr:title = 'foo' or @jcr:title = 'bar' or @jcr:title = 'test')
final boolean and = p.getBool(AND);
StringBuilder builder = new StringBuilder();
builder.append(XPath.OPENING_BRACKET);
// loop through params and find those with N_value pattern
// (to support wholes in the list, eg. 1_value, 4_value, 27_value)
for(Entry entry: p.getParameters().entrySet()) {
final String key = entry.getKey();
if (key != null && key.endsWith("_" + VALUE)) {
// if we have more than the opening bracket "("
if (builder.length() > 1) {
builder.append(and ? XPath.AND : XPath.OR);
}
builder.append(getXPathExpression(property, entry.getValue(), operation, depth));
}
}
// if we have only the opening bracket "(", no value was found and return null
if (builder.length() == 1) {
return null;
}
builder.append(XPath.CLOSING_BRACKET);
return builder.toString();
}
protected String getXPathExpression(String property, String value, String operation, int depth) {
String expr = getXPathExpression(property, value, operation);
// If depth > 0 we'll be OR-ing the various depth checks together:
// ( prop='value' or */prop='value' or */*/prop='value' )
// which requires parentheses.
//
StringBuilder builder = new StringBuilder();
if (depth > 0) {
builder.append(XPath.OPENING_BRACKET);
}
// Specially handle like operation with depth to generate query :
// (jcr:like(@jcr:createdBy, 'admin') or jcr:like(*/@jcr:createdBy, 'admin') or jcr:like(*/*/@jcr:createdBy, 'admin') )
if(null!=operation && operation.equals(OP_LIKE)) {
StringBuilder wildCardBuilder = new StringBuilder();
String exprWithWildCard = "";
for (int i = 0; i <= depth; i++) {
if (i > 0) {
builder.append(XPath.OR);
wildCardBuilder.setLength(0);
for (int j = 0; j < i; j++) {
wildCardBuilder.append(STEP);
}
exprWithWildCard = expr.replace(XPath.getPropertyPath(property), wildCardBuilder.toString()+XPath.getPropertyPath(property));
builder.append(exprWithWildCard);
}
else
builder.append(expr);
}
}else {
for (int i = 0; i <= depth; i++) {
if (i > 0) {
builder.append(XPath.OR);
for (int j = 0; j < i; j++) {
builder.append(STEP);
}
}
builder.append(expr);
}
}
if (depth > 0) {
builder.append(XPath.CLOSING_BRACKET);
}
return builder.toString();
}
protected String getXPathExpression(String property, String value, String operation) {
if (property == null || property.length() == 0 ||
(!OP_NOT.equals(operation) && !OP_EXISTS.equals(operation) && (value == null || value.length() == 0))) {
return null;
}
if (OP_EQUALS.equals(operation)) {
return XPath.getEqualsExpression(property, value);
} else if (OP_UNEQUALS.equals(operation)) {
return XPath.getUnequalsExpression(property, value);
} else if (OP_LIKE.equals(operation)) {
return XPath.getJcrLikeExpression(property, value);
} else if (OP_EQUALS_IGNORE_CASE.equals(operation)) {
return XPath.getCaseInsensitiveEqualsExpression(property, value);
} else if (OP_UNEQUALS_IGNORE_CASE.equals(operation)) {
return XPath.getCaseInsensitiveUnqualsExpression(property, value);
} else if (OP_EXISTS.equals(operation)) {
return XPath.getPropertyPath(property);
} else if (OP_NOT.equals(operation)) {
return XPath.getNotExpression(property);
} else {
return XPath.getEqualsExpression(property, value);
}
}
/**
* @deprecated since 5.4; use {@link XPath#getEqualsExpression(String, String)} instead
*/
protected String getEqualsExpression(String property, String value) {
return XPath.getEqualsExpression(property, value);
}
/**
* Takes care of converting an operation=exists with a value=false to an
* operation=not.
*/
private String getOperation(Predicate p, String operation) {
if (OP_EXISTS.equals(operation)) {
if ("false".equals(p.get(VALUE, "true"))) {
operation = OP_NOT;
}
}
return operation;
}
@Override
public String[] getOrderByProperties(Predicate p, EvaluationContext context) {
return new String[] { p.get(PROPERTY) };
}
@Override
public FacetExtractor getFacetExtractor(Predicate p, EvaluationContext context) {
if (p.hasNonEmptyValue(PROPERTY)) {
Predicate template = p.clone();
template.set(OPERATION, OP_EQUALS);
return new DistinctValuesFacetExtractor(p.get(PROPERTY), null, template, VALUE);
} else {
return null;
}
}
@Override
public boolean includes(Predicate p, Row row, EvaluationContext context) {
String operation = p.get(OPERATION, OP_EQUALS);
int depth = Integer.parseInt(p.get(DEPTH, "0"));
if (OP_NOT.equals(operation) || OP_EXISTS.equals(operation) || p.hasNonEmptyValue(VALUE)) {
// single value or NOT or EXISTS
return includes(context.getNode(row), context.getPath(row), p.get(PROPERTY), p.get(VALUE), getOperation(p, operation), depth);
}
// multi value (in the query) case
final boolean and = p.getBool(AND);
operation = getOperation(p, operation);
// loop through params and find those with N_value pattern
// (to support wholes in the list, eg. 1_value, 4_value, 27_value)
boolean emptyPredicate = true;
for (Entry entry: p.getParameters().entrySet()) {
final String key = entry.getKey();
if (key != null && key.endsWith("_" + VALUE)) {
emptyPredicate = false;
boolean match = includes(context.getNode(row), context.getPath(row), p.get(PROPERTY), entry.getValue(), operation, depth);
if (and) {
// all must match
if (!match) {
return false;
}
} else {
// only one must match
if (match) {
return true;
}
}
}
}
if (and) {
// all matched
return true;
} else {
// none matched
// special case: the CQ touch-optimized filters always submit predicate query parameters and signal "disabled"
// by the lack of any specified values -- so if the predicate was empty of values, don't filter
return emptyPredicate;
}
}
protected boolean includes(Node node, String path, String property, String value, String operation, int depth) {
boolean matches = includes(node, path, property, value, operation);
if (!matches && depth > 0) {
try {
Iterator it = node.getNodes();
while (!matches && it.hasNext()) {
matches = includes(it.next(), path, property, value, operation, depth - 1);
}
} catch (RepositoryException e) {
log.error("Could not evaluate property = '" + property + "', value = '" + value + "', node = '" + path + "'", e);
throw new RuntimeException("", e);
}
}
return matches;
}
protected boolean includes(Node node, String path, String property, String value, String operation) {
if (property == null || property.length() == 0 ||
(!OP_NOT.equals(operation) && !OP_EXISTS.equals(operation) && (value == null || value.length() == 0))) {
return true;
}
try {
// might be relative property path: "childnode/prop"
String childNode = Text.getRelativeParent(property, 1);
String propName = Text.getName(property);
if (childNode.length() > 0) {
if (node.hasNode(childNode)) {
// node exists => normal behaviour
node = node.getNode(childNode);
} else {
// node does not exist (special case)
// => a constraint such as "childnode/@prop = 'value'" means:
// given that childnode exists, check if prop is of 'value';
// but if childnode does not exist at all, we cannot check
// and hence never include this in the result at all
// (same behavior as in Xpath, of course)
return false;
}
}
if (node.hasProperty(propName)) {
Property prop = node.getProperty(propName);
if (prop.isMultiple()) {
// additional check for null array or empty array... should never be the case
if (OP_NOT.equals(operation)) {
return (prop.getValues() == null || prop.getValues().length == 0);
} else if (OP_EXISTS.equals(operation)) {
return (prop.getValues() != null && prop.getValues().length > 0);
}
for (Value v : prop.getValues()) {
// at least one match in a multi-value property => true
if (matches(value, operation, v.getString())) {
return true;
}
}
return false;
} else {
return matches(value, operation, prop.getString());
}
} else {
// non-existent property
if (OP_NOT.equals(operation)) {
return true;
} else if (OP_EXISTS.equals(operation)) {
return false;
}
return false;
}
} catch (ValueFormatException e) {
log.warn("Could not evaluate property = '" + property + "', value = '" + value + "', node = '" + path + "'", e);
} catch (RepositoryException e) {
log.error("Could not evaluate property = '" + property + "', value = '" + value + "', node = '" + path + "'", e);
throw new RuntimeException("", e);
}
return true;
}
private boolean matches(String value, String operation, String propValue) {
if (OP_NOT.equals(operation)) {
return propValue == null;
} else if (OP_EXISTS.equals(operation)) {
return propValue != null;
} else if (OP_EQUALS.equals(operation)) {
return propValue.equals(value);
} else if (OP_UNEQUALS.equals(operation)) {
return !propValue.equals(value);
} else if (OP_LIKE.equals(operation)) {
// emulate jcr:like
return Pattern.matches(convertWildcardsForGlobPattern(value), propValue);
} else if (OP_EQUALS_IGNORE_CASE.equals(operation)) {
return propValue.equalsIgnoreCase(value);
} else if (OP_UNEQUALS_IGNORE_CASE.equals(operation)) {
return !propValue.equalsIgnoreCase(value);
} else /* unknown */ {
return false;
}
}
private String convertWildcardsForGlobPattern(String term) {
term = term.replace(XPath.JCR_LIKE_ANY_WILDCARD, '*')
.replace(XPath.JCR_LIKE_SINGLE_WILDCARD, '?');
return GlobPatternUtil.convertWildcardToRegex(term);
}
@Override
public boolean canXpath(Predicate predicate, EvaluationContext context) {
return true;
}
@Override
public boolean canFilter(Predicate predicate, EvaluationContext context) {
return true;
}
}