You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ss...@apache.org on 2024/06/25 11:22:21 UTC

(sling-org-apache-sling-models-impl) branch master updated: SLING-12359 - Extend records support (#51)

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

sseifert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-models-impl.git


The following commit(s) were added to refs/heads/master by this push:
     new 9d1c725  SLING-12359 - Extend records support (#51)
9d1c725 is described below

commit 9d1c72501226d3dfdfb0644c5632f5e29be5370e
Author: Herman Ciechanowiec <he...@ciechanowiec.eu>
AuthorDate: Tue Jun 25 13:22:16 2024 +0200

    SLING-12359 - Extend records support (#51)
---
 .../sling/models/impl/ModelAdapterFactory.java     |   3 +
 .../apache/sling/models/impl/ReflectionUtil.java   |  33 ++++-
 .../apache/sling/models/impl/model/ModelClass.java |   2 +-
 .../models/impl/model/ModelClassConstructor.java   |   6 +
 .../sling/models/impl/ReflectionUtilTest.java      | 148 +++++++++++++++++++++
 5 files changed, 187 insertions(+), 5 deletions(-)

diff --git a/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java b/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
index 9724013..94e6be6 100644
--- a/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
+++ b/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
@@ -812,6 +812,9 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
             if (constructor.hasInjectAnnotation()) {
                 return constructor;
             }
+            if (constructor.isCanonicalRecordConstructor()) {
+                return constructor;
+            }
             // compatibility mode for sling models implementation <= 1.0.6:
             // support constructor without @Inject if it has exactly one parameter matching the adaptable class
             final Class<?>[] paramTypes = constructor.getConstructor().getParameterTypes();
diff --git a/src/main/java/org/apache/sling/models/impl/ReflectionUtil.java b/src/main/java/org/apache/sling/models/impl/ReflectionUtil.java
index 456b1c7..f8253a1 100644
--- a/src/main/java/org/apache/sling/models/impl/ReflectionUtil.java
+++ b/src/main/java/org/apache/sling/models/impl/ReflectionUtil.java
@@ -19,11 +19,9 @@
 package org.apache.sling.models.impl;
 
 import java.lang.annotation.Annotation;
-import java.lang.reflect.AnnotatedElement;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.lang.reflect.Type;
+import java.lang.reflect.*;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -125,4 +123,31 @@ public final class ReflectionUtil {
         }
     }
 
+    public static boolean isRecord(Class<?> checkedType) {
+        try {
+            Class<?> recordType = Class.forName("java.lang.Record");
+            return recordType.isAssignableFrom(checkedType);
+        } catch (@SuppressWarnings("squid:S1166") ClassNotFoundException exception) {
+            return false;
+        }
+    }
+
+    /**
+     * Checks if the number of parameters in the specified constructor matches the number of fields in the class
+     * that declares the constructor. Can be useful for detection of canonical constructors in records.
+     * Synthetic fields are ignored.
+     *
+     * @param constructor the constructor to check
+     * @return {@code true} if the number of constructor parameters equals the number of fields in the declaring class,
+     *         {@code false} otherwise
+     */
+    public static boolean areBalancedCtorParamsAndFields(Constructor<?> constructor) {
+        int numOfCtorParams = constructor.getParameterCount();
+        Class<?> declaringClass = constructor.getDeclaringClass();
+        Field[] fields = declaringClass.getDeclaredFields();
+        long numOfFields = Arrays.stream(fields)
+                                 .filter(field -> !field.isSynthetic())
+                                 .count();
+        return numOfCtorParams == numOfFields;
+    }
 }
diff --git a/src/main/java/org/apache/sling/models/impl/model/ModelClass.java b/src/main/java/org/apache/sling/models/impl/model/ModelClass.java
index 4f81a62..f3d776c 100644
--- a/src/main/java/org/apache/sling/models/impl/model/ModelClass.java
+++ b/src/main/java/org/apache/sling/models/impl/model/ModelClass.java
@@ -77,7 +77,7 @@ public class ModelClass<ModelType> {
     }
 
     private static InjectableField[] getInjectableFields(Class<?> type, StaticInjectAnnotationProcessorFactory[] processorFactories, DefaultInjectionStrategy defaultInjectionStrategy) {
-        if (type.isInterface()) {
+        if (type.isInterface() || ReflectionUtil.isRecord(type)) {
             return new InjectableField[0];
         }
         List<Field> injectableFields = ReflectionUtil.collectInjectableFields(type);
diff --git a/src/main/java/org/apache/sling/models/impl/model/ModelClassConstructor.java b/src/main/java/org/apache/sling/models/impl/model/ModelClassConstructor.java
index 23460be..ffb6a30 100644
--- a/src/main/java/org/apache/sling/models/impl/model/ModelClassConstructor.java
+++ b/src/main/java/org/apache/sling/models/impl/model/ModelClassConstructor.java
@@ -84,4 +84,10 @@ public class ModelClassConstructor<M> {
         return constructorParametersArray;
     };
 
+    public boolean isCanonicalRecordConstructor() {
+        Class<M> declaringClass = constructor.getDeclaringClass();
+        boolean areBalancedCtorParamsAndFields = ReflectionUtil.areBalancedCtorParamsAndFields(constructor);
+        boolean isRecordDeclaringClass = ReflectionUtil.isRecord(declaringClass);
+        return areBalancedCtorParamsAndFields && isRecordDeclaringClass;
+    }
 }
diff --git a/src/test/java/org/apache/sling/models/impl/ReflectionUtilTest.java b/src/test/java/org/apache/sling/models/impl/ReflectionUtilTest.java
new file mode 100644
index 0000000..95121da
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/impl/ReflectionUtilTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.sling.models.impl;
+
+import org.junit.Test;
+
+import java.lang.reflect.Constructor;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@SuppressWarnings({
+        "PackageVisibleInnerClass", "FieldCanBeLocal", "unused", "java:S1172",
+        "InstanceVariableMayNotBeInitialized", "PublicConstructorInNonPublicClass"
+})
+public class ReflectionUtilTest {
+
+    static class TestClassOne {
+        private final int field1;
+        private final String field2;
+
+        public TestClassOne(int field1, String field2) {
+            this.field1 = field1;
+            this.field2 = field2;
+        }
+    }
+
+    static class TestClassTwo {
+        private final int field1;
+        private final String field2;
+        private boolean field3;
+
+        public TestClassTwo(int field1, String field2) {
+            this.field1 = field1;
+            this.field2 = field2;
+        }
+    }
+
+    static class TestClassThree {
+        private final int field1;
+
+        public TestClassThree(int field1, String field2) {
+            this.field1 = field1;
+        }
+    }
+
+    static class TestClassFour {
+
+        public TestClassFour(int field1, String field2, boolean field3) {
+        }
+    }
+
+    static class TestClassFive {
+        private final int field1;
+
+        public TestClassFive(int field1) {
+            this.field1 = field1;
+        }
+    }
+
+    static class TestClassSix {
+        private final int field1;
+
+        public TestClassSix() {
+            this.field1 = 0;
+        }
+    }
+
+    static class TestClassSeven {
+        private final String field1;
+        private final String field2;
+
+        public TestClassSeven(String field1, String field2) {
+            this.field1 = field1;
+            this.field2 = field2;
+        }
+    }
+
+    static class TestClassEight {
+        private final String field1;
+
+        public TestClassEight(String field1, String field2) {
+            this.field1 = field1;
+        }
+    }
+
+    @Test
+    public void testBalancedConstructor() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassOne.class.getConstructor(int.class, String.class);
+        assertTrue(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+
+    @Test
+    public void testMoreFieldsThanParams() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassTwo.class.getConstructor(int.class, String.class);
+        assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+
+    @Test
+    public void testMoreParamsThanFields() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassThree.class.getConstructor(int.class, String.class);
+        assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+
+    @Test
+    public void testNoFields() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassFour.class.getConstructor(int.class, String.class, boolean.class);
+        assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+
+    @Test
+    public void testOneFieldOneParam() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassFive.class.getConstructor(int.class);
+        assertTrue(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+
+    @Test
+    public void testNoParams() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassSix.class.getConstructor();
+        assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+
+    @Test
+    public void testBalancedRepeatableTypes() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassSeven.class.getConstructor(String.class, String.class);
+        assertTrue(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+
+    @Test
+    public void testUnbalancedRepeatableTypes() throws NoSuchMethodException {
+        Constructor<?> constructor = TestClassEight.class.getConstructor(String.class, String.class);
+        assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
+    }
+}