All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy