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 b10477ec JEXL-421: added methods to find 'best' common class; - 
updated ArrayBuilder; - added test; - updated changes & release notes;
b10477ec is described below

commit b10477ec25c7b7ed52e58aca30e8ab3c940eeaa9
Author: Henri Biestro <hbies...@cloudera.com>
AuthorDate: Thu Feb 15 13:16:13 2024 +0100

    JEXL-421: added methods to find 'best' common class;
    - updated ArrayBuilder;
    - added test;
    - updated changes & release notes;
---
 RELEASE-NOTES.txt                                  |   1 +
 src/changes/changes.xml                            |   3 +
 .../commons/jexl3/internal/ArrayBuilder.java       |  41 ++++--
 .../jexl3/internal/introspection/ClassMisc.java    | 128 ++++++++++++++++++
 .../org/apache/commons/jexl3/ArrayTypeTest.java    | 149 +++++++++++++++++++++
 .../org/apache/commons/jexl3/Issues400Test.java    |   4 -
 6 files changed, 310 insertions(+), 16 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index d8bac03e..8e40650f 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -30,6 +30,7 @@ Version 3.3.1 is source and binary compatible with 3.3.
 
 New Features in 3.3.1:
 ====================
+* JEXL-421:     ArrayBuilder: array type should reflect common class of its 
entries
 * JEXL-419:     Add permission syntax to allow class/method/field
 * JEXL-408:     Using JexlFeatures is tedious
 * JEXL-404:     Support array-access safe navigation (x?[y])
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index b5666812..0ef1e059 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -29,6 +29,9 @@
     <body>
         <release version="3.3.1" date="20YY-MM-DD">
             <!-- ADD -->
+            <action dev="henrib" type="add" issue="JEXL-421">
+                ArrayBuilder: array type should reflect common class of its 
entries
+            </action>
             <action dev="henrib" type="add" issue="JEXL-419">
                 Add permission syntax to allow class/method/field
             </action>
diff --git a/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java 
b/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java
index a7825b1c..f738df30 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/ArrayBuilder.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.jexl3.JexlArithmetic;
+import org.apache.commons.jexl3.internal.introspection.ClassMisc;
 
 /**
  * Helper class to create typed arrays.
@@ -67,6 +68,17 @@ public class ArrayBuilder implements 
JexlArithmetic.ArrayBuilder {
     /** Extended? */
     protected final boolean extended;
 
