[engine][VELOCITY-915] Add new backward compatibility flags to mimic 1.7 behavior for invalid references event handlers

git-svn-id: https://svn.apache.org/repos/asf/velocity/engine/trunk@1860691 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/app/event/implement/ReportInvalidReferences.java b/velocity-engine-core/src/main/java/org/apache/velocity/app/event/implement/ReportInvalidReferences.java
index 42ffa91..c76aa17 100644
--- a/velocity-engine-core/src/main/java/org/apache/velocity/app/event/implement/ReportInvalidReferences.java
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/app/event/implement/ReportInvalidReferences.java
@@ -25,6 +25,7 @@
 import org.apache.velocity.runtime.RuntimeServices;
 import org.apache.velocity.util.RuntimeServicesAware;
 import org.apache.velocity.util.introspection.Info;
+import org.slf4j.Logger;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -39,7 +40,7 @@
  * Note that InvalidReferenceHandler can be used
  * in two modes.  If the Velocity properties file contains the following:
  * <pre>
- * eventhandler.invalidreference.exception = true
+ * event_handler.invalid_references.exception = true
  * </pre>
  * then the event handler will throw a ParseErrorRuntimeException upon
  * hitting the first invalid reference.  This stops processing and is
@@ -61,8 +62,10 @@
 public class ReportInvalidReferences implements
     InvalidReferenceEventHandler, RuntimeServicesAware
 {
+    public static final String EVENTHANDLER_INVALIDREFERENCE_EXCEPTION = "event_handler.invalid_references.exception";
 
-    public static final String EVENTHANDLER_INVALIDREFERENCE_EXCEPTION = "eventhandler.invalidreference.exception";
+    @Deprecated
+    public static final String OLD_EVENTHANDLER_INVALIDREFERENCE_EXCEPTION = "eventhandler.invalidreference.exception";
 
     /**
      * List of InvalidReferenceInfo objects
@@ -172,9 +175,16 @@
      */
     public void setRuntimeServices(RuntimeServices rs)
     {
-        stopOnFirstInvalidReference = rs.getConfiguration().getBoolean(
-                EVENTHANDLER_INVALIDREFERENCE_EXCEPTION,
-                false);
+        Boolean b = rs.getConfiguration().getBoolean(OLD_EVENTHANDLER_INVALIDREFERENCE_EXCEPTION, null);
+        if (b == null)
+        {
+            b = rs.getConfiguration().getBoolean(EVENTHANDLER_INVALIDREFERENCE_EXCEPTION, false);
+        }
+        else
+        {
+            rs.getLog().warn("configuration key '{}' has been deprecated in favor of '{}'", OLD_EVENTHANDLER_INVALIDREFERENCE_EXCEPTION, EVENTHANDLER_INVALIDREFERENCE_EXCEPTION);
+        }
+        stopOnFirstInvalidReference = b.booleanValue();
     }
 
 }
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
index 2778263..c4d59ef 100644
--- a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
@@ -274,6 +274,30 @@
      */
     String EVENTHANDLER_INVALIDREFERENCES = "event_handler.invalid_references.class";
 
