This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/11.0.x by this push:
     new a198aaea91 Review "Correct equals() implementations in method 
expression classes"
a198aaea91 is described below

commit a198aaea9114ebd685010f5e08fd1aa74a84c2dd
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Nov 5 12:52:37 2026 +0000

    Review "Correct equals() implementations in method expression classes"
    
    Fix various additional edge cases for method expression equality
---
 .../servlet/jsp/el/ImplicitObjectELResolver.java   |  11 +-
 java/org/apache/el/MethodExpressionImpl.java       |   6 +-
 java/org/apache/el/ValueExpressionImpl.java        |   6 +-
 java/org/apache/el/lang/FunctionMapperImpl.java    |   7 +-
 java/org/apache/el/util/ExpressionUtils.java       |  93 +++++++++++++
 .../jsp/el/TestImplicitObjectELResolver.java       | 152 +++++++++++++++++++++
 test/org/apache/el/TestMethodExpressionImpl.java   | 117 ++++++++++++++++
 test/org/apache/el/TestValueExpressionImpl.java    |  43 ++++++
 .../org/apache/el/lang/TestFunctionMapperImpl.java | 104 ++++++++++++++
 9 files changed, 530 insertions(+), 9 deletions(-)

diff --git a/java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java 
b/java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java
index ebb791d68e..f5e4f34974 100644
--- a/java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java
+++ b/java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java
@@ -487,12 +487,19 @@ public class ImplicitObjectELResolver extends ELResolver {
 
             @Override
             public boolean equals(Object obj) {
-                return obj != null && this.hashCode() == obj.hashCode();
+                if (this == obj) {
+                    return true;
+                }
+                if (!(obj instanceof Map.Entry<?,?> other)) {
+                    return false;
+                }
+                return Objects.equals(this.key, other.getKey()) && 
Objects.equals(getValue(), other.getValue());
             }
 
             @Override
             public int hashCode() {
-                return this.key.hashCode();
+                // As required by Map.Entry
+                return Objects.hashCode(this.key) ^ 
Objects.hashCode(getValue());
             }
 
         }
diff --git a/java/org/apache/el/MethodExpressionImpl.java 
b/java/org/apache/el/MethodExpressionImpl.java
index 00c1ee7fcb..ec75d0eabb 100644
--- a/java/org/apache/el/MethodExpressionImpl.java
+++ b/java/org/apache/el/MethodExpressionImpl.java
@@ -35,6 +35,7 @@ import jakarta.el.VariableMapper;
 import org.apache.el.lang.EvaluationContext;
 import org.apache.el.lang.ExpressionBuilder;
 import org.apache.el.parser.Node;
+import org.apache.el.util.ExpressionUtils;
 import org.apache.el.util.ReflectionUtil;
 
 
@@ -139,7 +140,8 @@ public final class MethodExpressionImpl extends 
MethodExpression implements Exte
             return false;
         }
 
