Skip to content

Commit

Permalink
JEXL-252, JEXL-250: added safe navigation and string interpolation to…
Browse files Browse the repository at this point in the history
… identifier resolution
  • Loading branch information
henrib committed Feb 5, 2018
1 parent 2996187 commit dffcc16
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 13 deletions.
13 changes: 13 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions src/main/java/org/apache/commons/jexl3/internal/Debugger.java
Original file line number Diff line number Diff line change
Expand Up @@ -611,13 +611,20 @@ protected Object visit(ASTIdentifier node, Object data) {

@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;
}

Expand Down
39 changes: 35 additions & 4 deletions src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
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;
Expand Down Expand Up @@ -1000,9 +1001,34 @@ protected boolean isLocalVariable(ASTReference node, int which) {
&& ((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
Expand Down Expand Up @@ -1245,7 +1271,7 @@ protected Object executeAssign(JexlNode node, JexlOperator assignop, Object data
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) {
Expand Down Expand Up @@ -1774,8 +1800,13 @@ protected Object getAttribute(Object object, Object attribute, JexlNode node) {
}
// 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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -36,6 +37,22 @@ void setIdentifier(String id) {
identifier = parseIdentifier(id);
}

/**
* 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]*)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
21 changes: 17 additions & 4 deletions src/main/java/org/apache/commons/jexl3/parser/Parser.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -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 : "..." >
}

Expand Down Expand Up @@ -259,7 +260,7 @@ TOKEN_MGR_DECLS : {
{
< JXLT_LITERAL:
"`" (~["`","\\"] | "\\" ~["\u0000"])* "`"
>
> { popDot(); } /* Revert state to default if was DOT_ID. */
}

/***************************************
Expand Down Expand Up @@ -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
)
}

Expand All @@ -815,6 +826,8 @@ void MemberAccess() #void : {}
LOOKAHEAD(<LBRACKET>) ArrayAccess()
|
LOOKAHEAD(<DOT>) IdentifierAccess()
|
LOOKAHEAD(<QDOT>) IdentifierAccess()
}

void ReferenceExpression() #MethodNode(>1) : {}
Expand Down
6 changes: 6 additions & 0 deletions src/site/xdoc/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
22 changes: 22 additions & 0 deletions src/test/java/org/apache/commons/jexl3/IssuesTest200.java
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,26 @@ public void test245() throws Exception {
}
}
}

@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();
}
}
}

0 comments on commit dffcc16

Please sign in to comment.