All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.jexl2.UnifiedJEXL Maven / Gradle / Ivy

/*
 * 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.commons.jexl2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.jexl2.introspection.JexlMethod;
import org.apache.commons.jexl2.introspection.Uberspect;
import org.apache.commons.jexl2.parser.ASTJexlScript;
import org.apache.commons.jexl2.parser.JexlNode;
import org.apache.commons.jexl2.parser.StringParser;

/**
 * An evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL.
 * It is intended to be used in configuration modules, XML based frameworks or JSP taglibs
 * and facilitate the implementation of expression evaluation.
 * 

* An expression can mix immediate, deferred and nested sub-expressions as well as string constants; *

    *
  • The "immediate" syntax is of the form "...${jexl-expr}..."
  • *
  • The "deferred" syntax is of the form "...#{jexl-expr}..."
  • *
  • The "nested" syntax is of the form "...#{...${jexl-expr0}...}..."
  • *
  • The "composite" syntax is of the form "...${jexl-expr0}... #{jexl-expr1}..."
  • *
*

*

* Deferred & immediate expression carry different intentions: *

    *
  • An immediate expression indicate that evaluation is intended to be performed close to * the definition/parsing point.
  • *
  • A deferred expression indicate that evaluation is intended to occur at a later stage.
  • *
*

*

* For instance: "Hello ${name}, now is #{time}" is a composite "deferred" expression since one * of its subexpressions is deferred. Furthermore, this (composite) expression intent is * to perform two evaluations; one close to its definition and another one in a later * phase. *

*

* The API reflects this feature in 2 methods, prepare and evaluate. The prepare method * will evaluate the immediate subexpression and return an expression that contains only * the deferred subexpressions (& constants), a prepared expression. Such a prepared expression * is suitable for a later phase evaluation that may occur with a different JexlContext. * Note that it is valid to call evaluate without prepare in which case the same JexlContext * is used for the 2 evaluation phases. *

*

* In the most common use-case where deferred expressions are to be kept around as properties of objects, * one should parse & prepare an expression before storing it and evaluate it each time * the property storing it is accessed. *

*

* Note that nested expression use the JEXL syntax as in: * "#{${bar}+'.charAt(2)'}" * The most common mistake leading to an invalid expression being the following: * "#{${bar}charAt(2)}" *

*

Also note that methods that parse evaluate expressions may throw unchecked exceptions; * The {@link UnifiedJEXL.Exception} are thrown when the engine instance is in "non-silent" mode * but since these are RuntimeException, user-code should catch them where appropriate. *

* @since 2.0 */ public final class UnifiedJEXL { /** The JEXL engine instance. */ private final JexlEngine jexl; /** The expression cache. */ private final JexlEngine.SoftCache cache; /** The default cache size. */ private static final int CACHE_SIZE = 256; /** The first character for immediate expressions. */ private static final char IMM_CHAR = '$'; /** The first character for deferred expressions. */ private static final char DEF_CHAR = '#'; /** * Creates a new instance of UnifiedJEXL with a default size cache. * @param aJexl the JexlEngine to use. */ public UnifiedJEXL(JexlEngine aJexl) { this(aJexl, CACHE_SIZE); } /** * Creates a new instance of UnifiedJEXL creating a local cache. * @param aJexl the JexlEngine to use. * @param cacheSize the number of expressions in this cache */ public UnifiedJEXL(JexlEngine aJexl, int cacheSize) { this.jexl = aJexl; this.cache = aJexl.new SoftCache(cacheSize); } /** * Types of expressions. * Each instance carries a counter index per (composite sub-) expression type. * @see ExpressionBuilder */ private static enum ExpressionType { /** Constant expression, count index 0. */ CONSTANT(0), /** Immediate expression, count index 1. */ IMMEDIATE(1), /** Deferred expression, count index 2. */ DEFERRED(2), /** Nested (which are deferred) expressions, count index 2. */ NESTED(2), /** Composite expressions are not counted, index -1. */ COMPOSITE(-1); /** The index in arrays of expression counters for composite expressions. */ private final int index; /** * Creates an ExpressionType. * @param idx the index for this type in counters arrays. */ ExpressionType(int idx) { this.index = idx; } } /** * A helper class to build expressions. * Keeps count of sub-expressions by type. */ private static class ExpressionBuilder { /** Per expression type counters. */ private final int[] counts; /** The list of expressions. */ private final ArrayList expressions; /** * Creates a builder. * @param size the initial expression array size */ ExpressionBuilder(int size) { counts = new int[]{0, 0, 0}; expressions = new ArrayList(size <= 0 ? 3 : size); } /** * Adds an expression to the list of expressions, maintain per-type counts. * @param expr the expression to add */ void add(Expression expr) { counts[expr.getType().index] += 1; expressions.add(expr); } /** * Builds an expression from a source, performs checks. * @param el the unified el instance * @param source the source expression * @return an expression */ Expression build(UnifiedJEXL el, Expression source) { int sum = 0; for (int count : counts) { sum += count; } if (expressions.size() != sum) { StringBuilder error = new StringBuilder("parsing algorithm error, exprs: "); error.append(expressions.size()); error.append(", constant:"); error.append(counts[ExpressionType.CONSTANT.index]); error.append(", immediate:"); error.append(counts[ExpressionType.IMMEDIATE.index]); error.append(", deferred:"); error.append(counts[ExpressionType.DEFERRED.index]); throw new IllegalStateException(error.toString()); } // if only one sub-expr, no need to create a composite if (expressions.size() == 1) { return expressions.get(0); } else { return el.new CompositeExpression(counts, expressions, source); } } } /** * Gets the JexlEngine underlying the UnifiedJEXL. * @return the JexlEngine */ public JexlEngine getEngine() { return jexl; } /** * Clears the cache. * @since 2.1 */ public void clearCache() { synchronized (cache) { cache.clear(); } } /** * The sole type of (runtime) exception the UnifiedJEXL can throw. */ public static class Exception extends RuntimeException { /** Serial version UID. */ private static final long serialVersionUID = -8201402995815975726L; /** * Creates a UnifiedJEXL.Exception. * @param msg the exception message * @param cause the exception cause */ public Exception(String msg, Throwable cause) { super(msg, cause); } } /** * The abstract base class for all expressions, immediate '${...}' and deferred '#{...}'. */ public abstract class Expression { /** The source of this expression (see {@link UnifiedJEXL.Expression#prepare}). */ protected final Expression source; /** * Creates an expression. * @param src the source expression if any */ Expression(Expression src) { this.source = src != null ? src : this; } /** * Checks whether this expression is immediate. * @return true if immediate, false otherwise */ public boolean isImmediate() { return true; } /** * Checks whether this expression is deferred. * @return true if deferred, false otherwise */ public final boolean isDeferred() { return !isImmediate(); } /** * Gets this expression type. * @return its type */ abstract ExpressionType getType(); /** * Formats this expression, adding its source string representation in * comments if available: 'expression /*= source *\/'' . * Note: do not override; will be made final in a future release. * @return the formatted expression string */ @Override public String toString() { StringBuilder strb = new StringBuilder(); asString(strb); if (source != this) { strb.append(" /*= "); strb.append(source.toString()); strb.append(" */"); } return strb.toString(); } /** * Generates this expression's string representation. * @return the string representation */ public String asString() { StringBuilder strb = new StringBuilder(); asString(strb); return strb.toString(); } /** * Adds this expression's string representation to a StringBuilder. * @param strb the builder to fill * @return the builder argument */ public abstract StringBuilder asString(StringBuilder strb); /** * Gets the list of variables accessed by this expression. *

This method will visit all nodes of the sub-expressions and extract all variables whether they * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).

* @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string) * or the empty set if no variables are used * @since 2.1 */ public Set> getVariables() { return Collections.emptySet(); } /** * Fills up the list of variables accessed by this expression. * @param refs the set of variable being filled * @since 2.1 */ protected void getVariables(Set> refs) { // nothing to do } /** * Evaluates the immediate sub-expressions. *

* When the expression is dependant upon immediate and deferred sub-expressions, * evaluates the immediate sub-expressions with the context passed as parameter * and returns this expression deferred form. *

*

* In effect, this binds the result of the immediate sub-expressions evaluation in the * context, allowing to differ evaluation of the remaining (deferred) expression within another context. * This only has an effect to nested & composite expressions that contain differed & immediate sub-expressions. *

*

* If the underlying JEXL engine is silent, errors will be logged through its logger as warning. *

* Note: do not override; will be made final in a future release. * @param context the context to use for immediate expression evaluations * @return an expression or null if an error occurs and the {@link JexlEngine} is running in silent mode * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not in silent mode */ public Expression prepare(JexlContext context) { try { Interpreter interpreter = new Interpreter(jexl, context, !jexl.isLenient(), jexl.isSilent()); if (context instanceof TemplateContext) { interpreter.setFrame(((TemplateContext) context).getFrame()); } return prepare(interpreter); } catch (JexlException xjexl) { Exception xuel = createException("prepare", this, xjexl); if (jexl.isSilent()) { jexl.logger.warn(xuel.getMessage(), xuel.getCause()); return null; } throw xuel; } } /** * Evaluates this expression. *

* If the underlying JEXL engine is silent, errors will be logged through its logger as warning. *

* Note: do not override; will be made final in a future release. * @param context the variable context * @return the result of this expression evaluation or null if an error occurs and the {@link JexlEngine} is * running in silent mode * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent */ public Object evaluate(JexlContext context) { try { Interpreter interpreter = new Interpreter(jexl, context, !jexl.isLenient(), jexl.isSilent()); if (context instanceof TemplateContext) { interpreter.setFrame(((TemplateContext) context).getFrame()); } return evaluate(interpreter); } catch (JexlException xjexl) { Exception xuel = createException("prepare", this, xjexl); if (jexl.isSilent()) { jexl.logger.warn(xuel.getMessage(), xuel.getCause()); return null; } throw xuel; } } /** * Retrieves this expression's source expression. * If this expression was prepared, this allows to retrieve the * original expression that lead to it. * Other expressions return themselves. * @return the source expression */ public final Expression getSource() { return source; } /** * Prepares a sub-expression for interpretation. * @param interpreter a JEXL interpreter * @return a prepared expression * @throws JexlException (only for nested & composite) */ protected Expression prepare(Interpreter interpreter) { return this; } /** * Intreprets a sub-expression. * @param interpreter a JEXL interpreter * @return the result of interpretation * @throws JexlException (only for nested & composite) */ protected abstract Object evaluate(Interpreter interpreter); } /** A constant expression. */ private class ConstantExpression extends Expression { /** The constant held by this expression. */ private final Object value; /** * Creates a constant expression. *

* If the wrapped constant is a string, it is treated * as a JEXL strings with respect to escaping. *

* @param val the constant value * @param source the source expression if any */ ConstantExpression(Object val, Expression source) { super(source); if (val == null) { throw new NullPointerException("constant can not be null"); } if (val instanceof String) { val = StringParser.buildString((String) val, false); } this.value = val; } /** {@inheritDoc} */ @Override ExpressionType getType() { return ExpressionType.CONSTANT; } /** {@inheritDoc} */ @Override public StringBuilder asString(StringBuilder strb) { if (value != null) { strb.append(value.toString()); } return strb; } /** {@inheritDoc} */ @Override protected Object evaluate(Interpreter interpreter) { return value; } } /** The base for Jexl based expressions. */ private abstract class JexlBasedExpression extends Expression { /** The JEXL string for this expression. */ protected final CharSequence expr; /** The JEXL node for this expression. */ protected final JexlNode node; /** * Creates a JEXL interpretable expression. * @param theExpr the expression as a string * @param theNode the expression as an AST * @param theSource the source expression if any */ protected JexlBasedExpression(CharSequence theExpr, JexlNode theNode, Expression theSource) { super(theSource); this.expr = theExpr; this.node = theNode; } /** {@inheritDoc} */ @Override public StringBuilder asString(StringBuilder strb) { strb.append(isImmediate() ? IMM_CHAR : DEF_CHAR); strb.append("{"); strb.append(expr); strb.append("}"); return strb; } /** {@inheritDoc} */ @Override protected Object evaluate(Interpreter interpreter) { return interpreter.interpret(node); } /** {@inheritDoc} */ @Override public Set> getVariables() { Set> refs = new LinkedHashSet>(); getVariables(refs); return refs; } /** {@inheritDoc} */ @Override protected void getVariables(Set> refs) { jexl.getVariables(node, refs, null); } } /** An immediate expression: ${jexl}. */ private class ImmediateExpression extends JexlBasedExpression { /** * Creates an immediate expression. * @param expr the expression as a string * @param node the expression as an AST * @param source the source expression if any */ ImmediateExpression(CharSequence expr, JexlNode node, Expression source) { super(expr, node, source); } /** {@inheritDoc} */ @Override ExpressionType getType() { return ExpressionType.IMMEDIATE; } /** {@inheritDoc} */ @Override protected Expression prepare(Interpreter interpreter) { // evaluate immediate as constant Object value = evaluate(interpreter); return value != null ? new ConstantExpression(value, source) : null; } } /** A deferred expression: #{jexl}. */ private class DeferredExpression extends JexlBasedExpression { /** * Creates a deferred expression. * @param expr the expression as a string * @param node the expression as an AST * @param source the source expression if any */ DeferredExpression(CharSequence expr, JexlNode node, Expression source) { super(expr, node, source); } /** {@inheritDoc} */ @Override public boolean isImmediate() { return false; } /** {@inheritDoc} */ @Override ExpressionType getType() { return ExpressionType.DEFERRED; } /** {@inheritDoc} */ @Override protected Expression prepare(Interpreter interpreter) { return new ImmediateExpression(expr, node, source); } /** {@inheritDoc} */ @Override protected void getVariables(Set> refs) { // noop } } /** * An immediate expression nested into a deferred expression. * #{...${jexl}...} * Note that the deferred syntax is JEXL's, not UnifiedJEXL. */ private class NestedExpression extends JexlBasedExpression { /** * Creates a nested expression. * @param expr the expression as a string * @param node the expression as an AST * @param source the source expression if any */ NestedExpression(CharSequence expr, JexlNode node, Expression source) { super(expr, node, source); if (this.source != this) { throw new IllegalArgumentException("Nested expression can not have a source"); } } @Override public StringBuilder asString(StringBuilder strb) { strb.append(expr); return strb; } /** {@inheritDoc} */ @Override public boolean isImmediate() { return false; } /** {@inheritDoc} */ @Override ExpressionType getType() { return ExpressionType.NESTED; } /** {@inheritDoc} */ @Override protected Expression prepare(Interpreter interpreter) { String value = interpreter.interpret(node).toString(); JexlNode dnode = jexl.parse(value, jexl.isDebug() ? node.debugInfo() : null, null); return new ImmediateExpression(value, dnode, this); } /** {@inheritDoc} */ @Override protected Object evaluate(Interpreter interpreter) { return prepare(interpreter).evaluate(interpreter); } } /** A composite expression: "... ${...} ... #{...} ...". */ private class CompositeExpression extends Expression { /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */ private final int meta; /** The list of sub-expression resulting from parsing. */ protected final Expression[] exprs; /** * Creates a composite expression. * @param counters counters of expression per type * @param list the sub-expressions * @param src the source for this expresion if any */ CompositeExpression(int[] counters, ArrayList list, Expression src) { super(src); this.exprs = list.toArray(new Expression[list.size()]); this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0) | (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0); } /** {@inheritDoc} */ @Override public boolean isImmediate() { // immediate if no deferred return (meta & 2) == 0; } /** {@inheritDoc} */ @Override ExpressionType getType() { return ExpressionType.COMPOSITE; } /** {@inheritDoc} */ @Override public StringBuilder asString(StringBuilder strb) { for (Expression e : exprs) { e.asString(strb); } return strb; } /** {@inheritDoc} */ @Override public Set> getVariables() { Set> refs = new LinkedHashSet>(); for (Expression expr : exprs) { expr.getVariables(refs); } return refs; } /** {@inheritDoc} */ @Override protected Expression prepare(Interpreter interpreter) { // if this composite is not its own source, it is already prepared if (source != this) { return this; } // we need to prepare all sub-expressions final int size = exprs.length; final ExpressionBuilder builder = new ExpressionBuilder(size); // tracking whether prepare will return a different expression boolean eq = true; for (int e = 0; e < size; ++e) { Expression expr = exprs[e]; Expression prepared = expr.prepare(interpreter); // add it if not null if (prepared != null) { builder.add(prepared); } // keep track of expression equivalence eq &= expr == prepared; } Expression ready = eq ? this : builder.build(UnifiedJEXL.this, this); return ready; } /** {@inheritDoc} */ @Override protected Object evaluate(Interpreter interpreter) { final int size = exprs.length; Object value = null; // common case: evaluate all expressions & concatenate them as a string StringBuilder strb = new StringBuilder(); for (int e = 0; e < size; ++e) { value = exprs[e].evaluate(interpreter); if (value != null) { strb.append(value.toString()); } } value = strb.toString(); return value; } } /** Creates a a {@link UnifiedJEXL.Expression} from an expression string. * Uses & fills up the expression cache if any. *

* If the underlying JEXL engine is silent, errors will be logged through its logger as warnings. *

* @param expression the UnifiedJEXL string expression * @return the UnifiedJEXL object expression, null if silent and an error occured * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent */ public Expression parse(String expression) { Exception xuel = null; Expression stmt = null; try { if (cache == null) { stmt = parseExpression(expression, null); } else { synchronized (cache) { stmt = cache.get(expression); if (stmt == null) { stmt = parseExpression(expression, null); cache.put(expression, stmt); } } } } catch (JexlException xjexl) { xuel = new Exception("failed to parse '" + expression + "'", xjexl); } catch (Exception xany) { xuel = xany; } finally { if (xuel != null) { if (jexl.isSilent()) { jexl.logger.warn(xuel.getMessage(), xuel.getCause()); return null; } throw xuel; } } return stmt; } /** * Creates a UnifiedJEXL.Exception from a JexlException. * @param action parse, prepare, evaluate * @param expr the expression * @param xany the exception * @return an exception containing an explicit error message */ private Exception createException(String action, Expression expr, java.lang.Exception xany) { StringBuilder strb = new StringBuilder("failed to "); strb.append(action); if (expr != null) { strb.append(" '"); strb.append(expr.toString()); strb.append("'"); } Throwable cause = xany.getCause(); if (cause != null) { String causeMsg = cause.getMessage(); if (causeMsg != null) { strb.append(", "); strb.append(causeMsg); } } return new Exception(strb.toString(), xany); } /** The different parsing states. */ private static enum ParseState { /** Parsing a constant. */ CONST, /** Parsing after $ .*/ IMMEDIATE0, /** Parsing after # .*/ DEFERRED0, /** Parsing after ${ .*/ IMMEDIATE1, /** Parsing after #{ .*/ DEFERRED1, /** Parsing after \ .*/ ESCAPE } /** * Parses a unified expression. * @param expr the string expression * @param scope the expression scope * @return the expression instance * @throws JexlException if an error occur during parsing */ private Expression parseExpression(String expr, JexlEngine.Scope scope) { final int size = expr.length(); ExpressionBuilder builder = new ExpressionBuilder(0); StringBuilder strb = new StringBuilder(size); ParseState state = ParseState.CONST; int inner = 0; boolean nested = false; int inested = -1; for (int i = 0; i < size; ++i) { char c = expr.charAt(i); switch (state) { default: // in case we ever add new expression type throw new UnsupportedOperationException("unexpected expression type"); case CONST: if (c == IMM_CHAR) { state = ParseState.IMMEDIATE0; } else if (c == DEF_CHAR) { inested = i; state = ParseState.DEFERRED0; } else if (c == '\\') { state = ParseState.ESCAPE; } else { // do buildup expr strb.append(c); } break; case IMMEDIATE0: // $ if (c == '{') { state = ParseState.IMMEDIATE1; // if chars in buffer, create constant if (strb.length() > 0) { Expression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); strb.delete(0, Integer.MAX_VALUE); } } else { // revert to CONST strb.append(IMM_CHAR); strb.append(c); state = ParseState.CONST; } break; case DEFERRED0: // # if (c == '{') { state = ParseState.DEFERRED1; // if chars in buffer, create constant if (strb.length() > 0) { Expression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); strb.delete(0, Integer.MAX_VALUE); } } else { // revert to CONST strb.append(DEF_CHAR); strb.append(c); state = ParseState.CONST; } break; case IMMEDIATE1: // ${... if (c == '}') { // materialize the immediate expr Expression iexpr = new ImmediateExpression( strb.toString(), jexl.parse(strb, null, scope), null); builder.add(iexpr); strb.delete(0, Integer.MAX_VALUE); state = ParseState.CONST; } else { // do buildup expr strb.append(c); } break; case DEFERRED1: // #{... // skip inner strings (for '}') if (c == '"' || c == '\'') { strb.append(c); i = StringParser.readString(strb, expr, i + 1, c); continue; } // nested immediate in deferred; need to balance count of '{' & '}' if (c == '{') { if (expr.charAt(i - 1) == IMM_CHAR) { inner += 1; strb.deleteCharAt(strb.length() - 1); nested = true; } continue; } // closing '}' if (c == '}') { // balance nested immediate if (inner > 0) { inner -= 1; } else { // materialize the nested/deferred expr Expression dexpr = null; if (nested) { dexpr = new NestedExpression( expr.substring(inested, i + 1), jexl.parse(strb, null, scope), null); } else { dexpr = new DeferredExpression( strb.toString(), jexl.parse(strb, null, scope), null); } builder.add(dexpr); strb.delete(0, Integer.MAX_VALUE); nested = false; state = ParseState.CONST; } } else { // do buildup expr strb.append(c); } break; case ESCAPE: if (c == DEF_CHAR) { strb.append(DEF_CHAR); } else if (c == IMM_CHAR) { strb.append(IMM_CHAR); } else { strb.append('\\'); strb.append(c); } state = ParseState.CONST; } } // we should be in that state if (state != ParseState.CONST) { throw new Exception("malformed expression: " + expr, null); } // if any chars were buffered, add them as a constant if (strb.length() > 0) { Expression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); } return builder.build(this, null); } /** * The enum capturing the difference between verbatim and code source fragments. */ private static enum BlockType { /** Block is to be output "as is". */ VERBATIM, /** Block is a directive, ie a fragment of code. */ DIRECTIVE; } /** * Abstract the source fragments, verbatim or immediate typed text blocks. * @since 2.1 */ private static final class TemplateBlock { /** The type of block, verbatim or directive. */ private final BlockType type; /** The actual contexnt. */ private final String body; /** * Creates a new block. * @param theType the type * @param theBlock the content */ TemplateBlock(BlockType theType, String theBlock) { type = theType; body = theBlock; } @Override public String toString() { return body; } } /** * A Template is a script that evaluates by writing its content through a Writer. * This is a simplified replacement for Velocity that uses JEXL (instead of OGNL/VTL) as the scripting * language. *

* The source text is parsed considering each line beginning with '$$' (as default pattern) as JEXL script code * and all others as Unified JEXL expressions; those expressions will be invoked from the script during * evaluation and their output gathered through a writer. * It is thus possible to use looping or conditional construct "around" expressions generating output. *

* For instance: *

     * $$ for(var x : [1, 3, 5, 42, 169]) {
     * $$   if (x == 42) {
     * Life, the universe, and everything
     * $$   } else if (x > 42) {
     * The value $(x} is over fourty-two
     * $$   } else {
     * The value ${x} is under fourty-two
     * $$   }
     * $$ }
     * 
* Will evaluate as: *

     * The value 1 is under fourty-two
     * The value 3 is under fourty-two
     * The value 5 is under fourty-two
     * Life, the universe, and everything
     * The value 169 is over fourty-two
     * 
*

* During evaluation, the template context exposes its writer as '$jexl' which is safe to use in this case. * This allows writing directly through the writer without adding new-lines as in: *

     * $$ for(var cell : cells) { $jexl.print(cell); $jexl.print(';') }
     * 
*

*

* A template is expanded as one JEXL script and a list of UnifiedJEXL expressions; each UnifiedJEXL expression * being replace in the script by a call to jexl:print(expr) (the expr is in fact the expr number in the template). * This integration uses a specialized JexlContext (TemplateContext) that serves as a namespace (for jexl:) * and stores the expression array and the writer (java.io.Writer) that the 'jexl:print(...)' * delegates the output generation to. *

* @since 2.1 */ public final class Template { /** The prefix marker. */ private final String prefix; /** The array of source blocks. */ private final TemplateBlock[] source; /** The resulting script. */ private final ASTJexlScript script; /** The UnifiedJEXL expressions called by the script. */ private final Expression[] exprs; /** * Creates a new template from an input. * @param directive the prefix for lines of code; can not be "$", "${", "#" or "#{" * since this would preclude being able to differentiate directives and UnifiedJEXL expressions * @param reader the input reader * @param parms the parameter names * @throws NullPointerException if either the directive prefix or input is null * @throws IllegalArgumentException if the directive prefix is invalid */ public Template(String directive, Reader reader, String... parms) { if (directive == null) { throw new NullPointerException("null prefix"); } if ("$".equals(directive) || "${".equals(directive) || "#".equals(directive) || "#{".equals(directive)) { throw new IllegalArgumentException(directive + ": is not a valid directive pattern"); } if (reader == null) { throw new NullPointerException("null input"); } JexlEngine.Scope scope = new JexlEngine.Scope(parms); prefix = directive; List blocks = readTemplate(prefix, reader); List uexprs = new ArrayList(); StringBuilder strb = new StringBuilder(); int nuexpr = 0; int codeStart = -1; for (int b = 0; b < blocks.size(); ++b) { TemplateBlock block = blocks.get(b); if (block.type == BlockType.VERBATIM) { strb.append("jexl:print("); strb.append(nuexpr++); strb.append(");"); } else { // keep track of first block of code, the frame creator if (codeStart < 0) { codeStart = b; } strb.append(block.body); } } // parse the script script = getEngine().parse(strb.toString(), null, scope); scope = script.getScope(); // parse the exprs using the code frame for those appearing after the first block of code for (int b = 0; b < blocks.size(); ++b) { TemplateBlock block = blocks.get(b); if (block.type == BlockType.VERBATIM) { uexprs.add(UnifiedJEXL.this.parseExpression(block.body, b > codeStart ? scope : null)); } } source = blocks.toArray(new TemplateBlock[blocks.size()]); exprs = uexprs.toArray(new Expression[uexprs.size()]); } /** * Private ctor used to expand deferred expressions during prepare. * @param thePrefix the directive prefix * @param theSource the source * @param theScript the script * @param theExprs the expressions */ private Template(String thePrefix, TemplateBlock[] theSource, ASTJexlScript theScript, Expression[] theExprs) { prefix = thePrefix; source = theSource; script = theScript; exprs = theExprs; } @Override public String toString() { StringBuilder strb = new StringBuilder(); for (TemplateBlock block : source) { if (block.type == BlockType.DIRECTIVE) { strb.append(prefix); } strb.append(block.toString()); strb.append('\n'); } return strb.toString(); } /** * Recreate the template source from its inner components. * @return the template source rewritten */ public String asString() { StringBuilder strb = new StringBuilder(); int e = 0; for (int b = 0; b < source.length; ++b) { TemplateBlock block = source[b]; if (block.type == BlockType.DIRECTIVE) { strb.append(prefix); } else { exprs[e++].asString(strb); } } return strb.toString(); } /** * Prepares this template by expanding any contained deferred expression. * @param context the context to prepare against * @return the prepared version of the template */ public Template prepare(JexlContext context) { JexlEngine.Frame frame = script.createFrame((Object[]) null); TemplateContext tcontext = new TemplateContext(context, frame, exprs, null); Expression[] immediates = new Expression[exprs.length]; for (int e = 0; e < exprs.length; ++e) { immediates[e] = exprs[e].prepare(tcontext); } return new Template(prefix, source, script, immediates); } /** * Evaluates this template. * @param context the context to use during evaluation * @param writer the writer to use for output */ public void evaluate(JexlContext context, Writer writer) { evaluate(context, writer, (Object[]) null); } /** * Evaluates this template. * @param context the context to use during evaluation * @param writer the writer to use for output * @param args the arguments */ public void evaluate(JexlContext context, Writer writer, Object... args) { JexlEngine.Frame frame = script.createFrame(args); TemplateContext tcontext = new TemplateContext(context, frame, exprs, writer); Interpreter interpreter = jexl.createInterpreter(tcontext, !jexl.isLenient(), false); interpreter.setFrame(frame); interpreter.interpret(script); } } /** * The type of context to use during evaluation of templates. *

This context exposes its writer as '$jexl' to the scripts.

*

public for introspection purpose.

* @since 2.1 */ public final class TemplateContext implements JexlContext, NamespaceResolver { /** The wrapped context. */ private final JexlContext wrap; /** The array of UnifiedJEXL expressions. */ private final Expression[] exprs; /** The writer used to output. */ private final Writer writer; /** The call frame. */ private final JexlEngine.Frame frame; /** * Creates a template context instance. * @param jcontext the base context * @param jframe the calling frame * @param expressions the list of expression from the template to evaluate * @param out the output writer */ protected TemplateContext(JexlContext jcontext, JexlEngine.Frame jframe, Expression[] expressions, Writer out) { wrap = jcontext; frame = jframe; exprs = expressions; writer = out; } /** * Gets this context calling frame. * @return the engine frame */ public JexlEngine.Frame getFrame() { return frame; } /** {@inheritDoc} */ public Object get(String name) { if ("$jexl".equals(name)) { return writer; } else { return wrap.get(name); } } /** {@inheritDoc} */ public void set(String name, Object value) { wrap.set(name, value); } /** {@inheritDoc} */ public boolean has(String name) { return wrap.has(name); } /** {@inheritDoc} */ public Object resolveNamespace(String ns) { if ("jexl".equals(ns)) { return this; } else if (wrap instanceof NamespaceResolver) { return ((NamespaceResolver) wrap).resolveNamespace(ns); } else { return null; } } /** * Includes a call to another template. *

Evaluates a template using this template initial context and writer.

* @param template the template to evaluate * @param args the arguments */ public void include(Template template, Object... args) { template.evaluate(wrap, writer, args); } /** * Prints an expression result. * @param e the expression number */ public void print(int e) { if (e < 0 || e >= exprs.length) { return; } Expression expr = exprs[e]; if (expr.isDeferred()) { expr = expr.prepare(wrap); } if (expr instanceof CompositeExpression) { printComposite((CompositeExpression) expr); } else { doPrint(expr.evaluate(this)); } } /** * Prints a composite expression. * @param composite the composite expression */ protected void printComposite(CompositeExpression composite) { Expression[] cexprs = composite.exprs; final int size = cexprs.length; Object value = null; for (int e = 0; e < size; ++e) { value = cexprs[e].evaluate(this); doPrint(value); } } /** * Prints to output. *

This will dynamically try to find the best suitable method in the writer through uberspection. * Subclassing Writer by adding 'print' methods should be the preferred way to specialize output. *

* @param arg the argument to print out */ private void doPrint(Object arg) { try { if (arg instanceof CharSequence) { writer.write(arg.toString()); } else if (arg != null) { Object[] value = {arg}; Uberspect uber = getEngine().getUberspect(); JexlMethod method = uber.getMethod(writer, "print", value, null); if (method != null) { method.invoke(writer, value); } else { writer.write(arg.toString()); } } } catch (java.io.IOException xio) { throw createException("call print", null, xio); } catch (java.lang.Exception xany) { throw createException("invoke print", null, xany); } } } /** * Whether a sequence starts with a given set of characters (following spaces). *

Space characters at beginning of line before the pattern are discarded.

* @param sequence the sequence * @param pattern the pattern to match at start of sequence * @return the first position after end of pattern if it matches, -1 otherwise * @since 2.1 */ protected int startsWith(CharSequence sequence, CharSequence pattern) { int s = 0; while (Character.isSpaceChar(sequence.charAt(s))) { s += 1; } sequence = sequence.subSequence(s, sequence.length()); if (pattern.length() <= sequence.length() && sequence.subSequence(0, pattern.length()).equals(pattern)) { return s + pattern.length(); } else { return -1; } } /** * Reads lines of a template grouping them by typed blocks. * @param prefix the directive prefix * @param source the source reader * @return the list of blocks * @since 2.1 */ protected List readTemplate(final String prefix, Reader source) { try { int prefixLen = prefix.length(); List blocks = new ArrayList(); BufferedReader reader; if (source instanceof BufferedReader) { reader = (BufferedReader) source; } else { reader = new BufferedReader(source); } StringBuilder strb = new StringBuilder(); BlockType type = null; while (true) { CharSequence line = reader.readLine(); if (line == null) { // at end TemplateBlock block = new TemplateBlock(type, strb.toString()); blocks.add(block); break; } else if (type == null) { // determine starting type if not known yet prefixLen = startsWith(line, prefix); if (prefixLen >= 0) { type = BlockType.DIRECTIVE; strb.append(line.subSequence(prefixLen, line.length())); } else { type = BlockType.VERBATIM; strb.append(line.subSequence(0, line.length())); strb.append('\n'); } } else if (type == BlockType.DIRECTIVE) { // switch to verbatim if necessary prefixLen = startsWith(line, prefix); if (prefixLen < 0) { TemplateBlock code = new TemplateBlock(BlockType.DIRECTIVE, strb.toString()); strb.delete(0, Integer.MAX_VALUE); blocks.add(code); type = BlockType.VERBATIM; strb.append(line.subSequence(0, line.length())); } else { strb.append(line.subSequence(prefixLen, line.length())); } } else if (type == BlockType.VERBATIM) { // switch to code if necessary( prefixLen = startsWith(line, prefix); if (prefixLen >= 0) { strb.append('\n'); TemplateBlock verbatim = new TemplateBlock(BlockType.VERBATIM, strb.toString()); strb.delete(0, Integer.MAX_VALUE); blocks.add(verbatim); type = BlockType.DIRECTIVE; strb.append(line.subSequence(prefixLen, line.length())); } else { strb.append(line.subSequence(0, line.length())); } } } return blocks; } catch (IOException xio) { return null; } } /** * Creates a new template. * @param prefix the directive prefix * @param source the source * @param parms the parameter names * @return the template * @since 2.1 */ public Template createTemplate(String prefix, Reader source, String... parms) { return new Template(prefix, source, parms); } /** * Creates a new template. * @param source the source * @param parms the parameter names * @return the template * @since 2.1 */ public Template createTemplate(String source, String... parms) { return new Template("$$", new StringReader(source), parms); } /** * Creates a new template. * @param source the source * @return the template * @since 2.1 */ public Template createTemplate(String source) { return new Template("$$", new StringReader(source), (String[]) null); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy