/*
* 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();
}
}
}