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

org.apache.commons.jexl3.internal.TemplateEngine Maven / Gradle / Ivy

Go to download

The Apache Commons JEXL library is an implementation of the JSTL Expression Language with extensions.

The newest version!
/*
 * 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.jexl3.internal;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.jexl3.JexlCache;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlInfo;
import org.apache.commons.jexl3.JexlOptions;
import org.apache.commons.jexl3.JxltEngine;
import org.apache.commons.jexl3.parser.ASTJexlScript;
import org.apache.commons.jexl3.parser.JexlNode;
import org.apache.commons.jexl3.parser.StringParser;
import org.apache.commons.logging.Log;

/**
 * A JxltEngine implementation.
 * @since 3.0
 */
public final class TemplateEngine extends JxltEngine {
    /**
     * Abstract the source fragments, verbatim or immediate typed text blocks.
     */
    static final class Block {
        /** The type of block, verbatim or directive. */
        private final BlockType type;
        /** The block start line info. */
        private final int line;
        /** The actual content. */
        private final String body;

        /**
         * Creates a new block.
         * @param theType  the block type
         * @param theLine  the line number
         * @param theBlock the content
         */
        Block(final BlockType theType, final int theLine, final String theBlock) {
            type = theType;
            line = theLine;
            body = theBlock;
        }

        /**
         * @return body
         */
        String getBody() {
            return body;
        }

        /**
         * @return line
         */
        int getLine() {
            return line;
        }

        /**
         * @return type
         */
        BlockType getType() {
            return type;
        }

        /**
         * Appends this block string representation to a builder.
         * @param strb   the string builder to append to
         * @param prefix the line prefix (immediate or deferred)
         */
        protected void toString(final StringBuilder strb, final String prefix) {
            if (BlockType.VERBATIM.equals(type)) {
                strb.append(body);
            } else {
                final Iterator lines = readLines(new StringReader(body));
                while (lines.hasNext()) {
                    strb.append(prefix).append(lines.next());
                }
            }
        }
    }
    /**
     * The enum capturing the difference between verbatim and code source fragments.
     */
    enum BlockType {
        /** Block is to be output "as is" but may be a unified expression. */
        VERBATIM,
        /** Block is a directive, ie a fragment of JEXL code. */
        DIRECTIVE
    }
    /** A composite unified expression: "... ${...} ... #{...} ...". */
    final class CompositeExpression extends TemplateExpression {
        /** 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 TemplateExpression[] exprs;

        /**
         * Creates a composite expression.
         * @param counters counters of expressions per type
         * @param list     the sub-expressions
         * @param src      the source for this expression if any
         */
        CompositeExpression(final int[] counters, final List list, final TemplateExpression src) {
            super(src);
            this.exprs = list.toArray(new TemplateExpression[0]);
            this.meta = (counters[ExpressionType.DEFERRED.getIndex()] > 0 ? 2 : 0)
                    | (counters[ExpressionType.IMMEDIATE.getIndex()] > 0 ? 1 : 0);
        }

        @Override
        public StringBuilder asString(final StringBuilder strb) {
            for (final TemplateExpression e : exprs) {
                e.asString(strb);
            }
            return strb;
        }

        @Override
        protected Object evaluate(final Interpreter interpreter) {
            Object value;
            // common case: evaluate all expressions & concatenate them as a string
            final StringBuilder strb = new StringBuilder();
            for (final TemplateExpression expr : exprs) {
                value = expr.evaluate(interpreter);
                if (value != null) {
                    strb.append(value.toString());
                }
            }
            return strb.toString();
        }

        @Override
        ExpressionType getType() {
            return ExpressionType.COMPOSITE;
        }

        @Override
        public Set> getVariables() {
            final Engine.VarCollector collector = jexl.varCollector();
            for (final TemplateExpression expr : exprs) {
                expr.getVariables(collector);
            }
            return collector.collected();
        }

        /**
         * Fills up the list of variables accessed by this unified expression.
         * @param collector the variable collector
         */
        @Override
        protected void getVariables(final Engine.VarCollector collector) {
            for (final TemplateExpression expr : exprs) {
                expr.getVariables(collector);
            }
        }

        @Override
        public boolean isImmediate() {
            // immediate if no deferred
            return (meta & 2) == 0;
        }

