You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2023/09/11 00:54:30 UTC
[groovy] branch GROOVY_4_0_X updated: GROOVY-11167: JsonOutput should handle Records like POGOs
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch GROOVY_4_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY_4_0_X by this push:
new 62d4f9cd80 GROOVY-11167: JsonOutput should handle Records like POGOs
62d4f9cd80 is described below
commit 62d4f9cd8083089a930e21e5c647cabbb3d0f19f
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 d38152f931..53a18cdef4 100644
--- a/src/main/java/groovy/lang/MetaClassImpl.java
+++ b/src/main/java/groovy/lang/MetaClassImpl.java
@@ -88,6 +88,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.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -104,6 +105,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;
@@ -3471,6 +3473,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) {
@@ -3492,6 +3495,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 97d8ef8e2f..7a092db1b0 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.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
@@ -31,7 +32,9 @@ import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
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;
/**
@@ -120,4 +123,13 @@ public class Java16 extends Java10 {
throw new GroovyRuntimeException(e);
}
}
+
+ @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
+}