com.day.cq.search.eval.PredicateGroupEvaluator 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.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.jcr.query.Row;
import org.apache.felix.scr.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.search.Predicate;
import com.day.cq.search.PredicateGroup;
import com.day.cq.search.facets.FacetExtractor;
/**
* Allows to build nested conditions. Groups can contain nested groups.
* Everything in a querybuilder query is implicitly in a root group, which
* can have p.or
and p.not
as well.
*
*
* Example for matching either one of two properties against a value:
* group.p.or=true
* group.1_property=jcr:title
* group.1_property.value=My Page
* group.2_property=navTitle
* group.2_property.value=My Page
*
* This is conceptually (1_property OR 2_property)
.
*
*
* Example for nested groups:
* fulltext=Management
* group.p.or=true
* group.1_group.path=/content/geometrixx/en
* group.1_group.type=cq:Page
* group.2_group.path=/content/dam/geometrixx
* group.2_group.type=dam:Asset
*
* This searches for the term "Management" within pages in /content/geometrixx/en
* or in assets in /content/dam/geometrixx. This is conceptually
* fulltext AND ( (path AND type) OR (path AND type) )
.
* Be aware that such OR joins need good indexes for performance.
*
*
Name:
* group
*
* Properties:
*
* - p.or
- if set to "true", only one predicate in the group must match (defaults to "false", meaning all must match)
* - p.not
- if set to "true", negates the group (defaults to "false")
* - <predicate>
* - add nested predicates
* - N_<predicate>
* - add multiple nested predicates of the same time, e.g. 1_property, 2_property, ...
*
*
* @since 5.2
*/
@Component(metatype = false, factory="com.day.cq.search.eval.PredicateEvaluator/group")
public class PredicateGroupEvaluator extends AbstractPredicateEvaluator {
private static final Logger log = LoggerFactory.getLogger(PredicateGroupEvaluator.class);
protected static String FORCED_FILTERING = PredicateGroupEvaluator.class.getName() + "forced-filtering";
protected static String UNSUPPORTED_FILTER_WARNING_GIVEN = PredicateGroupEvaluator.class.getName() + ".filter-warning";
protected String getOpeningBracket() {
return "(";
}
protected String getClosingBracket() {
return ")";
}
@Override
public String getXPathExpression(Predicate p, EvaluationContext context) {
if (p == null || !(p instanceof PredicateGroup)) {
return null;
}
PredicateGroup group = (PredicateGroup) p;
// fast check: if this group is filtering, no xpath at all
if (isForcedFiltering(group, context)) {
return "";
}
// first collect all expressions and skip empty strings
List expressions = new ArrayList();
for (Predicate pred : group) {
if (pred.ignored()) {
continue;
}
PredicateEvaluator evaluator = context.getPredicateEvaluator(pred.getType());
if (evaluator != null /* && evaluator.canXpath(pred, context) */) {
String ex = evaluator.getXPathExpression(pred, context);
if (ex != null && ex.length() > 0) {
expressions.add(ex);
}
}
}
// then collect xpath expressions in a stringbuffer
StringBuffer xpath = new StringBuffer();
if (expressions.size() > 0) {
// for example: (@jcr:primaryType = 'nt:file' and jcr:contains(. "foobar"))
if (group.isNegated()) {
// for example: not(@jcr:primaryType = 'nt:file' and jcr:contains(. "foobar"))
xpath.append("not");
}
xpath.append(getOpeningBracket());
Iterator exIter = expressions.iterator();
while (exIter.hasNext()) {
// for example: @jcr:primaryType = 'nt:file'
xpath.append(exIter.next());
if (exIter.hasNext()) {
if (group.allRequired()) {
xpath.append(" and ");
} else {
xpath.append(" or ");
}
}
}
xpath.append(getClosingBracket());
}
return xpath.toString();
}
@Override
public boolean includes(Predicate p, Row row, EvaluationContext context) {
if (p == null || !(p instanceof PredicateGroup)) {
return false;
}
PredicateGroup group = (PredicateGroup) p;
// and vs. or
boolean result = group.allRequired() ? andInclude(group, row, context) : orInclude(group, row, context);
// negate group
return group.isNegated() ? !result : result;
}
private boolean andInclude(PredicateGroup group, Row row, EvaluationContext context) {
if (group.isEmpty()) {
// an empty group should not have any influence, thus return true
return true;
}
final boolean forcedFiltering = (context.get(FORCED_FILTERING) != null);
for (Predicate p : group) {
if (p.ignored()) {
continue;
}
PredicateEvaluator evaluator = context.getPredicateEvaluator(p.getType());
// only ask filtering-only evaluators (or when filtering is forced in general for this group)
if (evaluator != null && (forcedFiltering || !evaluator.canXpath(p, context))) {
// check for non-inclusion
if (!evaluator.includes(p, row, context)) {
if (log.isTraceEnabled()) {
log.trace("AND group: predicate '" + p.getName() + "' (" + p.getType() + ") denied row " + context.getPath(row));
}
return false;
}
}
}
return true;
}
private boolean orInclude(PredicateGroup group, Row row, EvaluationContext context) {
int predicatesAsked = 0;
final boolean inheritedForcedFiltering = (context.get(FORCED_FILTERING) != null);
final boolean forcedFiltering = inheritedForcedFiltering || isForcedFiltering(group, context);
if (!inheritedForcedFiltering && forcedFiltering) {
context.put(FORCED_FILTERING, true);
}
try {
for (Predicate p : group) {
if (p.ignored()) {
continue;
}
PredicateEvaluator evaluator = context.getPredicateEvaluator(p.getType());
// only ask filtering-only evaluators (or when filtering is forced in general for this group)
if (evaluator != null && (forcedFiltering || !evaluator.canXpath(p, context))) {
// check for inclusion
if (evaluator.includes(p, row, context)) {
// log if evaluator is not supporting filtering
if (forcedFiltering && context.get(UNSUPPORTED_FILTER_WARNING_GIVEN) == null && !evaluator.canFilter(p, context)) {
log.warn("Search result might be incorrect - forcing filtering with a PredicateEvaluator " +
"that does NOT support filtering: '" + p.getPath() + "' = " + evaluator.getClass().getName());
context.put(UNSUPPORTED_FILTER_WARNING_GIVEN, true);
}
return true;
}
predicatesAsked++;
}
}
if (predicatesAsked == 0) {
// an empty group (or one where all are ignored) should not have any influence, thus return true
return true;
}
if (log.isTraceEnabled()) {
log.trace("OR group: no predicate in group '" + group.getName() + "' accepted row " + context.getPath(row));
}
// no child predicate ever returned true => don't include
return false;
} finally {
if (!inheritedForcedFiltering && forcedFiltering) {
context.put(FORCED_FILTERING, null);
}
}
}
protected boolean isForcedFiltering(PredicateGroup group, EvaluationContext context) {
// force filtering = OR group + at least one predicate that requires filtering
if (group.allRequired()) {
// skip AND group
return false;
}
for (Predicate p: group) {
if (p.ignored()) {
continue;
}
PredicateEvaluator evaluator = context.getPredicateEvaluator(p.getType());
// filtering is required if no xpath is available
if (evaluator != null && !evaluator.canXpath(p, context)) {
return true;
}
}
return false;
}
@Override
public boolean canXpath(Predicate predicate, EvaluationContext context) {
if (predicate == null || !(predicate instanceof PredicateGroup)) {
return false;
}
PredicateGroup group = (PredicateGroup) predicate;
// a group can do xpath only if *all* (non-ignored) child predicates can do xpath
for (Predicate p: group) {
if (p.ignored()) {
continue;
}
PredicateEvaluator evaluator = context.getPredicateEvaluator(p.getType());
if (evaluator != null && !evaluator.canXpath(p, context)) {
return false;
}
}
return true;
}
@Override
public boolean canFilter(Predicate predicate, EvaluationContext context) {
if (predicate == null || !(predicate instanceof PredicateGroup)) {
return false;
}
PredicateGroup group = (PredicateGroup) predicate;
// a group can filter only if *all* (non-ignored) child predicates can filter
for (Predicate p: group) {
if (p.ignored()) {
continue;
}
PredicateEvaluator evaluator = context.getPredicateEvaluator(p.getType());
if (evaluator != null && !evaluator.canFilter(p, context)) {
return false;
}
}
return true;
}
public String listFilteringPredicates(PredicateGroup group, EvaluationContext context) {
StringBuffer result = new StringBuffer();
// or + filtering forced
boolean groupHasForcedFiltering = isForcedFiltering(group, context);
for (Predicate p : group) {
if (p.ignored()) {
continue;
}
if (groupHasForcedFiltering) {
// all child predicates are "forced" to filter
if (result.length() > 0) {
result.append(", ");
}
PredicateEvaluator evaluator = context.getPredicateEvaluator(p.getType());
if (evaluator != null && !evaluator.canFilter(p, context)) {
result.append("WARN - NO FILTERING SUPPORT: ");
}
result.append("{").append(p.toString()).append("}");
} else if (p instanceof PredicateGroup) {
// recursively walk down the predicate tree
if (result.length() > 0) {
result.append(", ");
}
result.append(listFilteringPredicates((PredicateGroup) p, context));
} else {
PredicateEvaluator evaluator = context.getPredicateEvaluator(p.getType());
// filtering ones are those with no xpath capability
if (evaluator != null && !evaluator.canXpath(p, context)) {
if (result.length() > 0) {
result.append(", ");
}
result.append("{").append(p.toString()).append("}");
}
}
}
return result.toString();
}
@Override
public FacetExtractor getFacetExtractor(Predicate predicate, EvaluationContext context) {
// Facets map one-to-one on concrete predicateEvaluator (types), so there is no
// such thing as a "GroupFacetExtractor" which is comparable to the GroupPredicate.
// Collecting the facet extractors from the sub-predicates is handled by the calling
// framework, thus we return null here.
return null;
}
}