You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by he...@apache.org on 2019/05/15 16:45:18 UTC

[commons-jexl] branch master updated: JEXL-300: limit antish resolution to non-safe non-expression cases; refactored IntrepreterBase/Interpreter for a better technical/functional split

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 ee40b6e  JEXL-300: limit antish resolution to non-safe non-expression cases; refactored IntrepreterBase/Interpreter for a better technical/functional split
ee40b6e is described below

commit ee40b6eeded0acd7057bf9fea75f89139ae4528a
Author: henrib <he...@apache.org>
AuthorDate: Wed May 15 18:44:31 2019 +0200

    JEXL-300: limit antish resolution to non-safe non-expression cases; refactored IntrepreterBase/Interpreter for a better technical/functional split
---
 RELEASE-NOTES.txt                                  |   1 +
 .../apache/commons/jexl3/internal/Interpreter.java | 312 ++++-----------------
 .../commons/jexl3/internal/InterpreterBase.java    | 228 +++++++++++++++
 .../apache/commons/jexl3/internal/Operators.java   |   4 +-
 src/site/xdoc/changes.xml                          |   3 +
 .../org/apache/commons/jexl3/AntishCallTest.java   |  56 ++++
 6 files changed, 341 insertions(+), 263 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index df56b3b..aa33c24 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -72,6 +72,7 @@ New Features in 3.2:
 Bugs Fixed in 3.2:
 ==================
 
+* JEXL-300:      Ant-ish variables should not use safe-access operator syntax
 * JEXL-299:      Improve message error when method could not be found
 * JEXL-296:      Real literal in scientific format is not parsed without suffix
 * JEXL-291:      Using sandbox prevents array-syntax lookup by number in Map
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 82ff17d..fbf6c5b 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
@@ -26,8 +26,6 @@ import org.apache.commons.jexl3.JexlScript;
 import org.apache.commons.jexl3.JxltEngine;
 import org.apache.commons.jexl3.introspection.JexlMethod;
 import org.apache.commons.jexl3.introspection.JexlPropertyGet;
-import org.apache.commons.jexl3.introspection.JexlPropertySet;
-import org.apache.commons.jexl3.introspection.JexlUberspect.PropertyResolver;
 
 import org.apache.commons.jexl3.parser.ASTAddNode;
 import org.apache.commons.jexl3.parser.ASTAndNode;
@@ -109,10 +107,7 @@ import org.apache.commons.jexl3.parser.ASTWhileStatement;
 import org.apache.commons.jexl3.parser.JexlNode;
 import org.apache.commons.jexl3.parser.Node;
 
