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