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

com.day.cq.search.PredicateConverter Maven / Gradle / Ivy

There is a newer version: 6.5.21
Show newest version
/*
 * Copyright 1997-2009 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;

import com.day.cq.search.eval.FulltextPredicateEvaluator;
import com.day.cq.search.eval.JcrPropertyPredicateEvaluator;
import com.day.cq.search.eval.PathPredicateEvaluator;
import com.day.cq.search.eval.RangePropertyPredicateEvaluator;
import com.day.cq.search.eval.TypePredicateEvaluator;
import com.day.cq.search.impl.builder.PredicateWalker;
import org.apache.jackrabbit.commons.query.GQL;
import org.apache.jackrabbit.util.Text;

import javax.jcr.RepositoryException;
import java.util.Comparator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * {@linkplain PredicateConverter} provides mappings from the
 * predicate/predicate group data structure to others, such as a simple
 * key/value string map (eg. a request parameter map).
 * 
 * @since 5.2
 */
public abstract class PredicateConverter {

    /**
     * Prefix to separate predicate group parameters from child predicates (to
     * be exact: from parameters of child predicates). This also means that
     * there cannot be a predicate type named like this (ie.
     * {@value}). Examples to show the difference:
     * 
    *
  • p.limit = true (group parameter)
  • *
  • type = nt:file (child predicate)
  • *
  • path.exact = true (child predicate parameter)
  • *
  • group.type = nt:file (child predicate group w/ * predicate)
  • *
  • group.p.or = true (group parameter of child group)
  • *
*/ public static final String GROUP_PARAMETER_PREFIX = "p"; /** * All parameters starting with "_" will be ignored. Typical examples * are "_charset_" or "_dc". */ public static final String IGNORE_PARAMETER_PREFIX = "_"; private static final String RANGE_DELIMITER = ".."; private static final String ORDER = "order"; private static final String DATERANGE_PRED = "daterange"; private static final String RANGEPROP_PRED = "rangeproperty"; private static final String DATE_REGEX = ".*[0-9]+-.*"; private static final String RANGE_REGEX = "(.*)\\.{2,}(.*)"; /** * Converts a map with predicates and their parameters into a predicate * tree. Accepts a map with strings as keys and either simple strings as * values or string arrays as values. In the array case, the first value * will be chosen. * *

* Note that all parameters starting with a "_" (see * {@link #IGNORE_PARAMETER_PREFIX}) will be ignored. Typical examples are * "_charset_" or "_dc". */ public static PredicateGroup createPredicates(Map predicateParameterMap) { PredicateGroup root = new PredicateGroup(); PredicateTreeNode rootNode = new PredicateTreeNode(root); for (final Object nameObj : predicateParameterMap.keySet()) { String name = (String) nameObj; if (name != null && name.startsWith(IGNORE_PARAMETER_PREFIX)) { continue; } String value; Object valueObj = predicateParameterMap.get(name); if (valueObj instanceof String) { value = (String) valueObj; } else if (valueObj instanceof String[]) { // take only the first value String[] valueArray = (String[]) valueObj; value = valueArray.length == 0 ? null : valueArray[0]; } else { value = null; } if (isGroupParameter(name)) { // excerpt, order by, limit, etc. root.set(getGroupParameterName(name), value); } else { // predicates rootNode.insert(name, value); } } rootNode.generatePredicateTree(); return root; } /** * Converts a predicate tree into a parameter map, the inverse * transformation of {@link #createPredicates(Map)}. */ public static Map createMap(PredicateGroup root) { CollectParameters collector = new CollectParameters(); collector.visit(root); return collector.getParameters(); } /** * Returns an URL query part containing the given group. This is the same * mapping as used in {@link #createMap(PredicateGroup)} and * {@link #createPredicates(Map)}. For example, the returned value could be: * type=cq:Page&path=/content. Note that this won't be a * complete URL, just a list of parameters for an URL query part. The keys * and values will be properly escaped for use in an URL. */ public static String toURL(PredicateGroup group) { StringBuffer urlPart = new StringBuffer(); Map params = createMap(group); for (String key : params.keySet()) { if (urlPart.length() > 0) { urlPart.append("&"); } urlPart.append(Text.escape(key)).append("="); String value = params.get(key); if (value != null) { urlPart.append(Text.escape(value)); } } return urlPart.toString(); } // -----------------------------------------------------< private > private static class CollectParameters extends PredicateWalker { private Map result = new TreeMap(); @Override protected void visitInternal(Predicate predicate) { if (predicate instanceof PredicateGroup) { PredicateGroup group = (PredicateGroup) predicate; Map groupParams = group.getParameters(); String path = group.getPath(); if (path == null) { path = ""; } else { path = path + "."; } for (String key : groupParams.keySet()) { result.put(path + PredicateConverter.GROUP_PARAMETER_PREFIX + "." + key, groupParams.get(key)); } } else { Map params = predicate.getParameters(); String path = predicate.getPath(); for (String key : params.keySet()) { if (key.equals(predicate.getType())) { // embrace short form result.put(path, params.get(key)); } else { result.put(path + "." + key, params.get(key)); } } } } public Map getParameters() { return result; } } private static boolean isGroupParameter(String subParameter) { return subParameter.startsWith(GROUP_PARAMETER_PREFIX + "."); } private static String getGroupParameterName(String subParameter) { return subParameter.substring((GROUP_PARAMETER_PREFIX + ".").length()); } /** * PredicateTreeNode is a helper data structure for parsing * key-value based predicateEvaluator parameter maps and turning them into a * "tree" of {@link Predicate Predicates} ({@link PredicateGroup} acts as * the folder "node"). It ensures the order given by the naming schema. * Parameters and their predicates can be inserted in any order (via * {@link #insert(String, String)}) and the tree-structured predicates are * generated at the end (via {@link #generatePredicateTree()}). * * Schema Examples: * predicateType = foobar * 2.predicateType = foobar * 2.predicateType.param = something * group.1.group.2.predicateType = bla */ private static class PredicateTreeNode { public Predicate predicate; /** Sorted map required to ensure order of keys */ private SortedMap kids = new TreeMap(LongOrStringComparator.INSTANCE); public PredicateTreeNode(Predicate p) { this.predicate = p; } public void insert(final String name, final String value) { // either "name.subparameter" or "2_name.subparameter" (explicit ordering) String[] split = name.split("\\.", 2); final String predicateName = split[0]; String subParameter = split.length > 1 ? split[1] : null; // key for sorting (type or number) Object key = predicateName; // first assume simple case: name is simply the type "mytype" String predicateType = predicateName; // check if we might have the explicit ordering, eg. "2_type.subparameter" if (predicateName.contains("_")) { split = predicateName.split("_", 2); try { // throws NFE if no number long index = Long.parseLong(split[0]); key = index; predicateType = split.length > 1 ? split[1] : null; } catch (NumberFormatException e) { // means there is no explicit ordering, original variable assignments are fine } } if (predicateType == null) { return; } PredicateTreeNode node = null; if (kids.containsKey(key)) { node = kids.get(key); } else { Predicate p; if (PredicateGroup.TYPE.equals(predicateType)) { p = new PredicateGroup(predicateName); } else { p = new Predicate(predicateName, predicateType); } node = new PredicateTreeNode(p); kids.put(key, node); } // short form // instead of "type.type=nt:file", allow the shorter "type=nt:file" for all predicates // with a single property or a main property which has the same name as the type if (subParameter == null) { subParameter = predicateType; } if (node != null && subParameter != null) { if (node.predicate instanceof PredicateGroup) { if (isGroupParameter(subParameter)) { node.predicate.set(getGroupParameterName(subParameter), value); } else { node.insert(subParameter, value); } } else { node.predicate.set(subParameter, value); } } } public void generatePredicateTree() { if (predicate instanceof PredicateGroup) { PredicateGroup group = (PredicateGroup) predicate; // make sure all sub-predicates are added in the proper order // values() gives us an ordered list of the kids (ordered by the keys) for (PredicateTreeNode node : kids.values()) { node.generatePredicateTree(); group.add(node.predicate); } } } } /** * Custom comparator for the sorted map used in {@link PredicateTreeNode}, * which contains either Longs or Strings. */ private static class LongOrStringComparator implements Comparator { public static final LongOrStringComparator INSTANCE = new LongOrStringComparator(); public int compare(Object o1, Object o2) { // if both have the same type, use built-in natural compareTo methods if (o1 instanceof Long && o2 instanceof Long) { return ((Long) o1).compareTo((Long) o2); } else if (o1 instanceof String && o2 instanceof String) { return ((String) o1).compareTo((String) o2); // otherwise prefer the string } else if (o1 instanceof Long) { return 1; } else if (o2 instanceof Long) { return -1; } // not String and not long => should not happen, don't care return -1; } } /** * Parse and converts GQL statement to QueryBuilder PredicateGroup. * * @param statement the statement to be processed for extracting conditions. * @return PredicateGroup containing all conditions formed from statement * @throws RepositoryException */ public static PredicateGroup createPredicatesFromGQL(String statement) throws RepositoryException { final PredicateGroup parentGroup = new PredicateGroup(); final PredicateGroup conjointGroup = new PredicateGroup(); GQL.ParserCallback callback = new GQL.ParserCallback() { public void term(String property, String value, boolean optional) throws RepositoryException { pushExpression(property, value, optional, parentGroup, conjointGroup); } }; // removing redundant spaces statement = statement.replaceAll(" *: *", ":").replaceAll(" *- *", "-"); GQL.parse(statement, null, callback); parentGroup.addAll(conjointGroup); return parentGroup; } /** * @param property property name of the currently parsed expression. * @param value value of the currently parsed expression. * @param optional whether the previous token was the OR operator. * @param parentGroup root group for GQL predicates. * @param conjointGroup group containing general predicates like path, order, type, limit etc which can't be ORed */ private static void pushExpression(String property, String value, boolean optional, PredicateGroup parentGroup, PredicateGroup conjointGroup) { if (property.equals(PathPredicateEvaluator.PATH) || property.equals(ORDER) || property.equals(TypePredicateEvaluator.TYPE) || property.equals(Predicate.PARAM_LIMIT) || property.equals(Predicate.PARAM_OFFSET) || property.equals(Predicate.PARAM_FACET_STRATEGY) || property.equals(Predicate.PARAM_GUESS_TOTAL) || property.equals(Predicate.PARAM_EXCERPT)) { // Special handling for path, order, type and limit. As in GQL they can never be ORed if (property.equals(ORDER)) { Predicate orderPredicate = new Predicate(Predicate.ORDER_BY); if(value.startsWith("-")) { orderPredicate.set(Predicate.ORDER_BY, value.length() > 1 ? "@" + value.substring(1) : ""); orderPredicate.set(Predicate.PARAM_SORT, Predicate.SORT_DESCENDING); } else if (value.startsWith("+")) { orderPredicate.set(Predicate.ORDER_BY, value.length() > 1 ? "@" + value.substring(1) : ""); orderPredicate.set(Predicate.PARAM_SORT, Predicate.SORT_ASCENDING); } else { orderPredicate.set(Predicate.ORDER_BY, "@" + value); } parentGroup.add(orderPredicate); } else if (property.equals(PathPredicateEvaluator.PATH) || property.equals(TypePredicateEvaluator.TYPE)) { conjointGroup.add(new Predicate(property).set(property, value)); } else if (property.equals(Predicate.PARAM_FACET_STRATEGY) ) { parentGroup.set(Predicate.PARAM_FACET_STRATEGY, value); } else { parentGroup.set(Predicate.PARAM_LIMIT, value); } } else { Predicate predicate; if (property.length() > 0) { if (value.contains(RANGE_DELIMITER)) { // range predicate String predName; String lower = "", upper = ""; Pattern p = Pattern.compile(RANGE_REGEX); Matcher m = p.matcher(value); if (m.find()) { lower = m.group(1).trim(); upper = m.group(2).trim(); } if (lower.matches(DATE_REGEX) || upper.matches(DATE_REGEX)) { predName = DATERANGE_PRED; } else { predName = RANGEPROP_PRED; } if (RANGEPROP_PRED.equals(predName)) { predicate = new Predicate(RANGEPROP_PRED); if (lower.length() == 0) { lower = Long.toString(Long.MIN_VALUE + 1); } if (upper.length() == 0) { upper = Long.toString(Long.MAX_VALUE - 1); } } else { predicate = new Predicate(DATERANGE_PRED); } predicate.set(RangePropertyPredicateEvaluator.PROPERTY, property); predicate.set(RangePropertyPredicateEvaluator.LOWER_BOUND, lower); predicate.set(RangePropertyPredicateEvaluator.UPPER_BOUND, upper); } else { // property predicate predicate = new Predicate(JcrPropertyPredicateEvaluator.PROPERTY); predicate.set(JcrPropertyPredicateEvaluator.PROPERTY, property); predicate.set(JcrPropertyPredicateEvaluator.VALUE, value); } } else { // fulltext predicate predicate = new Predicate(FulltextPredicateEvaluator.FULLTEXT); predicate.set(FulltextPredicateEvaluator.FULLTEXT, value); } // if previous token is OR operator & there exists a condition that can be ORed with current condition if (optional && parentGroup.size() > 0) { Object lastElement = parentGroup.get(parentGroup.size() - 1); if (lastElement instanceof PredicateGroup && !((PredicateGroup) lastElement).allRequired()) { // if lastElement of parent group is an optional group simply add lastElement to the optional group ((PredicateGroup) lastElement).add(predicate); parentGroup.set(parentGroup.size() - 1, (PredicateGroup) lastElement); } else { // creating new optional group containing current and last condition PredicateGroup optionalGrp = new PredicateGroup(); optionalGrp.setAllRequired(false); optionalGrp.add((Predicate)lastElement); optionalGrp.add(predicate); // replacing last element of parent group with optional group parentGroup.set(parentGroup.size() - 1, optionalGrp); } } else { parentGroup.add(predicate); } } } }