-import java.util.HashMap;
 import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
 import java.util.concurrent.Callable;
 
 /**
@@ -121,18 +116,10 @@ import java.util.concurrent.Callable;
  * @since 2.0
  */
 public class Interpreter extends InterpreterBase {
-    /** The operators evaluation delegate. */
-    protected final Operators operators;
     /** Frame height. */
     protected int fp = 0;
     /** Symbol values. */
     protected final Scope.Frame frame;
-    /** The context to store/retrieve variables. */
-    protected final JexlContext.NamespaceResolver ns;
-    /** The map of 'prefix:function' to object resolving as namespaces. */
-    protected final Map<String, Object> functions;
-    /** The map of dynamically creates namespaces, NamespaceFunctor or duck-types of those. */
-    protected Map<String, Object> functors;
 
     /**
      * The thread local interpreter.
@@ -148,15 +135,7 @@ public class Interpreter extends InterpreterBase {
      */
     protected Interpreter(Engine engine, JexlContext aContext, Scope.Frame eFrame) {
         super(engine, aContext);
-        this.operators = new Operators(this);
         this.frame = eFrame;
-        if (this.context instanceof JexlContext.NamespaceResolver) {
-            ns = ((JexlContext.NamespaceResolver) context);
-        } else {
-            ns = Engine.EMPTY_NS;
-        }
-        this.functions = jexl.functions;
-        this.functors = null;
     }
 
     /**
@@ -166,11 +145,7 @@ public class Interpreter extends InterpreterBase {
      */
     protected Interpreter(Interpreter ii, JexlArithmetic jexla) {
         super(ii, jexla);
-        operators = ii.operators;
         frame = ii.frame;
-        ns = ii.ns;
-        functions = ii.functions;
-        functors = ii.functors;
     }
         
     /**
@@ -256,81 +231,28 @@ public class Interpreter extends InterpreterBase {
         }
         return null;
     }
-
+    
     /**
-     * Resolves a namespace, eventually allocating an instance using context as constructor argument.
-     * <p>
-     * The lifetime of such instances span the current expression or script evaluation.</p>
-     * @param prefix the prefix name (may be null for global namespace)
-     * @param node   the AST node
-     * @return the namespace instance
+     * Gets an attribute of an object.
+     *
+     * @param object    to retrieve value from
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
+     * @return the attribute value
      */
-    protected Object resolveNamespace(String prefix, JexlNode node) {
-        Object namespace;
-        // check whether this namespace is a functor
-        synchronized (this) {
-            if (functors != null) {
-                namespace = functors.get(prefix);
-                if (namespace != null) {
-                    return namespace;
-                }
-            }
-        }
-        // check if namespace is a resolver
-        namespace = ns.resolveNamespace(prefix);
-        if (namespace == null) {
-            namespace = functions.get(prefix);
-            if (prefix != null && namespace == null) {
-                throw new JexlException(node, "no such function namespace " + prefix, null);
-            }
-        }
-        // shortcut if ns is known to be not-a-functor
-        final boolean cacheable = cache;
-        Object cached = cacheable ? node.jjtGetValue() : null;
-        if (cached != JexlContext.NamespaceFunctor.class) {
-            // allow namespace to instantiate a functor with context if possible, not an error otherwise
-            Object functor = null;
-            if (namespace instanceof JexlContext.NamespaceFunctor) {
-                functor = ((JexlContext.NamespaceFunctor) namespace).createFunctor(context);
-            } else if (namespace instanceof Class<?> || namespace instanceof String) {
-                // attempt to reuse last ctor cached in volatile JexlNode.value
-                if (cached instanceof JexlMethod) {
-                    Object eval = ((JexlMethod) cached).tryInvoke(null, context);
-                    if (JexlEngine.TRY_FAILED != eval) {
-                        functor = eval;
-                    }
-                }
-                if (functor == null) {
-                    JexlMethod ctor = uberspect.getConstructor(namespace, context);
-                    if (ctor != null) {
-                        try {
-                            functor = ctor.invoke(namespace, context);
-                            if (cacheable && ctor.isCacheable()) {
-                                node.jjtSetValue(ctor);
-                            }
-                        } catch (Exception xinst) {
-                            throw new JexlException(node, "unable to instantiate namespace " + prefix, xinst);
-                        }
-                    }
-                }
-            }
-            // got a functor, store it and return it
-            if (functor != null) {
-                synchronized (this) {
-                    if (functors == null) {
-                        functors = new HashMap<String, Object>();
-                    }
-                    functors.put(prefix, functor);
-                }
-                return functor;
-            } else {
-                // use the NamespaceFunctor class to tag this node as not-a-functor
-                node.jjtSetValue(JexlContext.NamespaceFunctor.class);
-            }
-        }
-        return namespace;
+    public Object getAttribute(Object object, Object attribute) {
+        return getAttribute(object, attribute, null);
     }
-
+    /**
+     * Sets an attribute of an object.
+     *
+     * @param object    to set the value to
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
+     * @param value     the value to assign to the object's attribute
+     */
+    public void setAttribute(Object object, Object attribute, Object value) {
+        setAttribute(object, attribute, value, null);
+    }
+    
     @Override
     protected Object visit(ASTAddNode node, Object data) {
         Object left = node.jjtGetChild(0).jjtAccept(this, data);
@@ -1183,7 +1105,7 @@ public class Interpreter extends InterpreterBase {
                             break main;
                         }
                     }
-                    // catch up
+                    // catch up to current node
                     for (; v <= c; ++v) {
                         JexlNode child = node.jjtGetChild(v);
                         if (child instanceof ASTIdentifierAccess) {
@@ -1192,13 +1114,14 @@ public class Interpreter extends InterpreterBase {
                                 break main;
                             }
                             ant.append('.');
-                            ant.append(((ASTIdentifierAccess) objectNode).getName());
+                            ant.append(achild.getName());
                         } else {
                             // not an identifier, not antish
                             ptyNode = objectNode;
                             break main;
                         }
                     }
+                    // solve antish
                     object = context.get(ant.toString());
                 }
             } else if (c != numChildren - 1) {
@@ -1344,30 +1267,43 @@ public class Interpreter extends InterpreterBase {
         StringBuilder ant = null;
         int v = 1;
         // start at 1 if symbol
-        for (int c = symbol >= 0 ? 1 : 0; c < last; ++c) {
+        main: for (int c = symbol >= 0 ? 1 : 0; c < last; ++c) {
             objectNode = left.jjtGetChild(c);
             object = objectNode.jjtAccept(this, object);
             if (object != null) {
                 // disallow mixing antish variable & bean with same root; avoid ambiguity
                 antish = false;
             } else if (antish) {
+                // initialize if first time
                 if (ant == null) {
                     JexlNode first = left.jjtGetChild(0);
-                    if (first instanceof ASTIdentifier && ((ASTIdentifier) first).getSymbol() < 0) {
-                        ant = new StringBuilder(((ASTIdentifier) first).getName());
+                    ASTIdentifier firstId = first instanceof ASTIdentifier
+                            ? (ASTIdentifier) first
+                            : null;
+                    if (firstId != null && firstId.getSymbol() < 0) {
+                        ant = new StringBuilder(firstId.getName());
                     } else {
-                        break;
+                        // ant remains null, object is null, stop solving
+                        antish = false;
+                        break main;
                     }
                 }
+                // catch up to current child
                 for (; v <= c; ++v) {
                     JexlNode child = left.jjtGetChild(v);
-                    if (child instanceof ASTIdentifierAccess) {
+                    ASTIdentifierAccess aid = child instanceof ASTIdentifierAccess
+                            ? (ASTIdentifierAccess) child
+                            : null;
+                    // remain antish only if unsafe navigation
+                    if (aid != null && !aid.isSafe() && !aid.isExpression()) {
                         ant.append('.');
-                        ant.append(((ASTIdentifierAccess) objectNode).getName());
+                        ant.append(aid.getName());
                     } else {
-                        break;
+                        antish = false;
+                        break main;
                     }
                 }
+                // solve antish
                 object = context.get(ant.toString());
             } else {
                 throw new JexlException(objectNode, "illegal assignment form");
@@ -1376,14 +1312,16 @@ public class Interpreter extends InterpreterBase {
         // 2: last objectNode will perform assignement in all cases
         Object property = null;
         JexlNode propertyNode = left.jjtGetChild(last);
-        if (propertyNode instanceof ASTIdentifierAccess) {
-            property = evalIdentifier((ASTIdentifierAccess) propertyNode);
-            // deal with antish variable
-            if (ant != null && object == null) {
+        ASTIdentifierAccess propertyId = propertyNode instanceof ASTIdentifierAccess
+                ? (ASTIdentifierAccess) propertyNode
+                : null;
+        if (propertyId != null) {
+            // deal with creating/assignining antish variable
+            if (antish && ant != null && object == null && !propertyId.isSafe() && !propertyId.isExpression()) {
                 if (last > 0) {
                     ant.append('.');
                 }
-                ant.append(String.valueOf(property));
+                ant.append(propertyId.getName());
                 if (assignop != null) {
                     Object self = context.get(ant.toString());
                     right = operators.tryAssignOverload(node, assignop, self, right);
@@ -1398,6 +1336,8 @@ public class Interpreter extends InterpreterBase {
                 }
                 return right; // 3
             }
+            // property of an object ?
+            property = evalIdentifier(propertyId);
         } else if (propertyNode instanceof ASTArrayAccess) {
             // can have multiple nodes - either an expression, integer literal or reference
             int numChildren = propertyNode.jjtGetNumChildren() - 1;
@@ -1747,156 +1687,6 @@ public class Interpreter extends InterpreterBase {
         }
     }
 
-    /**
-     * Gets an attribute of an object.
-     *
-     * @param object    to retrieve value from
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
-     * @return the attribute value
-     */
-    public Object getAttribute(Object object, Object attribute) {
-        return getAttribute(object, attribute, null);
-    }
-
-    /**
-     * Gets an attribute of an object.
-     *
-     * @param object    to retrieve value from
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
-     * @param node      the node that evaluated as the object
-     * @return the attribute value
-     */
-    protected Object getAttribute(Object object, Object attribute, JexlNode node) {
-        if (object == null) {
-            throw new JexlException(node, "object is null");
-        }
-        cancelCheck(node);
-        final JexlOperator operator = node != null && node.jjtGetParent() instanceof ASTArrayAccess
-                ? JexlOperator.ARRAY_GET : JexlOperator.PROPERTY_GET;
-        Object result = operators.tryOverload(node, operator, object, attribute);
-        if (result != JexlEngine.TRY_FAILED) {
-            return result;
-        }
-        Exception xcause = null;
-        try {
-            // attempt to reuse last executor cached in volatile JexlNode.value
-            if (node != null && cache) {
-                Object cached = node.jjtGetValue();
-                if (cached instanceof JexlPropertyGet) {
-                    JexlPropertyGet vg = (JexlPropertyGet) cached;
-                    Object value = vg.tryInvoke(object, attribute);
-                    if (!vg.tryFailed(value)) {
-                        return value;
-                    }
-                }
-            }
-            // resolve that property
-            List<PropertyResolver> resolvers = uberspect.getResolvers(operator, object);
-            JexlPropertyGet vg = uberspect.getPropertyGet(resolvers, object, attribute);
-            if (vg != null) {
-                Object value = vg.invoke(object);
-                // cache executor in volatile JexlNode.value
-                if (node != null && cache && vg.isCacheable()) {
-                    node.jjtSetValue(vg);
-                }
-                return value;
-            }
-        } catch (Exception xany) {
-            xcause = xany;
-        }
-        // lets fail
-        if (node != null) {
-            boolean safe = (node instanceof ASTIdentifierAccess) && ((ASTIdentifierAccess) node).isSafe();
-            if (safe) {
-                return null;
-            } else {
-                String attrStr = attribute != null ? attribute.toString() : null;
-                return unsolvableProperty(node, attrStr, true, xcause);
-            }
-        } else {
-            // direct call
-            String error = "unable to get object property"
-                    + ", class: " + object.getClass().getName()
-                    + ", property: " + attribute;
-            throw new UnsupportedOperationException(error, xcause);
-        }
-    }
-
-    /**
-     * Sets an attribute of an object.
-     *
-     * @param object    to set the value to
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
-     * @param value     the value to assign to the object's attribute
-     */
-    public void setAttribute(Object object, Object attribute, Object value) {
-        setAttribute(object, attribute, value, null);
-    }
-
-    /**
-     * Sets an attribute of an object.
-     *
-     * @param object    to set the value to
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
-     * @param value     the value to assign to the object's attribute
-     * @param node      the node that evaluated as the object
-     */
-    protected void setAttribute(Object object, Object attribute, Object value, JexlNode node) {
-        cancelCheck(node);
-        final JexlOperator operator = node != null && node.jjtGetParent() instanceof ASTArrayAccess
-                                      ? JexlOperator.ARRAY_SET : JexlOperator.PROPERTY_SET;
-        Object result = operators.tryOverload(node, operator, object, attribute, value);
-        if (result != JexlEngine.TRY_FAILED) {
-            return;
-        }
-        Exception xcause = null;
-        try {
-            // attempt to reuse last executor cached in volatile JexlNode.value
-            if (node != null && cache) {
-                Object cached = node.jjtGetValue();
-                if (cached instanceof JexlPropertySet) {
-                    JexlPropertySet setter = (JexlPropertySet) cached;
-                    Object eval = setter.tryInvoke(object, attribute, value);
-                    if (!setter.tryFailed(eval)) {
-                        return;
-                    }
-                }
-            }
-            List<PropertyResolver> resolvers = uberspect.getResolvers(operator, object);
-            JexlPropertySet vs = uberspect.getPropertySet(resolvers, object, attribute, value);
-            // if we can't find an exact match, narrow the value argument and try again
-            if (vs == null) {
-                // replace all numbers with the smallest type that will fit
-                Object[] narrow = {value};
-                if (arithmetic.narrowArguments(narrow)) {
-                    vs = uberspect.getPropertySet(resolvers, object, attribute, narrow[0]);
-                }
-            }
-            if (vs != null) {
-                // cache executor in volatile JexlNode.value
-                vs.invoke(object, value);
-                if (node != null && cache && vs.isCacheable()) {
-                    node.jjtSetValue(vs);
-                }
-                return;
-            }
-        } catch (Exception xany) {
-            xcause = xany;
-        }
-        // lets fail
-        if (node != null) {
-            String attrStr = attribute != null ? attribute.toString() : null;
-            unsolvableProperty(node, attrStr, true, xcause);
-        } else {
-            // direct call
-            String error = "unable to set object property"
-                    + ", class: " + object.getClass().getName()
-                    + ", property: " + attribute
-                    + ", argument: " + value.getClass().getSimpleName();
-            throw new UnsupportedOperationException(error, xcause);
-        }
-    }
-
     @Override
     protected Object visit(ASTJxltLiteral node, Object data) {
         TemplateEngine.TemplateExpression tp = (TemplateEngine.TemplateExpression) node.jjtGetValue();
diff --git a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
index 3c473c2..c2669e0 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
@@ -17,16 +17,22 @@
 package org.apache.commons.jexl3.internal;
 
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import org.apache.commons.jexl3.JexlArithmetic;
 import org.apache.commons.jexl3.JexlContext;
 import org.apache.commons.jexl3.JexlEngine;
 import org.apache.commons.jexl3.JexlException;
 import org.apache.commons.jexl3.JexlOperator;
 import org.apache.commons.jexl3.introspection.JexlMethod;
+import org.apache.commons.jexl3.introspection.JexlPropertyGet;
+import org.apache.commons.jexl3.introspection.JexlPropertySet;
 import org.apache.commons.jexl3.introspection.JexlUberspect;
 import org.apache.commons.jexl3.parser.ASTArrayAccess;
 import org.apache.commons.jexl3.parser.ASTFunctionNode;
 import org.apache.commons.jexl3.parser.ASTIdentifier;
+import org.apache.commons.jexl3.parser.ASTIdentifierAccess;
 import org.apache.commons.jexl3.parser.ASTMethodNode;
 import org.apache.commons.jexl3.parser.ASTReference;
 import org.apache.commons.jexl3.parser.JexlNode;
@@ -56,6 +62,14 @@ public abstract class InterpreterBase extends ParserVisitor {
     protected volatile boolean cancelled = false;
     /** Empty parameters for method matching. */
     protected static final Object[] EMPTY_PARAMS = new Object[0];
+    /** The context to store/retrieve variables. */
+    protected final JexlContext.NamespaceResolver ns;
+    /** The map of 'prefix:function' to object resolving as namespaces. */
+    protected final Map<String, Object> functions;
+    /** The map of dynamically creates namespaces, NamespaceFunctor or duck-types of those. */
+    protected Map<String, Object> functors;
+    /** The operators evaluation delegate. */
+    protected final Operators operators;
 
     /**
      * Creates an interpreter base.
@@ -75,6 +89,14 @@ public abstract class InterpreterBase extends ParserVisitor {
                           + ", got " + arithmetic.getClass().getSimpleName()
             );
         }
+        if (this.context instanceof JexlContext.NamespaceResolver) {
+            ns = ((JexlContext.NamespaceResolver) context);
+        } else {
+            ns = Engine.EMPTY_NS;
+        }
+        this.functions = jexl.functions;
+        this.functors = null;
+        this.operators = new Operators(this);
     }
 
     /**
@@ -89,6 +111,10 @@ public abstract class InterpreterBase extends ParserVisitor {
         context = ii.context;
         arithmetic = ii.arithmetic;
         cache = ii.cache;
+        ns = ii.ns;
+        functions = ii.functions;
+        functors = ii.functors;
+        operators = ii.operators;
     }
 
 
@@ -125,6 +151,80 @@ public abstract class InterpreterBase extends ParserVisitor {
     }
               
     /**
+     * Resolves a namespace, eventually allocating an instance using context as constructor argument.
+     * <p>
+     * The lifetime of such instances span the current expression or script evaluation.</p>
+     * @param prefix the prefix name (may be null for global namespace)
+     * @param node   the AST node
+     * @return the namespace instance
+     */
+    protected Object resolveNamespace(String prefix, JexlNode node) {
+        Object namespace;
+        // check whether this namespace is a functor
+        synchronized (this) {
+            if (functors != null) {
+                namespace = functors.get(prefix);
+                if (namespace != null) {
+                    return namespace;
+                }
+            }
+        }
+        // check if namespace is a resolver
+        namespace = ns.resolveNamespace(prefix);
+        if (namespace == null) {
+            namespace = functions.get(prefix);
+            if (prefix != null && namespace == null) {
+                throw new JexlException(node, "no such function namespace " + prefix, null);
+            }
+        }
+        // shortcut if ns is known to be not-a-functor
+        final boolean cacheable = cache;
+        Object cached = cacheable ? node.jjtGetValue() : null;
+        if (cached != JexlContext.NamespaceFunctor.class) {
+            // allow namespace to instantiate a functor with context if possible, not an error otherwise
+            Object functor = null;
+            if (namespace instanceof JexlContext.NamespaceFunctor) {
+                functor = ((JexlContext.NamespaceFunctor) namespace).createFunctor(context);
+            } else if (namespace instanceof Class<?> || namespace instanceof String) {
+                // attempt to reuse last ctor cached in volatile JexlNode.value
+                if (cached instanceof JexlMethod) {
+                    Object eval = ((JexlMethod) cached).tryInvoke(null, context);
+                    if (JexlEngine.TRY_FAILED != eval) {
+                        functor = eval;
+                    }
+                }
+                if (functor == null) {
+                    JexlMethod ctor = uberspect.getConstructor(namespace, context);
+                    if (ctor != null) {
+                        try {
+                            functor = ctor.invoke(namespace, context);
+                            if (cacheable && ctor.isCacheable()) {
+                                node.jjtSetValue(ctor);
+                            }
+                        } catch (Exception xinst) {
+                            throw new JexlException(node, "unable to instantiate namespace " + prefix, xinst);
+                        }
+                    }
+                }
+            }
+            // got a functor, store it and return it
+            if (functor != null) {
+                synchronized (this) {
+                    if (functors == null) {
+                        functors = new HashMap<String, Object>();
+                    }
+                    functors.put(prefix, functor);
+                }
+                return functor;
+            } else {
+                // use the NamespaceFunctor class to tag this node as not-a-functor
+                node.jjtSetValue(JexlContext.NamespaceFunctor.class);
+            }
+        }
+        return namespace;
+    }
+
+    /**
      * Gets a value of a defined local variable or from the context.
      * @param frame the local frame
      * @param node the variable node
@@ -693,4 +793,132 @@ public abstract class InterpreterBase extends ParserVisitor {
             return unsolvableMethod(node, mname, argv);
         }
     }
+
+    /**
+     * Gets an attribute of an object.
+     *
+     * @param object    to retrieve value from
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
+     * @param node      the node that evaluated as the object
+     * @return the attribute value
+     */
+    protected Object getAttribute(Object object, Object attribute, JexlNode node) {
+        if (object == null) {
+            throw new JexlException(node, "object is null");
+        }
+        cancelCheck(node);
+        final JexlOperator operator = node != null && node.jjtGetParent() instanceof ASTArrayAccess
+                ? JexlOperator.ARRAY_GET : JexlOperator.PROPERTY_GET;
+        Object result = operators.tryOverload(node, operator, object, attribute);
+        if (result != JexlEngine.TRY_FAILED) {
+            return result;
+        }
+        Exception xcause = null;
+        try {
+            // attempt to reuse last executor cached in volatile JexlNode.value
+            if (node != null && cache) {
+                Object cached = node.jjtGetValue();
+                if (cached instanceof JexlPropertyGet) {
+                    JexlPropertyGet vg = (JexlPropertyGet) cached;
+                    Object value = vg.tryInvoke(object, attribute);
+                    if (!vg.tryFailed(value)) {
+                        return value;
+                    }
+                }
+            }
+            // resolve that property
+            List<JexlUberspect.PropertyResolver> resolvers = uberspect.getResolvers(operator, object);
+            JexlPropertyGet vg = uberspect.getPropertyGet(resolvers, object, attribute);
+            if (vg != null) {
+                Object value = vg.invoke(object);
+                // cache executor in volatile JexlNode.value
+                if (node != null && cache && vg.isCacheable()) {
+                    node.jjtSetValue(vg);
+                }
+                return value;
+            }
+        } catch (Exception xany) {
+            xcause = xany;
+        }
+        // lets fail
+        if (node != null) {
+            boolean safe = (node instanceof ASTIdentifierAccess) && ((ASTIdentifierAccess) node).isSafe();
+            if (safe) {
+                return null;
+            } else {
+                String attrStr = attribute != null ? attribute.toString() : null;
+                return unsolvableProperty(node, attrStr, true, xcause);
+            }
+        } else {
+            // direct call
+            String error = "unable to get object property"
+                    + ", class: " + object.getClass().getName()
+                    + ", property: " + attribute;
+            throw new UnsupportedOperationException(error, xcause);
+        }
+    }
+
+    /**
+     * Sets an attribute of an object.
+     *
+     * @param object    to set the value to
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
+     * @param value     the value to assign to the object's attribute
+     * @param node      the node that evaluated as the object
+     */
+    protected void setAttribute(Object object, Object attribute, Object value, JexlNode node) {
+        cancelCheck(node);
+        final JexlOperator operator = node != null && node.jjtGetParent() instanceof ASTArrayAccess
+                                      ? JexlOperator.ARRAY_SET : JexlOperator.PROPERTY_SET;
+        Object result = operators.tryOverload(node, operator, object, attribute, value);
+        if (result != JexlEngine.TRY_FAILED) {
+            return;
+        }
+        Exception xcause = null;
+        try {
+            // attempt to reuse last executor cached in volatile JexlNode.value
+            if (node != null && cache) {
+                Object cached = node.jjtGetValue();
+                if (cached instanceof JexlPropertySet) {
+                    JexlPropertySet setter = (JexlPropertySet) cached;
+                    Object eval = setter.tryInvoke(object, attribute, value);
+                    if (!setter.tryFailed(eval)) {
+                        return;
+                    }
+                }
+            }
+            List<JexlUberspect.PropertyResolver> resolvers = uberspect.getResolvers(operator, object);
+            JexlPropertySet vs = uberspect.getPropertySet(resolvers, object, attribute, value);
+            // if we can't find an exact match, narrow the value argument and try again
+            if (vs == null) {
+                // replace all numbers with the smallest type that will fit
+                Object[] narrow = {value};
+                if (arithmetic.narrowArguments(narrow)) {
+                    vs = uberspect.getPropertySet(resolvers, object, attribute, narrow[0]);
+                }
+            }
+            if (vs != null) {
+                // cache executor in volatile JexlNode.value
+                vs.invoke(object, value);
+                if (node != null && cache && vs.isCacheable()) {
+                    node.jjtSetValue(vs);
+                }
+                return;
+            }
+        } catch (Exception xany) {
+            xcause = xany;
+        }
+        // lets fail
+        if (node != null) {
+            String attrStr = attribute != null ? attribute.toString() : null;
+            unsolvableProperty(node, attrStr, true, xcause);
+        } else {
+            // direct call
+            String error = "unable to set object property"
+                    + ", class: " + object.getClass().getName()
+                    + ", property: " + attribute
+                    + ", argument: " + value.getClass().getSimpleName();
+            throw new UnsupportedOperationException(error, xcause);
+        }
+    }
 }
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Operators.java b/src/main/java/org/apache/commons/jexl3/internal/Operators.java
index a4ab493..5efd656 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Operators.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Operators.java
@@ -30,7 +30,7 @@ import org.apache.commons.jexl3.parser.JexlNode;
  */
 public class Operators {
     /** The owner. */
-    protected final Interpreter interpreter;
+    protected final InterpreterBase interpreter;
     /** The overloaded arithmetic operators. */
     protected final JexlArithmetic.Uberspect operators;
 
@@ -38,7 +38,7 @@ public class Operators {
      * Constructor.
      * @param owner the owning interpreter
      */
-    protected Operators(Interpreter owner) {
+    protected Operators(InterpreterBase owner) {
         final JexlArithmetic arithmetic = owner.arithmetic;
         final JexlUberspect uberspect = owner.uberspect;
         this.interpreter = owner;
diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml
index 7c79851..29416bd 100644
--- a/src/site/xdoc/changes.xml
+++ b/src/site/xdoc/changes.xml
@@ -26,6 +26,9 @@
     </properties>
     <body>
         <release version="3.2" date="unreleased">
+            <action dev="henrib" type="fix" issue="JEXL-296" due-to="Dmitri Blinov">
+                Ant-ish variables should not use safe-access operator syntax
+            </action>
             <action dev="henrib" type="fix" issue="JEXL-299" due-to="Jarek Cecho">
                 Improve message error when method could not be found
             </action>
diff --git a/src/test/java/org/apache/commons/jexl3/AntishCallTest.java b/src/test/java/org/apache/commons/jexl3/AntishCallTest.java
index 5e98149..058abb4 100644
--- a/src/test/java/org/apache/commons/jexl3/AntishCallTest.java
+++ b/src/test/java/org/apache/commons/jexl3/AntishCallTest.java
@@ -170,5 +170,61 @@ public class AntishCallTest extends JexlTestCase {
         Object o2 = check2.execute(jc);
         Assert.assertEquals("Result is not 4321", new java.math.BigInteger("4321"), o2);
     }
+    
+    // JEXL-300
+    @Test
+    public void testSafeAnt() throws Exception {
+        JexlEngine jexl = new JexlBuilder().strict(true).safe(false).create();
+        JexlContext ctxt = new MapContext();
+        ctxt.set("x.y.z", 42);
+        JexlScript script;
+        Object result;
+        
+        script = jexl.createScript("x.y.z");
+        result = script.execute(ctxt);
+        Assert.assertEquals(42, result);
+        Assert.assertEquals(42, ctxt.get("x.y.z"));
+                
+        result = null;
+        script = jexl.createScript("x?.y?.z");
+        result = script.execute(ctxt);
+        Assert.assertNull(result); // safe navigation, null
+        
+        result = null;
+        script = jexl.createScript("x?.y?.z = 3");
+        try {
+             result = script.execute(ctxt);
+             Assert.fail("not antish assign");
+        } catch(JexlException xjexl) {
+            Assert.assertNull(result);
+        }
+        
+        result = null;
+        script = jexl.createScript("x.y?.z");
+        try {
+             result = script.execute(ctxt);
+             Assert.fail("x not defined");
+        } catch(JexlException xjexl) {
+            Assert.assertNull(result);
+        }
+        
+        result = null;
+        script = jexl.createScript("x.y?.z = 3");
+        try {
+             result = script.execute(ctxt);
+             Assert.fail("not antish assign");
+        } catch(JexlException xjexl) {
+            Assert.assertNull(result);
+        } 
+        
+        result = null;
+        script = jexl.createScript("x.`'y'`.z = 3");
+        try {
+             result = script.execute(ctxt);
+             Assert.fail("not antish assign");
+        } catch(JexlException xjexl) {
+            Assert.assertNull(result);
+        }
+    }
 
 }
\ No newline at end of file