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

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new c0d69539f9 GROOVY-11167: JsonOutput should handle Records like POGOs
c0d69539f9 is described below

commit c0d69539f916e31e623ad901242904334d85975d
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Sep 7 14:33:41 2023 +1000

    GROOVY-11167: JsonOutput should handle Records like POGOs
---
 src/main/java/groovy/lang/MetaClassImpl.java       | 23 ++++++++
 .../transform/RecordTypeASTTransformation.java     | 63 ++++++++++++++++++++++
 .../org/codehaus/groovy/vmplugin/VMPlugin.java     | 15 ++++++
 .../org/codehaus/groovy/vmplugin/v16/Java16.java   | 12 +++++
 .../test/groovy/groovy/json/JsonOutputTest.groovy  | 23 ++++++++
 5 files changed, 136 insertions(+)

diff --git a/src/main/java/groovy/lang/MetaClassImpl.java 
b/src/main/java/groovy/lang/MetaClassImpl.java
index 31cea6b0e3..1c2acff035 100644
--- a/src/main/java/groovy/lang/MetaClassImpl.java
+++ b/src/main/java/groovy/lang/MetaClassImpl.java
@@ -85,6 +85,7 @@ import org.objectweb.asm.Opcodes;
 import java.beans.BeanInfo;
 import java.beans.EventSetDescriptor;
 import java.beans.Introspector;
+import java.beans.MethodDescriptor;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
@@ -98,6 +99,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -3351,6 +3353,7 @@ public class MetaClassImpl implements MetaClass, 
MutableMetaClass {
         // build up the metaproperties based on the public fields, property 
descriptors,
         // and the getters and setters
         setupProperties(descriptors);
+        addRecordProperties(info);
 
         EventSetDescriptor[] eventDescriptors = info.getEventSetDescriptors();
         for (EventSetDescriptor descriptor : eventDescriptors) {
@@ -3372,6 +3375,26 @@ public class MetaClassImpl implements MetaClass, 
MutableMetaClass {
         }
     }
 
+    private void addRecordProperties(BeanInfo info) {
+        VMPlugin plugin = VMPluginFactory.getPlugin();
+        Set<String> componentNames = new 
HashSet<>(plugin.getRecordComponentNames(theClass));
+        if (!componentNames.isEmpty()) {
+            MethodDescriptor[] methodDescriptors = info.getMethodDescriptors();
+            Map<String, MetaProperty> propIndex = 
classPropertyIndex.computeIfAbsent(theCachedClass, x -> new LinkedHashMap<>());
+            for (MethodDescriptor md : methodDescriptors) {
+                if (md.getMethod().getParameterCount() != 0) continue;
+                String name = md.getName();
+                if (componentNames.contains(name)) {
+                    CachedMethod cachedMethod = 
CachedMethod.find(md.getMethod());
+                    if (cachedMethod != null) {
+                        MetaMethod accessor = findMethod(cachedMethod);
+                        createMetaBeanProperty(propIndex, name, true, 
accessor);
+                    }
+                }
+            }
+        }
+    }
+
     @SuppressWarnings("removal") // TODO a future Groovy version should 
perform the operation not as a privileged action
     private Object doPrivileged(PrivilegedExceptionAction action) throws 
PrivilegedActionException {
         return java.security.AccessController.doPrivileged(action);
diff --git 
a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java 
b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
index 821b1f81ee..de58c2ff56 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
@@ -38,10 +38,13 @@ import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.PropertyNode;
 import org.codehaus.groovy.ast.RecordComponentNode;
 import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.ArrayExpression;
 import org.codehaus.groovy.ast.expr.ClassExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.MapEntryExpression;
 import org.codehaus.groovy.ast.expr.PropertyExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
 import org.codehaus.groovy.ast.stmt.Statement;
 import org.codehaus.groovy.ast.stmt.SwitchStatement;
 import org.codehaus.groovy.ast.tools.GenericsUtils;
@@ -54,8 +57,15 @@ import org.objectweb.asm.Handle;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 
+import java.awt.Image;
+import java.beans.BeanDescriptor;
+import java.beans.EventSetDescriptor;
+import java.beans.MethodDescriptor;
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
 import java.lang.annotation.Annotation;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -75,6 +85,7 @@ import static org.codehaus.groovy.ast.ClassHelper.long_TYPE;
 import static org.codehaus.groovy.ast.ClassHelper.make;
 import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
@@ -82,11 +93,14 @@ import static 
org.codehaus.groovy.ast.tools.GeneralUtils.caseS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceProperties;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.hasDeclaredMethod;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.indexX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.mapEntryX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.mapX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX;
@@ -97,6 +111,7 @@ import static 
org.codehaus.groovy.ast.tools.GeneralUtils.switchS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.tryCatchS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 import static org.codehaus.groovy.runtime.StringGroovyMethods.isAtLeast;
 import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
@@ -120,6 +135,15 @@ public class RecordTypeASTTransformation extends 
AbstractASTTransformation imple
     private static final ClassNode LHMAP_TYPE = 
makeWithoutCaching(LinkedHashMap.class, false);
     private static final ClassNode NAMED_PARAM_TYPE = make(NamedParam.class);
     private static final ClassNode RECORD_OPTIONS_TYPE = 
make(RecordOptions.class);
+    private static final ClassNode SIMPLE_BEAN_INFO_TYPE = 
make(SimpleBeanInfo.class);
+    private static final ClassNode BEAN_DESCRIPTOR_TYPE = 
make(BeanDescriptor.class);
+    private static final ClassNode PROPERTY_DESCRIPTOR_TYPE = 
make(PropertyDescriptor.class);
+    private static final ClassNode PROPERTY_DESCRIPTOR_ARRAY_TYPE = 
make(PropertyDescriptor[].class);
+    private static final ClassNode EVENT_SET_DESCRIPTOR_TYPE = 
make(EventSetDescriptor.class);
+    private static final ClassNode EVENT_SET_DESCRIPTOR_ARRAY_TYPE = 
make(EventSetDescriptor[].class);
+    private static final ClassNode METHOD_DESCRIPTOR_TYPE = 
make(MethodDescriptor.class);
+    private static final ClassNode METHOD_DESCRIPTOR_ARRAY_TYPE = 
make(MethodDescriptor[].class);
+    private static final ClassNode IMAGE_TYPE = make(Image.class);
 
     private static final String COMPONENTS = "components";
     private static final String COPY_WITH = "copyWith";
@@ -215,6 +239,8 @@ public class RecordTypeASTTransformation extends 
AbstractASTTransformation imple
             }
         } else if (mode == RecordTypeMode.NATIVE) {
             addError(message + " when attempting to create a native record", 
cNode);
+        } else {
+            createBeanInfoClass(cNode);
         }
 
         String cName = cNode.getName();
