/** * Copyright 2015 StreamSets Inc. * * Licensed under 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 com.streamsets.datacollector.el; import com.streamsets.datacollector.definition.ELDefinitionExtractor; import com.streamsets.pipeline.api.el.ELEval; import com.streamsets.pipeline.api.el.ELEvalException; import com.streamsets.pipeline.api.el.ELVars; import com.streamsets.pipeline.api.impl.Utils; import com.streamsets.pipeline.lib.util.CommonError; import org.apache.commons.el.LruExpressionEvaluatorImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.jsp.el.ELException; import javax.servlet.jsp.el.FunctionMapper; import javax.servlet.jsp.el.VariableResolver; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ELEvaluator extends ELEval { private static final Logger LOG = LoggerFactory.getLogger(ELEvaluator.class); private final String configName; private final Map<String, Object> constants; private final Map<String, Map<String,Method>> functionsByNamespace; private final FunctionMapperImpl functionMapper; private final List<ElFunctionDefinition> elFunctionDefinitions; private final List<ElConstantDefinition> elConstantDefinitions; // ExpressionEvaluatorImpl can be used as a singleton private static final LruExpressionEvaluatorImpl EVALUATOR = new LruExpressionEvaluatorImpl(); public ELEvaluator(String configName, Map<String, Object> constants, List<Class> elFuncConstDefClasses) { this(configName, constants, elFuncConstDefClasses.toArray(new Class[elFuncConstDefClasses.size()])); } public ELEvaluator(String configName, Map<String, Object> constants, Class<?>... elFuncConstDefClasses) { this.configName = configName; this.constants = new HashMap<>(constants); functionsByNamespace = new HashMap<>(); elFunctionDefinitions = new ArrayList<>(); elConstantDefinitions = new ArrayList<>(); populateConstantsAndFunctions(elFuncConstDefClasses); this.functionMapper = new FunctionMapperImpl(); } public ELEvaluator(String configName, Class<?>... elFuncConstDefClasses) { this(configName, new HashMap<String, Object>(), elFuncConstDefClasses); } private void populateConstantsAndFunctions(Class<?>... elFuncConstDefClasses) { if(elFuncConstDefClasses != null) { for (ElFunctionDefinition function : ELDefinitionExtractor.get().extractFunctions(elFuncConstDefClasses, "")) { elFunctionDefinitions.add(function); registerFunction(function); } for (ElConstantDefinition constant : ELDefinitionExtractor.get().extractConstants(elFuncConstDefClasses, "")) { elConstantDefinitions.add(constant); constants.put(constant.getName(), constant.getValue()); } } } // stuff a function into the namespace/name dual level map structure private void registerFunction(ElFunctionDefinition function) { String namespace; String functionName; if (function.getName().contains(":")) { String[] tokens = function.getName().split(":"); namespace = tokens[0]; functionName = tokens[1]; } else { namespace = ""; functionName = function.getName(); } Map<String, Method> namespaceFunctions = functionsByNamespace.get(namespace); if (namespaceFunctions == null) { namespaceFunctions = new HashMap<>(); functionsByNamespace.put(namespace, namespaceFunctions); } namespaceFunctions.put(functionName, function.getMethod()); } @Override public String getConfigName() { return configName; } @Override public ELVars createVariables() { return new ELVariables(constants); } public static void parseEL(String el) throws ELEvalException { try { EVALUATOR.parseExpressionString(el); } catch (ELException e) { LOG.debug("Error parsering EL '{}': {}", el, e.toString(), e); throw new ELEvalException(CommonError.CMN_0105, el, e.toString(), e); } } @Override @SuppressWarnings("unchecked") public <T> T evaluate (final ELVars vars, String expression, Class<T> returnType) throws ELEvalException { VariableResolver variableResolver = new VariableResolver() { @Override public Object resolveVariable(String name) throws ELException { Object value = constants.get(name); if (!vars.hasVariable(name)) { if (value == null && !constants.containsKey(name)) { throw new ELException(Utils.format("Constants/Variable '{}' cannot be resolved", name)); } } else { value = vars.getVariable(name); } return value; } }; try { return (T) EVALUATOR.evaluate(expression, returnType, variableResolver, functionMapper); } catch (ELException e) { // Apache evaluator is not using the getCause exception chaining that is available in Java but rather a custom // chaining mechanism. This doesn't work well for us as we're effectively swallowing the cause that is not // available in log, ... Throwable t = e; if(e.getRootCause() != null) { t = e.getRootCause(); if(e.getCause() == null) { e.initCause(t); } } LOG.debug("Error valuating EL '{}': {}", expression, e.toString(), e); throw new ELEvalException(CommonError.CMN_0104, expression, t.toString(), e); } } private class FunctionMapperImpl implements FunctionMapper { @Override public Method resolveFunction(String functionNamespace, String functionName) { Map<String, Method> namespaceFunctions = functionsByNamespace.get(functionNamespace); if (namespaceFunctions == null) { return null; } return namespaceFunctions.get(functionName); } } public List<ElFunctionDefinition> getElFunctionDefinitions() { return elFunctionDefinitions; } public List<ElConstantDefinition> getElConstantDefinitions() { return elConstantDefinitions; } }