diff --git a/src/main/java/com/metaweb/gridworks/expr/ExpressionUtils.java b/src/main/java/com/metaweb/gridworks/expr/ExpressionUtils.java index c1b973fc3..72485c4a9 100644 --- a/src/main/java/com/metaweb/gridworks/expr/ExpressionUtils.java +++ b/src/main/java/com/metaweb/gridworks/expr/ExpressionUtils.java @@ -91,6 +91,18 @@ public class ExpressionUtils { new EvalError(v.getClass().getSimpleName() + " value not storable"); } + static public boolean isArray(Object v) { + return v != null && v.getClass().isArray(); + } + + static public boolean isArrayOrCollection(Object v) { + return v != null && (v.getClass().isArray() || v instanceof Collection); + } + + static public boolean isArrayOrList(Object v) { + return v != null && (v.getClass().isArray() || v instanceof List); + } + @SuppressWarnings("unchecked") static public List toObjectList(Object v) { return (List) v; diff --git a/src/main/java/com/metaweb/gridworks/gel/Control.java b/src/main/java/com/metaweb/gridworks/gel/Control.java index 83598c0ae..4de85e3ad 100644 --- a/src/main/java/com/metaweb/gridworks/gel/Control.java +++ b/src/main/java/com/metaweb/gridworks/gel/Control.java @@ -5,6 +5,11 @@ import java.util.Properties; import com.metaweb.gridworks.Jsonizable; import com.metaweb.gridworks.expr.Evaluable; +/** + * Interface of GEL controls such as if, forEach, forNonBlank, with. A control can + * decide which part of the code to execute and can affect the environment bindings. + * Functions, on the other hand, can't do either. + */ public interface Control extends Jsonizable { public Object call(Properties bindings, Evaluable[] args); diff --git a/src/main/java/com/metaweb/gridworks/gel/Function.java b/src/main/java/com/metaweb/gridworks/gel/Function.java index 0bbafbfa2..a620e1f73 100644 --- a/src/main/java/com/metaweb/gridworks/gel/Function.java +++ b/src/main/java/com/metaweb/gridworks/gel/Function.java @@ -4,6 +4,10 @@ import java.util.Properties; import com.metaweb.gridworks.Jsonizable; +/** + * Interface for functions. When a function is called, its arguments have already + * been evaluated down into non-error values. + */ public interface Function extends Jsonizable { public Object call(Properties bindings, Object[] args); } diff --git a/src/main/java/com/metaweb/gridworks/gel/Parser.java b/src/main/java/com/metaweb/gridworks/gel/Parser.java index 4befab2e2..e0ed9ff70 100644 --- a/src/main/java/com/metaweb/gridworks/gel/Parser.java +++ b/src/main/java/com/metaweb/gridworks/gel/Parser.java @@ -18,7 +18,7 @@ import com.metaweb.gridworks.gel.ast.OperatorCallExpr; import com.metaweb.gridworks.gel.ast.VariableExpr; public class Parser { - protected Scanner _scanner; + protected Scanner _scanner; protected Token _token; protected Evaluable _root; @@ -47,6 +47,10 @@ public class Parser { return new ParsingException("Parsing error at offset " + index + ": " + desc); } + /** + * := + * | [ "<" "<=" ">" ">=" "==" "!=" ] + */ protected Evaluable parseExpression() throws ParsingException { Evaluable sub = parseSubExpression(); @@ -66,6 +70,10 @@ public class Parser { return sub; } + /** + * := + * | [ "+" "-" ] + */ protected Evaluable parseSubExpression() throws ParsingException { Evaluable sub = parseTerm(); @@ -85,6 +93,10 @@ public class Parser { return sub; } + /** + * := + * | [ "*" "/" ] + */ protected Evaluable parseTerm() throws ParsingException { Evaluable factor = parseFactor(); @@ -104,6 +116,17 @@ public class Parser { return factor; } + /** + * := ( )* + * := + * | | - | | | + * ( ) + * + * := "[" "]" + * | "." + * | "." "(" ")" + * + */ protected Evaluable parseFactor() throws ParsingException { if (_token == null) { throw makeException("Expecting something more at end of expression"); @@ -214,6 +237,11 @@ public class Parser { return eval; } + /** + * := + * | ( "," )* + * + */ protected List parseExpressionList(String closingDelimiter) throws ParsingException { List l = new LinkedList(); diff --git a/src/main/java/com/metaweb/gridworks/gel/Scanner.java b/src/main/java/com/metaweb/gridworks/gel/Scanner.java index 383767260..ea6f43909 100644 --- a/src/main/java/com/metaweb/gridworks/gel/Scanner.java +++ b/src/main/java/com/metaweb/gridworks/gel/Scanner.java @@ -14,8 +14,8 @@ public class Scanner { static public class Token { final public int start; final public int end; - final public TokenType type; - final public String text; + final public TokenType type; + final public String text; Token(int start, int end, TokenType type, String text) { this.start = start; @@ -26,7 +26,7 @@ public class Scanner { } static public class ErrorToken extends Token { - final public String detail; // error detail + final public String detail; // error detail public ErrorToken(int start, int end, String text, String detail) { super(start, end, TokenType.Error, text); @@ -52,9 +52,9 @@ public class Scanner { } } - protected String _text; - protected int _index; - protected int _limit; + protected String _text; // input text to tokenize + protected int _index; // index of the next character to process + protected int _limit; // process up to this index public Scanner(String s) { this(s, 0, s.length()); @@ -70,6 +70,16 @@ public class Scanner { return _index; } + /** + * The regexPossible flag is used by the parser to hint the scanner what to do + * when it encounters a slash. Since the divide operator / and the opening + * delimiter of a regex literal are the same, but divide operators and regex + * literals can't occur at the same place in an expression, this flag is a cheap + * way to distinguish the two without having to look ahead. + * + * @param regexPossible + * @return + */ public Token next(boolean regexPossible) { // skip whitespace while (_index < _limit && Character.isWhitespace(_text.charAt(_index))) { diff --git a/src/main/java/com/metaweb/gridworks/gel/ast/ControlCallExpr.java b/src/main/java/com/metaweb/gridworks/gel/ast/ControlCallExpr.java index 0fcb38b45..08272854e 100644 --- a/src/main/java/com/metaweb/gridworks/gel/ast/ControlCallExpr.java +++ b/src/main/java/com/metaweb/gridworks/gel/ast/ControlCallExpr.java @@ -5,6 +5,9 @@ import java.util.Properties; import com.metaweb.gridworks.expr.Evaluable; import com.metaweb.gridworks.gel.Control; +/** + * An abstract syntax tree node encapsulating a control call, such as "if". + */ public class ControlCallExpr implements Evaluable { final protected Evaluable[] _args; final protected Control _control; diff --git a/src/main/java/com/metaweb/gridworks/gel/ast/FieldAccessorExpr.java b/src/main/java/com/metaweb/gridworks/gel/ast/FieldAccessorExpr.java index 586598a59..bb5a045da 100644 --- a/src/main/java/com/metaweb/gridworks/gel/ast/FieldAccessorExpr.java +++ b/src/main/java/com/metaweb/gridworks/gel/ast/FieldAccessorExpr.java @@ -7,6 +7,11 @@ import com.metaweb.gridworks.expr.Evaluable; import com.metaweb.gridworks.expr.ExpressionUtils; import com.metaweb.gridworks.expr.HasFields; +/** + * An abstract syntax tree node encapsulating a field accessor, + * e.g., "cell.value" is accessing the field named "value" on the + * variable called "cell". + */ public class FieldAccessorExpr implements Evaluable { final protected Evaluable _inner; final protected String _fieldName; @@ -19,7 +24,7 @@ public class FieldAccessorExpr implements Evaluable { public Object evaluate(Properties bindings) { Object o = _inner.evaluate(bindings); if (ExpressionUtils.isError(o)) { - return o; + return o; // bubble the error up } else if (o == null) { return new EvalError("Cannot retrieve field from null"); } else if (o instanceof HasFields) { diff --git a/src/main/java/com/metaweb/gridworks/gel/ast/FunctionCallExpr.java b/src/main/java/com/metaweb/gridworks/gel/ast/FunctionCallExpr.java index 70f770bd1..058d71bf2 100644 --- a/src/main/java/com/metaweb/gridworks/gel/ast/FunctionCallExpr.java +++ b/src/main/java/com/metaweb/gridworks/gel/ast/FunctionCallExpr.java @@ -6,6 +6,12 @@ import com.metaweb.gridworks.expr.Evaluable; import com.metaweb.gridworks.expr.ExpressionUtils; import com.metaweb.gridworks.gel.Function; +/** + * An abstract syntax tree node encapsulating a function call. The function's + * arguments are all evaluated down to values before the function is applied. + * If any argument is an error, the function is not applied, and the error is + * the result of the expression. + */ public class FunctionCallExpr implements Evaluable { final protected Evaluable[] _args; final protected Function _function; @@ -20,7 +26,7 @@ public class FunctionCallExpr implements Evaluable { for (int i = 0; i < _args.length; i++) { Object v = _args[i].evaluate(bindings); if (ExpressionUtils.isError(v)) { - return v; + return v; // bubble up the error } args[i] = v; } diff --git a/src/main/java/com/metaweb/gridworks/gel/ast/LiteralExpr.java b/src/main/java/com/metaweb/gridworks/gel/ast/LiteralExpr.java index 6c3b70ab3..34337ef23 100644 --- a/src/main/java/com/metaweb/gridworks/gel/ast/LiteralExpr.java +++ b/src/main/java/com/metaweb/gridworks/gel/ast/LiteralExpr.java @@ -6,6 +6,9 @@ import org.json.JSONObject; import com.metaweb.gridworks.expr.Evaluable; +/** + * An abstract syntax tree node encapsulating a literal value. + */ public class LiteralExpr implements Evaluable { final protected Object _value; diff --git a/src/main/java/com/metaweb/gridworks/gel/ast/OperatorCallExpr.java b/src/main/java/com/metaweb/gridworks/gel/ast/OperatorCallExpr.java index 309e7caac..0731b1e41 100644 --- a/src/main/java/com/metaweb/gridworks/gel/ast/OperatorCallExpr.java +++ b/src/main/java/com/metaweb/gridworks/gel/ast/OperatorCallExpr.java @@ -5,6 +5,9 @@ import java.util.Properties; import com.metaweb.gridworks.expr.Evaluable; import com.metaweb.gridworks.expr.ExpressionUtils; +/** + * An abstract syntax tree node encapsulating an operator call, such as "+". + */ public class OperatorCallExpr implements Evaluable { final protected Evaluable[] _args; final protected String _op; diff --git a/src/main/java/com/metaweb/gridworks/gel/ast/VariableExpr.java b/src/main/java/com/metaweb/gridworks/gel/ast/VariableExpr.java index 2a05e7702..9fc574d59 100644 --- a/src/main/java/com/metaweb/gridworks/gel/ast/VariableExpr.java +++ b/src/main/java/com/metaweb/gridworks/gel/ast/VariableExpr.java @@ -4,6 +4,9 @@ import java.util.Properties; import com.metaweb.gridworks.expr.Evaluable; +/** + * An abstract syntax tree node encapsulating the retrieval of a variable's content. + */ public class VariableExpr implements Evaluable { final protected String _name; diff --git a/src/main/java/com/metaweb/gridworks/gel/controls/ForEach.java b/src/main/java/com/metaweb/gridworks/gel/controls/ForEach.java index fa4ff65cf..232ee820d 100644 --- a/src/main/java/com/metaweb/gridworks/gel/controls/ForEach.java +++ b/src/main/java/com/metaweb/gridworks/gel/controls/ForEach.java @@ -2,7 +2,6 @@ package com.metaweb.gridworks.gel.controls; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; import java.util.List; import java.util.Properties; @@ -21,7 +20,8 @@ public class ForEach implements Control { if (args.length != 3) { return ControlFunctionRegistry.getControlName(this) + " expects 3 arguments"; } else if (!(args[1] instanceof VariableExpr)) { - return ControlFunctionRegistry.getControlName(this) + " expects second argument to be a variable name"; + return ControlFunctionRegistry.getControlName(this) + + " expects second argument to be a variable name"; } return null; } @@ -30,7 +30,7 @@ public class ForEach implements Control { Object o = args[0].evaluate(bindings); if (ExpressionUtils.isError(o)) { return o; - } else if (o == null || (!o.getClass().isArray() && !(o instanceof Iterable))) { + } else if (!ExpressionUtils.isArrayOrCollection(o)) { return new EvalError("First argument to forEach is not an array"); } @@ -40,36 +40,36 @@ public class ForEach implements Control { try { List results = null; - if (o.getClass().isArray()) { - Object[] values = (Object[]) o; - - results = new ArrayList(values.length); - for (Object v : values) { - bindings.put(name, v); - - Object r = args[2].evaluate(bindings); - - results.add(r); - } - } else { - results = o instanceof Collection ? - new ArrayList(ExpressionUtils.toObjectCollection(o).size()) : - new ArrayList(); - - Iterator i = ExpressionUtils.toObjectCollection(o).iterator(); - while (i.hasNext()) { - Object v = i.next(); - - bindings.put(name, v); - - Object r = args[2].evaluate(bindings); - - results.add(r); - } - } - + if (o.getClass().isArray()) { + Object[] values = (Object[]) o; + + results = new ArrayList(values.length); + for (Object v : values) { + bindings.put(name, v); + + Object r = args[2].evaluate(bindings); + + results.add(r); + } + } else { + Collection collection = ExpressionUtils.toObjectCollection(o); + + results = new ArrayList(collection.size()); + + for (Object v : collection) { + bindings.put(name, v); + + Object r = args[2].evaluate(bindings); + + results.add(r); + } + } + return results.toArray(); } finally { + /* + * Restore the old value bound to the variable, if any. + */ if (oldValue != null) { bindings.put(name, oldValue); } else { diff --git a/src/main/java/com/metaweb/gridworks/gel/controls/ForNonBlank.java b/src/main/java/com/metaweb/gridworks/gel/controls/ForNonBlank.java index 0f8572fdf..8416518d6 100644 --- a/src/main/java/com/metaweb/gridworks/gel/controls/ForNonBlank.java +++ b/src/main/java/com/metaweb/gridworks/gel/controls/ForNonBlank.java @@ -16,7 +16,8 @@ public class ForNonBlank implements Control { if (args.length != 4) { return ControlFunctionRegistry.getControlName(this) + " expects 4 arguments"; } else if (!(args[1] instanceof VariableExpr)) { - return ControlFunctionRegistry.getControlName(this) + " expects second argument to be a variable name"; + return ControlFunctionRegistry.getControlName(this) + + " expects second argument to be a variable name"; } return null; } @@ -28,12 +29,15 @@ public class ForNonBlank implements Control { String name = ((VariableExpr) var).getName(); if (ExpressionUtils.isNonBlankData(o)) { - Object oldValue = bindings.containsKey(name) ? bindings.get(name) : null; + Object oldValue = bindings.get(name); bindings.put(name, o); try { return args[2].evaluate(bindings); } finally { + /* + * Restore the old value bound to the variable, if any. + */ if (oldValue != null) { bindings.put(name, oldValue); } else { diff --git a/src/main/java/com/metaweb/gridworks/gel/controls/If.java b/src/main/java/com/metaweb/gridworks/gel/controls/If.java index b851d66d5..2c6468fee 100644 --- a/src/main/java/com/metaweb/gridworks/gel/controls/If.java +++ b/src/main/java/com/metaweb/gridworks/gel/controls/If.java @@ -21,7 +21,7 @@ public class If implements Control { public Object call(Properties bindings, Evaluable[] args) { Object o = args[0].evaluate(bindings); if (ExpressionUtils.isError(o)) { - return o; + return o; // bubble the error up } else if (ExpressionUtils.isTrue(o)) { return args[1].evaluate(bindings); } else { diff --git a/src/main/java/com/metaweb/gridworks/gel/controls/IsTest.java b/src/main/java/com/metaweb/gridworks/gel/controls/IsTest.java index 9bc5357f4..7b0c9cb26 100644 --- a/src/main/java/com/metaweb/gridworks/gel/controls/IsTest.java +++ b/src/main/java/com/metaweb/gridworks/gel/controls/IsTest.java @@ -27,10 +27,7 @@ abstract class IsTest implements Control { throws JSONException { writer.object(); - writer.key("description"); writer.value( - "Evaluates expression o. If it is true, evaluates expression eTrue and returns the result. " + - "Otherwise, evaluates expression eFalse and returns that result instead." - ); + writer.key("description"); writer.value(getDescription()); writer.key("params"); writer.value("expression o"); writer.key("returns"); writer.value("boolean"); writer.endObject(); diff --git a/src/main/java/com/metaweb/gridworks/gel/controls/With.java b/src/main/java/com/metaweb/gridworks/gel/controls/With.java index 8bd760f9f..269213055 100644 --- a/src/main/java/com/metaweb/gridworks/gel/controls/With.java +++ b/src/main/java/com/metaweb/gridworks/gel/controls/With.java @@ -15,7 +15,8 @@ public class With implements Control { if (args.length != 3) { return ControlFunctionRegistry.getControlName(this) + " expects 3 arguments"; } else if (!(args[1] instanceof VariableExpr)) { - return ControlFunctionRegistry.getControlName(this) + " expects second argument to be a variable name"; + return ControlFunctionRegistry.getControlName(this) + + " expects second argument to be a variable name"; } return null; } @@ -34,6 +35,9 @@ public class With implements Control { return args[2].evaluate(bindings); } finally { + /* + * Restore the old value bound to the variable, if any. + */ if (oldValue != null) { bindings.put(name, oldValue); } else {