Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.metron.stellar.common; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.base.Joiner; import org.antlr.v4.runtime.ANTLRInputStream; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.TokenStream; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.metron.stellar.dsl.Context; import org.apache.metron.stellar.dsl.Context.ActivityType; import org.apache.metron.stellar.dsl.DefaultVariableResolver; import org.apache.metron.stellar.dsl.ErrorListener; import org.apache.metron.stellar.dsl.ParseException; import org.apache.metron.stellar.dsl.StellarFunctions; import org.apache.metron.stellar.dsl.VariableResolver; import org.apache.metron.stellar.dsl.functions.resolver.FunctionResolver; import org.apache.metron.stellar.common.evaluators.ArithmeticEvaluator; import org.apache.metron.stellar.common.evaluators.ComparisonExpressionWithOperatorEvaluator; import org.apache.metron.stellar.common.evaluators.NumberLiteralEvaluator; import org.apache.metron.stellar.common.generated.StellarLexer; import org.apache.metron.stellar.common.generated.StellarParser; import static org.apache.commons.lang3.StringUtils.isEmpty; /** * The base implementation of a Stellar processor. This is used to evaluate Stellar expressions. * * @param <T> The type that the processor expects to return after processing a Stellar expression. * @see StellarProcessor * @see StellarPredicateProcessor */ public class BaseStellarProcessor<T> { public static final int DEFAULT_CACHE_SIZE = 500; public static final int DEFAULT_EXPIRY_TIME = 10; public static final TimeUnit DEFAULT_EXPIRY_TIME_UNITS = TimeUnit.MINUTES; /** * The default expression cache. This is used when the expression cache is not otherwise specified. */ private static Cache<String, StellarCompiler.Expression> defaultExpressionCache; static { defaultExpressionCache = createCache(DEFAULT_CACHE_SIZE, DEFAULT_EXPIRY_TIME, DEFAULT_EXPIRY_TIME_UNITS); } /** * The class containing the type that the Stellar expression being processed will evaluate to. */ private Class<T> clazz; /** * @param clazz The class containing the type that the Stellar expression being processed will evaluate to. */ Cache<String, StellarCompiler.Expression> expressionCache; /** * Create a default stellar processor. This processor uses the static expression cache. */ BaseStellarProcessor(final Class<T> clazz) { this(clazz, defaultExpressionCache); } BaseStellarProcessor(final Class<T> clazz, int cacheSize, int expiryTime, TimeUnit expiryUnit) { this(clazz, createCache(cacheSize, expiryTime, expiryUnit)); } BaseStellarProcessor(final Class<T> clazz, Cache<String, StellarCompiler.Expression> expressionCache) { this.clazz = clazz; this.expressionCache = expressionCache; } static Cache<String, StellarCompiler.Expression> createCache(int cacheSize, int expiryTime, TimeUnit expiryUnit) { CacheLoader<String, StellarCompiler.Expression> loader = key -> compile(key); return Caffeine.newBuilder().maximumSize(cacheSize).expireAfterAccess(expiryTime, expiryUnit).build(loader); } /** * Parses the given rule and returns a set of variables that are used in the given Stellar expression, {@code rule}. * * @param rule The Stellar expression to find out what variables are used. * @return A set of variables used in the given Stellar expression. */ public Set<String> variablesUsed(final String rule) { if (rule == null || isEmpty(rule.trim())) { return null; } StellarCompiler.Expression expression = null; try { expression = expressionCache.get(rule, r -> compile(r)); } catch (Throwable e) { throw new ParseException("Unable to parse: " + rule + " due to: " + e.getMessage(), e); } return expression.variablesUsed; } /** * Parses and evaluates the given Stellar expression, {@code rule}. * @param rule The Stellar expression to parse and evaluate. * @param variableResolver The {@link VariableResolver} to determine values of variables used in the Stellar expression, {@code rule}. * @param functionResolver The {@link FunctionResolver} to determine values of functions used in the Stellar expression, {@code rule}. * @param context The context used during validation. * @return The value of the evaluated Stellar expression, {@code rule}. */ public T parse(final String rule, final VariableResolver variableResolver, final FunctionResolver functionResolver, final Context context) { StellarCompiler.Expression expression = null; if (rule == null || isEmpty(rule.trim())) { return null; } if (context.getActivityType() == null) { context.setActivityType(ActivityType.PARSE_ACTIVITY); } try { expression = expressionCache.get(rule, r -> compile(r)); } catch (Throwable e) { throw createException(rule, variableResolver, e); } try { return clazz.cast(expression .apply(new StellarCompiler.ExpressionState(context, functionResolver, variableResolver))); } catch (Throwable e) { throw createException(rule, variableResolver, e); } finally { // always reset the activity type context.setActivityType(null); } } private ParseException createException(String rule, VariableResolver resolver, Throwable t) { String message = "Unable to parse: " + rule + " due to: " + t.getMessage(); Set<String> variablesUsed = variablesUsed(rule); if (variablesUsed.isEmpty()) { return new ParseException(message, t); } List<Map.Entry<String, Object>> messagesUsed = new ArrayList<>(variablesUsed.size()); for (String v : variablesUsed) { Optional<Object> resolved = Optional.ofNullable(resolver.resolve(v)); messagesUsed.add(new AbstractMap.SimpleEntry<>(v, resolved.orElse("missing"))); } return new ParseException(message + " with relevant variables " + Joiner.on(",").join(messagesUsed), t); } /** * Parses and evaluates the given Stellar expression, {@code rule}. * @param rule The Stellar expression to parse and evaluate. * @return The Expression, which can be reevaluated without reparsing in different Contexts and Resolvers. */ public static StellarCompiler.Expression compile(final String rule) { if (rule == null || isEmpty(rule.trim())) { return null; } ANTLRInputStream input = new ANTLRInputStream(rule); StellarLexer lexer = new StellarLexer(input); lexer.removeErrorListeners(); lexer.addErrorListener(new ErrorListener()); TokenStream tokens = new CommonTokenStream(lexer); StellarParser parser = new StellarParser(tokens); StellarCompiler treeBuilder = new StellarCompiler(ArithmeticEvaluator.INSTANCE, NumberLiteralEvaluator.INSTANCE, ComparisonExpressionWithOperatorEvaluator.INSTANCE); parser.addParseListener(treeBuilder); parser.removeErrorListeners(); parser.addErrorListener(new ErrorListener()); parser.transformation(); return treeBuilder.getExpression(); } /** * This method determines if a given rule is valid or not. If the given rule is valid then true * will be returned otherwise a {@link ParseException} is thrown. If it is desired to return a boolean * whether the rule is valid or not, use the {@link #validate(String, boolean, Context) validate} method. It is important * to note that all variables will resolve to 'null.' * * @param rule The rule to validate. * @return If the given rule is valid then true otherwise an exception is thrown. * @throws ParseException If the rule is invalid an exception of this type is thrown. */ public boolean validate(final String rule) throws ParseException { return validate(rule, true, Context.EMPTY_CONTEXT()); } /** * Validates a given Stellar expression based on given context. * @param rule The Stellar expression to validate. * @param context The context used to validate the Stellar expression. * @return If valid Stellar expression true, otherwise an exception will be thrown. * @throws ParseException The exception containing the information as to why the expression is not valid. */ public boolean validate(final String rule, final Context context) throws ParseException { return validate(rule, true, context); } /** * Here it is not desirable to add our custom listener. It is not the intent to evaluate the rule. * The rule is only meant to be validated. Validate in this instance means check whether or not the * rule is syntactically valid and whether the functions used exist. For example, it will not check * for variables that are not defined. Currently all variables resolve to 'null.' This is mainly to * make sure things function as expected when values are null. * * @param rule The Stellar transformation to validate. * @param throwException If true an invalid Stellar transformation will throw a {@link ParseException} otherwise a boolean will be returned. * @param context The Stellar context to be used when validating the Stellar transformation. * @return If {@code throwException} is true and {@code rule} is invalid a {@link ParseException} is thrown. If * {@code throwException} is false and {@code rule} is invalid then false is returned. Otherwise true if {@code rule} is valid, * false if {@code rule} is invalid. * @throws ParseException Thrown if {@code rule} is invalid and {@code throwException} is true. */ public boolean validate(final String rule, final boolean throwException, final Context context) throws ParseException { if (rule == null || isEmpty(rule.trim())) { return true; } // set the context to validation // it will be reset in parse() context.setActivityType(ActivityType.VALIDATION_ACTIVITY); try { parse(rule, DefaultVariableResolver.NULL_RESOLVER(), StellarFunctions.FUNCTION_RESOLVER(), context); } catch (Throwable t) { if (throwException) { throw new ParseException("Unable to parse " + rule + ": " + t.getMessage(), t); } else { return false; } } return true; } }