+    /**
+     * The <code>event_handler.invalid_references.quiet</code> property specifies if invalid quiet references
+     * (as in <code>$!foo</code>) trigger events (defaults to false).
+     * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+     * @since 2.2
+     */
+    String EVENTHANDLER_INVALIDREFERENCES_QUIET = "event_handler.invalid_references.quiet";
+
+    /**
+     * The <code>event_handler.invalid_references.null</code> property specifies if invalid null references
+     * (aka the value is present in the context or parent object but is null or a method returned null)
+     * trigger invalid reference events (defaults to false).
+     * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+     * @since 2.2
+     */
+    String EVENTHANDLER_INVALIDREFERENCES_NULL = "event_handler.invalid_references.null";
+
+    /**
+     * The <code>event_handler.invalid_references.tested</code> property specifies if invalid tested references
+     * (as in <code>#if($foo)</code> ) trigger invalid reference events (defaults to false).
+     * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+     * @since 2.2
+     */
+    String EVENTHANDLER_INVALIDREFERENCES_TESTED = "event_handler.invalid_references.tested";
 
     /*
      * ----------------------------------------------------------------------
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java
index 058dff3..e78e146 100644
--- a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java
@@ -102,6 +102,25 @@
 
     private int numChildren = 0;
 
+    /**
+     * Whether to trigger an event for invalid quiet references
+     * @since 2.2
+     */
+    private boolean warnInvalidQuietReferences = false;
+
+    /**
+     * Whether to trigger an event for invalid null references, that is when a value
+     * is present in the context or parent object but is null
+     * @since 2.2
+     */
+    private boolean warnInvalidNullReferences = false;
+
+    /**
+     * Whether to trigger an event for invalid tested references - as in #if($foo)
+     * @since 2.2
+     */
+    private boolean warnInvalidTestedReferences = false;
+
     protected Info uberInfo;
 
     /**
@@ -196,6 +215,16 @@
         checkEmpty =
             rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true);
 
+        /* invalid references special cases */
+
+        warnInvalidQuietReferences =
+            rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_QUIET, false);
+        warnInvalidNullReferences =
+            rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_NULL, false);
+        warnInvalidTestedReferences =
+            rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_TESTED, false);
+
+
         /**
          * In the case we are referencing a variable with #if($foo) or
          * #if( ! $foo) then we allow variables to be undefined and we
@@ -272,23 +301,28 @@
 
             Object result = getRootVariableValue(context);
 
+            /* a reference which has been provided an alternate value
+             * is *knowingly* potentially null and should be accepted
+             * in strict mode (except if the alternate value is null)
+             */
+            if (astAlternateValue != null && (result == null || !DuckType.asBoolean(result, false)))
+            {
+                result = astAlternateValue.value(context);
+            }
+
             if (result == null && !strictRef)
             {
                 /*
                  * do not trigger an invalid reference if the reference is present, but with a null value
                  * don't either for a quiet reference or inside an #if/#elseif evaluation context
                  */
-                if (referenceType != QUIET_REFERENCE  &&
-                        (numChildren > 0 ||
-                                !context.containsKey(rootString) && !onlyTestingReference))
+                if ((referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) &&
+                    (numChildren > 0 ||
+                        (!context.containsKey(rootString) || warnInvalidNullReferences) &&
+                            (!onlyTestingReference || warnInvalidTestedReferences)))
                 {
                     result = EventHandlerUtil.invalidGetMethod(rsvc, context,
-                            "$" + rootString, null, null, uberInfo);
-                }
-
-                if (result == null && astAlternateValue != null)
-                {
-                    result = astAlternateValue.value(context);
+                        "$" + rootString, null, null, uberInfo);
                 }
 
                 return result;
