/*
***************************************************************************************
* Copyright (C) 2006 EsperTech, Inc. All rights reserved. *
* http://www.espertech.com/esper *
* http://www.espertech.com *
* ---------------------------------------------------------------------------------- *
* The software in this package is published under the terms of the GPL license *
* a copy of which has been included with this distribution in the license.txt file. *
***************************************************************************************
*/
package com.espertech.esper.client.annotation;
import com.espertech.esper.client.EPException;
import com.espertech.esper.epl.annotation.AnnotationException;
import java.lang.annotation.Annotation;
import java.util.*;
/**
* Enumeration of hint values. Since hints may be a comma-separate list in a single @Hint annotation
* they are listed as enumeration values here.
*/
public enum HintEnum {
/**
* For use with match_recognize, iterate-only matching.
*/
ITERATE_ONLY("ITERATE_ONLY", false, false, false),
/**
* For use with group-by, disabled reclaim groups.
*/
DISABLE_RECLAIM_GROUP("DISABLE_RECLAIM_GROUP", false, false, false),
/**
* For use with group-by and std:groupwin, reclaim groups for unbound streams based on time. The number of seconds after which a groups is reclaimed if inactive.
*/
RECLAIM_GROUP_AGED("RECLAIM_GROUP_AGED", true, true, false),
/**
* For use with group-by and std:groupwin, reclaim groups for unbound streams based on time, this number is the frequency in seconds at which a sweep occurs for aged
* groups, if not provided then the sweep frequency is the same number as the age.
*/
RECLAIM_GROUP_FREQ("RECLAIM_GROUP_FREQ", true, true, false),
/**
* For use with create-named-window statements only, to indicate that statements that subquery the named window
* use named window data structures (unless the subquery statement specifies below DISBABLE hint and as listed below).
* <p>
* By default and if this hint is not specified or subqueries specify a stream filter on a named window,
* subqueries use statement-local data structures representing named window contents (table, index).
* Such data structure is maintained by consuming the named window insert and remove stream.
*/
ENABLE_WINDOW_SUBQUERY_INDEXSHARE("ENABLE_WINDOW_SUBQUERY_INDEXSHARE", false, false, false),
/**
* If ENABLE_WINDOW_SUBQUERY_INDEXSHARE is not specified for a named window (the default) then this instruction is ignored.
* <p>
* For use with statements that subquery a named window and that benefit from a statement-local data structure representing named window contents (table, index),
* maintained through consuming the named window insert and remove stream.
* </p>
*/
DISABLE_WINDOW_SUBQUERY_INDEXSHARE("DISABLE_WINDOW_SUBQUERY_INDEXSHARE", false, false, false),
/**
* For use with subqueries and on-select, on-merge, on-update and on-delete to specify the query engine neither
* build an implicit index nor use an existing index, always performing a full table scan.
*/
SET_NOINDEX("SET_NOINDEX", false, false, false),
/**
* For use with join query plans to force a nested iteration plan.
*/
FORCE_NESTED_ITER("FORCE_NESTED_ITER", false, false, false),
/**
* For use with join query plans to indicate preferance of the merge-join query plan.
*/
PREFER_MERGE_JOIN("PREFER_MERGE_JOIN", false, false, false),
/**
* For use everywhere where indexes are used (subquery, joins, fire-and-forget, onl-select etc.), index hint.
*/
INDEX("INDEX", false, false, true),
/**
* For use where query planning applies.
*/
EXCLUDE_PLAN("EXCLUDE_PLAN", false, false, true),
/**
* For use everywhere where unique data window are used
*/
DISABLE_UNIQUE_IMPLICIT_IDX("DISABLE_UNIQUE_IMPLICIT_IDX", false, false, false),
/**
* For use when filter expression optimization may widen the filter expression.
*/
MAX_FILTER_WIDTH("MAX_FILTER_WIDTH", true, true, false),
/**
* For use everywhere where unique data window are used
*/
DISABLE_WHEREEXPR_MOVETO_FILTER("DISABLE_WHEREEXPR_MOVETO_FILTER", false, false, false),
/**
* For use with output rate limiting to enable certain optimization that may however change output.
*/
ENABLE_OUTPUTLIMIT_OPT("ENABLE_OUTPUTLIMIT_OPT", false, false, false);
private final String value;
private final boolean acceptsParameters;
private final boolean requiresParameters;
private final boolean requiresParentheses;
private HintEnum(String value, boolean acceptsParameters, boolean requiresParameters, boolean requiresParentheses) {
this.value = value.toUpperCase(Locale.ENGLISH);
this.acceptsParameters = acceptsParameters;
if (acceptsParameters) {
this.requiresParameters = true;
} else {
this.requiresParameters = requiresParameters;
}
this.requiresParentheses = requiresParentheses;
}
/**
* Returns the constant.
*
* @return constant
*/
public String getValue() {
return value;
}
/**
* True if the hint accepts params.
*
* @return indicator
*/
public boolean isAcceptsParameters() {
return acceptsParameters;
}
/**
* True if the hint requires params.
*
* @return indicator
*/
public boolean isRequiresParameters() {
return requiresParameters;
}
/**
* Check if the hint is present in the annotations provided.
*
* @param annotations the annotations to inspect
* @return indicator
*/
public Hint getHint(Annotation[] annotations) {
if (annotations == null) {
return null;
}
for (Annotation annotation : annotations) {
if (!(annotation instanceof Hint)) {
continue;
}
Hint hintAnnotation = (Hint) annotation;
try {
Map<HintEnum, List<String>> setOfHints = HintEnum.validateGetListed(hintAnnotation);
if (setOfHints.containsKey(this)) {
return hintAnnotation;
}
} catch (AnnotationException e) {
throw new EPException("Invalid hint: " + e.getMessage(), e);
}
}
return null;
}
/**
* Validate a hint annotation ensuring it contains only recognized hints.
*
* @param annotation to validate
* @return validated hint enums and their parameter list
* @throws AnnotationException if an invalid text was found
*/
public static Map<HintEnum, List<String>> validateGetListed(Annotation annotation) throws AnnotationException {
if (!(annotation instanceof Hint)) {
return Collections.emptyMap();
}
Hint hint = (Hint) annotation;
String hintValueCaseNeutral = hint.value().trim();
String hintValueUppercase = hintValueCaseNeutral.toUpperCase(Locale.ENGLISH);
for (HintEnum val : HintEnum.values()) {
if (val.getValue().equals(hintValueUppercase) && !val.requiresParentheses) {
validateParameters(val, hint.value().trim());
List<String> parameters;
if (val.acceptsParameters) {
String assignment = getAssignedValue(hint.value().trim(), val.value);
if (assignment == null) {
parameters = Collections.emptyList();
} else {
parameters = Collections.singletonList(assignment);
}
} else {
parameters = Collections.emptyList();
}
return Collections.singletonMap(val, parameters);
}
}
String[] hints = splitCommaUnlessInParen(hint.value());
Map<HintEnum, List<String>> listed = new HashMap<HintEnum, List<String>>();
for (int i = 0; i < hints.length; i++) {
String hintValUppercase = hints[i].trim().toUpperCase(Locale.ENGLISH);
String hintValNeutralcase = hints[i].trim();
HintEnum found = null;
String parameter = null;
for (HintEnum val : HintEnum.values()) {
if (val.getValue().equals(hintValUppercase) && !val.requiresParentheses) {
found = val;
parameter = getAssignedValue(hint.value().trim(), val.value);
break;
}
if (val.requiresParentheses) {
int indexOpen = hintValUppercase.indexOf('(');
int indexClosed = hintValUppercase.lastIndexOf(')');
if (indexOpen != -1) {
String hintNameNoParen = hintValUppercase.substring(0, indexOpen);
if (val.getValue().equals(hintNameNoParen)) {
if (indexClosed == -1 || indexClosed < indexOpen) {
throw new AnnotationException("Hint '" + val + "' mismatches parentheses");
}
if (indexClosed != hintValUppercase.length() - 1) {
throw new AnnotationException("Hint '" + val + "' has additional text after parentheses");
}
found = val;
parameter = hintValNeutralcase.substring(indexOpen + 1, indexClosed);
break;
}
}
if (hintValUppercase.equals(val.getValue()) && indexOpen == -1) {
throw new AnnotationException("Hint '" + val + "' requires additional parameters in parentheses");
}
}
if (hintValUppercase.indexOf('=') != -1) {
String hintName = hintValUppercase.substring(0, hintValUppercase.indexOf('='));
if (val.getValue().equals(hintName.trim().toUpperCase(Locale.ENGLISH))) {
found = val;
parameter = getAssignedValue(hint.value().trim(), val.value);
break;
}
}
}
if (found == null) {
String hintName = hints[i].trim();
if (hintName.indexOf('=') != -1) {
hintName = hintName.substring(0, hintName.indexOf('='));
}
throw new AnnotationException("Hint annotation value '" + hintName.trim() + "' is not one of the known values");
} else {
if (!found.requiresParentheses) {
validateParameters(found, hintValUppercase);
}
List<String> existing = listed.get(found);
if (existing == null) {
existing = new ArrayList<String>();
listed.put(found, existing);
}
if (parameter != null) {
existing.add(parameter);
}
}
}
return listed;
}
private static void validateParameters(HintEnum val, String hintVal) throws AnnotationException {
if (val.isRequiresParameters()) {
if (hintVal.indexOf('=') == -1) {
throw new AnnotationException("Hint '" + val + "' requires a parameter value");
}
}
if (!val.isAcceptsParameters()) {
if (hintVal.indexOf('=') != -1) {
throw new AnnotationException("Hint '" + val + "' does not accept a parameter value");
}
}
}
/**
* Returns hint value.
*
* @param annotation to look for
* @return hint assigned first value provided
*/
public String getHintAssignedValue(Hint annotation) {
try {
Map<HintEnum, List<String>> hintValues = validateGetListed(annotation);
if (hintValues == null || !hintValues.containsKey(this)) {
return null;
}
return hintValues.get(this).get(0);
} catch (AnnotationException ex) {
throw new EPException("Failed to interpret hint annotation: " + ex.getMessage(), ex);
}
}
/**
* Returns all values assigned for a given hint, if any
*
* @param annotations to be interogated
* @return hint assigned values or null if none found
*/
public List<String> getHintAssignedValues(Annotation[] annotations) {
List<String> allHints = null;
try {
for (Annotation annotation : annotations) {
Map<HintEnum, List<String>> hintValues = validateGetListed(annotation);
if (hintValues == null || !hintValues.containsKey(this)) {
continue;
}
if (allHints == null) {
allHints = hintValues.get(this);
} else {
allHints.addAll(hintValues.get(this));
}
}
} catch (AnnotationException ex) {
throw new EPException("Failed to interpret hint annotation: " + ex.getMessage(), ex);
}
return allHints;
}
private static String getAssignedValue(String value, String enumValue) {
String valMixed = value.trim();
String val = valMixed.toUpperCase(Locale.ENGLISH);
if (!val.contains(",")) {
if (val.indexOf('=') == -1) {
return null;
}
String hintName = val.substring(0, val.indexOf('='));
if (!hintName.equals(enumValue)) {
return null;
}
return valMixed.substring(val.indexOf('=') + 1, val.length());
}
String[] hints = valMixed.split(",");
for (String hint : hints) {
int indexOfEquals = hint.indexOf('=');
if (indexOfEquals == -1) {
continue;
}
val = hint.substring(0, indexOfEquals).trim().toUpperCase(Locale.ENGLISH);
if (!val.equals(enumValue)) {
continue;
}
String strValue = hint.substring(indexOfEquals + 1).trim();
if (strValue.length() == 0) {
return null;
}
return strValue;
}
return null;
}
/**
* Split a line of comma-separated values allowing parenthesis.
*
* @param line to split
* @return parameters
*/
public static String[] splitCommaUnlessInParen(String line) {
int nestingLevelParen = 0;
int lastComma = -1;
List<String> parts = new ArrayList<String>();
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '(') {
nestingLevelParen++;
}
if (c == ')') {
if (nestingLevelParen == 0) {
throw new RuntimeException("Close parenthesis ')' found but none open");
}
nestingLevelParen--;
}
if (c == ',' && nestingLevelParen == 0) {
String part = line.substring(lastComma + 1, i);
if (part.trim().length() > 0) {
parts.add(part);
}
lastComma = i;
}
}
String lastPart = line.substring(lastComma + 1);
if (lastPart.trim().length() > 0) {
parts.add(lastPart);
}
return parts.toArray(new String[parts.size()]);
}
}