@@ -290,6 +316,43 @@ public class RecordTypeASTTransformation extends 
AbstractASTTransformation imple
         }
     }
 
+    private void createBeanInfoClass(ClassNode cNode) {
+        ClassNode beanInfoClass = new ClassNode(cNode.getName() + "BeanInfo", 
ACC_PUBLIC, SIMPLE_BEAN_INFO_TYPE);
+        beanInfoClass.addMethod("getBeanDescriptor", ACC_PUBLIC, 
BEAN_DESCRIPTOR_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
+            ctorX(BEAN_DESCRIPTOR_TYPE, args(classX(cNode)))
+        ));
+        final List<PropertyNode> pList = getInstanceProperties(cNode);
+        BlockStatement block = new BlockStatement();
+        VariableExpression p = varX("p", PROPERTY_DESCRIPTOR_ARRAY_TYPE);
+        block.addStatement(
+            declS(p, new ArrayExpression(PROPERTY_DESCRIPTOR_TYPE, 
Collections.emptyList(), List.of(constX(pList.size()))))
+        );
+        for (int i = 0; i < pList.size(); i++) {
+            String name = pList.get(i).getName();
+            block.addStatement(tryCatchS(
+                assignS(indexX(p, constX(i)), ctorX(PROPERTY_DESCRIPTOR_TYPE, 
args(constX(name), classX(cNode), constX(name), nullX())))
+            ));
+        }
+        block.addStatement(returnS(p));
+        beanInfoClass.addMethod("getPropertyDescriptors", ACC_PUBLIC, 
PROPERTY_DESCRIPTOR_ARRAY_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, 
block);
+        beanInfoClass.addMethod("getEventSetDescriptors", ACC_PUBLIC, 
EVENT_SET_DESCRIPTOR_ARRAY_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, 
returnS(
+            new ArrayExpression(EVENT_SET_DESCRIPTOR_TYPE, 
Collections.emptyList(), List.of(constX(0)))
+        ));
+        beanInfoClass.addMethod("getMethodDescriptors", ACC_PUBLIC, 
METHOD_DESCRIPTOR_ARRAY_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, 
returnS(
+            new ArrayExpression(METHOD_DESCRIPTOR_TYPE, 
Collections.emptyList(), List.of(constX(0)))
+        ));
+        beanInfoClass.addMethod("getDefaultPropertyIndex", ACC_PUBLIC, 
int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
+            constX(-1)
+        ));
+        beanInfoClass.addMethod("getDefaultEventIndex", ACC_PUBLIC, int_TYPE, 
Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
+            constX(-1)
+        ));
+        beanInfoClass.addMethod("getIcon", ACC_PUBLIC, IMAGE_TYPE, 
params(param(int_TYPE, "iconKind")), ClassNode.EMPTY_ARRAY, returnS(
+            nullX()
+        ));
+        cNode.getModule().addClass(beanInfoClass);
+    }
+
     private void createComponents(ClassNode cNode, List<PropertyNode> pList) {
         if (pList.size() > 16) { // Groovy currently only goes to Tuple16
             addError("Record has too many components for a components() 
method", cNode);
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java 
b/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java
index aea8c112f7..5c27d9bb8e 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java
@@ -20,6 +20,7 @@ package org.codehaus.groovy.vmplugin;
 
 import groovy.lang.MetaClass;
 import groovy.lang.MetaMethod;
+import org.apache.groovy.lang.annotation.Incubating;
 import org.codehaus.groovy.GroovyBugError;
 import org.codehaus.groovy.ast.AnnotationNode;
 import org.codehaus.groovy.ast.ClassNode;
@@ -30,6 +31,7 @@ import java.lang.invoke.MethodHandles;
 import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Method;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -179,4 +181,17 @@ public interface VMPlugin {
     default Map<String, Set<String>> getDefaultImportClasses(String[] 
packageNames) {
         return Collections.emptyMap();
     }
+
+    /**
+     * Returns the list of record component names or the empty list if
+     * the class is not a record or running on a pre16 JDK.
+     *
+     * @param maybeRecord the class in question
+     * @return the default list of names
+     * @since 4.0.15
+     */
+    @Incubating
+    default List<String> getRecordComponentNames(Class<?> maybeRecord) {
+        return Collections.emptyList();
+    }
 }
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java 
b/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java
index ae02c4295b..f8169a73af 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java
@@ -19,6 +19,7 @@
 package org.codehaus.groovy.vmplugin.v16;
 
 import groovy.lang.GroovyRuntimeException;
+import org.apache.groovy.lang.annotation.Incubating;
 import org.codehaus.groovy.ast.AnnotationNode;
 import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.CompileUnit;
@@ -29,7 +30,9 @@ import java.lang.annotation.ElementType;
 import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.lang.reflect.RecordComponent;
 import java.util.Arrays;
+import java.util.List;
 import java.util.stream.Collectors;
 
 public class Java16 extends Java10 {
@@ -79,4 +82,13 @@ public class Java16 extends Java10 {
             return new RecordComponentNode(cn, rc.getName(), type, 
Arrays.stream(rc.getAnnotations()).map(this::toAnnotationNode).collect(Collectors.toList()));
         }).collect(Collectors.toList()));
     }
+
+    @Override
+    @Incubating
+    public List<String> getRecordComponentNames(Class<?> maybeRecord) {
+        if (maybeRecord.isRecord()) {
+            return 
Arrays.stream(maybeRecord.getRecordComponents()).map(RecordComponent::getName).toList();
+        }
+        return super.getRecordComponentNames(maybeRecord);
+    }
 }