@@ -311,6 +345,7 @@
             {
                 Object previousResult = result;
                 int failedChild = -1;
+
                 for (int i = 0; i < numChildren; i++)
                 {
                     if (strictRef && result == null)
@@ -327,6 +362,10 @@
                     }
                     previousResult = result;
                     result = jjtGetChild(i).execute(result,context);
+                    if (astAlternateValue != null && (result == null || !DuckType.asBoolean(result, checkEmpty)))
+                    {
+                        result = astAlternateValue.value(context);
+                    }
                     if (result == null && !strictRef)  // If strict and null then well catch this
                                                        // next time through the loop
                     {
@@ -344,7 +383,9 @@
                          * don't either for a quiet reference,
                          * or inside an #if/#elseif evaluation context when there's no child
                          */
-                        if (!context.containsKey(rootString) && referenceType != QUIET_REFERENCE && (!onlyTestingReference || numChildren > 0))
+                        if ((!context.containsKey(rootString) || warnInvalidNullReferences) &&
+                            (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) &&
+                            (!onlyTestingReference || warnInvalidTestedReferences || numChildren > 0))
                         {
                             result = EventHandlerUtil.invalidGetMethod(rsvc, context,
                                     "$" + rootString, previousResult, null, uberInfo);
@@ -357,9 +398,9 @@
                         // (it means the getter has been called and returned null)
                         // do not either for a quiet reference or if the *last* child failed while testing the reference
                         Object getter = context.icacheGet(child);
-                        if (getter == null &&
-                            referenceType != QUIET_REFERENCE  &&
-                            (!onlyTestingReference || failedChild < numChildren - 1))
+                        if ((getter == null || warnInvalidNullReferences) &&
+                            (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) &&
+                            (!onlyTestingReference || warnInvalidTestedReferences || failedChild < numChildren - 1))
                         {
                             StringBuilder name = new StringBuilder("$").append(rootString);
                             for (int i = 0; i <= failedChild; i++)
@@ -391,14 +432,6 @@
                     }
                 }
 
-                /*
-                 * Time to try the alternate value if needed
-                 */
-                if (astAlternateValue != null && (result == null || !DuckType.asBoolean(result, checkEmpty)))
-                {
-                    result = astAlternateValue.value(context);
-                }
-
                 return result;
             }
             catch(MethodInvocationException mie)
diff --git a/velocity-engine-core/src/test/java/org/apache/velocity/test/BuiltInEventHandlerTestCase.java b/velocity-engine-core/src/test/java/org/apache/velocity/test/BuiltInEventHandlerTestCase.java
index 22d5c38..ad94641 100644
--- a/velocity-engine-core/src/test/java/org/apache/velocity/test/BuiltInEventHandlerTestCase.java
+++ b/velocity-engine-core/src/test/java/org/apache/velocity/test/BuiltInEventHandlerTestCase.java
@@ -128,9 +128,10 @@
 
         context.put("a1","test");
         context.put("b1","test");
+        context.put("n1", null);
         Writer writer = new StringWriter();
 
-        ve.evaluate(context,writer,"test","$a1 $c1 $a1.length() $a1.foobar()");
+        ve.evaluate(context,writer,"test","$a1 $c1 $a1.length() $a1.foobar() $!c1 $n1 $!n1 #if($c1) nop #end");
 
         List errors = reporter.getInvalidReferences();
         assertEquals(2,errors.size());
@@ -143,7 +144,7 @@
     public void testReportInvalidReferences2() throws Exception
     {
         VelocityEngine ve = new VelocityEngine();
-        ve.setProperty("eventhandler.invalidreference.exception","true");
+        ve.setProperty("event_handler.invalid_references.exception","true");
         ReportInvalidReferences reporter = new ReportInvalidReferences();
         ve.init();
 
@@ -169,6 +170,137 @@
     }
 
     /**
+     * Test reporting of invalid syntax
+     * @throws Exception
+     */
+    public void testReportQuietInvalidReferences() throws Exception
+    {
+        VelocityEngine ve = new VelocityEngine();
+        ve.setProperty("event_handler.invalid_references.quiet","true");
+        ReportInvalidReferences reporter = new ReportInvalidReferences();
+        ve.init();
+
+        VelocityContext context = new VelocityContext();
+        EventCartridge ec = new EventCartridge();
+        ec.addEventHandler(reporter);
+        ec.attachToContext(context);
+
+        context.put("a1","test");
+        context.put("b1","test");
+        context.put("n1", null);
+        Writer writer = new StringWriter();
+
+        ve.evaluate(context,writer,"test","$a1 $c1 $a1.length() $a1.foobar() $!c1 $n1 $!n1 #if($c1) nop #end");
+
+        List errors = reporter.getInvalidReferences();
+        assertEquals(3,errors.size());
+        assertEquals("$c1",((InvalidReferenceInfo) errors.get(0)).getInvalidReference());
+        assertEquals("$a1.foobar()",((InvalidReferenceInfo) errors.get(1)).getInvalidReference());
+        assertEquals("$c1",((InvalidReferenceInfo) errors.get(2)).getInvalidReference());
+
+        log("Caught invalid references (local configuration).");
+    }
+
+    /**
+     * Test reporting of invalid syntax
+     * @throws Exception
+     */
+    public void testReportNullInvalidReferences() throws Exception
+    {
+        VelocityEngine ve = new VelocityEngine();
+        ve.setProperty("event_handler.invalid_references.null","true");
+        ReportInvalidReferences reporter = new ReportInvalidReferences();
+        ve.init();
+
+        VelocityContext context = new VelocityContext();
+        EventCartridge ec = new EventCartridge();
+        ec.addEventHandler(reporter);
+        ec.attachToContext(context);
+
+        context.put("a1","test");
+        context.put("b1","test");
+        context.put("n1", null);
+        Writer writer = new StringWriter();
+
+        ve.evaluate(context,writer,"test","$a1 $c1 $a1.length() $a1.foobar() $!c1 $n1 $!n1 #if($c1) nop #end");
+
+        List errors = reporter.getInvalidReferences();
+        assertEquals(3,errors.size());
+        assertEquals("$c1",((InvalidReferenceInfo) errors.get(0)).getInvalidReference());
+        assertEquals("$a1.foobar()",((InvalidReferenceInfo) errors.get(1)).getInvalidReference());
+        assertEquals("$n1",((InvalidReferenceInfo) errors.get(2)).getInvalidReference());
+
+        log("Caught invalid references (local configuration).");
+    }
+
+    /**
+     * Test reporting of invalid syntax
+     * @throws Exception
+     */
+    public void testReportNullQuietInvalidReferences() throws Exception
+    {
+        VelocityEngine ve = new VelocityEngine();
+        ve.setProperty("event_handler.invalid_references.quiet","true");
+        ve.setProperty("event_handler.invalid_references.null","true");
+        ReportInvalidReferences reporter = new ReportInvalidReferences();
+        ve.init();
+
+        VelocityContext context = new VelocityContext();
+        EventCartridge ec = new EventCartridge();
+        ec.addEventHandler(reporter);
+        ec.attachToContext(context);
+
+        context.put("a1","test");
+        context.put("b1","test");
+        context.put("n1", null);
+        Writer writer = new StringWriter();
+
+        ve.evaluate(context,writer,"test","$a1 $c1 $a1.length() $a1.foobar() $!c1 $n1 $!n1 #if($c1) nop #end");
+
+        List errors = reporter.getInvalidReferences();
+        assertEquals(5,errors.size());
+        assertEquals("$c1",((InvalidReferenceInfo) errors.get(0)).getInvalidReference());
+        assertEquals("$a1.foobar()",((InvalidReferenceInfo) errors.get(1)).getInvalidReference());
+        assertEquals("$c1",((InvalidReferenceInfo) errors.get(2)).getInvalidReference());
+        assertEquals("$n1",((InvalidReferenceInfo) errors.get(3)).getInvalidReference());
+        assertEquals("$n1",((InvalidReferenceInfo) errors.get(4)).getInvalidReference());
+
+        log("Caught invalid references (local configuration).");
+    }
+
+    /**
+     * Test reporting of invalid syntax
+     * @throws Exception
+     */
+    public void testReportTestedInvalidReferences() throws Exception
+    {
+        VelocityEngine ve = new VelocityEngine();
+        ve.setProperty("event_handler.invalid_references.tested","true");
+        ReportInvalidReferences reporter = new ReportInvalidReferences();
+        ve.init();
+
+        VelocityContext context = new VelocityContext();
+        EventCartridge ec = new EventCartridge();
+        ec.addEventHandler(reporter);
+        ec.attachToContext(context);
+
+        context.put("a1","test");
+        context.put("b1","test");
+        context.put("n1", null);
+        Writer writer = new StringWriter();
+
+        ve.evaluate(context,writer,"test","$a1 $c1 $a1.length() $a1.foobar() $!c1 $n1 $!n1 #if($c1) nop #end");
+
+        List errors = reporter.getInvalidReferences();
+        assertEquals(3,errors.size());
+        assertEquals("$c1",((InvalidReferenceInfo) errors.get(0)).getInvalidReference());
+        assertEquals("$a1.foobar()",((InvalidReferenceInfo) errors.get(1)).getInvalidReference());
+        assertEquals("$c1",((InvalidReferenceInfo) errors.get(2)).getInvalidReference());
+
+        log("Caught invalid references (local configuration).");
+    }
+
+    /**
      * Test escaping
      * @throws Exception
      */