-        return this.getNode().equals(((MethodExpressionImpl) obj).getNode());
+        MethodExpressionImpl other = (MethodExpressionImpl) obj;
+        return ExpressionUtils.equals(getNode(), fnMapper, other.getNode(), 
other.fnMapper);
     }
 
     /**
@@ -218,7 +220,7 @@ public final class MethodExpressionImpl extends 
MethodExpression implements Exte
      */
     @Override
     public int hashCode() {
-        return this.getNode().hashCode();
+        return ExpressionUtils.hashCode(getNode(), fnMapper);
     }
 
     /**
diff --git a/java/org/apache/el/ValueExpressionImpl.java 
b/java/org/apache/el/ValueExpressionImpl.java
index a6a46c3321..def44a0b16 100644
--- a/java/org/apache/el/ValueExpressionImpl.java
+++ b/java/org/apache/el/ValueExpressionImpl.java
@@ -35,6 +35,7 @@ import org.apache.el.lang.EvaluationContext;
 import org.apache.el.lang.ExpressionBuilder;
 import org.apache.el.parser.AstLiteralExpression;
 import org.apache.el.parser.Node;
+import org.apache.el.util.ExpressionUtils;
 import org.apache.el.util.ReflectionUtil;
 
 
@@ -122,7 +123,8 @@ public final class ValueExpressionImpl extends 
ValueExpression implements Extern
             return false;
         }
 
-        return this.getNode().equals(((ValueExpressionImpl) obj).getNode());
+        ValueExpressionImpl other = (ValueExpressionImpl) obj;
+        return ExpressionUtils.equals(getNode(), fnMapper, other.getNode(), 
other.fnMapper);
     }
 
     /**
@@ -177,7 +179,7 @@ public final class ValueExpressionImpl extends 
ValueExpression implements Extern
 
     @Override
     public int hashCode() {
-        return this.getNode().hashCode();
+        return ExpressionUtils.hashCode(getNode(), fnMapper);
     }
 
     @Override
diff --git a/java/org/apache/el/lang/FunctionMapperImpl.java 
b/java/org/apache/el/lang/FunctionMapperImpl.java
index 330f09fb06..d65890e6a5 100644
--- a/java/org/apache/el/lang/FunctionMapperImpl.java
+++ b/java/org/apache/el/lang/FunctionMapperImpl.java
@@ -22,6 +22,7 @@ import java.io.ObjectInput;
 import java.io.ObjectOutput;
 import java.io.Serial;
 import java.lang.reflect.Method;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
@@ -185,15 +186,15 @@ public class FunctionMapperImpl extends FunctionMapper 
implements Externalizable
 
         @Override
         public boolean equals(Object obj) {
-            if (obj instanceof Function) {
-                return this.hashCode() == obj.hashCode();
+            if (obj instanceof Function other) {
+                return Objects.equals(this.prefix, other.prefix) && 
Objects.equals(this.localName, other.localName);
             }
             return false;
         }
 
         @Override
         public int hashCode() {
-            return (this.prefix + this.localName).hashCode();
+            return Objects.hash(this.prefix, this.localName);
         }
     }
 
diff --git a/java/org/apache/el/util/ExpressionUtils.java 
b/java/org/apache/el/util/ExpressionUtils.java
new file mode 100644
index 0000000000..9639bc0d57
--- /dev/null
+++ b/java/org/apache/el/util/ExpressionUtils.java
@@ -0,0 +1,93 @@
+/*
+ * 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.el.util;
+
+import java.lang.reflect.Method;
+
+import jakarta.el.FunctionMapper;
+
+import org.apache.el.parser.AstFunction;
+import org.apache.el.parser.Node;
+
+/**
+ * Compares expression trees using their captured function mappings. Parsed 
nodes are shared between expressions, so
+ * function mappings must be considered separately rather than stored on the 
nodes.
+ */
+public final class ExpressionUtils {
+
+    private ExpressionUtils() {
+        // Utility class. Hide default constructor.
+    }
+
+
+    /**
+     * Compare parsed expressions and the functions they reference.
+     *
+     * @param first        First expression tree
+     * @param firstMapper  First expression's captured function mapper
+     * @param second       Second expression tree
+     * @param secondMapper Second expression's captured function mapper
+     * @return {@code true} if the expressions have identical parsed 
representations and function mappings
+     */
+    public static boolean equals(Node first, FunctionMapper firstMapper, Node 
second, FunctionMapper secondMapper) {
+        return first.equals(second) && equalsFunctions(first, firstMapper, 
second, secondMapper);
+    }
+
+
+    private static boolean equalsFunctions(Node first, FunctionMapper 
firstMapper, Node second,
+            FunctionMapper secondMapper) {
+        if (first instanceof AstFunction firstFunction && second instanceof 
AstFunction secondFunction &&
+                !getFunction(firstFunction, 
firstMapper).equals(getFunction(secondFunction, secondMapper))) {
+            return false;
+        }
+        for (int i = 0; i < first.jjtGetNumChildren(); i++) {
+            if (!equalsFunctions(first.jjtGetChild(i), firstMapper, 
second.jjtGetChild(i), secondMapper)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /**
+     * Hash a parsed expression and the functions it references.
+     *
+     * @param node   Expression tree
+     * @param mapper Expression's captured function mapper
+     * @return Hash consistent with function-aware expression equality
+     */
+    public static int hashCode(Node node, FunctionMapper mapper) {
+        return node.hashCode() + hashFunctions(node, mapper);
+    }
+
+
+    private static int hashFunctions(Node node, FunctionMapper mapper) {
+        int result = node instanceof AstFunction function ? 
getFunction(function, mapper).hashCode() : 0;
+        for (int i = 0; i < node.jjtGetNumChildren(); i++) {
+            result = 31 * result + hashFunctions(node.jjtGetChild(i), mapper);
+        }
+        return result;
+    }
+
+
+    private static Object getFunction(AstFunction function, FunctionMapper 
mapper) {
+        Method method = mapper == null ? null : 
mapper.resolveFunction(function.getPrefix(), function.getLocalName());
+        // Unmapped calls (such as lambdas and imported constructors) are 
resolved at evaluation time. Compare their
+        // names so distinct unresolved calls are not treated as aliases.
+        return method == null ? function.getOutputName() : method;
+    }
+}
diff --git a/test/jakarta/servlet/jsp/el/TestImplicitObjectELResolver.java 
b/test/jakarta/servlet/jsp/el/TestImplicitObjectELResolver.java
new file mode 100644
index 0000000000..6eeb6e84f2
--- /dev/null
+++ b/test/jakarta/servlet/jsp/el/TestImplicitObjectELResolver.java
@@ -0,0 +1,152 @@
+/*
+ * 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 jakarta.servlet.jsp.el;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.el.ELContext;
+import jakarta.el.ExpressionFactory;
+import jakarta.el.StandardELContext;
+import jakarta.servlet.jsp.JspContext;
+import jakarta.servlet.jsp.PageContext;
+import jakarta.servlet.jsp.TesterPageContext;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/*
+ * The entries of the implicit object scope maps must honour the Map.Entry 
equals() contract rather than deciding
+ * equality from the hash code of the key alone.
+ */
+public class TestImplicitObjectELResolver {
+
+    private ELContext context;
+
+    @Before
+    public void setUp() {
+        PageContext page = new AttributePageContext();
+        page.setAttribute("Aa", "value1");
+        page.setAttribute("BB", "value2");
+        page.setAttribute("other", "value3");
+
+        context = new StandardELContext(ExpressionFactory.newInstance());
+        context.putContext(JspContext.class, page);
+    }
+
+
+    @SuppressWarnings("unchecked")
+    private Set<Map.Entry<String,Object>> getPageScopeEntries() {
+        Object pageScope = new ImplicitObjectELResolver().getValue(context, 
null, "pageScope");
+        Assert.assertTrue(pageScope instanceof Map);
+        return ((Map<String,Object>) pageScope).entrySet();
+    }
+
+
+    private Map.Entry<String,Object> getEntry(String key) {
+        for (Map.Entry<String,Object> entry : getPageScopeEntries()) {
+            if (entry.getKey().equals(key)) {
+                return entry;
+            }
+        }
+        Assert.fail("No entry for key [" + key + "]");
+        return null;
+    }
+
+
+    @Test
+    public void testEntrySetContents() {
+        Assert.assertEquals(3, getPageScopeEntries().size());
+        Assert.assertEquals("value1", getEntry("Aa").getValue());
+    }
+
+
+    @Test
+    public void testEntryEqualsSameKey() {
+        Assert.assertEquals(getEntry("other"), getEntry("other"));
+        Assert.assertEquals(getEntry("other").hashCode(), 
getEntry("other").hashCode());
+    }
+
+
+    @Test
+    public void testEntryNotEqualsHashCollidingKeys() {
+        // "Aa" and "BB" are a well known pair of strings with the same hash 
code
+        Assert.assertEquals("Aa".hashCode(), "BB".hashCode());
+
+        Assert.assertNotEquals(getEntry("Aa"), getEntry("BB"));
+    }
+
+
+    @Test
+    public void testEntryNotEqualsOtherType() {
+        Map.Entry<String,Object> entry = getEntry("Aa");
+
+        // A String with the same hash code as the key is not an equal 
Map.Entry
+        Assert.assertNotEquals(entry, "Aa");
+        Assert.assertNotEquals(entry, null);
+    }
+
+
+    @Test
+    public void testEntryEqualsGenericEntry() {
+        // Map.Entry contract: equal if the keys and values are equal, 
whatever the implementation
+        Map.Entry<String,Object> entry = getEntry("Aa");
+        Map.Entry<String,Object> generic = Collections.singletonMap("Aa", 
(Object) "value1").entrySet().iterator().next();
+        Map.Entry<String,Object> differentValue =
+                Collections.singletonMap("Aa", (Object) 
"other").entrySet().iterator().next();
+
+        Assert.assertEquals(generic, entry);
+        Assert.assertEquals(generic.hashCode(), entry.hashCode());
+        Assert.assertNotEquals(differentValue, entry);
+    }
+
+
+    @Test
+    public void testEntrySetHashCollidingKeysBothPresent() {
+        Set<Map.Entry<String,Object>> set = new 
HashSet<>(getPageScopeEntries());
+        Assert.assertEquals(3, set.size());
+    }
+
+
+    private static class AttributePageContext extends TesterPageContext {
+
+        private final Map<String,Object> attributes = new HashMap<>();
+
+        @Override
+        public void setAttribute(String name, Object value) {
+            // The resolver stores its own scope manager as a page attribute. 
Only keep the test's attributes.
+            if (value instanceof String) {
+                attributes.put(name, value);
+            }
+        }
+
+        @Override
+        public Object getAttribute(String name) {
+            return attributes.get(name);
+        }
+
+        @Override
+        public Enumeration<String> getAttributeNamesInScope(int scope) {
+            return Collections.enumeration(new HashSet<>(attributes.keySet()));
+        }
+    }
+}
diff --git a/test/org/apache/el/TestMethodExpressionImpl.java 
b/test/org/apache/el/TestMethodExpressionImpl.java
index 443225c264..4630a08e34 100644
--- a/test/org/apache/el/TestMethodExpressionImpl.java
+++ b/test/org/apache/el/TestMethodExpressionImpl.java
@@ -16,6 +16,8 @@
  */
 package org.apache.el;
 
+import java.util.HashSet;
+import java.util.Set;
 import java.util.function.Function;
 
 import jakarta.el.ELBaseTest;
@@ -851,6 +853,121 @@ public class TestMethodExpressionImpl extends ELBaseTest {
     }
 
 
+    @Test
+    public void testNotEqualsDifferentFunctionArguments() throws Exception {
+        ELProcessor elp = new ELProcessor();
+        elp.defineBean("beanB", beanB);
+        elp.defineFunction("fn", "decimal", 
Integer.class.getMethod("toString", int.class));
+        elp.defineFunction("fn", "hex", Integer.class.getMethod("toHexString", 
int.class));
+        ELContext elpContext = elp.getELManager().getELContext();
+
+        MethodExpression me1 = factory.createMethodExpression(elpContext, 
"${beanB.echo(fn:decimal(10))}",
+                String.class, null);
+        MethodExpression me2 = factory.createMethodExpression(elpContext, 
"${beanB.echo(fn:hex(10))}", String.class,
+                null);
+
+        // Function names must distinguish expressions even when their 
argument trees are identical.
+        Assert.assertEquals("10", me1.invoke(elpContext, null));
+        Assert.assertEquals("a", me2.invoke(elpContext, null));
+        Assert.assertNotEquals(me1, me2);
+
+        Set<MethodExpression> expressions = new HashSet<>();
+        expressions.add(me1);
+        expressions.add(me2);
+        Assert.assertEquals(2, expressions.size());
+    }
+
+
+    @Test
+    public void testNotEqualsDifferentFunctionPrefixes() throws Exception {
+        ELProcessor elp = new ELProcessor();
+        elp.defineBean("beanB", beanB);
+        elp.defineFunction("decimal", "convert", 
Integer.class.getMethod("toString", int.class));
+        elp.defineFunction("hex", "convert", 
Integer.class.getMethod("toHexString", int.class));
+        ELContext elpContext = elp.getELManager().getELContext();
+
+        MethodExpression me1 = factory.createMethodExpression(elpContext, 
"${beanB.echo(decimal:convert(10))}",
+                String.class, null);
+        MethodExpression me2 = factory.createMethodExpression(elpContext, 
"${beanB.echo(hex:convert(10))}",
+                String.class, null);
+
+        Assert.assertEquals("10", me1.invoke(elpContext, null));
+        Assert.assertEquals("a", me2.invoke(elpContext, null));
+        Assert.assertNotEquals(me1, me2);
+    }
+
+
+    @Test
+    public void testEqualsHashCodeSameFunctionArgument() throws Exception {
+        ELProcessor elp = new ELProcessor();
+        elp.defineBean("beanB", beanB);
+        elp.defineFunction("fn", "hex", Integer.class.getMethod("toHexString", 
int.class));
+        ELContext elpContext = elp.getELManager().getELContext();
+
+        MethodExpression me1 = factory.createMethodExpression(elpContext, 
"${beanB.echo(fn:hex(10))}", String.class,
+                null);
+        MethodExpression me2 = factory.createMethodExpression(elpContext, "${ 
beanB.echo( fn:hex(10) ) }",
+                String.class, null);
+        MethodExpression me3 = factory.createMethodExpression(elpContext, 
"${beanB.echo(fn:hex(11))}", String.class,
+                null);
+
+        Assert.assertEquals(me1, me2);
+        Assert.assertEquals(me1.hashCode(), me2.hashCode());
+        Assert.assertNotEquals(me1, me3);
+    }
+
+
+    @Test
+    public void testEqualsHashCodeFunctionAliases() throws Exception {
+        ELProcessor elp = new ELProcessor();
+        elp.defineBean("beanB", beanB);
+        elp.defineFunction("one", "convert", 
Integer.class.getMethod("toString", int.class));
+        elp.defineFunction("two", "convert", 
Integer.class.getMethod("toString", int.class));
+        elp.defineFunction("one", "alias", Integer.class.getMethod("toString", 
int.class));
+        ELContext elpContext = elp.getELManager().getELContext();
+
+        MethodExpression me1 = factory.createMethodExpression(elpContext, 
"${beanB.echo(one:convert(10))}",
+                String.class, null);
+        MethodExpression me2 = factory.createMethodExpression(elpContext, 
"${beanB.echo(two:convert(10))}",
+                String.class, null);
+        MethodExpression me3 = factory.createMethodExpression(elpContext, 
"${beanB.echo(one:alias(10))}",
+                String.class, null);
+
+        Assert.assertEquals("10", me1.invoke(elpContext, null));
+        Assert.assertEquals("10", me2.invoke(elpContext, null));
+        Assert.assertEquals("10", me3.invoke(elpContext, null));
+        Assert.assertEquals(me1, me2);
+        Assert.assertEquals(me1, me3);
+        Assert.assertEquals(me1.hashCode(), me2.hashCode());
+        Assert.assertEquals(me1.hashCode(), me3.hashCode());
+
+        Set<MethodExpression> expressions = new HashSet<>();
+        expressions.add(me1);
+        expressions.add(me2);
+        expressions.add(me3);
+        Assert.assertEquals(1, expressions.size());
+    }
+
+
+    @Test
+    public void testNotEqualsRemappedFunction() throws Exception {
+        ELProcessor elp = new ELProcessor();
+        elp.defineBean("beanB", beanB);
+        elp.defineFunction("fn", "convert", 
Integer.class.getMethod("toString", int.class));
+        ELContext elpContext = elp.getELManager().getELContext();
+        MethodExpression me1 = factory.createMethodExpression(elpContext, 
"${beanB.echo(fn:convert(10))}",
+                String.class, null);
+
+        elp.defineFunction("fn", "convert", 
Integer.class.getMethod("toHexString", int.class));
+        MethodExpression me2 = factory.createMethodExpression(elpContext, 
"${beanB.echo(fn:convert(10))}",
+                String.class, null);
+
+        Assert.assertEquals("10", me1.invoke(elpContext, null));
+        Assert.assertEquals("a", me2.invoke(elpContext, null));
+        Assert.assertNotEquals(me1, me2);
+    }
+
+
     @Test
     public void testNotEqualsHashCollidingExpressions() {
         // "Aa" and "BB" are a well known pair of strings with the same hash 
code
diff --git a/test/org/apache/el/TestValueExpressionImpl.java 
b/test/org/apache/el/TestValueExpressionImpl.java
index 786acff235..5b038c311e 100644
--- a/test/org/apache/el/TestValueExpressionImpl.java
+++ b/test/org/apache/el/TestValueExpressionImpl.java
@@ -27,6 +27,7 @@ import jakarta.el.ELBaseTest;
 import jakarta.el.ELContext;
 import jakarta.el.ExpressionFactory;
 import jakarta.el.PropertyNotFoundException;
+import jakarta.el.StandardELContext;
 import jakarta.el.ValueExpression;
 import jakarta.el.ValueReference;
 
@@ -37,6 +38,48 @@ import org.apache.jasper.el.ELContextImpl;
 
 public class TestValueExpressionImpl extends ELBaseTest {
 
+    @Test
+    public void testFunctionMappingEquality() throws Exception {
+        ExpressionFactory factory = ExpressionFactory.newInstance();
+        ELContext context = new StandardELContext(factory);
+        context.getFunctionMapper().mapFunction("one", "convert", 
Integer.class.getMethod("toString", int.class));
+        context.getFunctionMapper().mapFunction("two", "convert", 
Integer.class.getMethod("toString", int.class));
+        context.getFunctionMapper().mapFunction("one", "alias", 
Integer.class.getMethod("toString", int.class));
+
+        ValueExpression ve1 = factory.createValueExpression(context, 
"${one:convert(10)}", String.class);
+        ValueExpression ve2 = factory.createValueExpression(context, 
"${two:convert(10)}", String.class);
+        ValueExpression ve3 = factory.createValueExpression(context, 
"${one:alias(10)}", String.class);
+
+        Assert.assertEquals(ve1, ve2);
+        Assert.assertEquals(ve1, ve3);
+        Assert.assertEquals(ve1.hashCode(), ve2.hashCode());
+        Assert.assertEquals(ve1.hashCode(), ve3.hashCode());
+
+        context.getFunctionMapper().mapFunction("one", "convert", 
Integer.class.getMethod("toHexString", int.class));
+        ValueExpression ve4 = factory.createValueExpression(context, 
"${one:convert(10)}", String.class);
+
+        Assert.assertEquals("10", ve1.getValue(context));
+        Assert.assertEquals("a", ve4.getValue(context));
+        Assert.assertNotEquals(ve1, ve4);
+    }
+
+
+    @Test
+    public void testImportedFunctionEquality() {
+        ExpressionFactory factory = ExpressionFactory.newInstance();
+        ELContext context = new StandardELContext(factory);
+        ValueExpression ve1 = factory.createValueExpression(context, 
"${Integer(10)}", Object.class);
+        ValueExpression ve2 = factory.createValueExpression(context, "${ 
Integer(10) }", Object.class);
+        ValueExpression ve3 = factory.createValueExpression(context, 
"${Long(10)}", Object.class);
+
+        Assert.assertEquals(Integer.valueOf(10), ve1.getValue(context));
+        Assert.assertEquals(Long.valueOf(10), ve3.getValue(context));
+        Assert.assertEquals(ve1, ve2);
+        Assert.assertEquals(ve1.hashCode(), ve2.hashCode());
+        Assert.assertNotEquals(ve1, ve3);
+    }
+
+
     @Test
     public void testGetValueReference() {
         ExpressionFactory factory = ExpressionFactory.newInstance();
diff --git a/test/org/apache/el/lang/TestFunctionMapperImpl.java 
b/test/org/apache/el/lang/TestFunctionMapperImpl.java
new file mode 100644
index 0000000000..78ab7c2c25
--- /dev/null
+++ b/test/org/apache/el/lang/TestFunctionMapperImpl.java
@@ -0,0 +1,104 @@
+/*
+ * 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.el.lang;
+
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/*
+ * FunctionMapperImpl.Function must not decide equality from its hash code 
alone.
+ */
+public class TestFunctionMapperImpl {
+
+    private static final Method METHOD;
+
+    static {
+        try {
+            METHOD = Integer.class.getMethod("toString", int.class);
+        } catch (NoSuchMethodException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+
+    @Test
+    public void testEqualsSameFunction() {
+        FunctionMapperImpl.Function f1 = new FunctionMapperImpl.Function("fn", 
"convert", METHOD);
+        FunctionMapperImpl.Function f2 = new FunctionMapperImpl.Function("fn", 
"convert", METHOD);
+
+        Assert.assertEquals(f1, f2);
+        Assert.assertEquals(f1.hashCode(), f2.hashCode());
+    }
+
+
+    @Test
+    public void testNotEqualsDifferentLocalName() {
+        FunctionMapperImpl.Function f1 = new FunctionMapperImpl.Function("fn", 
"convert", METHOD);
+        FunctionMapperImpl.Function f2 = new FunctionMapperImpl.Function("fn", 
"other", METHOD);
+
+        Assert.assertNotEquals(f1, f2);
+    }
+
+
+    @Test
+    public void testNotEqualsDifferentPrefix() {
+        FunctionMapperImpl.Function f1 = new 
FunctionMapperImpl.Function("fn1", "convert", METHOD);
+        FunctionMapperImpl.Function f2 = new 
FunctionMapperImpl.Function("fn2", "convert", METHOD);
+
+        Assert.assertNotEquals(f1, f2);
+    }
+
+
+    @Test
+    public void testNotEqualsHashCollidingLocalNames() {
+        // "Aa" and "BB" are a well known pair of strings with the same hash 
code
+        Assert.assertEquals("Aa".hashCode(), "BB".hashCode());
+
+        FunctionMapperImpl.Function f1 = new FunctionMapperImpl.Function("fn", 
"Aa", METHOD);
+        FunctionMapperImpl.Function f2 = new FunctionMapperImpl.Function("fn", 
"BB", METHOD);
+
+        Assert.assertNotEquals(f1, f2);
+    }
+
+
+    @Test
+    public void testNotEqualsPrefixLocalNameBoundary() {
+        // Same concatenation of prefix and local name but different functions
+        FunctionMapperImpl.Function f1 = new FunctionMapperImpl.Function("ab", 
"c", METHOD);
+        FunctionMapperImpl.Function f2 = new FunctionMapperImpl.Function("a", 
"bc", METHOD);
+
+        Assert.assertNotEquals(f1, f2);
+
+        Set<FunctionMapperImpl.Function> functions = new HashSet<>();
+        functions.add(f1);
+        functions.add(f2);
+        Assert.assertEquals(2, functions.size());
+    }
+
+
+    @Test
+    public void testNotEqualsOtherType() {
+        FunctionMapperImpl.Function f1 = new FunctionMapperImpl.Function("fn", 
"convert", METHOD);
+
+        Assert.assertNotEquals(f1, "fnconvert");
+        Assert.assertNotEquals(f1, null);
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to