+    /**
+     * Computes the best super class/super interface.
+     * <p>Used to try and maintain type safe arrays.</p>
+     * @param baseClass the baseClass
+     * @param other another class
+     * @return a common ancestor, class or interface, worst case being class 
Object
+     */
+    protected Class<?> getCommonSuperClass(final Class<?> baseClass, final 
Class<?> other) {
+        return ClassMisc.getCommonSuperClass(baseClass, other);
+    }
+
     /**
      * Creates a new builder.
      * @param size the exact array size
@@ -97,19 +109,13 @@ public class ArrayBuilder implements 
JexlArithmetic.ArrayBuilder {
                 if (commonClass == null) {
                     commonClass = eclass;
                     isNumber = isNumber && 
Number.class.isAssignableFrom(commonClass);
-                } else if (!commonClass.equals(eclass)) {
+                } else if (!commonClass.isAssignableFrom(eclass)) {
                     // if both are numbers...
                     if (isNumber && Number.class.isAssignableFrom(eclass)) {
                         commonClass = Number.class;
                     } else {
-                        // attempt to find valid superclass
-                        do {
-                            eclass = eclass.getSuperclass();
-                            if (eclass == null) {
-                                commonClass = Object.class;
-                                break;
-                            }
-                        } while (!commonClass.isAssignableFrom(eclass));
+                        isNumber = false;
+                        commonClass = getCommonSuperClass(commonClass, eclass);
                     }
                 }
             }
@@ -120,21 +126,32 @@ public class ArrayBuilder implements 
JexlArithmetic.ArrayBuilder {
         untyped[added++] = value;
     }
 
+    /**
+     * Creates a new list (aka extended array)/
+     * @param clazz the class
+     * @param size the size
+     * @return the instance
+     * @param <T> the type
+     */
+    protected <T> List<T> newList(Class<? extends T> clazz, int size) {
+        return new ArrayList<>(size);
+    }
+
     @Override
     public Object create(final boolean e) {
         if (untyped == null) {
             return new Object[0];
         }
+        final int size = added;
         if (extended || e) {
-            final List<Object> list = new ArrayList<>(added);
-            list.addAll(Arrays.asList(untyped).subList(0, added));
+            final List<Object> list = newList(commonClass, size);
+            list.addAll(Arrays.asList(untyped).subList(0, size));
             return list;
         }
         // convert untyped array to the common class if not Object.class
         if (commonClass == null || Object.class.equals(commonClass)) {
             return untyped.clone();
         }
-        final int size = added;
         // if the commonClass is a number, it has an equivalent primitive 
type, get it
         if (unboxing) {
             commonClass = unboxingClass(commonClass);
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMisc.java 
b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMisc.java
new file mode 100644
index 00000000..438a13d7
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMisc.java
@@ -0,0 +1,128 @@
+/*
+ * 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.internal.introspection;
+
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Miscellaneous introspection methods.
+ * <p>The main algorithm is computing the actual ordered complete set of 
classes and interfaces that a given class
+ * extends or implements. This set order is based on the super-class then 
(recursive interface) declaration order,
+ * attempting to reflect the (hopefully intended) abstraction order (from 
strong to weak).</p>
+ */
+public class ClassMisc {
+  /**
+   * Lets not instantiate it.
+   */
+  private ClassMisc() {}
+  /**
+   * Collect super classes and interfaces in super-order.
+   * <p>This orders from stronger to weaker abstraction in the sense that
+   * Integer is a stronger abstraction than Number.</p>
+   *
+   * @param superSet  the set of super classes to collect into
+   * @param baseClass the root class.
+   */
+  private static void addSuperClasses(final Set<Class<?>> superSet, final 
Class<?> baseClass) {
+    for (Class<?> clazz = baseClass.getSuperclass(); clazz != null && 
!Object.class.equals(clazz); clazz = clazz.getSuperclass()) {
+      if (Modifier.isPublic(clazz.getModifiers())) {
+        superSet.add(clazz);
+      }
+    }
+    // recursive visit interfaces in super order
+    for (Class<?> clazz = baseClass; clazz != null && 
!Object.class.equals(clazz); clazz = clazz.getSuperclass()) {
+      addSuperInterfaces(superSet, clazz);
+    }
+  }
+
+  /**
+   * Recursively add super-interfaces in super-order.
+   * <p>On the premise that a class also tends to enumerate interface in the 
order of weaker abstraction and
+   * that interfaces follow the same convention (strong implements weak).</p>
+   *
+   * @param superSet the set of super classes to fill
+   * @param clazz    the root class.
+   */
+  private static void addSuperInterfaces(final Set<Class<?>> superSet, final 
Class<?> clazz) {
+    for (Class<?> inter : clazz.getInterfaces()) {
+      superSet.add(inter);
+      addSuperInterfaces(superSet, inter);
+    }
+  }
+
+  /**
+   * Gets the closest common super-class of two classes.
+   * <p>When building an array, this helps strong-typing the result.</p>
+   *
+   * @param baseClass the class to serve as base
+   * @param other     the other class
+   * @return Object.class if nothing in common, the closest common class or 
interface otherwise
+   */
+  public static Class<?> getCommonSuperClass(final Class<?> baseClass, final 
Class<?> other) {
+    if (baseClass == null || other == null) {
+      return null;
+    }
+    if (baseClass != Object.class && other != Object.class) {
+      final Set<Class<?>> superSet = new LinkedHashSet<>();
+      addSuperClasses(superSet, baseClass);
+      for (Class<?> superClass : superSet) {
+        if (superClass.isAssignableFrom(other)) {
+          return superClass;
+        }
+      }
+    }
+    return Object.class;
+  }
+
+  /**
+   * Build the set of super classes and interfaces common to a collection of 
classes.
+   * <p>The returned set is ordered and puts classes in order of super-class 
appearance then
+   * interfaces of each super-class.</p>
+   *
+   * @param baseClass    the class to serve as base
+   * @param otherClasses the (optional) other classes
+   * @return an empty set if nothing in common, the set of common classes and 
interfaces that
+   *  does not contain the baseClass nor Object class
+   */
+  public static Set<Class<?>> getSuperClasses(final Class<?> baseClass, final 
Class<?>... otherClasses) {
+    if (baseClass == null) {
+      return Collections.emptySet();
+    }
+    final Set<Class<?>> superSet = new LinkedHashSet<>();
+    addSuperClasses(superSet, baseClass);
+    // intersect otherClasses
+    if (otherClasses.length > 0) {
+      for (Class<?> other : otherClasses) {
+        // remove classes from $superSet that $other is not assignable to
+        final Iterator<Class<?>> superClass = superSet.iterator();
+        while (superClass.hasNext()) {
+          if (!superClass.next().isAssignableFrom(other)) {
+            superClass.remove();
+          }
+        }
+        if (superSet.isEmpty()) {
+          return Collections.emptySet();
+        }
+      }
+    }
+    return superSet;
+  }
+}
diff --git a/src/test/java/org/apache/commons/jexl3/ArrayTypeTest.java 
b/src/test/java/org/apache/commons/jexl3/ArrayTypeTest.java
new file mode 100644
index 00000000..cebafbcc
--- /dev/null
+++ b/src/test/java/org/apache/commons/jexl3/ArrayTypeTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+import org.apache.commons.jexl3.internal.ArrayBuilder;
+import org.apache.commons.jexl3.internal.introspection.ClassMisc;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.Serializable;
+import java.util.AbstractCollection;
+import java.util.AbstractList;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Ensure ArrayBuilder types its output by finding some common ancestor class 
or interface (besides Object.class)
+ * from its entries when possible.
+ */
+public class ArrayTypeTest {
+  // A dependency tree with some complexity follows:
+  public interface Inter0 {}
+  public interface InterA {}
+  public interface InterB {}
+  public interface InterC {}
+  public interface InterX extends InterB {}
+  public abstract static class Class0 implements Inter0 {
+    private int value;
+    public Class0(int v) {
+      value = v;
+    }
+    @Override public String toString() {
+      return getClass().getSimpleName() + "{" + value + "}";
+    }
+  }
+  public static class ClassA extends Class0 implements InterA {
+    public ClassA(int v) { super(v); }
+  }
+  public static class ClassX extends Class0 implements InterX {
+    public ClassX(int v) { super(v); }
+  }
+  public static class ClassB extends ClassA implements InterB {
+    public ClassB(int v) { super(v); }
+  }
+  public static class ClassC extends ClassB implements InterC, InterX {
+    public ClassC(int v) { super(v); }
+  }
+  public static class ClassD implements InterB, Inter0 {
+    @Override public String toString() {
+      return getClass().getSimpleName() + "{" + 
Integer.toHexString(hashCode()) + "}";
+    }
+  }
+
+  @Test
+  public void testArrayTypes() {
+    ArrayBuilder ab = new ArrayBuilder(1);
+    // An engine for expressions with args
+    JexlFeatures features = JexlFeatures.createScript().script(false);
+    JexlEngine jexl = new JexlBuilder()
+        .features(features)
+        .create();
+    // Super for ClassC
+    Set<Class<?>> superSet = ClassMisc.getSuperClasses(ClassC.class);
+    Assert.assertTrue(superSet.size() > 0);
+    // verify the order
+    List<Class<?>> ordered = Arrays.asList(
+        ClassB.class, ClassA.class, Class0.class,
+        InterC.class, InterX.class, InterB.class,
+        InterA.class, Inter0.class);
+    int i = 0;
+    for(Class<?> clazz : superSet) {
+      Assert.assertEquals("order " + i, clazz, ordered.get(i++));
+    }
+    // intersect ClassC, ClassX -> Class0
+    Class<?> inter = ClassMisc.getCommonSuperClass(ClassC.class, ClassX.class);
+    Assert.assertEquals(Class0.class, inter);
+
+    // intersect ClassC, ClassB -> ClassB
+    inter = ClassMisc.getCommonSuperClass(ClassC.class, ClassB.class);
+    Assert.assertEquals(ClassB.class, inter);
+
+    // intersect ArrayList, ArrayDeque -> AbstractCollection
+    Class<?> list = ClassMisc.getCommonSuperClass(ArrayList.class, 
ArrayDeque.class);
+    Assert.assertEquals(list, AbstractCollection.class);
+
+    Set<Class<?>> sset = ClassMisc.getSuperClasses(ArrayList.class, 
ArrayDeque.class);
+    Assert.assertFalse(sset.isEmpty());
+    List<Class<?>> expected = Arrays.asList(AbstractCollection.class, 
Collection.class, Iterable.class, Cloneable.class, Serializable.class);
+    Assert.assertEquals(expected, new ArrayList(sset));
+    Class<?> collection = ClassMisc.getCommonSuperClass(ArrayList.class, 
Collections.emptyList().getClass());
+    Assert.assertEquals(AbstractList.class, collection);
+    collection = ClassMisc.getSuperClasses(ArrayList.class, 
Collections.emptyList().getClass())
+                          .stream().findFirst().orElse(Object.class);
+
+    // apply on objects
+    Object a = new ClassA(1);
+    Object b = new ClassB(2);
+    Object c = new ClassC(3);
+    Object x = new ClassX(4);
+    JexlScript script;
+    Object result;
+
+    script = jexl.createScript("[ a, b, c, d ]", "a", "b", "c", "d");
+    // intersect a, b, c, c -> classA
+    result = script.execute(null, a, b, c, c);
+    Assert.assertTrue(result.getClass().isArray()
+        && result.getClass().getComponentType().equals(ClassA.class));
+
+    // intersect a, b, c, x -> class0
+    result = script.execute(null, a, b, c, x);
+    Assert.assertTrue(result.getClass().isArray()
+        && result.getClass().getComponentType().equals(Class0.class));
+
+    // intersect x, c, b, a -> class0
+    result = script.execute(null, x, c, b, a);
+    Assert.assertTrue(result.getClass().isArray()
+        & result.getClass().getComponentType().equals(Class0.class));
+
+    // intersect a, b, c, d -> inter0
+    Object d = new ClassD();
+    result = script.execute(null, a, b, c, d);
+    Assert.assertTrue(result.getClass().isArray()
+        && result.getClass().getComponentType().equals(Inter0.class));
+
+    script = jexl.createScript("[ a, b, c, d, ... ]", "a", "b", "c", "d");
+    // intersect a, b, c, c -> classA
+    result = script.execute(null, a, b, c, c);
+    Assert.assertTrue(result instanceof List);
+  }
+}
diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java 
b/src/test/java/org/apache/commons/jexl3/Issues400Test.java
index 0a5695bc..d3dee093 100644
--- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java
+++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java
@@ -22,19 +22,15 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicLong;
 
-import org.apache.commons.jexl3.internal.introspection.Permissions;
 import org.apache.commons.jexl3.introspection.JexlPermissions;
-import org.apache.commons.jexl3.introspection.JexlSandbox;
 import org.junit.Assert;
 import org.junit.Test;
 
 import static 
org.apache.commons.jexl3.introspection.JexlPermissions.RESTRICTED;
-import static org.hamcrest.MatcherAssert.assertThat;
 
 /**
  * Test cases for reported issue between JEXL-300 and JEXL-399.

Reply via email to