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 +}