org.apache.commons.jexl3.internal.TemplateEngine Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of commons-jexl3 Show documentation
Show all versions of commons-jexl3 Show documentation
The Apache Commons JEXL library is an implementation of the JSTL Expression Language with extensions.
/*
* 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 org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlInfo;
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 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.Set;
/**
* A JxltEngine implementation.
* @since 3.0
*/
public final class TemplateEngine extends JxltEngine {
/** The TemplateExpression cache. */
private final SoftCache cache;
/** The JEXL engine instance. */
private final Engine jexl;
/** The first character for immediate expressions. */
private final char immediateChar;
/** The first character for deferred expressions. */
private final char deferredChar;
/** Whether expressions can use JEXL script or only expressions (ie, no for, var, etc). */
private boolean noscript = true;
/**
* Creates a new instance of {@link JxltEngine} creating a local cache.
* @param aJexl 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(Engine aJexl, boolean noScript, int cacheSize, char immediate, char deferred) {
this.jexl = aJexl;
this.cache = new SoftCache(cacheSize);
immediateChar = immediate;
deferredChar = deferred;
noscript = noScript;
}
/**
* @return the immediate character
*/
char getImmediateChar() {
return immediateChar;
}
/**
* @return the deferred character
*/
char getDeferredChar() {
return deferredChar;
}
/**
* 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(int idx) {
this.index = idx;
}
}
/**
* 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 ArrayList expressions;
/**
* Creates a builder.
* @param size the initial TemplateExpression array size
*/
private ExpressionBuilder(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
*/
private void add(TemplateExpression expr) {
counts[expr.getType().index] += 1;
expressions.add(expr);
}
@Override
public String toString() {
return toString(new StringBuilder()).toString();
}
/**
* Base for to-string.
* @param error the builder to fill
* @return the builder
*/
private StringBuilder toString(StringBuilder error) {
error.append("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]);
error.append("}");
return error;
}
/**
* Builds an TemplateExpression from a source, performs checks.
* @param el the unified el instance
* @param source the source TemplateExpression
* @return an TemplateExpression
*/
private TemplateExpression build(TemplateEngine el, TemplateExpression source) {
int sum = 0;
for (int count : counts) {
sum += count;
}
if (expressions.size() != sum) {
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);
} else {
return el.new CompositeExpression(counts, expressions, source);
}
}
}
/**
* Gets the JexlEngine underlying this JxltEngine.
* @return the JexlEngine
*/
@Override
public Engine getEngine() {
return jexl;
}
/**
* Clears the cache.
*/
@Override
public void clearCache() {
synchronized (cache) {
cache.clear();
}
}
/**
* 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(TemplateExpression src) {
this.source = src != null ? src : this;
}
@Override
public boolean isImmediate() {
return true;
}
@Override
public final boolean isDeferred() {
return !isImmediate();
}
/**
* Gets this TemplateExpression type.
* @return its type
*/
abstract ExpressionType getType();
/** @return the info */
JexlInfo getInfo() {
return null;
}
@Override
public final String toString() {
StringBuilder strb = new StringBuilder();
asString(strb);
if (source != this) {
strb.append(" /*= ");
strb.append(source.toString());
strb.append(" */");
}
return strb.toString();
}
@Override
public String asString() {
StringBuilder strb = new StringBuilder();
asString(strb);
return strb.toString();
}
@Override
public Set> getVariables() {
return Collections.emptySet();
}
@Override
public final TemplateExpression getSource() {
return source;
}
/**
* Fills up the list of variables accessed by this unified expression.
* @param collector the variable collector
*/
protected void getVariables(Engine.VarCollector collector) {
// nothing to do
}
@Override
public final TemplateExpression prepare(JexlContext context) {
return prepare(null, context);
}
/**
* Prepares this expression.
* @param frame the frame storing parameters and local variables
* @param context the context storing global variables
* @return the expression value
* @throws JexlException
*/
protected final TemplateExpression prepare(Scope.Frame frame, JexlContext context) {
try {
Interpreter interpreter = new TemplateInterpreter(jexl, context, frame, null, null);
return prepare(interpreter);
} catch (JexlException xjexl) {
JexlException xuel = createException(xjexl.getInfo(), "prepare", this, xjexl);
if (jexl.isSilent()) {
jexl.logger.warn(xuel.getMessage(), xuel.getCause());
return null;
}
throw xuel;
}
}
/**
* 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(Interpreter interpreter) {
return this;
}
@Override
public final Object evaluate(JexlContext context) {
return evaluate(null, context);
}
/**
* Evaluates this expression.
* @param frame the frame storing parameters and local variables
* @param context the context storing global variables
* @return the expression value
* @throws JexlException
*/
protected final Object evaluate(Scope.Frame frame, JexlContext context) {
try {
Interpreter interpreter = new TemplateInterpreter(jexl, context, frame, null, null);
return evaluate(interpreter);
} catch (JexlException xjexl) {
JexlException xuel = createException(xjexl.getInfo(), "evaluate", this, xjexl);
if (jexl.isSilent()) {
jexl.logger.warn(xuel.getMessage(), xuel.getCause());
return null;
}
throw xuel;
}
}
/**
* 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);
}
/** A constant unified expression. */
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(Object val, TemplateExpression 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;
}
@Override
ExpressionType getType() {
return ExpressionType.CONSTANT;
}
@Override
public StringBuilder asString(StringBuilder strb) {
if (value != null) {
strb.append(value.toString());
}
return strb;
}
@Override
protected Object evaluate(Interpreter interpreter) {
return value;
}
}
/** 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(CharSequence theExpr, JexlNode theNode, TemplateExpression theSource) {
super(theSource);
this.expr = theExpr;
this.node = theNode;
}
@Override
public StringBuilder asString(StringBuilder strb) {
strb.append(isImmediate() ? immediateChar : deferredChar);
strb.append("{");
strb.append(expr);
strb.append("}");
return strb;
}
@Override
protected Object evaluate(Interpreter interpreter) {
return interpreter.interpret(node);
}
@Override
public Set> getVariables() {
Engine.VarCollector collector = new Engine.VarCollector();
getVariables(collector);
return collector.collected();
}
@Override
protected void getVariables(Engine.VarCollector collector) {
jexl.getVariables(node instanceof ASTJexlScript? (ASTJexlScript) node : null, node, collector);
}
@Override
JexlInfo getInfo() {
return node.jexlInfo();
}
}
/** An immediate unified expression: ${jexl}. */
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(CharSequence expr, JexlNode node, TemplateExpression source) {
super(expr, node, source);
}
@Override
ExpressionType getType() {
return ExpressionType.IMMEDIATE;
}
@Override
protected TemplateExpression prepare(Interpreter interpreter) {
// evaluate immediate as constant
Object value = evaluate(interpreter);
return value != null ? new ConstantExpression(value, source) : null;
}
}
/** A deferred unified expression: #{jexl}. */
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(CharSequence expr, JexlNode node, TemplateExpression source) {
super(expr, node, source);
}
@Override
public boolean isImmediate() {
return false;
}
@Override
ExpressionType getType() {
return ExpressionType.DEFERRED;
}
@Override
protected TemplateExpression prepare(Interpreter interpreter) {
return new ImmediateExpression(expr, node, source);
}
@Override
protected void getVariables(Engine.VarCollector collector) {
// noop
}
}
/**
* An immediate unified expression nested into a deferred unified expression.
* #{...${jexl}...}
* Note that the deferred syntax is JEXL's.
*/
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(CharSequence expr, JexlNode node, TemplateExpression source) {
super(expr, node, source);
if (this.source != this) {
throw new IllegalArgumentException("Nested TemplateExpression can not have a source");
}
}
@Override
public StringBuilder asString(StringBuilder strb) {
strb.append(expr);
return strb;
}
@Override
public boolean isImmediate() {
return false;
}
@Override
ExpressionType getType() {
return ExpressionType.NESTED;
}
@Override
protected TemplateExpression prepare(Interpreter interpreter) {
String value = interpreter.interpret(node).toString();
JexlNode dnode = jexl.parse(node.jexlInfo(), value, null, false, noscript);
return new ImmediateExpression(value, dnode, this);
}
@Override
protected Object evaluate(Interpreter interpreter) {
return prepare(interpreter).evaluate(interpreter);
}
}
/** A composite unified expression: "... ${...} ... #{...} ...". */
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 expresion if any
*/
CompositeExpression(int[] counters, ArrayList list, TemplateExpression src) {
super(src);
this.exprs = list.toArray(new TemplateExpression[list.size()]);
this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0)
| (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0);
}
@Override
public boolean isImmediate() {
// immediate if no deferred
return (meta & 2) == 0;
}
@Override
ExpressionType getType() {
return ExpressionType.COMPOSITE;
}
@Override
public StringBuilder asString(StringBuilder strb) {
for (TemplateExpression e : exprs) {
e.asString(strb);
}
return strb;
}
@Override
public Set> getVariables() {
Engine.VarCollector collector = new Engine.VarCollector();
for (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(Engine.VarCollector collector) {
for (TemplateExpression expr : exprs) {
expr.getVariables(collector);
}
}
@Override
protected TemplateExpression 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) {
TemplateExpression expr = exprs[e];
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);
}
@Override
protected Object evaluate(Interpreter interpreter) {
final int size = exprs.length;
Object value;
// 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;
}
}
@Override
public JxltEngine.Expression createExpression(JexlInfo info, String expression) {
if (info == null) {
info = jexl.createInfo();
}
Exception xuel = null;
TemplateExpression stmt = null;
try {
stmt = cache.get(expression);
if (stmt == null) {
stmt = parseExpression(info, expression, null);
cache.put(expression, stmt);
}
} catch (JexlException xjexl) {
xuel = new Exception(xjexl.getInfo(), "failed to parse '" + expression + "'", xjexl);
}
if (xuel != null) {
if (jexl.isSilent()) {
jexl.logger.warn(xuel.getMessage(), xuel.getCause());
stmt = null;
} else {
throw xuel;
}
}
return stmt;
}
/**
* 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(JexlInfo info, String action, TemplateExpression 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(info, strb.toString(), xany);
}
/** 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
}
/**
* 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(JexlInfo info, String expr, 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) {
char c = expr.charAt(column);
switch (state) {
default: // in case we ever add new unified expresssion type
throw new UnsupportedOperationException("unexpected unified expression type");
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) {
TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
builder.add(cexpr);
strb.delete(0, Integer.MAX_VALUE);
}
} else {
// revert to CONST
strb.append(immediateChar);
strb.append(c);
state = ParseState.CONST;
}
break;
case DEFERRED0: // #
if (c == '{') {
state = ParseState.DEFERRED1;
// if chars in buffer, create constant
if (strb.length() > 0) {
TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
builder.add(cexpr);
strb.delete(0, Integer.MAX_VALUE);
}
} else {
// revert to CONST
strb.append(deferredChar);
strb.append(c);
state = ParseState.CONST;
}
break;
case IMMEDIATE1: // ${...
if (c == '}') {
if (immediate1 > 0) {
immediate1 -= 1;
strb.append(c);
} else {
// materialize the immediate expr
String src = strb.toString();
TemplateExpression iexpr = new ImmediateExpression(
src,
jexl.parse(info.at(lineno, column), src, scope, false, noscript),
null);
builder.add(iexpr);
strb.delete(0, Integer.MAX_VALUE);
state = ParseState.CONST;
}
} else {
if (c == '{') {
immediate1 += 1;
}
// do buildup expr
strb.append(c);
}
break;
case DEFERRED1: // #{...
// skip inner strings (for '}')
if (c == '"' || c == '\'') {
strb.append(c);
column = StringParser.readString(strb, expr, column + 1, c);
continue;
}
// nested immediate in deferred; need to balance count of '{' & '}'
if (c == '{') {
if (expr.charAt(column - 1) == immediateChar) {
inner1 += 1;
strb.deleteCharAt(strb.length() - 1);
nested = true;
} else {
deferred1 += 1;
strb.append(c);
}
continue;
}
// closing '}'
if (c == '}') {
// balance nested immediate
if (deferred1 > 0) {
deferred1 -= 1;
strb.append(c);
} else if (inner1 > 0) {
inner1 -= 1;
} else {
// materialize the nested/deferred expr
String src = strb.toString();
TemplateExpression dexpr;
if (nested) {
dexpr = new NestedExpression(
expr.substring(inested, column + 1),
jexl.parse(info.at(lineno, column), src, scope, false, noscript),
null);
} else {
dexpr = new DeferredExpression(
strb.toString(),
jexl.parse(info.at(lineno, column), src, scope, false, noscript),
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 == deferredChar) {
strb.append(deferredChar);
} else if (c == immediateChar) {
strb.append(immediateChar);
} else {
strb.append('\\');
strb.append(c);
}
state = ParseState.CONST;
}
if (c == '\n') {
lineno += 1;
}
}
// we should be in that state
if (state != ParseState.CONST) {
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) {
TemplateExpression 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.
*/
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
}
/**
* 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(BlockType theType, int theLine, String theBlock) {
type = theType;
line = theLine;
body = theBlock;
}
/**
* @return type
*/
BlockType getType() {
return type;
}
/**
* @return line
*/
int getLine() {
return line;
}
/**
* @return body
*/
String getBody() {
return body;
}
@Override
public String toString() {
if (BlockType.VERBATIM.equals(type)) {
return body;
} else {
// CHECKSTYLE:OFF
StringBuilder strb = new StringBuilder(64); // CSOFF: MagicNumber
// CHECKSTYLE:ON
Iterator lines = readLines(new StringReader(body));
while (lines.hasNext()) {
strb.append("$$").append(lines.next());
}
return strb.toString();
}
}
/**
* 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(StringBuilder strb, String prefix) {
if (BlockType.VERBATIM.equals(type)) {
strb.append(body);
} else {
Iterator lines = readLines(new StringReader(body));
while (lines.hasNext()) {
strb.append(prefix).append(lines.next());
}
}
}
}
/**
* 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(CharSequence sequence, CharSequence pattern) {
int length = sequence.length();
int s = 0;
while (s < length && Character.isSpaceChar(sequence.charAt(s))) {
s += 1;
}
if (s < length && pattern.length() <= (length - s)) {
sequence = sequence.subSequence(s, length);
if (sequence.subSequence(0, pattern.length()).equals(pattern)) {
return s + pattern.length();
}
}
return -1;
}
/**
* 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() {
StringBuffer strb = new StringBuffer(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 (IOException xio) {
return null;
}
return strb.length() > 0 ? strb : null;
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public CharSequence next() {
CharSequence current = next;
if (current != null) {
next = doNext();
}
return current;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Not supported.");
}
};
}
/**
* 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, 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;
Iterator lines = readLines(reader);
int lineno = 0;
int start = 0;
while (lines.hasNext()) {
CharSequence line = lines.next();
if (line == null) {
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()));
}
start = lineno;
} else if (type == BlockType.DIRECTIVE) {
// switch to verbatim if necessary
prefixLen = startsWith(line, prefix);
if (prefixLen < 0) {
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) {
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) {
Block block = new Block(type, start, strb.toString());
blocks.add(block);
}
blocks.trimToSize();
return blocks;
}
@Override
public TemplateScript createTemplate(JexlInfo info, String prefix, Reader source, String... parms) {
return new TemplateScript(this, info, prefix, source, parms);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy