This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
The following commit(s) were added to refs/heads/master by this push: new dffcc16 JEXL-252, JEXL-250: added safe navigation and string interpolation to identifier resolution dffcc16 is described below commit dffcc160edaa1be6dbf57095e67596f9a0a89ee9 Author: Henri Biestro <hen...@apache.org> AuthorDate: Mon Feb 5 15:00:09 2018 +0100 JEXL-252, JEXL-250: added safe navigation and string interpolation to identifier resolution --- RELEASE-NOTES.txt | 13 ++++++ .../apache/commons/jexl3/internal/Debugger.java | 15 +++++-- .../apache/commons/jexl3/internal/Interpreter.java | 39 +++++++++++++++-- .../commons/jexl3/parser/ASTIdentifierAccess.java | 19 ++++++++- .../jexl3/parser/ASTIdentifierAccessJxlt.java | 49 ++++++++++++++++++++++ .../jexl3/parser/ASTIdentifierAccessSafe.java | 37 ++++++++++++++++ .../jexl3/parser/ASTIdentifierAccessSafeJxlt.java | 37 ++++++++++++++++ .../org/apache/commons/jexl3/parser/Parser.jjt | 21 ++++++++-- src/site/xdoc/changes.xml | 6 +++ .../org/apache/commons/jexl3/IssuesTest200.java | 22 ++++++++++ 10 files changed, 245 insertions(+), 13 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 5dd65e9..48a5f1a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -25,9 +25,22 @@ Release 3.2 Version 3.2 is a minor release. +What's new in 3.2: +================== + +* Interpolated strings in identifiers, as in x.`${prefix}_${suffix}` that in many cases would be equivalent to + x[prefix + '_' + suffix] or x[`${prefix}_${suffix}`]. +* Safe-navigation operator '?.' allowing lenient handling of non-existent or null properties so that an expression + like 'x?.y?.z' would behave as 'x.y.z' in nominal execution and return null instead of throwing an exception in error + cases. +* A set of syntactic restrictions can be applied to scripts ranging from not allowing side-effects to not allowing + loops enabling fine control over what end-users may be able to enter as expressions/scripts. + New Features in 3.2: ==================== +* JEXL-252: Allow for interpolated strings to be used in property access operators +* JEXL-250: Safe navigation operator * JEXL-248: Allow range subexpression as an array property assignment identifier * JEXL-243: Allow restricting available features in Script/Expressions * JEXL-238: Restrict getLiteralClass to a Number for NumberLiterals diff --git a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java index 03134b2..5656932 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java @@ -611,13 +611,20 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { @Override protected Object visit(ASTIdentifierAccess node, Object data) { - builder.append("."); + builder.append(node.isSafe() ? "?." : "."); String image = node.getName(); - if (needQuotes(image)) { + if (node.isExpression()) { + builder.append('`'); + builder.append(image.replace("`", "\\`")); + builder.append('`'); + } else if (needQuotes(image)) { // quote it - image = "'" + image.replace("'", "\\'") + "'"; + builder.append('\''); + builder.append(image.replace("'", "\\'")); + builder.append('\''); + } else { + builder.append(image); } - builder.append(image); return data; } diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 63ce0d3..167de1a 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -59,6 +59,7 @@ import org.apache.commons.jexl3.parser.ASTGENode; import org.apache.commons.jexl3.parser.ASTGTNode; import org.apache.commons.jexl3.parser.ASTIdentifier; import org.apache.commons.jexl3.parser.ASTIdentifierAccess; +import org.apache.commons.jexl3.parser.ASTIdentifierAccessJxlt; import org.apache.commons.jexl3.parser.ASTIfStatement; import org.apache.commons.jexl3.parser.ASTJexlLambda; import org.apache.commons.jexl3.parser.ASTJexlScript; @@ -1000,9 +1001,34 @@ public class Interpreter extends InterpreterBase { && ((ASTIdentifier) node.jjtGetChild(which)).getSymbol() >= 0); } + /** + * Evaluates an access identifier based on the 2 main implementations; + * static (name or numbered identifier) or dynamic (jxlt). + * @param node the identifier access node + * @return the evaluated identifier + */ + private Object evalIdentifier(ASTIdentifierAccess node) { + if (node instanceof ASTIdentifierAccessJxlt) { + ASTIdentifierAccessJxlt accessJxlt = (ASTIdentifierAccessJxlt) node; + TemplateEngine.TemplateExpression expr = (TemplateEngine.TemplateExpression) accessJxlt.getExpression(); + if (expr == null) { + TemplateEngine jxlt = jexl.jxlt(); + expr = jxlt.parseExpression(node.jexlInfo(), node.getName(), frame != null ? frame.getScope() : null); + accessJxlt.setExpression(expr); + } + if (expr != null) { + return expr.evaluate(frame, context); + } + throw new JexlException.Property(node, node.getName()); + } else { + return node.getIdentifier(); + } + } + @Override protected Object visit(ASTIdentifierAccess node, Object data) { - return data != null ? getAttribute(data, node.getIdentifier(), node) : null; + Object id = evalIdentifier(node); + return data != null ? getAttribute(data, id, node) : null; } @Override @@ -1245,7 +1271,7 @@ public class Interpreter extends InterpreterBase { Object property = null; JexlNode propertyNode = left.jjtGetChild(last); if (propertyNode instanceof ASTIdentifierAccess) { - property = ((ASTIdentifierAccess) propertyNode).getIdentifier(); + property = evalIdentifier((ASTIdentifierAccess) propertyNode); // deal with antish variable if (ant != null && object == null) { if (last > 0) { @@ -1774,8 +1800,13 @@ public class Interpreter extends InterpreterBase { } // lets fail if (node != null) { - String attrStr = attribute != null ? attribute.toString() : null; - return unsolvableProperty(node, attrStr, xcause); + boolean safe = (node instanceof ASTIdentifierAccess) && ((ASTIdentifierAccess) node).isSafe(); + if (safe) { + return null; + } else { + String attrStr = attribute != null ? attribute.toString() : null; + return unsolvableProperty(node, attrStr, xcause); + } } else { // direct call String error = "unable to get object property" diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java index 6a53ae5..de61bef 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java @@ -16,10 +16,11 @@ */ package org.apache.commons.jexl3.parser; + /** * Identifiers, variables and registers. */ -public final class ASTIdentifierAccess extends JexlNode { +public class ASTIdentifierAccess extends JexlNode { private String name = null; private Integer identifier = null; @@ -37,6 +38,22 @@ public final class ASTIdentifierAccess extends JexlNode { } /** + * Whether this is a dot or a question-mark-dot aka safe-navigation access. + * @return true is ?., false if . + */ + public boolean isSafe() { + return false; + } + + /** + * Whether this is a Jxlt based identifier. + * @return true if `..${...}...`, false otherwise + */ + public boolean isExpression() { + return false; + } + + /** * Parse an identifier which must be of the form: * 0|([1-9][0-9]*) * @param id the identifier diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java new file mode 100644 index 0000000..e3f767d --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java @@ -0,0 +1,49 @@ +/* + * 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.JxltEngine; + +/** + * x.`expr`. + */ +public class ASTIdentifierAccessJxlt extends ASTIdentifierAccess { + protected JxltEngine.Expression jxltExpr; + + ASTIdentifierAccessJxlt(int id) { + super(id); + } + + ASTIdentifierAccessJxlt(Parser p, int id) { + super(p, id); + } + + @Override + public boolean isExpression() { + return true; + } + + public void setExpression(JxltEngine.Expression tp) { + jxltExpr = tp; + } + + public JxltEngine.Expression getExpression() { + return jxltExpr; + } + +} diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessSafe.java b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessSafe.java new file mode 100644 index 0000000..bd5ee34 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessSafe.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * x?.identifier . + */ +public class ASTIdentifierAccessSafe extends ASTIdentifierAccess { + ASTIdentifierAccessSafe(int id) { + super(id); + } + + ASTIdentifierAccessSafe(Parser p, int id) { + super(p, id); + } + + @Override + public boolean isSafe() { + return true; + } + +} diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessSafeJxlt.java b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessSafeJxlt.java new file mode 100644 index 0000000..7661981 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessSafeJxlt.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * x?.`expr` . + */ +public class ASTIdentifierAccessSafeJxlt extends ASTIdentifierAccessJxlt { + ASTIdentifierAccessSafeJxlt(int id) { + super(id); + } + + ASTIdentifierAccessSafeJxlt(Parser p, int id) { + super(p, id); + } + + @Override + public boolean isSafe() { + return true; + } + +} diff --git a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt index f1942f5..8bf4378 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -147,6 +147,7 @@ TOKEN_MGR_DECLS : { | < COLON : ":" > | < COMMA : "," > | < DOT : "." > { pushDot(); } /* Lexical state is now DOT_ID */ + | < QDOT : "?." > { pushDot(); } /* Lexical state is now DOT_ID */ | < ELIPSIS : "..." > } @@ -259,7 +260,7 @@ TOKEN_MGR_DECLS : { { < JXLT_LITERAL: "`" (~["`","\\"] | "\\" ~["\u0000"])* "`" - > + > { popDot(); } /* Revert state to default if was DOT_ID. */ } /*************************************** @@ -793,15 +794,25 @@ void Lambda() #JexlLambda() : * References ***************************************/ -void IdentifierAccess() : +void IdentifierAccess() #void : { Token t; } { <DOT> ( - t=<DOT_IDENTIFIER> { jjtThis.setIdentifier(t.image); } + t=<DOT_IDENTIFIER> { jjtThis.setIdentifier(t.image); } #IdentifierAccess + | + t=<STRING_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, true)); } #IdentifierAccess + | + t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, true)); } #IdentifierAccessJxlt + ) + | + <QDOT> ( + t=<DOT_IDENTIFIER> { jjtThis.setIdentifier(t.image); } #IdentifierAccessSafe | - t=<STRING_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, true)); } + t=<STRING_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, true)); } #IdentifierAccessSafe + | + t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, true)); } #IdentifierAccessSafeJxlt ) } @@ -815,6 +826,8 @@ void MemberAccess() #void : {} LOOKAHEAD(<LBRACKET>) ArrayAccess() | LOOKAHEAD(<DOT>) IdentifierAccess() + | + LOOKAHEAD(<QDOT>) IdentifierAccess() } void ReferenceExpression() #MethodNode(>1) : {} diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index f530ffe..b038449 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -26,6 +26,12 @@ </properties> <body> <release version="3.2" date="unreleased"> + <action dev="henrib" type="add" issue="JEXL-252" due-to="Dmitri Blinov"> + Allow for interpolated strings to be used in property access operators + </action> + <action dev="henrib" type="add" issue="JEXL-250" due-to="Dmitri Blinov"> + Safe navigation operator + </action> <action dev="henrib" type="add" issue="JEXL-248" due-to="Dmitri Blinov"> Allow range subexpression as an array property assignment identifier </action> diff --git a/src/test/java/org/apache/commons/jexl3/IssuesTest200.java b/src/test/java/org/apache/commons/jexl3/IssuesTest200.java index aa35d66..3faef0b 100644 --- a/src/test/java/org/apache/commons/jexl3/IssuesTest200.java +++ b/src/test/java/org/apache/commons/jexl3/IssuesTest200.java @@ -341,4 +341,26 @@ public class IssuesTest200 extends JexlTestCase { } } } + + @Test + public void test252() throws Exception { + MapContext ctx = new MapContext(); + JexlEngine engine = new JexlBuilder().strict(true).silent(false).create(); + String stmt = "(x, dflt)->{ x?.class1 ?? dflt }"; + JexlScript script = engine.createScript(stmt); + Object result = script.execute(ctx, "querty", "default"); + Assert.assertEquals("default", result); + try { + stmt = "(x, al, dflt)->{ x.`c${al}ss` ?? dflt }"; + script = engine.createScript(stmt); + result = script.execute(ctx, "querty", "la", "default"); + Assert.assertEquals(stmt.getClass(), result); + stmt = "(x, al, dflt)->{ x?.`c${al}ss` ?? dflt }"; + script = engine.createScript(stmt); + result = script.execute(ctx, "querty", "la", "default"); + Assert.assertEquals(stmt.getClass(), result); + } catch(JexlException xany) { + String xanystr = xany.toString(); + } + } } -- To stop receiving notification emails like this one, please contact hen...@apache.org.