        @Override
        protected TemplateExpression prepare(final 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 (final TemplateExpression expr : exprs) {
                final TemplateExpression prepared = expr.prepare(interpreter);
                // add it if not null
                if (prepared != null) {
                    builder.add(prepared);
                }
                // keep track of TemplateExpression equivalence
                eq &= expr == prepared;
            }
            return eq ? this : builder.build(TemplateEngine.this, this);
        }
    }
    /** A constant unified expression. */
    final class ConstantExpression extends TemplateExpression {
        /** The constant held by this unified expression. */
        private final Object value;

        /**
         * Creates a constant unified 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 TemplateExpression if any */ ConstantExpression(final Object val, final TemplateExpression source) { super(source); Objects.requireNonNull(val, "val"); this.value = val instanceof String ? StringParser.buildTemplate((String) val, false) : val; } @Override public StringBuilder asString(final StringBuilder strb) { if (value != null) { strb.append(value.toString()); } return strb; } @Override protected Object evaluate(final Interpreter interpreter) { return value; } @Override ExpressionType getType() { return ExpressionType.CONSTANT; } } /** A deferred unified expression: #{jexl}. */ final class DeferredExpression extends JexlBasedExpression { /** * Creates a deferred unified expression. * @param expr the unified expression as a string * @param node the unified expression as an AST * @param source the source unified expression if any */ DeferredExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) { super(expr, node, source); } @Override ExpressionType getType() { return ExpressionType.DEFERRED; } @Override protected void getVariables(final Engine.VarCollector collector) { // noop } @Override public boolean isImmediate() { return false; } @Override protected TemplateExpression prepare(final Interpreter interpreter) { return new ImmediateExpression(expr, node, source); } } /** * A helper class to build expressions. * Keeps count of sub-expressions by type. */ static final class ExpressionBuilder { /** Per TemplateExpression type counters. */ private final int[] counts; /** The list of expressions. */ private final List expressions; /** * Creates a builder. * @param size the initial TemplateExpression array size */ ExpressionBuilder(final int size) { counts = new int[]{0, 0, 0}; expressions = new ArrayList<>(size <= 0 ? 3 : size); } /** * Adds an TemplateExpression to the list of expressions, maintain per-type counts. * @param expr the TemplateExpression to add */ void add(final TemplateExpression expr) { counts[expr.getType().getIndex()] += 1; expressions.add(expr); } /** * Builds an TemplateExpression from a source, performs checks. * @param el the unified el instance * @param source the source TemplateExpression * @return an TemplateExpression */ TemplateExpression build(final TemplateEngine el, final TemplateExpression source) { int sum = 0; for (final int count : counts) { sum += count; } if (expressions.size() != sum) { final StringBuilder error = new StringBuilder("parsing algorithm error: "); throw new IllegalStateException(toString(error).toString()); } // if only one sub-expr, no need to create a composite if (expressions.size() == 1) { return expressions.get(0); } return el.new CompositeExpression(counts, expressions, source); } @Override public String toString() { return toString(new StringBuilder()).toString(); } /** * Base for to-string. * @param error the builder to fill * @return the builder */ StringBuilder toString(final StringBuilder error) { error.append("exprs{"); error.append(expressions.size()); error.append(", constant:"); error.append(counts[ExpressionType.CONSTANT.getIndex()]); error.append(", immediate:"); error.append(counts[ExpressionType.IMMEDIATE.getIndex()]); error.append(", deferred:"); error.append(counts[ExpressionType.DEFERRED.getIndex()]); error.append("}"); return error; } } /** * Types of expressions. * Each instance carries a counter index per (composite sub-) template expression type. * @see ExpressionBuilder */ enum ExpressionType { /** Constant TemplateExpression, count index 0. */ CONSTANT(0), /** Immediate TemplateExpression, count index 1. */ IMMEDIATE(1), /** Deferred TemplateExpression, 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 TemplateExpression counters for composite expressions. */ private final int index; /** * Creates an ExpressionType. * @param idx the index for this type in counters arrays. */ ExpressionType(final int idx) { this.index = idx; } /** * @return the index member */ int getIndex() { return index; } } /** An immediate unified expression: ${jexl}. */ final class ImmediateExpression extends JexlBasedExpression { /** * Creates an immediate unified expression. * @param expr the unified expression as a string * @param node the unified expression as an AST * @param source the source unified expression if any */ ImmediateExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) { super(expr, node, source); } @Override ExpressionType getType() { return ExpressionType.IMMEDIATE; } @Override protected TemplateExpression prepare(final Interpreter interpreter) { // evaluate immediate as constant final Object value = evaluate(interpreter); return value != null ? new ConstantExpression(value, source) : null; } } /** The base for JEXL based unified expressions. */ abstract class JexlBasedExpression extends TemplateExpression { /** The JEXL string for this unified expression. */ protected final CharSequence expr; /** The JEXL node for this unified expression. */ protected final JexlNode node; /** * Creates a JEXL interpretable unified expression. * @param theExpr the unified expression as a string * @param theNode the unified expression as an AST * @param theSource the source unified expression if any */ protected JexlBasedExpression(final CharSequence theExpr, final JexlNode theNode, final TemplateExpression theSource) { super(theSource); this.expr = theExpr; this.node = theNode; } @Override public StringBuilder asString(final StringBuilder strb) { strb.append(isImmediate() ? immediateChar : deferredChar); strb.append("{"); strb.append(expr); strb.append("}"); return strb; } @Override protected Object evaluate(final Interpreter interpreter) { return interpreter.interpret(node); } @Override JexlInfo getInfo() { return node.jexlInfo(); } @Override public Set> getVariables() { final Engine.VarCollector collector = jexl.varCollector(); getVariables(collector); return collector.collected(); } @Override protected void getVariables(final Engine.VarCollector collector) { jexl.getVariables(node instanceof ASTJexlScript? (ASTJexlScript) node : null, node, collector); } @Override protected JexlOptions options(final JexlContext context) { return jexl.evalOptions(node instanceof ASTJexlScript? (ASTJexlScript) node : null, context); } } /** * An immediate unified expression nested into a deferred unified expression. * #{...${jexl}...} * Note that the deferred syntax is JEXL's. */ final class NestedExpression extends JexlBasedExpression { /** * Creates a nested unified expression. * @param expr the unified expression as a string * @param node the unified expression as an AST * @param source the source unified expression if any */ NestedExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) { super(expr, node, source); if (this.source != this) { throw new IllegalArgumentException("Nested TemplateExpression cannot have a source"); } } @Override public StringBuilder asString(final StringBuilder strb) { strb.append(expr); return strb; } @Override protected Object evaluate(final Interpreter interpreter) { return prepare(interpreter).evaluate(interpreter); } @Override ExpressionType getType() { return ExpressionType.NESTED; } @Override public boolean isImmediate() { return false; } @Override protected TemplateExpression prepare(final Interpreter interpreter) { final String value = interpreter.interpret(node).toString(); final JexlNode dnode = jexl.parse(node.jexlInfo(), noscript, value, null); return new ImmediateExpression(value, dnode, this); } } /** The different parsing states. */ private enum ParseState { /** Parsing a constant. */ CONST, /** Parsing after $ . */ IMMEDIATE0, /** Parsing after # . */ DEFERRED0, /** Parsing after ${ . */ IMMEDIATE1, /** Parsing after #{ . */ DEFERRED1, /** Parsing after \ . */ ESCAPE } /** * The abstract base class for all unified expressions, immediate '${...}' and deferred '#{...}'. */ abstract class TemplateExpression implements Expression { /** The source of this template expression(see {@link TemplateEngine.TemplateExpression#prepare}). */ protected final TemplateExpression source; /** * Creates an TemplateExpression. * @param src the source TemplateExpression if any */ TemplateExpression(final TemplateExpression src) { this.source = src != null ? src : this; } @Override public String asString() { final StringBuilder strb = new StringBuilder(); asString(strb); return strb.toString(); } /** * Interprets a sub-expression. * @param interpreter a JEXL interpreter * @return the result of interpretation * @throws JexlException (only for nested and composite) */ protected abstract Object evaluate(Interpreter interpreter); @Override public final Object evaluate(final JexlContext context) { return evaluate(context, null, null); } /** * Evaluates this expression. * * @param frame the frame storing parameters and local variables * @param context the context storing global variables * @param options flags and properties that can alter the evaluation behavior. * @return the expression value * @throws JexlException */ protected final Object evaluate(final JexlContext context, final Frame frame, final JexlOptions options) { try { final TemplateInterpreter.Arguments args = new TemplateInterpreter.Arguments(jexl).context(context) .options(options != null ? options : options(context)).frame(frame); final Interpreter interpreter = jexl.createTemplateInterpreter(args); return evaluate(interpreter); } catch (final JexlException xjexl) { final JexlException xuel = createException(xjexl.getInfo(), "evaluate", this, xjexl); if (jexl.isSilent()) { if (logger.isWarnEnabled()) { logger.warn(xuel.getMessage(), xuel.getCause()); } return null; } throw xuel; } } /** @return the info */ JexlInfo getInfo() { return null; } @Override public final TemplateExpression getSource() { return source; } /** * Gets this TemplateExpression type. * @return its type */ abstract ExpressionType getType(); @Override public Set> getVariables() { return Collections.emptySet(); } /** * Fills up the list of variables accessed by this unified expression. * @param collector the variable collector */ protected void getVariables(final Engine.VarCollector collector) { // nothing to do } @Override public final boolean isDeferred() { return !isImmediate(); } @Override public boolean isImmediate() { return true; } /** * The options to use during evaluation. * @param context the context * @return the options */ protected JexlOptions options(final JexlContext context) { return jexl.evalOptions(null, context); } /** * Prepares a sub-expression for interpretation. * @param interpreter a JEXL interpreter * @return a prepared unified expression * @throws JexlException (only for nested and composite) */ protected TemplateExpression prepare(final Interpreter interpreter) { return this; } @Override public final TemplateExpression prepare(final JexlContext context) { return prepare(context, null, null); } /** * Prepares this expression. * * @param frame the frame storing parameters and local variables * @param context the context storing global variables * @param opts flags and properties that can alter the evaluation behavior. * @return the expression value * @throws JexlException */ protected final TemplateExpression prepare(final JexlContext context, final Frame frame, final JexlOptions opts) { try { final JexlOptions interOptions = opts != null ? opts : jexl.evalOptions(context); final Interpreter interpreter = jexl.createInterpreter(context, frame, interOptions); return prepare(interpreter); } catch (final JexlException xjexl) { final JexlException xuel = createException(xjexl.getInfo(), "prepare", this, xjexl); if (jexl.isSilent()) { if (logger.isWarnEnabled()) { logger.warn(xuel.getMessage(), xuel.getCause()); } return null; } throw xuel; } } @Override public final String toString() { final StringBuilder strb = new StringBuilder(); asString(strb); if (source != this) { strb.append(" /*= "); strb.append(source.toString()); strb.append(" */"); } return strb.toString(); } } /** * Helper for expression dealing with embedded strings. * @param strb the expression buffer to copy characters into * @param expr the source * @param position the offset into the source * @param c the separator character * @return the new position to read the source from */ private static int append(final StringBuilder strb, final CharSequence expr, final int position, final char c) { strb.append(c); if (c != '"' && c != '\'') { return position; } // read thru strings final int end = expr.length(); boolean escape= false; int index = position + 1; for (; index < end; ++index) { final char ec = expr.charAt(index); strb.append(ec); if (ec == '\\') { escape = !escape; } else if (escape) { escape = false; } else if (ec == c) { break; } } return index; } /** * Creates a JxltEngine.Exception from a JexlException. * @param info the source info * @param action createExpression, prepare, evaluate * @param expr the template expression * @param xany the exception * @return an exception containing an explicit error message */ static Exception createException(final JexlInfo info, final String action, final TemplateExpression expr, final java.lang.Exception xany) { final StringBuilder strb = new StringBuilder("failed to "); strb.append(action); if (expr != null) { strb.append(" '"); strb.append(expr.toString()); strb.append("'"); } final Throwable cause = xany.getCause(); if (cause != null) { final String causeMsg = cause.getMessage(); if (causeMsg != null) { strb.append(", "); strb.append(causeMsg); } } return new Exception(info, strb.toString(), xany); } /** * Read lines from a (buffered / mark-able) reader keeping all new-lines and line-feeds. * @param reader the reader * @return the line iterator */ protected static Iterator readLines(final Reader reader) { if (!reader.markSupported()) { throw new IllegalArgumentException("mark support in reader required"); } return new Iterator() { private CharSequence next = doNext(); private CharSequence doNext() { final StringBuilder strb = new StringBuilder(64); // CSOFF: MagicNumber int c; boolean eol = false; try { while ((c = reader.read()) >= 0) { if (eol) {// && (c != '\n' && c != '\r')) { reader.reset(); break; } if (c == '\n') { eol = true; } strb.append((char) c); reader.mark(1); } } catch (final IOException xio) { return null; } return strb.length() > 0 ? strb : null; } @Override public boolean hasNext() { return next != null; } @Override public CharSequence next() { final CharSequence current = next; if (current != null) { next = doNext(); } return current; } }; } /** The TemplateExpression cache. */ final JexlCache cache; /** The JEXL engine instance. */ final Engine jexl; /** The logger. */ final Log logger; /** The first character for immediate expressions. */ final char immediateChar; /** The first character for deferred expressions. */ final char deferredChar; /** Whether expressions can use JEXL script or only expressions (ie, no for, var, etc). */ final boolean noscript; /** * Creates a new instance of {@link JxltEngine} creating a local cache. * @param jexl the JexlEngine to use. * @param noScript whether this engine only allows JEXL expressions or scripts * @param cacheSize the number of expressions in this cache, default is 256 * @param immediate the immediate template expression character, default is '$' * @param deferred the deferred template expression character, default is '#' */ public TemplateEngine(final Engine jexl, final boolean noScript, final int cacheSize, final char immediate, final char deferred) { this.jexl = jexl; this.logger = jexl.logger; this.cache = (JexlCache) jexl.cacheFactory.apply(cacheSize); immediateChar = immediate; deferredChar = deferred; noscript = noScript; } /** * Clears the cache. */ @Override public void clearCache() { synchronized (cache) { cache.clear(); } } @Override public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, final String expression) { final JexlInfo info = jexlInfo == null ? jexl.createInfo() : jexlInfo; Exception xuel = null; TemplateExpression stmt = null; try { stmt = cache.get(expression); if (stmt == null) { stmt = parseExpression(info, expression, null); cache.put(expression, stmt); } } catch (final JexlException xjexl) { xuel = new Exception(xjexl.getInfo(), "failed to parse '" + expression + "'", xjexl); } if (xuel != null) { if (!jexl.isSilent()) { throw xuel; } if (logger.isWarnEnabled()) { logger.warn(xuel.getMessage(), xuel.getCause()); } stmt = null; } return stmt; } @Override public TemplateScript createTemplate(final JexlInfo info, final String prefix, final Reader source, final String... parms) { return new TemplateScript(this, info, prefix, source, parms); } /** * @return the deferred character */ char getDeferredChar() { return deferredChar; } /** * Gets the JexlEngine underlying this JxltEngine. * @return the JexlEngine */ @Override public Engine getEngine() { return jexl; } /** * @return the immediate character */ char getImmediateChar() { return immediateChar; } /** * Parses a unified expression. * @param info the source info * @param expr the string expression * @param scope the template scope * @return the unified expression instance * @throws JexlException if an error occur during parsing */ TemplateExpression parseExpression(final JexlInfo info, final String expr, final Scope scope) { // CSOFF: MethodLength final int size = expr.length(); final ExpressionBuilder builder = new ExpressionBuilder(0); final StringBuilder strb = new StringBuilder(size); ParseState state = ParseState.CONST; int immediate1 = 0; int deferred1 = 0; int inner1 = 0; boolean nested = false; int inested = -1; int lineno = info.getLine(); for (int column = 0; column < size; ++column) { final char c = expr.charAt(column); switch (state) { case CONST: if (c == immediateChar) { state = ParseState.IMMEDIATE0; } else if (c == deferredChar) { inested = column; 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) { final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); strb.delete(0, Integer.MAX_VALUE); } } else { // revert to CONST strb.append(immediateChar); state = ParseState.CONST; // 'unread' the current character column -= 1; continue; } break; case DEFERRED0: // # if (c == '{') { state = ParseState.DEFERRED1; // if chars in buffer, create constant if (strb.length() > 0) { final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); strb.delete(0, Integer.MAX_VALUE); } } else { // revert to CONST strb.append(deferredChar); state = ParseState.CONST; // 'unread' the current character column -= 1; continue; } break; case IMMEDIATE1: // ${... if (c == '}') { if (immediate1 > 0) { immediate1 -= 1; strb.append(c); } else { // materialize the immediate expr final String src = strb.toString(); final TemplateExpression iexpr = new ImmediateExpression( src, jexl.parse(info.at(lineno, column), noscript, src, scope), null); builder.add(iexpr); strb.delete(0, Integer.MAX_VALUE); state = ParseState.CONST; } } else { if (c == '{') { immediate1 += 1; } // do buildup expr column = append(strb, expr, column, c); } break; case DEFERRED1: // #{... // skip inner strings (for '}') // nested immediate in deferred; need to balance count of '{' & '}' // closing '}' switch (c) { case '"': case '\'': strb.append(c); column = StringParser.readString(strb, expr, column + 1, c); continue; case '{': if (expr.charAt(column - 1) == immediateChar) { inner1 += 1; strb.deleteCharAt(strb.length() - 1); nested = true; } else { deferred1 += 1; strb.append(c); } continue; case '}': // balance nested immediate if (deferred1 > 0) { deferred1 -= 1; strb.append(c); } else if (inner1 > 0) { inner1 -= 1; } else { // materialize the nested/deferred expr final String src = strb.toString(); TemplateExpression dexpr; if (nested) { dexpr = new NestedExpression( expr.substring(inested, column + 1), jexl.parse(info.at(lineno, column), noscript, src, scope), null); } else { dexpr = new DeferredExpression( strb.toString(), jexl.parse(info.at(lineno, column), noscript, src, scope), null); } builder.add(dexpr); strb.delete(0, Integer.MAX_VALUE); nested = false; state = ParseState.CONST; } break; default: // do buildup expr column = append(strb, expr, column, c); break; } break; case ESCAPE: if (c == deferredChar) { strb.append(deferredChar); } else if (c == immediateChar) { strb.append(immediateChar); } else { strb.append('\\'); strb.append(c); } state = ParseState.CONST; break; default: // in case we ever add new unified expression type throw new UnsupportedOperationException("unexpected unified expression type"); } if (c == '\n') { lineno += 1; } } // we should be in that state if (state != ParseState.CONST) { // otherwise, we ended a line with a \, $ or # switch (state) { case ESCAPE: strb.append('\\'); strb.append('\\'); break; case DEFERRED0: strb.append(deferredChar); break; case IMMEDIATE0: strb.append(immediateChar); break; default: throw new Exception(info.at(lineno, 0), "malformed expression: " + expr, null); } } // if any chars were buffered, add them as a constant if (strb.length() > 0) { final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null); builder.add(cexpr); } return builder.build(this, null); } /** * 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 */ protected List readTemplate(final String prefix, final Reader source) { final ArrayList blocks = new ArrayList<>(); final BufferedReader reader; if (source instanceof BufferedReader) { reader = (BufferedReader) source; } else { reader = new BufferedReader(source); } final StringBuilder strb = new StringBuilder(); BlockType type = null; int prefixLen; final Iterator lines = readLines(reader); int lineno = 1; int start = 0; while (lines.hasNext()) { final CharSequence line = lines.next(); if (line == null) { break; } 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())); } start = lineno; } else if (type == BlockType.DIRECTIVE) { // switch to verbatim if necessary prefixLen = startsWith(line, prefix); if (prefixLen < 0) { final Block directive = new Block(BlockType.DIRECTIVE, start, strb.toString()); strb.delete(0, Integer.MAX_VALUE); blocks.add(directive); type = BlockType.VERBATIM; strb.append(line.subSequence(0, line.length())); start = lineno; } else { // still a directive strb.append(line.subSequence(prefixLen, line.length())); } } else if (type == BlockType.VERBATIM) { // switch to directive if necessary prefixLen = startsWith(line, prefix); if (prefixLen >= 0) { final Block verbatim = new Block(BlockType.VERBATIM, start, strb.toString()); strb.delete(0, Integer.MAX_VALUE); blocks.add(verbatim); type = BlockType.DIRECTIVE; strb.append(line.subSequence(prefixLen, line.length())); start = lineno; } else { strb.append(line.subSequence(0, line.length())); } } lineno += 1; } // input may be null if (type != null && strb.length() > 0) { final Block block = new Block(type, start, strb.toString()); blocks.add(block); } blocks.trimToSize(); return blocks; } /** * Tests 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 */ protected int startsWith(final CharSequence sequence, final CharSequence pattern) { final int length = sequence.length(); int s = 0; while (s < length && Character.isSpaceChar(sequence.charAt(s))) { s += 1; } if (s < length && pattern.length() <= length - s) { final CharSequence subSequence = sequence.subSequence(s, length); if (subSequence.subSequence(0, pattern.length()).equals(pattern)) { return s + pattern.length(); } } return -1; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy