/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.server.resource.group.definition.framework; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.persistence.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.domain.measurement.AvailabilityType; import org.rhq.core.domain.resource.ResourceCategory; import org.rhq.core.domain.resource.group.DuplicateExpressionTypeException; import org.rhq.core.domain.resource.group.InvalidExpressionException; import org.rhq.enterprise.server.common.EntityManagerFacadeLocal; import org.rhq.enterprise.server.util.LookupUtil; import org.rhq.enterprise.server.util.QueryUtility; public class ExpressionEvaluator implements Iterable<ExpressionEvaluator.Result> { private final Log log = LogFactory.getLog(ExpressionEvaluator.class); private static final String INVALID_EXPRESSION_FORM_MSG = "Expression must be in one of the follow forms: " + // "'groupby condition', 'memberof = groupname', 'condition = value', 'empty condition', 'not empty condition"; private static final String PROP_SIMPLE_ALIAS = "simple"; private static final String PROP_SIMPLE_DEF_ALIAS = "simpleDef"; private static final String TRAIT_ALIAS = "trait"; private static final String METRIC_DEF_ALIAS = "def"; private enum JoinCondition { RESOURCE_CONFIGURATION(".resourceConfiguration", "conf"), // PLUGIN_CONFIGURATION(".pluginConfiguration", "pluginConf"), // SCHEDULES(".schedules", "sched"), // RESOURCE_CHILD(".childResources", "child"), // AVAILABILITY(".currentAvailability", "avail"), // RESOURCE_CONFIGURATION_DEFINITION(".resourceType.resourceConfigurationDefinition", "confDef"), // PLUGIN_CONFIGURATION_DEFINITION(".resourceType.pluginConfigurationDefinition", "pluginConfDef"); String subexpression; String alias; private JoinCondition(String subexpression, String alias) { this.subexpression = subexpression; this.alias = alias; } } private Map<JoinCondition, ResourceRelativeContext> joinConditions; private Map<String, String> whereConditions; private Map<String, Object> whereReplacements; private Map<String, Class<?>> whereReplacementTypes; private Set<String> whereStatics; private List<String> groupByElements; private List<String> memberOfElements; private List<String> simpleSubExpressions; private List<String> groupedSubExpressions; private List<String> memberSubExpressions; private int expressionCount; private boolean isInvalid; private boolean isTestMode; private boolean resultsComputed; private String computedJPQLStatement; private String computedJPQLGroupStatement; private EntityManagerFacadeLocal entityManagerFacade; private Map<String, String> resourceExpressions = new TreeMap<String, String>(); public ExpressionEvaluator() { /* * used LinkedHashMap for whereConditions on purpose so that the iterator will return them in the same order * they were added to the list, this ensures that the getComputed*Statement() methods will construct the * generated JPQL in the order that the bind parameter names are generated; * * the query technology, of course, doesn't require this...but it makes generating a test suite and verifying * expected output a lot easier */ joinConditions = new HashMap<JoinCondition, ResourceRelativeContext>(); whereConditions = new LinkedHashMap<String, String>(); whereReplacements = new HashMap<String, Object>(); whereReplacementTypes = new HashMap<String, Class<?>>(); whereStatics = new LinkedHashSet<String>(); groupByElements = new ArrayList<String>(); memberOfElements = new ArrayList<String>(); simpleSubExpressions = new ArrayList<String>(); groupedSubExpressions = new ArrayList<String>(); memberSubExpressions = new ArrayList<String>(); expressionCount = 0; isInvalid = false; isTestMode = false; resultsComputed = false; computedJPQLStatement = ""; computedJPQLGroupStatement = ""; entityManagerFacade = LookupUtil.getEntityManagerFacade(); /* * initialization for special handling that all dynagroups should get */ whereStatics.add("res.inventoryStatus = org.rhq.core.domain.resource.InventoryStatus.COMMITTED"); resourceExpressions.put("res.id", "resource id"); resourceExpressions.put("child.id", "resource id"); resourceExpressions.put("res.parentResource.id", "resource id"); resourceExpressions.put("res.parentResource.parentResource.id", "resource id"); resourceExpressions.put("res.parentResource.parentResource.parentResource.id", "resource id"); resourceExpressions.put("res.parentResource.parentResource.parentResource.parentResource.id", "resource id"); resourceExpressions.put("res.name", "resource name"); resourceExpressions.put("child.name", "resource name"); resourceExpressions.put("res.parentResource.name", "resource name"); resourceExpressions.put("res.parentResource.parentResource.name", "resource name"); resourceExpressions.put("res.parentResource.parentResource.parentResource.name", "resource name"); resourceExpressions .put("res.parentResource.parentResource.parentResource.parentResource.name", "resource name"); resourceExpressions.put("res.version", "resource version"); resourceExpressions.put("child.version", "resource version"); resourceExpressions.put("res.parentResource.version", "resource version"); resourceExpressions.put("res.parentResource.parentResource.version", "resource version"); resourceExpressions.put("res.parentResource.parentResource.parentResource.version", "resource version"); resourceExpressions.put("res.parentResource.parentResource.parentResource.parentResource.version", "resource version"); resourceExpressions.put("res.resourceType.plugin", "resource type"); resourceExpressions.put("res.resourceType.name", "resource type"); resourceExpressions.put("child.resourceType.plugin", "resource type"); resourceExpressions.put("child.resourceType.name", "resource type"); resourceExpressions.put("res.parentResource.resourceType.plugin", "resource type"); resourceExpressions.put("res.parentResource.resourceType.name", "resource type"); resourceExpressions.put("res.parentResource.parentResource.resourceType.plugin", "resource type"); resourceExpressions.put("res.parentResource.parentResource.resourceType.name", "resource type"); resourceExpressions .put("res.parentResource.parentResource.parentResource.resourceType.plugin", "resource type"); resourceExpressions.put("res.parentResource.parentResource.parentResource.resourceType.name", "resource type"); resourceExpressions.put("res.parentResource.parentResource.parentResource.parentResource.resourceType.plugin", "resource type"); resourceExpressions.put("res.parentResource.parentResource.parentResource.parentResource.resourceType.name", "resource type"); resourceExpressions.put("res.resourceType.category", "resource category"); resourceExpressions.put("child.resourceType.category", "resource category"); resourceExpressions.put("res.parentResource.resourceType.category", "resource category"); resourceExpressions.put("res.parentResource.parentResource.resourceType.category", "resource category"); resourceExpressions.put("res.parentResource.parentResource.parentResource.resourceType.category", "resource category"); resourceExpressions.put( "res.parentResource.parentResource.parentResource.parentResource.resourceType.category", "resource category"); resourceExpressions.put("avail.availabilityType", "availability"); resourceExpressions.put("child.avail.availabilityType", "availability"); resourceExpressions.put("res.parentResource.avail.availabilityType", "availability"); resourceExpressions.put("res.parentResource.parentResource.avail.availabilityType", "availability"); resourceExpressions.put("res.parentResource.parentResource.parentResource.avail.availabilityType", "availability"); resourceExpressions.put( "res.parentResource.parentResource.parentResource.parentResource.avail.availabilityType", "availability"); resourceExpressions.put("trait.value", "trait"); resourceExpressions.put("child.trait.value", "trait"); resourceExpressions.put("res.parentResource.trait.value", "trait"); resourceExpressions.put("res.parentResource.parentResource.trait.value", "trait"); resourceExpressions.put("res.parentResource.parentResource.parentResource.trait.value", "trait"); resourceExpressions.put("res.parentResource.parentResource.parentResource.parentResource.trait.value", "trait"); resourceExpressions.put("simple.name", "configuration"); resourceExpressions.put("child.simple.name", "configuration"); resourceExpressions.put("res.parentResource.simple.name", "configuration"); resourceExpressions.put("res.parentResource.parentResource.simple.name", "configuration"); resourceExpressions.put("res.parentResource.parentResource.parentResource.simple.name", "configuration"); resourceExpressions.put("res.parentResource.parentResource.parentResource.parentResource.simple.name", "configuration"); } public class Result { private final List<Integer> data; private final String groupByClause; public Result(List<Integer> data) { this.data = data; this.groupByClause = ""; } public Result(List<Integer> data, String groupByExpression) { this.data = data; this.groupByClause = groupByExpression; } public List<Integer> getData() { return data; } public String getGroupByClause() { return groupByClause; } } /** * @param mode passing a value of true will bypass query later and only compute the effective JPQL statements, handy * for testing */ public void setTestMode(boolean mode) { isTestMode = mode; whereStatics.remove("res.inventoryStatus = org.rhq.core.domain.resource.InventoryStatus.COMMITTED"); } /** * @param expression a string in the form of 'condition = value' or 'groupBy condition' * * @return a reference to itself, so that method chaining can be used * * @throws org.rhq.core.domain.resource.group.InvalidExpressionException if the expression can not be parsed for any reason, the message will try to * get the details as to the parse failure */ public ExpressionEvaluator addExpression(String expression) throws InvalidExpressionException { if (isInvalid) { throw new IllegalStateException("This evaluator previously threw an exception and can no longer be used"); } try { parseExpression(expression); expressionCount++; } catch (InvalidExpressionException iee) { isInvalid = true; throw iee; } return this; } /** * @return the JPQL statement that will be sent to the database, assuming test mode is false (the default): -- if no * groupBy expressions are present, it will query for the target object -- if at least one groupBy * expression was used, it will return the query to find the pivot data */ public String getComputedJPQLStatement() { if (resultsComputed == false) { throw new IllegalStateException("Results must be computed before this method can be called"); } return computedJPQLStatement; } /** * @return the JPQL statement that will be sent to the database, assuming test mode is false (the default): -- if no * groupBy expressions are present, this will return "" -- if at least one groupBy expression was used, it * will return the pivoted query */ public String getComputedJPQLGroupStatement() { if (resultsComputed == false) { throw new IllegalStateException("Results must be computed before this method can be called"); } return computedJPQLGroupStatement; } private enum ParseContext { BEGIN(false), // Modifier(false), // includes 'empty', 'not', and 'pivot' Resource(false), // ResourceParent(false), // ResourceGrandParent(false), // ResourceGreatGrandParent(false), // ResourceGreatGreatGrandParent(false), // ResourceChild(false), // ResourceType(false), // Availability(true), // Trait(true), // Configuration(true), // includes 'pluginConfiguration' and 'resourceConfiguration' StringMatch(true), // END(true), // Membership(true); private boolean canTerminateExpression; private ParseContext(boolean canTerminateExpression) { this.canTerminateExpression = canTerminateExpression; } public boolean isExpressionTerminator() { return this.canTerminateExpression; } } private enum ParseSubContext { Negated, // only relevant for Modifier context NotEmpty, // only relevant for Modifier context Empty, // only relevant for Modifier context Pivot, // only relevant for Modifier context PluginConfiguration, // only relevant for Configuration context ResourceConfiguration; // only relevant for Configuration context } private enum ComparisonType { NONE, // expression in the form of 'groupBy condition' EQUALS, // expression in the form of 'condition = value' EMPTY, // expression in the form of 'empty value' NOT_EMPTY; // expression in the form of 'not empty value' } private enum Literal { NULL, NOTNULL; } private ParseContext context = ParseContext.BEGIN; private ParseSubContext subcontext = null; private int parseIndex = 0; private boolean isGroupBy = false; private boolean isMemberOf = false; private ComparisonType comparisonType = null; private Class<?> expressionType; private ParseContext deepestResourceContext = null; /** * @param expression a string in the form of 'condition = value' or 'groupBy condition' * * @throws InvalidExpressionException if the expression can not be forward-only parsed for any reason, the exception * message will contain the details of the parse failure */ private void parseExpression(String expression) throws InvalidExpressionException { if (expression.trim().equals("")) { // allow empty lines, by simply ignoring them return; } if (expression.contains("<")) throw new InvalidExpressionException("Expressions must not contain the '<' character"); String condition; String value = null; /* * instead of building '= value' parsing into the below algorithm, let's chop this off early and store it; this * makes the rest of the parsing a bit simpler because some ParseContexts need the value immediately in order to * properly build up internal maps / constructs to be used in generating the requisite JPQL statement * * however, since '=' can occur in the names of configuration properties and trait names, we need * to process from the end of the word skipping over all characters that are inside brackets */ int equalsIndex = -1; boolean insideBrackets = false; for (int i = expression.length() - 1; i >= 0; i--) { char next = expression.charAt(i); if (insideBrackets) { if (next == '[') { insideBrackets = false; } } else { if (next == ']') { insideBrackets = true; } else if (next == '=') { equalsIndex = i; break; } } } if (equalsIndex == -1) { condition = expression; } else { condition = expression.substring(0, equalsIndex); value = expression.substring(equalsIndex + 1).trim(); if (value.equals("")) { throw new InvalidExpressionException(INVALID_EXPRESSION_FORM_MSG); } } /* * the remainder of the passed expression should be in the form of '[groupBy] condition', so let's tokenize on * '.' and ' ' and continue the parse */ List<String> originalTokens = tokenizeCondition(condition); String[] tokens = new String[originalTokens.size()]; for (int i = 0; i < tokens.length; i++) { tokens[i] = originalTokens.get(i).toLowerCase(); } log.debug("TOKENS: " + Arrays.asList(tokens)); /* * build the normalized expression outside of the parse, to keep the parse code as clean as possible; * however, this string will be used to determine if the expression being added to this evaluator, based * on whether it's grouped or not, compared against all expressions seen so far, is valid */ StringBuilder normalizedSubExpressionBuilder = new StringBuilder(); for (String subExpressionToken : tokens) { // do not add modifiers to the normalized expression if (subExpressionToken.equals("groupby")) { continue; } else if (subExpressionToken.equals("memberof")) { continue; } else if (subExpressionToken.equals("not")) { continue; } else if (subExpressionToken.equals("empty")) { continue; } normalizedSubExpressionBuilder.append(subExpressionToken); } String normalizedSubExpression = normalizedSubExpressionBuilder.toString(); /* * it's easier to code a quick solution when each ParseContext can ignore additional whitespace from the * original expression */ for (int i = 0; i < tokens.length; i++) { tokens[i] = tokens[i].trim(); } // setup some instance-level context data to be manipulated and leveraged during the parse context = ParseContext.BEGIN; subcontext = null; parseIndex = 0; isGroupBy = false; // this needs to be reset each time a new expression is added isMemberOf = false; // this needs to be reset each time a new expression is added comparisonType = ComparisonType.EQUALS; // assume equals, unless "(not) empty" found during the parse deepestResourceContext = null; expressionType = String.class; for (; parseIndex < tokens.length; parseIndex++) { String nextToken = tokens[parseIndex]; if (context == ParseContext.BEGIN) { if (nextToken.equals("resource")) { context = ParseContext.Resource; deepestResourceContext = context; } else if (nextToken.equals("memberof")) { context = ParseContext.Membership; // ensure proper expression termination String groupName = value; if (null == groupName || groupName.isEmpty() || "=".equals(groupName)) { throw new InvalidExpressionException(INVALID_EXPRESSION_FORM_MSG); } validateSubExpressionAgainstPreviouslySeen(groupName, false, true); isMemberOf = true; populatePredicateCollections(null, groupName); } else if (nextToken.equals("groupby")) { context = ParseContext.Modifier; subcontext = ParseSubContext.Pivot; } else if (nextToken.equals("not")) { context = ParseContext.Modifier; subcontext = ParseSubContext.Negated; // 'not' must be followed by 'empty' today, but we won't know until next parse iteration // furthermore, we may support other forms of negated expressions in the future } else if (nextToken.equals("empty")) { context = ParseContext.Modifier; subcontext = ParseSubContext.Empty; } else { throw new InvalidExpressionException( "Expression must either start with 'resource', 'groupby', 'empty', or 'not empty' tokens"); } } else if (context == ParseContext.Modifier) { if (subcontext == ParseSubContext.Negated) { if (nextToken.equals("empty")) { subcontext = ParseSubContext.NotEmpty; } else { throw new InvalidExpressionException( "Expression starting with 'not' must be followed by the 'empty' token"); } } else { // first check for valid forms given the subcontext if (subcontext == ParseSubContext.Pivot || subcontext == ParseSubContext.Empty || subcontext == ParseSubContext.NotEmpty) { if (value != null) { // these specific types of 'modified' expressions must NOT HAVE "= <value>" part throw new InvalidExpressionException(INVALID_EXPRESSION_FORM_MSG); } } // then perform individual processing based on current subcontext if (subcontext == ParseSubContext.Pivot) { // validates the uniqueness of the subexpression after checking for INVALID_EXPRESSION_FORM_MSG validateSubExpressionAgainstPreviouslySeen(normalizedSubExpression, true, false); isGroupBy = true; comparisonType = ComparisonType.NONE; } else if (subcontext == ParseSubContext.NotEmpty) { comparisonType = ComparisonType.NOT_EMPTY; } else if (subcontext == ParseSubContext.Empty) { comparisonType = ComparisonType.EMPTY; } else { throw new InvalidExpressionException("Unknown or unsupported ParseSubContext[" + subcontext + "] for ParseContext[" + context + "]"); } if (nextToken.equals("resource")) { context = ParseContext.Resource; deepestResourceContext = context; } else { throw new InvalidExpressionException( "Grouped expressions must be followed by the 'resource' token"); } } } else if (context == ParseContext.Resource) { if (comparisonType == ComparisonType.EQUALS) { if (value == null) { // EQUALS filter expressions must HAVE "= <value>" part throw new InvalidExpressionException(INVALID_EXPRESSION_FORM_MSG); } validateSubExpressionAgainstPreviouslySeen(normalizedSubExpression, false, false); } if (nextToken.equals("parent")) { context = ParseContext.ResourceParent; deepestResourceContext = context; } else if (nextToken.equals("grandparent")) { context = ParseContext.ResourceGrandParent; deepestResourceContext = context; } else if (nextToken.equals("greatgrandparent")) { context = ParseContext.ResourceGreatGrandParent; deepestResourceContext = context; } else if (nextToken.equals("greatgreatgrandparent")) { context = ParseContext.ResourceGreatGreatGrandParent; deepestResourceContext = context; } else if (nextToken.equals("child")) { context = ParseContext.ResourceChild; deepestResourceContext = context; } else { parseExpression_resourceContext(value, tokens, nextToken); } } else if ((context == ParseContext.ResourceParent) || (context == ParseContext.ResourceGrandParent) || (context == ParseContext.ResourceGreatGrandParent) || (context == ParseContext.ResourceGreatGreatGrandParent) || (context == ParseContext.ResourceChild)) { // since a parent or child *is* a resource, support the exact same processing parseExpression_resourceContext(value, tokens, nextToken); } else if (context == ParseContext.ResourceType) { if (nextToken.equals("plugin")) { populatePredicateCollections(getResourceRelativeContextToken() + ".resourceType.plugin", value); } else if (nextToken.equals("name")) { populatePredicateCollections(getResourceRelativeContextToken() + ".resourceType.name", value); } else if (nextToken.equals("category")) { populatePredicateCollections(getResourceRelativeContextToken() + ".resourceType.category", (value == null) ? null : ResourceCategory.valueOf(value.toUpperCase())); } else { throw new InvalidExpressionException("Invalid 'type' subexpression: " + PrintUtils.getDelimitedString(tokens, parseIndex, ".")); } } else if (context == ParseContext.Availability) { AvailabilityType type = null; if (isGroupBy == false) { if (value == null) { // pass through, NULL elements now supported } else if ("up".equalsIgnoreCase(value)) { type = AvailabilityType.UP; } else if ("down".equalsIgnoreCase(value)) { type = AvailabilityType.DOWN; } else if ("disabled".equalsIgnoreCase(value)) { type = AvailabilityType.DISABLED; } else if ("unknown".equalsIgnoreCase(value)) { type = AvailabilityType.UNKNOWN; } else { throw new InvalidExpressionException("Invalid 'resource.availability' comparision value, " + "only 'UP''DOWN''DISABLED''UNKNOWN' are valid values"); } } addJoinCondition(JoinCondition.AVAILABILITY); populatePredicateCollections(JoinCondition.AVAILABILITY.alias + ".availabilityType", type); } else if (context == ParseContext.Trait) { // SELECT res.id FROM Resource res JOIN res.schedules sched, sched.definition def, MeasurementDataTrait trait // WHERE def.name = :arg1 AND trait.value = :arg2 AND trait.schedule = sched AND trait.id.timestamp = // (SELECT max(mdt.id.timestamp) FROM MeasurementDataTrait mdt WHERE sched.id = mdt.schedule.id) String traitName = parseTraitName(originalTokens); addJoinCondition(JoinCondition.SCHEDULES); populatePredicateCollections(METRIC_DEF_ALIAS + ".name", "%" + traitName + "%", false, false); populatePredicateCollections(TRAIT_ALIAS + ".value", value); whereStatics.add(TRAIT_ALIAS + ".schedule = " + JoinCondition.SCHEDULES.alias); whereStatics.add(TRAIT_ALIAS + ".id.timestamp = (SELECT max(mdt.id.timestamp) FROM MeasurementDataTrait mdt WHERE " + JoinCondition.SCHEDULES.alias + ".id = mdt.schedule.id)"); } else if (context == ParseContext.Configuration) { String prefix; JoinCondition joinCondition; JoinCondition definitionJoinCondition; if (subcontext == ParseSubContext.PluginConfiguration) { prefix = "pluginconfiguration"; joinCondition = JoinCondition.PLUGIN_CONFIGURATION; definitionJoinCondition = JoinCondition.PLUGIN_CONFIGURATION_DEFINITION; } else if (subcontext == ParseSubContext.ResourceConfiguration) { prefix = "resourceconfiguration"; joinCondition = JoinCondition.RESOURCE_CONFIGURATION; definitionJoinCondition = JoinCondition.RESOURCE_CONFIGURATION_DEFINITION; } else { throw new InvalidExpressionException("Invalid 'configuration' subexpression: " + subcontext); } String suffix = originalTokens.get(parseIndex).substring(prefix.length()); if (suffix.length() < 3) { throw new InvalidExpressionException("Unrecognized connection property '" + suffix + "'"); } if ((suffix.charAt(0) != '[') || (suffix.charAt(suffix.length() - 1) != ']')) { throw new InvalidExpressionException("Property '" + suffix + "' must be contained within '[' and ']' characters"); } String propertyName = suffix.substring(1, suffix.length() - 1); addJoinCondition(joinCondition); addJoinCondition(definitionJoinCondition); populatePredicateCollections(PROP_SIMPLE_ALIAS + ".name", propertyName, false, false); populatePredicateCollections(PROP_SIMPLE_ALIAS + ".stringValue", value); whereStatics.add(PROP_SIMPLE_ALIAS + ".configuration = " + joinCondition.alias); whereStatics.add(PROP_SIMPLE_DEF_ALIAS + ".configurationDefinition = " + definitionJoinCondition.alias); whereStatics.add(PROP_SIMPLE_ALIAS + ".name = " + PROP_SIMPLE_DEF_ALIAS + ".name"); whereStatics.add(PROP_SIMPLE_DEF_ALIAS + ".type != 'PASSWORD'"); } else if (context == ParseContext.StringMatch) { if (expressionType != String.class) { throw new InvalidExpressionException( "Can not apply a string function to an expression that resolves to " + expressionType.getSimpleName()); } String lastArgumentName = getLastArgumentName(); String argumentValue = (String) whereReplacements.get(lastArgumentName); if (nextToken.equals("startswith")) { argumentValue = QueryUtility.escapeSearchParameter(argumentValue) + "%"; } else if (nextToken.equals("endswith")) { argumentValue = "%" + QueryUtility.escapeSearchParameter(argumentValue); } else if (nextToken.equals("contains")) { argumentValue = "%" + QueryUtility.escapeSearchParameter(argumentValue) + "%"; } else { throw new InvalidExpressionException("Unrecognized string function '" + nextToken + "' at end of condition"); } // fix the value replacement with the JPQL fragment that maps to the specified string function whereReplacements.put(lastArgumentName, argumentValue); context = ParseContext.END; } else if (context == ParseContext.END) { throw new InvalidExpressionException("Unrecognized tokens at end of expression"); } else { throw new InvalidExpressionException("Unknown parse context: " + context); } } if (context.isExpressionTerminator() == false) { throw new InvalidExpressionException("Unexpected termination of expression"); } } private enum ResourceRelativeContext { Resource("res"), // ResourceParent("res.parentResource"), // ResourceGrandParent("res.parentResource.parentResource"), // ResourceGreatGrandParent("res.parentResource.parentResource.parentResource"), // ResourceGreatGreatGrandParent("res.parentResource.parentResource.parentResource.parentResource"), // ResourceChild("child"); public String pathToken; private ResourceRelativeContext(String pathToken) { this.pathToken = pathToken; } } private void addJoinCondition(JoinCondition condition) { joinConditions.put(condition, getResourceRelativeContext()); } private String getResourceRelativeContextToken() { return getResourceRelativeContext().pathToken; } private ResourceRelativeContext getResourceRelativeContext() { if (deepestResourceContext == ParseContext.Resource) { return ResourceRelativeContext.Resource; } else if (deepestResourceContext == ParseContext.ResourceParent) { return ResourceRelativeContext.ResourceParent; } else if (deepestResourceContext == ParseContext.ResourceGrandParent) { return ResourceRelativeContext.ResourceGrandParent; } else if (deepestResourceContext == ParseContext.ResourceGreatGrandParent) { return ResourceRelativeContext.ResourceGreatGrandParent; } else if (deepestResourceContext == ParseContext.ResourceGreatGreatGrandParent) { return ResourceRelativeContext.ResourceGreatGreatGrandParent; } else if (deepestResourceContext == ParseContext.ResourceChild) { // populate child stuff joinConditions.put(JoinCondition.RESOURCE_CHILD, ResourceRelativeContext.Resource); return ResourceRelativeContext.ResourceChild; } else { throw new IllegalStateException("Expression only supports filtering on two levels of resource ancestry"); } } private void parseExpression_resourceContext(String value, String[] tokens, String nextToken) throws InvalidExpressionException { if (nextToken.equals("id")) { expressionType = Integer.class; populatePredicateCollections(getResourceRelativeContextToken() + ".id", value); } else if (nextToken.equals("name")) { populatePredicateCollections(getResourceRelativeContextToken() + ".name", value); } else if (nextToken.equals("version")) { populatePredicateCollections(getResourceRelativeContextToken() + ".version", value); } else if (nextToken.equals("type")) { context = ParseContext.ResourceType; } else if (nextToken.startsWith("availability")) { context = ParseContext.Availability; parseIndex--; // undo auto-inc, since this context requires element re-parse } else if (nextToken.startsWith("trait")) { context = ParseContext.Trait; parseIndex--; // undo auto-inc, since this context requires element re-parse } else if (nextToken.startsWith("pluginconfiguration")) { context = ParseContext.Configuration; subcontext = ParseSubContext.PluginConfiguration; parseIndex--; // undo auto-inc, since this context requires element re-parse } else if (nextToken.startsWith("resourceconfiguration")) { context = ParseContext.Configuration; subcontext = ParseSubContext.ResourceConfiguration; parseIndex--; // undo auto-inc, since this context requires element re-parse } else { throw new InvalidExpressionException("Invalid 'resource' subexpression: " + PrintUtils.getDelimitedString(tokens, parseIndex, ".")); } } // used to auto-generate unique names for bind variables private int counter = 0; private String getNextArgumentName() { counter++; String argumentName = "arg" + counter; return argumentName; } private String getLastArgumentName() { String argumentName = "arg" + (counter); return argumentName; } /* * the following two methods are used to add data to the appropriate predicate maps (whereConditions and * whereReplacements); * * it will only add data to the predicate list groupByElements if necessary, as determined by the instance-level * isGroupBy field or the explicitly overriding groupBy 3rd argument */ private void populatePredicateCollections(String predicateName, Object value) throws InvalidExpressionException { populatePredicateCollections(predicateName, value, isGroupBy, isMemberOf); } private void populatePredicateCollections(String predicateName, Object value, boolean groupBy, boolean memberOf) throws InvalidExpressionException { if (groupBy) { groupByElements.add(predicateName); } else if (memberOf) { memberOfElements.add((String) value); // this is the group name in this situation } else { String argumentName = getNextArgumentName(); // change the value as necessary based on the comparison type if (comparisonType == ComparisonType.EMPTY) { /* * a single parse context may populate several predicate collections, * but we want to make sure we are only performing extra comparison * computation on values representing the "empty" RHS of the expression */ if (value == null) { value = Literal.NULL; } } else if (comparisonType == ComparisonType.NOT_EMPTY) { // see comment for ComparisonType.EMPTY logic just above this block if (value == null) { value = Literal.NOTNULL; } } else if (comparisonType == ComparisonType.EQUALS || comparisonType == ComparisonType.NONE) { // pass through } else { throw new InvalidExpressionException("Unknown or unsupported ComparisonType[" + comparisonType + "] for predicate population"); } if (resourceExpressions.containsKey(predicateName) && whereConditions.containsKey(predicateName)) { throw new DuplicateExpressionTypeException(resourceExpressions.get(predicateName)); } whereConditions.put(predicateName, argumentName); whereReplacements.put(argumentName, value); whereReplacementTypes.put(argumentName, expressionType); } // always see if the user wants a portion of this, instead of an exact match context = ParseContext.StringMatch; } public void execute() { if (isInvalid) { throw new IllegalStateException("This evaluator previously threw an exception and can no longer be used"); } // if there are no expressions, leave the default value for computedJPQLStatement if (this.expressionCount == 0) { return; } // build the initial query String selectExpression = getQuerySelectExpression(false); String queryStr = "SELECT " + selectExpression + " FROM Resource res "; queryStr += getQueryJoinConditions(); queryStr += getQueryWhereConditions(); queryStr += getQueryGroupBy(selectExpression); queryStr = queryStr.trim(); // always save this query, group query is conditionally saved below computedJPQLStatement = queryStr; /* * one or more passed expressions were pivots, thus we have to query the database against, N times, once for * each unique N-tuple of results we got from executing the first query */ if (groupByElements.size() > 0) { // only group the group as necessary String groupQueryStr = "SELECT " + getQuerySelectExpression(true) + " FROM Resource res "; groupQueryStr += getQueryJoinConditions(); for (String groupedElement : groupByElements) { String argumentName = getNextArgumentName(); whereConditions.put(groupedElement, argumentName); } groupQueryStr += getQueryWhereConditions(); computedJPQLGroupStatement = groupQueryStr; } // mark processing complete, so getComputed* methods return successfully resultsComputed = true; } public Iterator<ExpressionEvaluator.Result> iterator() { if (resultsComputed == false) { execute(); } if (groupByElements.size() == 0) { return new SingleQueryIterator(); } else { return new MultipleQueryIterator(); } } private class SingleQueryIterator implements Iterator<ExpressionEvaluator.Result> { boolean firstTime = true; public boolean hasNext() { return firstTime; } @SuppressWarnings("unchecked") public ExpressionEvaluator.Result next() { log.debug("SingleQueryIterator: '" + computedJPQLStatement + "'"); List<Integer> results = getSingleResultList(computedJPQLStatement); firstTime = false; return new ExpressionEvaluator.Result(results); } public void remove() /* no-op */ { } } /* * each result is a unique pivot consisting of an N-tuple result, returned to us as an array ordered by the index * they were added to the predicate list groupByElements */ private class MultipleQueryIterator implements Iterator<ExpressionEvaluator.Result> { /* * support multi-layer capture groups for flexibility, today they resolve to: * group(0) = "token operator :argX" // where operator is '=' or 'LIKE' * group(1) = "operator :argX" * group(2) = "operator" * group(3) = ":argX" */ private final static String nullHandlerPattern = "" + // the 'token' will go in front "\\s+" + // followed by some whitespace "((\\=|LIKE)" + // and either an '=' or the word 'like' "\\s+" + // followed by more whitespace "(:arg[0-9]*))"; // ending in ':argX' where X is some integer @SuppressWarnings("unchecked") List uniqueTuples; int index; public MultipleQueryIterator() { log.debug("MultipleQueryIterator: '" + computedJPQLStatement + "'"); this.uniqueTuples = getSingleResultList(computedJPQLStatement); this.index = 0; } public boolean hasNext() { return (index < uniqueTuples.size()); } @SuppressWarnings("unchecked") public ExpressionEvaluator.Result next() { int i = 0; Object nextResult = uniqueTuples.get(index++); Object[] groupByExpression; if (nextResult == null) { groupByExpression = new Object[] { null }; } else if (nextResult.getClass().isArray()) { groupByExpression = (Object[]) nextResult; } else { groupByExpression = new Object[] { nextResult }; } /* * we built the basic structure earlier, now all we have to do is iterate over the unique N-tuples and set * the bind variables; conveniently, map semantics will, for each named group element, eject the current * replacement value for the newly added one in this iteration */ for (String groupedElement : groupByElements) { String bindArgumentName = whereConditions.get(groupedElement); Object groupByExpressionElement = groupByExpression[i++]; if (groupByExpressionElement == null) { whereReplacements.remove(bindArgumentName); String patternWtihArgument = "\\Q" + groupedElement + "\\E" + nullHandlerPattern; Pattern nullHandler = Pattern.compile(patternWtihArgument); Matcher nullMatcher = nullHandler.matcher(computedJPQLGroupStatement); if (nullMatcher.find() == false) { log.warn("Did not match for pivoted NULL result"); log.warn("Handler pattern was: " + patternWtihArgument); log.warn("Computed statement was: " + computedJPQLGroupStatement); return null; // default to classic, non-null-supported handling } log.debug("Dynamic replacement made for pivoted NULL result on subexpression bind argument '" + bindArgumentName + "'"); log.debug("Orginal query: " + computedJPQLGroupStatement); computedJPQLGroupStatement = nullMatcher.replaceFirst(groupedElement + " IS NULL "); log.debug("Updated query: " + computedJPQLGroupStatement); } else { whereReplacements.put(bindArgumentName, groupByExpressionElement); whereReplacementTypes.put(bindArgumentName, String.class); } } /* Object bindValue = whereReplacements.get(whereCondition.getValue()); if (bindValue == Literal.NOTNULL) { result += whereCondition.getKey() + " IS NOT NULL "; whereReplacements.remove(whereCondition.getValue()); // no longer needed, literal rendered here } else if (bindValue == Literal.NULL) { result += whereCondition.getKey() + " IS NULL "; whereReplacements.remove(whereCondition.getValue()); // no longer needed, literal rendered here */ log.debug("MultipleQueryIterator: '" + computedJPQLGroupStatement + "'"); List<Integer> results = getSingleResultList(computedJPQLGroupStatement); return new ExpressionEvaluator.Result(results, PrintUtils.getDelimitedString(groupByExpression, 0, ",")); } public void remove() /* no-op */ { } } /* * given a JPQL query in string form, and assuming the predicate map whereRepalcements is populated correctly, * return the result list: -- if no groupBy expressions are present, this will return a collection of Resource * objects -- if at least one groupBy expression was used, it will return an Object[] representing a unique * combination of value N-tuples returned from the set of N-pivoted expressions */ @SuppressWarnings("unchecked") private List getSingleResultList(String queryStr) { if (log.isDebugEnabled()) { String resolvedQuery = queryStr; for (Map.Entry<String, Object> replacement : whereReplacements.entrySet()) { String bindArgument = replacement.getKey(); String bindValueAsString = replacement.getValue().toString(); Class bindType = whereReplacementTypes.get(bindArgument); if (bindType.equals(Integer.class)) { resolvedQuery = resolvedQuery.replace(":" + bindArgument, bindValueAsString); } else if (bindType.equals(String.class)) { resolvedQuery = resolvedQuery.replace(":" + bindArgument, "'" + bindValueAsString + "'"); } else { throw new IllegalArgumentException("Unknown bindType " + bindType + " for " + bindArgument + " having value " + bindValueAsString); } } log.debug("Query: " + resolvedQuery); } if (isTestMode) { return Collections.emptyList(); } Query query = entityManagerFacade.createQuery(queryStr); for (Map.Entry<String, Object> replacement : whereReplacements.entrySet()) { String bindArgument = replacement.getKey(); Object bindValue = replacement.getValue(); Class bindType = whereReplacementTypes.get(bindArgument); if (bindType.equals(Integer.class)) { try { query.setParameter(bindArgument, Integer.valueOf(bindValue.toString())); } catch (NumberFormatException nfe) { query.setParameter(bindArgument, bindValue); } } else if (bindType.equals(String.class)) { query.setParameter(bindArgument, bindValue); } else { throw new IllegalArgumentException("Unknown bindType " + bindType + " for " + bindArgument + " having value " + bindValue); } } return query.getResultList(); } private String getQuerySelectExpression(boolean returnDefault) { String selectExpression = ""; if ((returnDefault == false) && (groupByElements.size() > 0)) { selectExpression = groupByElements.get(0); for (int i = 1; i < groupByElements.size(); i++) { selectExpression += ", " + groupByElements.get(i); } } else { selectExpression = "res.id"; } return selectExpression; } private String getQueryGroupBy(String selectExpression) { if (groupByElements.size() > 0) { return " GROUP BY " + selectExpression; } return ""; } private String getQueryJoinConditions() { String result = ""; // question: can we support multiple complex join clauses (schedules and plugin/resourceConfig) JoinCondition[] orderedConditionProcessing = new JoinCondition[] { JoinCondition.RESOURCE_CHILD, JoinCondition.AVAILABILITY, JoinCondition.SCHEDULES, JoinCondition.PLUGIN_CONFIGURATION, JoinCondition.PLUGIN_CONFIGURATION_DEFINITION, JoinCondition.RESOURCE_CONFIGURATION, JoinCondition.RESOURCE_CONFIGURATION_DEFINITION }; /* * process JoinConditions in a specific order, because hibernate AST parsing requires * tokens to have been identified in the JPQL before their first use; in this case, * JoinCondition.RESOURCE_CHILD must be first because ANY of the others might be joining * down the resource hierarchy (note: joining up the resource hierarchy doesn't require * any special processing because it follows from the "many" to the "one" side of the * relationship, for instance "resource.parent.parent" for grandparents. */ for (JoinCondition joinCondition : orderedConditionProcessing) { ResourceRelativeContext context = joinConditions.get(joinCondition); if (context == null) { continue; } result += " JOIN " + context.pathToken + joinCondition.subexpression + " " + joinCondition.alias; if (joinCondition == JoinCondition.SCHEDULES) { result += " JOIN " + joinCondition.alias + ".definition " + METRIC_DEF_ALIAS; result += ", MeasurementDataTrait " + TRAIT_ALIAS + " "; } else if (joinCondition == JoinCondition.PLUGIN_CONFIGURATION || joinCondition == JoinCondition.RESOURCE_CONFIGURATION) { result += ", PropertySimple " + PROP_SIMPLE_ALIAS; result += ", PropertyDefinition " + PROP_SIMPLE_DEF_ALIAS; } } // finally, if we are narrowing by group membership, add the join on implicit groups if (!memberOfElements.isEmpty()) { result += " JOIN res.implicitGroups implicitGroup"; } return result; } private String getQueryWhereConditions() { String result = ""; if (whereConditions.size() > 0) { result += " WHERE "; boolean first = true; for (Map.Entry<String, String> whereCondition : whereConditions.entrySet()) { if (!first) { result += " AND "; } Object bindValue = whereReplacements.get(whereCondition.getValue()); if (bindValue == Literal.NOTNULL) { result += whereCondition.getKey() + " IS NOT NULL "; whereReplacements.remove(whereCondition.getValue()); // no longer needed, literal rendered here } else if (bindValue == Literal.NULL) { result += whereCondition.getKey() + " IS NULL "; whereReplacements.remove(whereCondition.getValue()); // no longer needed, literal rendered here } else { String whereConditionOperator = " = "; String ending = " "; if (bindValue != null) { /* * there will *not* necessarily be a replacement value ready at this point in the processing; these * get set earlier if this is a SingleQuery, but a MultipleQuery will set these later based on the * results of the pivoted query; so, only attempt processing here if necessary */ String bindValueAsString = bindValue.toString(); if ((bindValueAsString != null) // whereConditionValue is null when whereCondition isn't a groupBy expression && (bindValueAsString.startsWith("%") || bindValueAsString.endsWith("%"))) { whereConditionOperator = " LIKE "; ending = QueryUtility.getEscapeClause(); } } result += whereCondition.getKey() + whereConditionOperator + ":" + whereCondition.getValue() + ending; } first = false; } } if (whereStatics.size() > 0) { boolean first; if (result.length() == 0) { result += " WHERE "; first = true; } else { first = false; } for (String whereStatic : whereStatics) { if (!first) { result += " AND "; } result += whereStatic + " "; first = false; } } // finally, if we are narrowing by group membership, add the implicit groups condition if (!memberOfElements.isEmpty()) { result += " AND implicitGroup.name IN ("; String separator = ""; for (String groupName : memberOfElements) { result += (separator + "'" + groupName + "'"); separator = ", "; } result += ")"; } return result; } public List<String> tokenizeCondition(String condition) { List<String> results = new ArrayList<String>(); boolean insideBracket = false; StringBuilder currentToken = new StringBuilder(); for (char c : condition.trim().toCharArray()) { if (insideBracket) { if (c == ']') { insideBracket = false; } // always add bracket-bounded chars currentToken.append(c); } else { if (c == '.' || c == ' ') { String token = currentToken.toString(); if (token.length() > 0) { results.add(token); } currentToken = new StringBuilder(); } else { if (c == '[') { insideBracket = true; } currentToken.append(c); } } } // and if there's anything left in the buffer String token = currentToken.toString(); if (token.length() > 0) { results.add(token); } return results; } private String parseTraitName(List<String> originalTokens) throws InvalidExpressionException { String prefix = "trait"; String suffix = originalTokens.get(parseIndex).substring(prefix.length()); if (suffix.length() < 3) { throw new InvalidExpressionException("Unrecognized trait name '" + suffix + "'"); } if ((suffix.charAt(0) != '[') || (suffix.charAt(suffix.length() - 1) != ']')) { throw new InvalidExpressionException("Trait name '" + suffix + "' must be contained within '[' and ']' characters"); } return suffix.substring(1, suffix.length() - 1); } private void validateSubExpressionAgainstPreviouslySeen(String normalizedSubExpression, boolean grouped, boolean membership) throws InvalidExpressionException { normalizedSubExpression = stripFunctionSuffix(normalizedSubExpression); if (grouped) { if (groupedSubExpressions.contains(normalizedSubExpression)) { throw new InvalidExpressionException("Redundant 'groupby' expression[" + normalizedSubExpression + "] - these expressions must be unique"); } if (simpleSubExpressions.contains(normalizedSubExpression)) { throw new InvalidExpressionException( "Can not group by the same condition you are filtering on, expression[" + normalizedSubExpression + "]"); } groupedSubExpressions.add(normalizedSubExpression); } else if (membership) { if (memberSubExpressions.contains(normalizedSubExpression)) { throw new InvalidExpressionException("Redundant 'memberof' expression[" + normalizedSubExpression + "] - these expressions must be unique"); } memberSubExpressions.add(normalizedSubExpression); } else { if (groupedSubExpressions.contains(normalizedSubExpression)) { throw new InvalidExpressionException( "Can not group by the same condition you are filtering on, expression[" + normalizedSubExpression + "]"); } simpleSubExpressions.add(normalizedSubExpression); } } private final String[] functions = { "contains", "startswith", "endswith" }; private String stripFunctionSuffix(String expression) { for (String function : functions) { if (expression.endsWith(function)) { return expression.substring(0, expression.length() - function.length()); } } return expression; } private static class PrintUtils { public static String getDelimitedString(Object[] tokens, int fromIndex, String delimiter) { StringBuilder builder = new StringBuilder(); for (int j = fromIndex; j < tokens.length; j++) { if (j != fromIndex) { builder.append(delimiter); } Object token = tokens[j]; if (token == null) { builder.append("empty"); } else { builder.append(token.toString()); } } return builder.toString(); } } }