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

stefanseifert 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 1eae10d  SLING-13235 Record-based Sling Model with static fields fails 
to instantiate (#87)
1eae10d is described below

commit 1eae10d1d78f9a733acd95b90fc401282045c392
Author: Herman Ciechanowiec <[email protected]>
AuthorDate: Fri Jun 26 18:04:18 2026 +0200

    SLING-13235 Record-based Sling Model with static fields fails to 
instantiate (#87)
    
    Detect the canonical record constructor exactly via Java 17 record APIs
    (Class.isRecord() / Class.getRecordComponents()) instead of the pre-Java-17
    field-counting heuristic, which miscounted static fields and prevented
    records with static fields from being instantiated as Sling Models.
    
    Drop the obsolete reflective record machinery from ReflectionUtil and add
    record-based test fixtures and RecordModelTest.
---
 .../apache/sling/models/impl/ReflectionUtil.java   |  38 -----
 .../apache/sling/models/impl/model/ModelClass.java |   2 +-
 .../models/impl/model/ModelClassConstructor.java   |  13 +-
 .../apache/sling/models/impl/RecordModelTest.java  |  85 ++++++++++++
 .../sling/models/impl/ReflectionUtilTest.java      | 153 ---------------------
 .../classes/constructorinjection/RecordModel.java  |  29 ++++
 .../RecordModelWithExtraConstructor.java           |  34 +++++
 .../RecordModelWithStaticFields.java               |  39 ++++++
 8 files changed, 197 insertions(+), 196 deletions(-)

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 7cd6b0a..5cfa000 100644
--- a/src/main/java/org/apache/sling/models/impl/ReflectionUtil.java
+++ b/src/main/java/org/apache/sling/models/impl/ReflectionUtil.java
@@ -23,7 +23,6 @@ import javax.inject.Inject;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.*;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 import org.apache.commons.lang3.ClassUtils;
@@ -34,18 +33,6 @@ import 
org.apache.sling.models.spi.injectorspecific.InjectAnnotation;
  */
 public final class ReflectionUtil {
 
-    static Class<?> recordType;
-
-    static {
-        try {
-            recordType = Class.forName("java.lang.Record");
-        } catch (ClassNotFoundException e) {
-            // this happens when running with Java11, which is supported, but
-            // of course does not have support for Record types
-            recordType = null;
-        }
-    }
-
     private ReflectionUtil() {
         // static methods only
     }
@@ -133,29 +120,4 @@ public final class ReflectionUtil {
             return type;
         }
     }
-
-    public static boolean isRecord(Class<?> checkedType) {
-        if (recordType == null) {
-            return false;
-        }
-        return recordType.isAssignableFrom(checkedType);
-    }
-
-    /**
-     * 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 9ee2b81..a49ec5f 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
@@ -84,7 +84,7 @@ public class ModelClass<ModelType> {
             Class<?> type,
             StaticInjectAnnotationProcessorFactory[] processorFactories,
             DefaultInjectionStrategy defaultInjectionStrategy) {
-        if (type.isInterface() || ReflectionUtil.isRecord(type)) {
+        if (type.isInterface() || type.isRecord()) {
             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 35fed65..be41e06 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
@@ -23,10 +23,11 @@ import javax.inject.Inject;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Parameter;
+import java.lang.reflect.RecordComponent;
+import java.util.Arrays;
 import java.util.stream.IntStream;
 
 import org.apache.sling.models.annotations.DefaultInjectionStrategy;
-import org.apache.sling.models.impl.ReflectionUtil;
 import 
org.apache.sling.models.spi.injectorspecific.StaticInjectAnnotationProcessorFactory;
 
 public class ModelClassConstructor<M> {
@@ -86,8 +87,12 @@ public class ModelClassConstructor<M> {
 
     public boolean isCanonicalRecordConstructor() {
         Class<M> declaringClass = constructor.getDeclaringClass();
-        boolean areBalancedCtorParamsAndFields = 
ReflectionUtil.areBalancedCtorParamsAndFields(constructor);
-        boolean isRecordDeclaringClass = 
ReflectionUtil.isRecord(declaringClass);
-        return areBalancedCtorParamsAndFields && isRecordDeclaringClass;
+        if (!declaringClass.isRecord()) {
+            return false;
+        }
+        Class<?>[] componentTypes = 
Arrays.stream(declaringClass.getRecordComponents())
+                .map(RecordComponent::getType)
+                .toArray(Class<?>[]::new);
+        return Arrays.equals(componentTypes, constructor.getParameterTypes());
     }
 }
diff --git a/src/test/java/org/apache/sling/models/impl/RecordModelTest.java 
b/src/test/java/org/apache/sling/models/impl/RecordModelTest.java
new file mode 100644
index 0000000..b690ffc
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/impl/RecordModelTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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 java.util.Arrays;
+
+import org.apache.sling.api.SlingJakartaHttpServletRequest;
+import org.apache.sling.models.impl.injectors.RequestAttributeInjector;
+import org.apache.sling.models.impl.injectors.SelfInjector;
+import 
org.apache.sling.models.testmodels.classes.constructorinjection.RecordModel;
+import 
org.apache.sling.models.testmodels.classes.constructorinjection.RecordModelWithExtraConstructor;
+import 
org.apache.sling.models.testmodels.classes.constructorinjection.RecordModelWithStaticFields;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.lenient;
+
+@ExtendWith(MockitoExtension.class)
+class RecordModelTest {
+
+    private ModelAdapterFactory factory;
+
+    @Mock
+    private SlingJakartaHttpServletRequest request;
+
+    private static final int INT_VALUE = 42;
+
+    private static final String STRING_VALUE = "myValue";
+
+    @BeforeEach
+    void setup() {
+        
lenient().when(request.getAttribute("attribute")).thenReturn(INT_VALUE);
+        
lenient().when(request.getAttribute("attribute2")).thenReturn(STRING_VALUE);
+
+        factory = AdapterFactoryTest.createModelAdapterFactory();
+        factory.injectors = Arrays.asList(new RequestAttributeInjector(), new 
SelfInjector());
+        factory.adapterImplementations.addClassesAsAdapterAndImplementation(
+                RecordModel.class, RecordModelWithStaticFields.class, 
RecordModelWithExtraConstructor.class);
+    }
+
+    @Test
+    void testRecordModel() {
+        RecordModel model = factory.getAdapter(request, RecordModel.class);
+        assertNotNull(model);
+        assertEquals(INT_VALUE, model.attribute());
+        assertEquals(STRING_VALUE, model.attribute2());
+    }
+
+    @Test
+    void testRecordModelWithStaticFields() {
+        RecordModelWithStaticFields model = factory.getAdapter(request, 
RecordModelWithStaticFields.class);
+        assertNotNull(model);
+        assertEquals(INT_VALUE, model.attribute());
+        assertEquals(STRING_VALUE, model.attribute2());
+    }
+
+    @Test
+    void testRecordModelWithExtraConstructorUsesCanonicalConstructor() {
+        RecordModelWithExtraConstructor model = factory.getAdapter(request, 
RecordModelWithExtraConstructor.class);
+        assertNotNull(model);
+        assertEquals(INT_VALUE, model.attribute());
+        assertEquals(STRING_VALUE, model.attribute2());
+    }
+}
diff --git a/src/test/java/org/apache/sling/models/impl/ReflectionUtilTest.java 
b/src/test/java/org/apache/sling/models/impl/ReflectionUtilTest.java
deleted file mode 100644
index 1dccb0f..0000000
--- a/src/test/java/org/apache/sling/models/impl/ReflectionUtilTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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 java.lang.reflect.Constructor;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@SuppressWarnings({
-    "PackageVisibleInnerClass",
-    "FieldCanBeLocal",
-    "unused",
-    "java:S1172",
-    "InstanceVariableMayNotBeInitialized",
-    "PublicConstructorInNonPublicClass"
-})
-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
-    void testBalancedConstructor() throws NoSuchMethodException {
-        Constructor<?> constructor = 
TestClassOne.class.getConstructor(int.class, String.class);
-        assertTrue(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-
-    @Test
-    void testMoreFieldsThanParams() throws NoSuchMethodException {
-        Constructor<?> constructor = 
TestClassTwo.class.getConstructor(int.class, String.class);
-        
assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-
-    @Test
-    void testMoreParamsThanFields() throws NoSuchMethodException {
-        Constructor<?> constructor = 
TestClassThree.class.getConstructor(int.class, String.class);
-        
assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-
-    @Test
-    void testNoFields() throws NoSuchMethodException {
-        Constructor<?> constructor = 
TestClassFour.class.getConstructor(int.class, String.class, boolean.class);
-        
assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-
-    @Test
-    void testOneFieldOneParam() throws NoSuchMethodException {
-        Constructor<?> constructor = 
TestClassFive.class.getConstructor(int.class);
-        assertTrue(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-
-    @Test
-    void testNoParams() throws NoSuchMethodException {
-        Constructor<?> constructor = TestClassSix.class.getConstructor();
-        
assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-
-    @Test
-    void testBalancedRepeatableTypes() throws NoSuchMethodException {
-        Constructor<?> constructor = 
TestClassSeven.class.getConstructor(String.class, String.class);
-        assertTrue(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-
-    @Test
-    void testUnbalancedRepeatableTypes() throws NoSuchMethodException {
-        Constructor<?> constructor = 
TestClassEight.class.getConstructor(String.class, String.class);
-        
assertFalse(ReflectionUtil.areBalancedCtorParamsAndFields(constructor));
-    }
-}
diff --git 
a/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModel.java
 
b/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModel.java
new file mode 100644
index 0000000..b38825c
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModel.java
@@ -0,0 +1,29 @@
+/*
+ * 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.testmodels.classes.constructorinjection;
+
+import javax.inject.Named;
+
+import org.apache.sling.api.SlingJakartaHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+
+@Model(adaptables = SlingJakartaHttpServletRequest.class)
+public record RecordModel(
+        @Named("attribute") int attribute,
+        @Named("attribute2") String attribute2) {}
diff --git 
a/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModelWithExtraConstructor.java
 
b/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModelWithExtraConstructor.java
new file mode 100644
index 0000000..ae3cc57
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModelWithExtraConstructor.java
@@ -0,0 +1,34 @@
+/*
+ * 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.testmodels.classes.constructorinjection;
+
+import javax.inject.Named;
+
+import org.apache.sling.api.SlingJakartaHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+
+@Model(adaptables = SlingJakartaHttpServletRequest.class)
+public record RecordModelWithExtraConstructor(
+        @Named("attribute") int attribute,
+        @Named("attribute2") String attribute2) {
+
+    public RecordModelWithExtraConstructor(@Named("attribute") int attribute) {
+        this(attribute, "fromExtraConstructor");
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModelWithStaticFields.java
 
b/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModelWithStaticFields.java
new file mode 100644
index 0000000..0b5f059
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/models/testmodels/classes/constructorinjection/RecordModelWithStaticFields.java
@@ -0,0 +1,39 @@
+/*
+ * 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.testmodels.classes.constructorinjection;
+
+import javax.inject.Named;
+
+import org.apache.sling.api.SlingJakartaHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+
+@Model(adaptables = SlingJakartaHttpServletRequest.class)
+public record RecordModelWithStaticFields(
+        @Named("attribute") int attribute,
+        @Named("attribute2") String attribute2) {
+
+    public static final String STATIC_CONSTANT = "constant";
+
+    @SuppressWarnings("unused")
+    private static int staticCounter;
+
+    public static String staticConstant() {
+        return STATIC_CONSTANT;
+    }
+}

Reply via email to