blob: 2a3b17ba2713ebc39ef10e907c4612a35668c63d [file] [log] [blame]
/*
* 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.parser;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlFeatures;
import org.apache.commons.jexl3.JexlInfo;
import org.apache.commons.jexl3.internal.Scope;
import org.apache.commons.jexl3.internal.LexicalScope;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Constructor;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* The base class for parsing, manages the parameter/local variable frame.
*/
public abstract class JexlParser extends StringParser {
/**
* The associated controller.
*/
protected final FeatureController featureController = new FeatureController(JexlEngine.DEFAULT_FEATURES);
/**
* The basic source info.
*/
protected JexlInfo info = null;
/**
* The source being processed.
*/
protected String source = null;
/**
* The map of named registers aka script parameters.
* <p>Each parameter is associated to a register and is materialized
* as an offset in the registers array used during evaluation.</p>
*/
protected Scope frame = null;
/**
* When parsing inner functions/lambda, need to stack the scope (sic).
*/
protected final Deque<Scope> frames = new ArrayDeque<Scope>();
/**
* The list of pragma declarations.
*/
protected Map<String, Object> pragmas = null;
/**
* The number of imbricated loops.
*/
protected int loopCount = 0;
/**
* Stack of parsing loop counts.
*/
protected final Deque<Integer> loopCounts = new ArrayDeque<Integer>();
/**
* The current lexical block.
*/
protected LexicalUnit block = null;
/**
* Stack of lexical blocks.
*/
protected final Deque<LexicalUnit> blocks = new ArrayDeque<LexicalUnit>();
/**
* A lexical unit is the container defining local symbols and their
* visibility boundaries.
*/
public interface LexicalUnit {
/**
* Declares a local symbol.
* @param symbol the symbol index in the scope
* @return true if declaration was successful, false if symbol was already declared
*/
boolean declareSymbol(int symbol);
/**
* Checks whether a symbol is declared in this lexical unit.
* @param symbol the symbol
* @return true if declared, false otherwise
*/
boolean hasSymbol(int symbol);
/**
* @return the number of local variables declared in this unit
*/
int getSymbolCount();
LexicalScope getLexicalScope();
}
/**
* Cleanup.
* @param features the feature set to restore if any
*/
protected void cleanup(final JexlFeatures features) {
info = null;
source = null;
frame = null;
frames.clear();
pragmas = null;
loopCounts.clear();
loopCount = 0;
blocks.clear();
block = null;
}
/**
* Utility function to create '.' separated string from a list of string.
* @param lstr the list of strings
* @return the dotted version
*/
protected static String stringify(final List<String> lstr) {
final StringBuilder strb = new StringBuilder();
boolean dot = false;
for(final String str : lstr) {
if (!dot) {
dot = true;
} else {
strb.append('.');
}
strb.append(str);
}
return strb.toString();
}
/**
* Read a given source line.
* @param src the source
* @param lineno the line number
* @return the line
*/
protected static String readSourceLine(final String src, final int lineno) {
String msg = "";
if (src != null && lineno >= 0) {
try {
final BufferedReader reader = new BufferedReader(new StringReader(src));
for (int l = 0; l < lineno; ++l) {
msg = reader.readLine();
}
} catch (final IOException xio) {
// ignore, very unlikely but then again...
}
}
return msg;
}
/**
* Internal, for debug purpose only.
* @param registers whether register syntax is recognized by this parser
*/
public void allowRegisters(final boolean registers) {
featureController.setFeatures(new JexlFeatures(featureController.getFeatures()).register(registers));
}
/**
* Sets a new set of options.
* @param features
*/
protected void setFeatures(final JexlFeatures features) {
this.featureController.setFeatures(features);
}
/**
* @return the current set of features active during parsing
*/
protected JexlFeatures getFeatures() {
return featureController.getFeatures();
}
/**
* Gets the frame used by this parser.
* <p> Since local variables create new symbols, it is important to
* regain access after parsing to known which / how-many registers are needed. </p>
* @return the named register map
*/
protected Scope getFrame() {
return frame;
}
/**
* Create a new local variable frame and push it as current scope.
*/
protected void pushFrame() {
if (frame != null) {
frames.push(frame);
}
frame = new Scope(frame, (String[]) null);
loopCounts.push(loopCount);
loopCount = 0;
}
/**
* Pops back to previous local variable frame.
*/
protected void popFrame() {
if (!frames.isEmpty()) {
frame = frames.pop();
} else {
frame = null;
}
if (!loopCounts.isEmpty()) {
loopCount = loopCounts.pop();
}
}
/**
* Gets the lexical unit currently used by this parser.
* @return the named register map
*/
protected LexicalUnit getUnit() {
return block;
}
/**
* Pushes a new lexical unit.
* @param unit the new lexical unit
*/
protected void pushUnit(final LexicalUnit unit) {
if (block != null) {
blocks.push(block);
}
block = unit;
}
/**
* Restores the previous lexical unit.
* @param unit restores the previous lexical scope
*/
protected void popUnit(final LexicalUnit unit) {
if (block == unit){
if (!blocks.isEmpty()) {
block = blocks.pop();
} else {
block = null;
}
}
}
/**
* Checks if a symbol is defined in lexical scopes.
* <p>This works with with parsed scripts in template resolution only.
* @param info an info linked to a node
* @param symbol
* @return true if symbol accessible in lexical scope
*/
private boolean isSymbolDeclared(final JexlNode.Info info, final int symbol) {
JexlNode walk = info.getNode();
while(walk != null) {
if (walk instanceof JexlParser.LexicalUnit) {
final LexicalScope scope = ((JexlParser.LexicalUnit) walk).getLexicalScope();
if (scope != null && scope.hasSymbol(symbol)) {
return true;
}
// stop at first new scope reset, aka lambda
if (walk instanceof ASTJexlLambda) {
break;
}
}
walk = walk.jjtGetParent();
}
return false;
}
/**
* Checks whether an identifier is a local variable or argument, ie a symbol, stored in a register.
* @param identifier the identifier
* @param name the identifier name
* @return the image
*/
protected String checkVariable(final ASTIdentifier identifier, final String name) {
if (frame != null) {
final Integer symbol = frame.getSymbol(name);
if (symbol != null) {
boolean declared = true;
if (frame.isCapturedSymbol(symbol)) {
// captured are declared in all cases
identifier.setCaptured(true);
} else {
declared = block.hasSymbol(symbol);
// one of the lexical blocks above should declare it
if (!declared) {
for (final LexicalUnit u : blocks) {
if (u.hasSymbol(symbol)) {
declared = true;
break;
}
}
}
if (!declared && info instanceof JexlNode.Info) {
declared = isSymbolDeclared((JexlNode.Info) info, symbol);
}
}
identifier.setSymbol(symbol, name);
if (!declared) {
identifier.setShaded(true);
if (getFeatures().isLexicalShade()) {
// can not reuse a local as a global
throw new JexlException(identifier, name + ": variable is not defined");
}
}
}
}
return name;
}
/**
* Whether a given variable name is allowed.
* @param image the name
* @return true if allowed, false if reserved
*/
protected boolean allowVariable(final String image) {
final JexlFeatures features = getFeatures();
if (!features.supportsLocalVar()) {
return false;
}
if (features.isReservedName(image)) {
return false;
}
return true;
}
/**
* Declares a symbol.
* @param symbol the symbol index
* @return true if symbol can be declared in lexical scope, false (error)
* if it is already declared
*/
private boolean declareSymbol(final int symbol) {
if (blocks != null) {
for (final LexicalUnit lu : blocks) {
if (lu.hasSymbol(symbol)) {
return false;
}
// stop at first new scope reset, aka lambda
if (lu instanceof ASTJexlLambda) {
break;
}
}
}
return block == null || block.declareSymbol(symbol);
}
/**
* Declares a local variable.
* <p> This method creates an new entry in the symbol map. </p>
* @param var the identifier used to declare
* @param token the variable name toekn
*/
protected void declareVariable(final ASTVar var, final Token token) {
final String name = token.image;
if (!allowVariable(name)) {
throwFeatureException(JexlFeatures.LOCAL_VAR, token);
}
if (frame == null) {
frame = new Scope(null, (String[]) null);
}
final int symbol = frame.declareVariable(name);
var.setSymbol(symbol, name);
if (frame.isCapturedSymbol(symbol)) {
var.setCaptured(true);
}
// lexical feature error
if (!declareSymbol(symbol)) {
if (getFeatures().isLexical()) {
throw new JexlException(var, name + ": variable is already declared");
} else {
var.setRedefined(true);
}
}
}
/**
* Adds a pragma declaration.
* @param key the pragma key
* @param value the pragma value
*/
protected void declarePragma(final String key, final Object value) {
if (!getFeatures().supportsPragma()) {
throwFeatureException(JexlFeatures.PRAGMA, getToken(0));
}
if (pragmas == null) {
pragmas = new TreeMap<String, Object>();
}
pragmas.put(key, value);
}
/**
* Declares a local parameter.
* <p> This method creates an new entry in the symbol map. </p>
* @param token the parameter name toekn
*/
protected void declareParameter(final Token token) {
final String identifier = token.image;
if (!allowVariable(identifier)) {
throwFeatureException(JexlFeatures.LOCAL_VAR, token);
}
if (frame == null) {
frame = new Scope(null, (String[]) null);
}
final int symbol = frame.declareParameter(identifier);
// not sure how declaring a parameter could fail...
// lexical feature error
if (!block.declareSymbol(symbol) && getFeatures().isLexical()) {
final JexlInfo xinfo = info.at(token.beginLine, token.beginColumn);
throw new JexlException(xinfo, identifier + ": variable is already declared", null);
}
}
/**
* Default implementation does nothing but is overriden by generated code.
* @param top whether the identifier is beginning an l/r value
* @throws ParseException subclasses may throw this
*/
protected void Identifier(final boolean top) throws ParseException {
// Overriden by generated code
}
final protected void Identifier() throws ParseException {
Identifier(false);
}
/**
* Overridden in actual parser to access tokens stack.
* @param index 0 to get current token
* @return the token on the stack
*/
protected abstract Token getToken(int index);
/**
* Overridden in actual parser to access tokens stack.
* @return the next token on the stack
*/
protected abstract Token getNextToken();
/**
* The set of assignment operators as classes.
*/
@SuppressWarnings("unchecked")
private static final Set<Class<? extends JexlNode>> ASSIGN_NODES = new HashSet<Class<? extends JexlNode>>(
Arrays.asList(
ASTAssignment.class,
ASTSetAddNode.class,
ASTSetMultNode.class,
ASTSetDivNode.class,
ASTSetAndNode.class,
ASTSetOrNode.class,
ASTSetXorNode.class,
ASTSetSubNode.class
)
);
/**
* Called by parser at beginning of node construction.
* @param node the node
*/
protected void jjtreeOpenNodeScope(final JexlNode node) {
// nothing
}
/**
* Called by parser at end of node construction.
* <p>
* Detects "Ambiguous statement" and 'non-left value assignment'.</p>
* @param node the node
* @throws ParseException
*/
protected void jjtreeCloseNodeScope(final JexlNode node) throws ParseException {
if (node instanceof ASTAmbiguous) {
throwAmbiguousException(node);
}
if (node instanceof ASTJexlScript) {
if (node instanceof ASTJexlLambda && !getFeatures().supportsLambda()) {
throwFeatureException(JexlFeatures.LAMBDA, node.jexlInfo());
}
final ASTJexlScript script = (ASTJexlScript) node;
// reaccess in case local variables have been declared
if (script.getScope() != frame) {
script.setScope(frame);
}
popFrame();
} else if (ASSIGN_NODES.contains(node.getClass())) {
final JexlNode lv = node.jjtGetChild(0);
if (!lv.isLeftValue()) {
throwParsingException(JexlException.Assignment.class, null);
}
}
// heavy check
featureController.controlNode(node);
}
/**
* Throws Ambiguous exception.
* <p>Seeks the end of the ambiguous statement to recover.
* @param node the first token in ambiguous expression
*/
protected void throwAmbiguousException(final JexlNode node) {
final JexlInfo begin = node.jexlInfo();
final Token t = getToken(0);
final JexlInfo end = info.at(t.beginLine, t.endColumn);
final String msg = readSourceLine(source, end.getLine());
throw new JexlException.Ambiguous(begin, end, msg);
}
/**
* Throws a feature exception.
* @param feature the feature code
* @param info the exception surroundings
*/
protected void throwFeatureException(final int feature, final JexlInfo info) {
final String msg = info != null? readSourceLine(source, info.getLine()) : null;
throw new JexlException.Feature(info, feature, msg);
}
/**
* Throws a feature exception.
* @param feature the feature code
* @param token the token that triggered it
*/
protected void throwFeatureException(final int feature, Token token) {
if (token == null) {
token = this.getToken(0);
if (token == null) {
throw new JexlException.Parsing(null, JexlFeatures.stringify(feature));
}
}
final JexlInfo xinfo = info.at(token.beginLine, token.beginColumn);
throwFeatureException(feature, xinfo);
}
/**
* Throws a parsing exception.
* @param node the node that caused it
*/
protected void throwParsingException(final JexlNode node) {
throwParsingException(null, null);
}
/**
* Creates a parsing exception.
* @param xclazz the class of exception
* @param tok the token to report
* @param <T> the parsing exception subclass
*/
protected <T extends JexlException.Parsing> void throwParsingException(final Class<T> xclazz, Token tok) {
JexlInfo xinfo = null;
String msg = "unrecoverable state";
JexlException.Parsing xparse = null;
if (tok == null) {
tok = this.getToken(0);
}
if (tok != null) {
xinfo = info.at(tok.beginLine, tok.beginColumn);
msg = tok.image;
if (xclazz != null) {
try {
final Constructor<T> ctor = xclazz.getConstructor(JexlInfo.class, String.class);
xparse = ctor.newInstance(xinfo, msg);
} catch (final Exception xany) {
// ignore, very unlikely but then again..
}
}
}
// unlikely but safe
throw xparse != null ? xparse : new JexlException.Parsing(xinfo, msg);
}
/**
* Pick the most significant token for error reporting.
* @param tokens the tokens to choose from
* @return the token
*/
protected static Token errorToken(final Token... tokens) {
for (final Token token : tokens) {
if (token != null && token.image != null && !token.image.isEmpty()) {
return token;
}
}
return null;
}
}