diff --git a/release-notes/VERSION b/release-notes/VERSION index 369fbe8970..0ac4bbaf3e 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -80,6 +80,7 @@ Versions: 3.x (for earlier see VERSION-2.x) (contributed by Joo-Hyuk K) #4818: Rename `AnnotationIntrospector.findDefaultCreator()` as `findPreferredCreator()` #4820: Change JDK baseline for Jackson 3.0 from Java 8 to Java 17 +#4835: Remove dynamic work-arounds wrt accessing `Record` definition #4840: Increase minimum Android SDK required to 34 for Jackson 3.0 - Remove `MappingJsonFactory` - Add context parameter for `TypeSerializer` contextualization (`forProperty()`) diff --git a/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java b/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java index e76ac0872b..b1629845c2 100644 --- a/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java +++ b/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java @@ -12,7 +12,7 @@ import tools.jackson.databind.MapperFeature; import tools.jackson.databind.annotation.JsonPOJOBuilder; import tools.jackson.databind.cfg.MapperConfig; -import tools.jackson.databind.jdk14.JDK14Util; +import tools.jackson.databind.util.RecordUtil; /** * Default {@link AccessorNamingStrategy} used by Jackson: to be used either as-is, @@ -502,7 +502,7 @@ public RecordNaming(MapperConfig config, AnnotatedClass forClass) { // trickier: regular fields are ok (handled differently), but should // we also allow getter discovery? For now let's do so "get", "is", null); - String[] recordFieldNames = JDK14Util.getRecordFieldNames(forClass.getRawType()); + String[] recordFieldNames = RecordUtil.getRecordFieldNames(forClass.getRawType()); // 01-May-2022, tatu: Due to [databind#3417] may return null when no info available _fieldNames = recordFieldNames == null ? Collections.emptySet() : diff --git a/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java index 030e707e33..f0db104d21 100644 --- a/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java @@ -12,8 +12,8 @@ import tools.jackson.databind.cfg.HandlerInstantiator; import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.deser.impl.UnwrappedPropertyHandler; -import tools.jackson.databind.jdk14.JDK14Util; import tools.jackson.databind.util.ClassUtil; +import tools.jackson.databind.util.RecordUtil; /** * Helper class used for aggregating information about all possible @@ -605,7 +605,7 @@ protected void _addCreators(Map props) // Needs to be done early to get implicit names populated final PotentialCreator primaryCreator; if (_isRecordType) { - primaryCreator = JDK14Util.findCanonicalRecordConstructor(_config, _classDef, constructors); + primaryCreator = RecordUtil.findCanonicalRecordConstructor(_config, _classDef, constructors); } else { // 02-Nov-2024, tatu: Alas, naming here is confusing: method properly // should have been "findPrimaryCreator()" so as not to confused with diff --git a/src/main/java/tools/jackson/databind/jdk14/JDK14Util.java b/src/main/java/tools/jackson/databind/jdk14/JDK14Util.java deleted file mode 100644 index bbfff902dd..0000000000 --- a/src/main/java/tools/jackson/databind/jdk14/JDK14Util.java +++ /dev/null @@ -1,187 +0,0 @@ -package tools.jackson.databind.jdk14; - -import java.lang.reflect.Method; -import java.util.List; - -import tools.jackson.databind.PropertyName; -import tools.jackson.databind.cfg.MapperConfig; -import tools.jackson.databind.introspect.AnnotatedClass; -import tools.jackson.databind.introspect.AnnotatedConstructor; -import tools.jackson.databind.introspect.PotentialCreator; -import tools.jackson.databind.util.ClassUtil; -import tools.jackson.databind.util.NativeImageUtil; - -/** - * Helper class to support some of JDK 14 (and later) features - * without Jackson itself being run on (or even built with) Java 14. - * In particular allows better support of {@code java.lang.Record} - * types (see JEP 359). - */ -public class JDK14Util -{ - public static String[] getRecordFieldNames(Class recordType) { - return RecordAccessor.instance().getRecordFieldNames(recordType); - } - - /** - * @since 2.18 - */ - public static PotentialCreator findCanonicalRecordConstructor(MapperConfig config, - AnnotatedClass recordClass, - List constructors) - { - final RawTypeName[] recordFields = RecordAccessor.instance().getRecordFields(recordClass.getRawType()); - - if (recordFields == null) { - // not a record, or no reflective access on native image - return null; - } - - // And then locate the canonical constructor - final int argCount = recordFields.length; - // One special case: zero-arg constructor not included in candidate List - if (argCount == 0) { - // Bit hacky but has to do: create new PotentialCreator let caller deal - AnnotatedConstructor defCtor = recordClass.getDefaultConstructor(); - if (defCtor != null) { - return new PotentialCreator(defCtor, null); - } - } - - main_loop: - for (PotentialCreator ctor : constructors) { - if (ctor.paramCount() != argCount) { - continue; - } - for (int i = 0; i < argCount; ++i) { - if (!ctor.creator().getRawParameterType(i).equals(recordFields[i].rawType)) { - continue main_loop; - } - } - // Found it! One more thing; get implicit Record field names: - final PropertyName[] implicits = new PropertyName[argCount]; - for (int i = 0; i < argCount; ++i) { - implicits[i] = PropertyName.construct(recordFields[i].name); - } - return ctor.introspectParamNames(config, implicits); - } - - throw new IllegalArgumentException("Failed to find the canonical Record constructor of type " - +ClassUtil.getTypeDescription(recordClass.getType())); - } - - static class RecordAccessor { - private final Method RECORD_GET_RECORD_COMPONENTS; - private final Method RECORD_COMPONENT_GET_NAME; - private final Method RECORD_COMPONENT_GET_TYPE; - - private final static RecordAccessor INSTANCE; - private final static RuntimeException PROBLEM; - - static { - RuntimeException prob = null; - RecordAccessor inst = null; - try { - inst = new RecordAccessor(); - } catch (RuntimeException e) { - prob = e; - } - INSTANCE = inst; - PROBLEM = prob; - } - - private RecordAccessor() throws RuntimeException { - try { - RECORD_GET_RECORD_COMPONENTS = Class.class.getMethod("getRecordComponents"); - Class c = Class.forName("java.lang.reflect.RecordComponent"); - RECORD_COMPONENT_GET_NAME = c.getMethod("getName"); - RECORD_COMPONENT_GET_TYPE = c.getMethod("getType"); - } catch (Exception e) { - throw new RuntimeException(String.format( -"Failed to access Methods needed to support `java.lang.Record`: (%s) %s", -e.getClass().getName(), e.getMessage()), e); - } - } - - public static RecordAccessor instance() { - if (PROBLEM != null) { - throw PROBLEM; - } - return INSTANCE; - } - - public String[] getRecordFieldNames(Class recordType) throws IllegalArgumentException - { - final Object[] components = recordComponents(recordType); - if (components == null) { - // not a record, or no reflective access on native image - return null; - } - final String[] names = new String[components.length]; - for (int i = 0; i < components.length; i++) { - try { - names[i] = (String) RECORD_COMPONENT_GET_NAME.invoke(components[i]); - } catch (Exception e) { - throw new IllegalArgumentException(String.format( -"Failed to access name of field #%d (of %d) of Record type %s", -i, components.length, ClassUtil.nameOf(recordType)), e); - } - } - return names; - } - - public RawTypeName[] getRecordFields(Class recordType) throws IllegalArgumentException - { - final Object[] components = recordComponents(recordType); - if (components == null) { - // not a record, or no reflective access on native image - return null; - } - final RawTypeName[] results = new RawTypeName[components.length]; - for (int i = 0; i < components.length; i++) { - String name; - try { - name = (String) RECORD_COMPONENT_GET_NAME.invoke(components[i]); - } catch (Exception e) { - throw new IllegalArgumentException(String.format( -"Failed to access name of field #%d (of %d) of Record type %s", -i, components.length, ClassUtil.nameOf(recordType)), e); - } - Class type; - try { - type = (Class) RECORD_COMPONENT_GET_TYPE.invoke(components[i]); - } catch (Exception e) { - throw new IllegalArgumentException(String.format( -"Failed to access type of field #%d (of %d) of Record type %s", -i, components.length, ClassUtil.nameOf(recordType)), e); - } - results[i] = new RawTypeName(type, name); - } - return results; - } - - protected Object[] recordComponents(Class recordType) throws IllegalArgumentException - { - try { - return (Object[]) RECORD_GET_RECORD_COMPONENTS.invoke(recordType); - } catch (Exception e) { - if (NativeImageUtil.isUnsupportedFeatureError(e)) { - return null; - } - throw new IllegalArgumentException("Failed to access RecordComponents of type " - +ClassUtil.nameOf(recordType)); - } - } - - } - - static class RawTypeName { - public final Class rawType; - public final String name; - - public RawTypeName(Class rt, String n) { - rawType = rt; - name = n; - } - } -} diff --git a/src/main/java/tools/jackson/databind/jdk14/package-info.java b/src/main/java/tools/jackson/databind/jdk14/package-info.java deleted file mode 100644 index 2ae921dff0..0000000000 --- a/src/main/java/tools/jackson/databind/jdk14/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** -Contains helper class(es) needed to support some of JDK14+ -features without requiring running or building using JDK 14. -*/ - -package tools.jackson.databind.jdk14; diff --git a/src/main/java/tools/jackson/databind/util/ClassUtil.java b/src/main/java/tools/jackson/databind/util/ClassUtil.java index 216ae94678..1d1255a128 100644 --- a/src/main/java/tools/jackson/databind/util/ClassUtil.java +++ b/src/main/java/tools/jackson/databind/util/ClassUtil.java @@ -219,11 +219,10 @@ public static boolean isBogusClass(Class cls) { } /** - * Helper method for detecting Java14-added new {@code Record} types + * Helper method for detecting Java14-added {@code Record} types */ public static boolean isRecordType(Class cls) { - Class parent = cls.getSuperclass(); - return (parent != null) && "java.lang.Record".equals(parent.getName()); + return cls.isRecord(); } public static boolean isObjectOrPrimitive(Class cls) { diff --git a/src/main/java/tools/jackson/databind/util/RecordUtil.java b/src/main/java/tools/jackson/databind/util/RecordUtil.java new file mode 100644 index 0000000000..5bf7048c06 --- /dev/null +++ b/src/main/java/tools/jackson/databind/util/RecordUtil.java @@ -0,0 +1,70 @@ +package tools.jackson.databind.util; + +import java.lang.reflect.RecordComponent; +import java.util.Arrays; +import java.util.List; + +import tools.jackson.databind.PropertyName; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.introspect.AnnotatedClass; +import tools.jackson.databind.introspect.AnnotatedConstructor; +import tools.jackson.databind.introspect.PotentialCreator; + +/** + * Helper class for finding so-called canonical constructor + * of Record types. + */ +public class RecordUtil +{ + public static String[] getRecordFieldNames(Class recordType) { + final RecordComponent[] components = recordType.getRecordComponents(); + if (components == null) { + // not a record, or no reflective access on native image + return null; + } + return Arrays.stream(components).map(RecordComponent::getName).toArray(String[]::new); + } + + public static PotentialCreator findCanonicalRecordConstructor(MapperConfig config, + AnnotatedClass recordClass, + List constructors) + { + final RecordComponent[] components = recordClass.getRawType().getRecordComponents(); + if (components == null) { + // not a record, or no reflective access on native image + return null; + } + + // And then locate the canonical constructor + final int argCount = components.length; + // One special case: zero-arg constructor not included in candidate List + if (argCount == 0) { + // Bit hacky but has to do: create new PotentialCreator let caller deal + AnnotatedConstructor defCtor = recordClass.getDefaultConstructor(); + if (defCtor != null) { + return new PotentialCreator(defCtor, null); + } + } + + main_loop: + for (PotentialCreator ctor : constructors) { + if (ctor.paramCount() != argCount) { + continue; + } + for (int i = 0; i < argCount; ++i) { + if (!ctor.creator().getRawParameterType(i).equals(components[i].getType())) { + continue main_loop; + } + } + // Found it! One more thing; get implicit Record field names: + final PropertyName[] implicits = new PropertyName[argCount]; + for (int i = 0; i < argCount; ++i) { + implicits[i] = PropertyName.construct(components[i].getName()); + } + return ctor.introspectParamNames(config, implicits); + } + + throw new IllegalArgumentException("Failed to find the canonical Record constructor of type " + +ClassUtil.getTypeDescription(recordClass.getType())); + } +}