diff --git 
a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonOutputTest.groovy 
b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonOutputTest.groovy
index 4aca996a61..8fdd2c7408 100644
--- a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonOutputTest.groovy
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonOutputTest.groovy
@@ -19,6 +19,7 @@
 package groovy.json
 
 import groovy.transform.Canonical
+import groovy.transform.TupleConstructor
 import org.junit.Test
 
 import static groovy.json.JsonOutput.toJson
@@ -543,6 +544,14 @@ final class JsonOutputTest {
             }""", json)
         '''
     }
+
+    @Test // GROOVY-11167
+    void testRecordsVsClass() {
+        def a = new PersonA('Guillaume')
+        def b = new PersonB('Jochen')
+        def c = new PersonC('Eric')
+        assert toJson([a,b,c]) == 
'[{"name":"Guillaume"},{"name":"Jochen"},{"name":"Eric"}]'
+    }
 }
 
 
//------------------------------------------------------------------------------
@@ -568,3 +577,17 @@ class JsonStreet {
 enum JsonStreetKind {
     street, boulevard, avenue
 }
+
+import groovy.transform.RecordOptions
+import static groovy.transform.RecordTypeMode.*
+
+@RecordOptions(mode=EMULATE)
+record PersonA(String name) {}
+
+// different JDK versions will cover the NATIVE case
+record PersonB(String name) {}
+
+@TupleConstructor
+class PersonC {
+    String name
+}

Reply via email to