This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
commit 42176847af11a29e1e5007247302f9a2d379e648 Author: James Bognar <[email protected]> AuthorDate: Tue May 12 17:31:49 2026 -0400 refactor: lift marshalling-only logic from bean-runtime types and retype fields to commons SPI seams (TODO-5 Step 8b-ii Phases A+B) Co-authored-by: Cursor <[email protected]> --- .../src/main/java/org/apache/juneau/BeanMap.java | 37 +---- .../main/java/org/apache/juneau/BeanMapLoader.java | 78 +++++++++++ .../src/main/java/org/apache/juneau/BeanMeta.java | 33 +++-- .../java/org/apache/juneau/BeanPropertyMeta.java | 132 +++++------------ .../apache/juneau/BeanProxyInvocationHandler.java | 4 +- .../juneau/MarshalledPropertyPostProcessor.java | 156 +++++++++++++++++++++ .../java/org/apache/juneau/MarshallingContext.java | 4 +- .../java/org/apache/juneau/MarshallingSession.java | 8 +- .../java/org/apache/juneau/Annotations_Test.java | 10 +- .../test/java/org/apache/juneau/BeanMap_Test.java | 8 +- todo/TODO-5-bean-runtime-types-to-commons.md | 46 +++--- 11 files changed, 339 insertions(+), 177 deletions(-) diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java index 2024ecf378..226e12adc9 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java @@ -22,17 +22,13 @@ import static org.apache.juneau.commons.utils.StringUtils.*; import static org.apache.juneau.commons.utils.ThrowableUtils.*; import static org.apache.juneau.commons.utils.Utils.*; -import java.io.*; import java.util.*; import java.util.function.*; import org.apache.juneau.annotation.*; -import org.apache.juneau.collections.*; import org.apache.juneau.commons.bean.*; import org.apache.juneau.commons.reflect.*; import org.apache.juneau.internal.*; -import org.apache.juneau.json5.*; -import org.apache.juneau.parser.*; import org.apache.juneau.swap.*; /** @@ -494,7 +490,7 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T propertyCache = null; } catch (IllegalArgumentException e) { throw bex(e, meta.getClassMeta().inner(), "IllegalArgumentException occurred on call to class constructor ''{0}'' with argument types ''{1}''", c.getNameSimple(), - Json5Serializer.DEFAULT.toString(getClasses(args))); + Arrays.toString(getClasses(args))); } catch (Exception e) { throw bex(e); } @@ -622,37 +618,6 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T return this; } - /** - * Convenience method for setting multiple property values by passing in a reader. - * - * @param r The text that will get parsed into a map and then added to this map. - * @param p The parser to use to parse the text. - * @return This object. - * @throws ParseException Malformed input encountered. - * @throws IOException Thrown by <c>Reader</c>. - */ - public BeanMap<T> load(Reader r, ReaderParser p) throws ParseException, IOException { - putAll(JsonMap.ofText(r, p)); - return this; - } - - /** - * Convenience method for setting multiple property values by passing in JSON text. - * - * <h5 class='section'>Example:</h5> - * <p class='bjava'> - * <jv>beanMap</jv>.load(<js>"{name:'John Smith',age:21}"</js>) - * </p> - * - * @param input The text that will get parsed into a map and then added to this map. - * @return This object. - * @throws ParseException Malformed input encountered. - */ - public BeanMap<T> load(String input) throws ParseException { - putAll(JsonMap.ofJson(input)); - return this; - } - /** * Sets a property on the bean. * diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java new file mode 100644 index 0000000000..754f59f744 --- /dev/null +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java @@ -0,0 +1,78 @@ +/* + * 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.juneau; + +import java.io.*; + +import org.apache.juneau.collections.*; +import org.apache.juneau.parser.*; + +/** + * Marshalling-side helper for loading {@link BeanMap} contents from JSON text or a reader/parser pair. + * + * <p> + * These entry points used to live as {@code BeanMap.load(Reader, ReaderParser)} and {@code BeanMap.load(String)}. + * They were lifted out as part of TODO-5 Step 8b-ii because they reach into the marshalling-side + * {@link JsonMap} parser and {@link ReaderParser} types — neither belongs in the bean-modeling layer. + * + * <h5 class='section'>Example:</h5> + * <p class='bjava'> + * <jc>// Populate a bean map from a JSON string.</jc> + * BeanMap<Person> <jv>m</jv> = <jv>bc</jv>.newBeanMap(Person.<jk>class</jk>); + * BeanMapLoader.<jsm>load</jsm>(<jv>m</jv>, <js>"{name:'John',age:21}"</js>); + * </p> + */ +public final class BeanMapLoader { + + private BeanMapLoader() {} + + /** + * Populates the supplied {@link BeanMap} with the contents of the JSON text in {@code input}. + * + * <p> + * Equivalent to the legacy {@code BeanMap.load(String)} method. + * + * @param <T> The bean type. + * @param m The bean map to populate. + * @param input The text that will get parsed into a map and then added to {@code m}. + * @return The supplied bean map for fluent chaining. + * @throws ParseException Malformed input encountered. + */ + public static <T> BeanMap<T> load(BeanMap<T> m, String input) throws ParseException { + m.putAll(JsonMap.ofJson(input)); + return m; + } + + /** + * Populates the supplied {@link BeanMap} with the contents of the reader using the specified {@link ReaderParser}. + * + * <p> + * Equivalent to the legacy {@code BeanMap.load(Reader, ReaderParser)} method. + * + * @param <T> The bean type. + * @param m The bean map to populate. + * @param r The reader containing serialized text. + * @param p The parser to use to parse the text. + * @return The supplied bean map for fluent chaining. + * @throws ParseException Malformed input encountered. + * @throws IOException Thrown by the reader. + */ + public static <T> BeanMap<T> load(BeanMap<T> m, Reader r, ReaderParser p) throws ParseException, IOException { + m.putAll(JsonMap.ofText(r, p)); + return m; + } +} diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java index 8795a98aba..a85d3577b9 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java @@ -363,9 +363,9 @@ public class BeanMeta<T> { private final BeanConfigContext config; // Bean-modeling settings facade — always non-null. Sources: marshallingContext.getBeanConfigContext() (marshalling-side) or the explicit BeanConfigContext (commons-side). private final BeanFilter beanFilter; // Optional bean filter associated with the target class. Typed as the bean-modeling-side SPI seam; marshalling-side callers cast back to {@link MarshalledFilter} via {@link #getMarshalledFilter()}. private final NullableSupplier<InvocationHandler> beanProxyInvocationHandler; // The invocation handler for this bean (if it's an interface). - private final Supplier<BeanRegistry> beanRegistry; // The bean registry for this bean. + private final Supplier<BeanRegistryLookup> beanRegistry; // The bean registry for this bean. Typed against the commons.bean SPI seam; marshalling-side callers (and the {@link #getBeanRegistry()} getter) cast back to {@link BeanRegistry} — the only concrete in-tree implementation. private final Supplier<List<ClassInfo>> classHierarchy; // List of all classes traversed in the class hierarchy. - private final ClassMeta<T> classMeta; // The target class type that this meta object describes. Null when constructed via {@link #of(Class, BeanConfigContext)}. + private final BeanTypeInfo<T> classMeta; // The target class type that this meta object describes. Null when constructed via {@link #of(Class, BeanConfigContext)}. Concrete instances are always {@link ClassMeta}; typed against the bean-modeling SPI seam for the eventual move to commons.bean. private final ClassInfo classInfo; // Pure-reflection view of the bean class (Step 2 of TODO-5 — decouples bean modeling from ClassMeta). Always non-null. private final Supplier<String> dictionaryName; // The @Marshalled(typeName) annotation defined on this bean class. private final BeanPropertyMeta dynaProperty; // "extras" property. @@ -382,7 +382,7 @@ public class BeanMeta<T> { private final ClassInfo stopClass; // The stop class for hierarchy traversal. private final BeanPropertyMeta typeProperty; // "_type" mock bean property. private final String typePropertyName; // "_type" property actual name. - private final Map<BeanPropertyMeta,BeanRegistry> propertyBeanRegistries; // Per-property BeanRegistry side-map (Step 5 of TODO-5 — keeps BeanRegistry off BeanPropertyMeta itself). + private final Map<BeanPropertyMeta,BeanRegistryLookup> propertyBeanRegistries; // Per-property BeanRegistry side-map (Step 5 of TODO-5 — keeps BeanRegistry off BeanPropertyMeta itself). Typed against the commons.bean SPI seam; the {@link #getPropertyBeanRegistry(BeanPropertyMeta)} getter casts back to {@link BeanRegistry}. /** * Creates a {@link BeanMeta} for the specified class using the supplied {@link BeanConfigContext}. @@ -444,8 +444,8 @@ public class BeanMeta<T> { * @param pNames Explicit list of property names and order. If <jk>null</jk>, properties are determined automatically. * @param implClass Optional implementation class constructor to use if one cannot be found. Can be <jk>null</jk>. */ - protected BeanMeta(ClassMeta<T> cm, BeanFilter bf, String[] pNames, ClassInfo implClass) { - this(cm, cm, cm.getMarshallingContext().getBeanConfigContext(), cm.getMarshallingContext(), bf, pNames, implClass); + protected BeanMeta(BeanTypeInfo<T> cm, BeanFilter bf, String[] pNames, ClassInfo implClass) { + this((ClassMeta<T>) cm, (ClassMeta<T>) cm, ((ClassMeta<T>) cm).getMarshallingContext().getBeanConfigContext(), ((ClassMeta<T>) cm).getMarshallingContext(), bf, pNames, implClass); } /** @@ -491,7 +491,7 @@ public class BeanMeta<T> { var getterPropsMap = CollectionUtils.<Method,String>map(); // Convert to MethodInfo keys var setterPropsMap = CollectionUtils.<Method,String>map(); var dynaPropertyValue = Value.<BeanPropertyMeta>empty(); - var propertyBeanRegistriesTemp = CollectionUtils.<BeanPropertyMeta,BeanRegistry>map(); // Per-property BeanRegistry side-map (TODO-5 Step 5). + var propertyBeanRegistriesTemp = CollectionUtils.<BeanPropertyMeta,BeanRegistryLookup>map(); // Per-property BeanRegistry side-map (TODO-5 Step 5). var unsortedPropertiesTemp = false; var ba = ap.find(Marshalled.class, classInfo); var btList = ap.find(org.apache.juneau.commons.bean.BeanType.class, classInfo); @@ -631,7 +631,7 @@ public class BeanMeta<T> { // entries chain on top of bean and global dictionaries. Skipped on the commons-side path // (no marshallingContext means no BeanRegistry construction). if (nn(marshallingContext) && nn(v.dictionaryClasses)) - propertyBeanRegistriesTemp.put(pMeta, new BeanRegistry(marshallingContext, beanRegistry.get(), v.dictionaryClasses)); + propertyBeanRegistriesTemp.put(pMeta, new BeanRegistry(marshallingContext, (BeanRegistry) beanRegistry.get(), v.dictionaryClasses)); }); // If a beanFilter is defined, look for inclusion and exclusion lists. @@ -704,6 +704,13 @@ public class BeanMeta<T> { // raw-reflection mode (no ClassMeta/ObjectSwap/BeanRegistry resolution). if (p.validate(marshallingContext, typeVarImpls, readOnlyProps, writeOnlyProps)) { + // Marshalling-side post-processor — applies @MarshalledProp / @Swap annotation effects + // (swap detection, properties override, dictionary classes) after validate() succeeds. + // Skipped on the commons-side path: those annotations require a MarshallingContext to + // resolve ObjectSwap/StringFormatSwap/Surrogate types and the swap class meta. + if (nn(marshallingContext)) + MarshalledPropertyPostProcessor.process(marshallingContext, p); + installSwapAwareTransforms(p); if (nn(p.getter)) @@ -743,8 +750,8 @@ public class BeanMeta<T> { "unchecked" // Wildcard ObjectSwap captured by raw alias to allow runtime polymorphic dispatch. }) private static void installSwapAwareTransforms(BeanPropertyMeta.Builder p) { - ObjectSwap sw = p.swap; - ClassMeta<?> rtm = p.rawTypeMeta; + ObjectSwap sw = (ObjectSwap) p.swap; + ClassMeta<?> rtm = (ClassMeta<?>) p.rawTypeMeta; if (sw == null && (rtm == null || ! rtm.hasChildSwaps())) return; if (p.readTransform == null) { @@ -832,7 +839,7 @@ public class BeanMeta<T> { * * @return The bean registry for this bean, or <jk>null</jk> if no bean registry is associated with it. */ - public BeanRegistry getBeanRegistry() { return beanRegistry.get(); } + public BeanRegistry getBeanRegistry() { return (BeanRegistry) beanRegistry.get(); } /** * Returns the per-property {@link BeanRegistry} associated with the given {@link BeanPropertyMeta}. @@ -857,7 +864,7 @@ public class BeanMeta<T> { * belongs to a different bean meta). */ public BeanRegistry getPropertyBeanRegistry(BeanPropertyMeta p) { - return propertyBeanRegistries.get(p); + return (BeanRegistry) propertyBeanRegistries.get(p); } /** @@ -870,7 +877,7 @@ public class BeanMeta<T> { * * @return The {@link ClassMeta} of this bean, or <jk>null</jk> for bean-modeling-only construction. */ - public ClassMeta<T> getClassMeta() { return classMeta; } + public ClassMeta<T> getClassMeta() { return (ClassMeta<T>) classMeta; } /** * Returns the {@link ClassInfo} of this bean. @@ -1523,7 +1530,7 @@ public class BeanMeta<T> { * @return A new {@link BeanRegistry} containing the dictionary classes for this bean, or an empty registry if * no dictionary classes are found. */ - private BeanRegistry findBeanRegistry() { + private BeanRegistryLookup findBeanRegistry() { // BeanRegistry is a marshalling-side concern (polymorphic dispatch via @Marshalled(typeName)/dictionary). // On the commons-side construction path there is no MarshallingContext to drive it — return null and let // callers route through getPropertyBeanRegistry(...) (which is also empty on this path). diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java index d21237c5a9..f56a3b2a45 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java @@ -44,7 +44,6 @@ import org.apache.juneau.internal.*; import org.apache.juneau.parser.*; import org.apache.juneau.serializer.*; import org.apache.juneau.swap.*; -import org.apache.juneau.swaps.*; /** * Contains metadata about a bean property. @@ -85,7 +84,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { */ public static class Builder { BeanMeta<?> beanMeta; // Package-private for BeanMeta access - MarshallingContext bc; // Package-private for BeanMeta access. Null when the owning BeanMeta was built via the commons-side path. + Object bc; // Object-typed (was MarshallingContext) so the field can live in commons.bean; cast to MarshallingContext at marshalling-side use sites. Null when the owning BeanMeta was built via the commons-side path. BeanConfigContext config; // Package-private for BeanMeta access. Always non-null — sourced from the owning BeanMeta. String name; // Package-private for BeanMeta access FieldInfo field; // Package-private for BeanMeta access @@ -93,8 +92,8 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { MethodInfo getter; // Package-private for BeanMeta access MethodInfo setter; // Package-private for BeanMeta access MethodInfo extraKeys; // Package-private for BeanMeta access - ClassMeta<?> rawTypeMeta; // Package-private for BeanMeta access (used to install swap-aware transforms). Null on commons-side path (no type resolution). - ObjectSwap swap; // Package-private for BeanMeta access (used to install swap-aware transforms) + BeanTypeInfo<?> rawTypeMeta; // Package-private for BeanMeta access (used to install swap-aware transforms). Null on commons-side path (no type resolution). Concrete instances are always {@link ClassMeta} since it's the only in-tree implementation; the field is typed against the bean-modeling SPI seam so the field can live in commons.bean. + Object swap; // Object-typed so the field can live in commons.bean; cast to ObjectSwap by marshalling-side consumers. Set only via MarshalledPropertyPostProcessor (marshalling-side post-processor). BiFunction<MarshallingSession,Object,Object> readTransform; // Package-private; defaults to identity if null. BiFunction<MarshallingSession,Object,Object> writeTransform; // Package-private; defaults to identity if null. List<ClassInfo> dictionaryClasses; // Package-private for BeanMeta access; @MarshalledProp(dictionary={}) classes scanned during validate(). @@ -102,8 +101,8 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { private boolean isUri; private boolean isDyna; private boolean isDynaGetterMap; - private ClassMeta<?> typeMeta; - private List<String> properties; + BeanTypeInfo<?> typeMeta; // Package-private so the marshalling-side post-processor can override after @Swap/@MarshalledProp detection. Concrete instances are always {@link ClassMeta}; typed against the bean-modeling SPI seam. + List<String> properties; // Package-private so the marshalling-side post-processor can install @MarshalledProp(properties) override list. private Object overrideValue; private BeanPropertyMeta delegateFor; private boolean canRead; @@ -219,38 +218,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { assertArgNotNull(ARG_value, value); if (bc == null) return this; - return rawMetaType(bc.getClassMeta(value)); - } - - private static ObjectSwap marshalledPropSwap(AnnotationInfo<MarshalledProp> ai) { - var p = ai.inner(); - if (! p.format().isEmpty()) - return BeanInstantiator.of(ObjectSwap.class).type(StringFormatSwap.class).addBean(String.class, p.format()).run(); - return null; - } - - @SuppressWarnings({ - "java:S112" // throws RuntimeException intentional - callback/lifecycle method for swap initialization - }) - private static ObjectSwap swapSwap(AnnotationInfo<Swap> ai) throws RuntimeException { - var s = ai.inner(); - var c = s.value(); - if (isVoid(c)) - c = s.impl(); - if (isVoid(c)) - return null; - var ci = info(c); - if (ci.isAssignableTo(ObjectSwap.class)) { - var ps = BeanInstantiator.of(ObjectSwap.class).type(ci).run(); - if (nn(ps.forMediaTypes())) - throw unsupportedOp("TODO - Media types on swaps not yet supported on bean properties."); - if (nn(ps.withTemplate())) - throw unsupportedOp("TODO - Templates on swaps not yet supported on bean properties."); - return ps; - } - if (ci.isAssignableTo(Surrogate.class)) - throw unsupportedOp("TODO - Surrogate swaps not yet supported on bean properties."); - throw rex("Invalid class used in @Swap annotation. Must be a subclass of ObjectSwap or Surrogate. {0}", cn(c)); + return rawMetaType(((MarshallingContext) bc).getClassMeta(value)); } /** @@ -373,7 +341,6 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { }) public boolean validate(MarshallingContext bc, TypeVariables typeVarImpls, Set<String> bpro, Set<String> bpwo) throws Exception { - var bdClasses = list(); var ap = nn(bc) ? bc.getAnnotationProvider() : config.getAnnotationProvider(); if (field == null && getter == null && setter == null) @@ -390,9 +357,14 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { var gi = getter; var si = setter; + // @MarshalledProp / @Swap annotation reads have been lifted out to the marshalling-side post-processor + // (see {@link MarshalledPropertyPostProcessor#process}). They are processed by {@link BeanMeta} + // after this method returns. The post-processor mutates {@link #swap}, {@link #properties}, + // {@link #dictionaryClasses}, and {@link #typeMeta} (when a swap is detected) on this builder. + dictionaryClasses = liste(); + if (nn(innerField)) { var lbp = ap.find(BeanProp.class, ifi); - var lmp = ap.find(MarshalledProp.class, ifi); if (nn(bc) && (nn(field) || ne(lbp))) { // Only use field type if it's a bean property or has @BeanProp annotation. // Otherwise, we want to infer the type from the getter or setter. @@ -406,21 +378,11 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (ne(beanp.wo())) writeOnly = bool(beanp.wo()); }); - lmp.forEach(x -> { - var beanp = x.inner(); - if (swap == null) - swap = marshalledPropSwap(x); - if (ne(beanp.properties())) - properties = split(beanp.properties()); - bdClasses.addAll(l(beanp.dictionary())); - }); - ap.find(Swap.class, ifi).stream().findFirst().ifPresent(x -> swap = swapSwap(x)); isUri |= ap.has(Uri.class, ifi); } if (nn(getter)) { var lbp = ap.find(BeanProp.class, gi); - var lmp = ap.find(MarshalledProp.class, gi); if (nn(bc) && rawTypeMeta == null) rawTypeMeta = bc.resolveClassMeta(opt(last(lbp)).orElse(null), getter.getReturnType(), typeVarImpls); if (nn(rawTypeMeta)) @@ -433,20 +395,10 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (ne(beanp.wo())) writeOnly = bool(beanp.wo()); }); - lmp.forEach(x -> { - var beanp = x.inner(); - if (swap == null) - swap = marshalledPropSwap(x); - if (nn(properties) && ne(beanp.properties())) - properties = split(beanp.properties()); - bdClasses.addAll(l(beanp.dictionary())); - }); - ap.find(Swap.class, gi).stream().forEach(x -> swap = swapSwap(x)); } if (nn(setter)) { var lbp = ap.find(BeanProp.class, si); - var lmp = ap.find(MarshalledProp.class, si); if (nn(bc) && rawTypeMeta == null) rawTypeMeta = bc.resolveClassMeta(opt(last(lbp)).orElse(null), setter.getParameterTypes().get(0), typeVarImpls); if (nn(rawTypeMeta)) @@ -459,15 +411,6 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (ne(beanp.wo())) writeOnly = bool(beanp.wo()); }); - lmp.forEach(x -> { - var beanp = x.inner(); - if (swap == null) - swap = marshalledPropSwap(x); - if (nn(properties) && ne(beanp.properties())) - properties = split(beanp.properties()); - bdClasses.addAll(l(beanp.dictionary())); - }); - ap.find(Swap.class, si).stream().forEach(x -> swap = swapSwap(x)); } // On the commons-side path (bc == null), rawTypeMeta stays null and validate() accepts the property @@ -475,8 +418,6 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (nn(bc) && rawTypeMeta == null) return false; - dictionaryClasses = bdClasses.stream().map(ReflectionUtils::info).toList(); - isDyna = "*".equals(name); // Do some annotation validation. Type-aware validation requires rawTypeMeta — on the commons-side @@ -528,13 +469,10 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { } if (typeMeta == null) { - if (nn(swap) && nn(bc)) { - typeMeta = bc.getClassMeta(swap.getSwapClass()); - } else if (rawTypeMeta == null && nn(bc)) { + if (rawTypeMeta == null && nn(bc)) typeMeta = bc.object(); - } else { + else typeMeta = rawTypeMeta; - } } if (typeMeta == null) typeMeta = rawTypeMeta; @@ -563,7 +501,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { private final AnnotationProvider ap; // Annotation provider for finding annotations on this property. Sourced from bc (marshalling-side) or beanMeta.getConfig() (commons-side). private final Supplier<List<AnnotationInfo<?>>> annotations; // Memoized list of all annotations on this property. - private final MarshallingContext bc; // The context that created this meta. Null when the owning BeanMeta was built via the commons-side path. + private final Object bc; // MarshallingContext, but Object-typed so the field can live in commons.bean. Cast at marshalling-side use sites. Null when the owning BeanMeta was built via the commons-side path. private final BeanConfigContext config; // Bean-modeling settings facade — always non-null. Mirrors the BeanMeta's config. private final BeanMeta<?> beanMeta; // The bean that this property belongs to. private final boolean canRead; // True if this property can be read. @@ -580,12 +518,12 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { private final String name; // The name of the property. private final Object overrideValue; // The bean property value (if it's an overridden delegate). private final List<String> properties; // The value of the @MarshalledProp(properties) annotation (unmodifiable). - private final ClassMeta<?> rawTypeMeta; // The real class type of the bean property. + private final BeanTypeInfo<?> rawTypeMeta; // The real class type of the bean property. Concrete instances are always {@link ClassMeta}; typed against the bean-modeling SPI seam for the eventual move to commons.bean. private final BiFunction<MarshallingSession,Object,Object> readTransform; // Applied to raw getter result; identity by default. private final boolean readOnly; // True if this property is read-only. private final MethodInfo setter; // The bean property setter. - private final ObjectSwap swap; // ObjectSwap defined only via @MarshalledProp(format=...) annotation. - private final ClassMeta<?> typeMeta; // The transformed class type of the bean property. + private final Object swap; // ObjectSwap, but Object-typed so the field can live in commons.bean; cast at marshalling-side use sites. Defined only via @MarshalledProp(format=...) or @Swap. + private final BeanTypeInfo<?> typeMeta; // The transformed class type of the bean property. Concrete instances are always {@link ClassMeta}; typed against the bean-modeling SPI seam. private final BiFunction<MarshallingSession,Object,Object> writeTransform; // Applied to incoming value before raw setter; identity by default. private final boolean writeOnly; // True if this property is write-only. @@ -621,7 +559,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { readTransform = b.readTransform != null ? b.readTransform : (session, o) -> o; writeTransform = b.writeTransform != null ? b.writeTransform : (session, o) -> o; - ap = nn(bc) ? bc.getAnnotationProvider() : b.config.getAnnotationProvider(); + ap = nn(bc) ? ((MarshallingContext) bc).getAnnotationProvider() : b.config.getAnnotationProvider(); hashCode = h(beanMeta, name); } @@ -661,8 +599,8 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { // Read-only beans get their properties stored in a cache. if (m.bean == null) { if (! m.propertyCache.containsKey(name)) - m.propertyCache.put(name, new JsonList(m.getMarshallingSession())); - ((JsonList)m.propertyCache.get(name)).add(value); + m.propertyCache.put(name, new ArrayList<>()); + ((List)m.propertyCache.get(name)).add(value); return; } @@ -696,7 +634,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (rawTypeMeta.canCreateNewInstance()) c = (Collection)rawTypeMeta.newInstance(); else - c = new JsonList(session); + c = new ArrayList<>(); if (c2 != null) c.addAll(c2); @@ -764,8 +702,8 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { // Read-only beans get their properties stored in a cache. if (m.bean == null) { if (! m.propertyCache.containsKey(name)) - m.propertyCache.put(name, new JsonMap(m.getMarshallingSession())); - ((JsonMap)m.propertyCache.get(name)).append(key, value); + m.propertyCache.put(name, new LinkedHashMap<>()); + ((Map)m.propertyCache.get(name)).put(key, value); return; } @@ -793,7 +731,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (rawTypeMeta.canCreateNewInstance()) map = (Map)rawTypeMeta.newInstance(); else - map = new JsonMap(session); + map = new LinkedHashMap<>(); map.put(key, v); @@ -965,7 +903,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { * * @return The {@link ClassMeta} of the class of this property. */ - public ClassMeta<?> getClassMeta() { return typeMeta; } + public ClassMeta<?> getClassMeta() { return (ClassMeta<?>) typeMeta; } /** * Returns the metadata on the property that this metadata is a delegate for. @@ -1184,7 +1122,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { "java:S3776", // Cognitive complexity acceptable for complex property setter "java:S6541" // Brain method acceptable - complex property value setting logic requires high LOC/complexity }) - private Object setPropertyValue(BeanMap<?> m, String pName, Object value1, Object bean, boolean isMap, boolean isCollection, MarshallingSession session) throws ParseException { + private Object setPropertyValue(BeanMap<?> m, String pName, Object value1, Object bean, boolean isMap, boolean isCollection, MarshallingSession session) { try { var r = (config.isBeanMapPutReturnsOldValue() || isMap || isCollection) && (nn(getter) || nn(field)) ? get(m, pName) : null; var propertyClass = rawTypeMeta.inner(); @@ -1314,10 +1252,12 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { }); } else { - if (nn(swap) && value1 != null && swap.getSwapClass().isAssignableFrom(value1.getClass())) { + if (nn(swap) && value1 != null && ((ObjectSwap)swap).getSwapClass().isAssignableFrom(value1.getClass())) { // Defensive double-unswap path: value1 is still in swapped form (the outer writeTransform // did not normalize it for some reason). Route through the install-time write transform so - // BeanPropertyMeta itself does not invoke ObjectSwap directly. + // BeanPropertyMeta itself does not invoke ObjectSwap directly. Note: cast lives here because + // the field is Object-typed for the eventual move to commons.bean; this entire branch is + // expected to migrate to a marshalling-side post-processor in Phase C. value1 = writeTransform.apply(session, value1); } else { // Pass bean as outer for non-static inner class instantiation (e.g. J2 with string constructor) @@ -1384,7 +1324,7 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { var propsArray = properties == null ? null : properties.toArray(new String[0]); return new FilteredKeyMap(cm, o2, propsArray); } - var bm = bc.getBeanMeta(o.getClass()); + var bm = ((MarshallingContext) bc).getBeanMeta(o.getClass()); if (nn(bm)) return newBeanMap(session, o, new BeanMetaFiltered(cm.getBeanMeta(), properties)); } @@ -1442,19 +1382,19 @@ public class BeanPropertyMeta implements Comparable<BeanPropertyMeta> { if (nn(properties) && nn(rawTypeMeta)) { if (rawTypeMeta.isArray()) { var a = (Object[])o; - var l1 = new DelegateList(rawTypeMeta); - var childType1 = rawTypeMeta.getElementType(); + var l1 = new DelegateList((ClassMeta<?>) rawTypeMeta); + var childType1 = (ClassMeta<?>) rawTypeMeta.getElementType(); for (var c1 : a) l1.add(applyChildPropertiesFilter(session, childType1, c1)); return l1; } else if (rawTypeMeta.isCollection()) { var c = (Collection)o; var l = listOfSize(c.size()); - var childType = rawTypeMeta.getElementType(); + var childType = (ClassMeta<?>) rawTypeMeta.getElementType(); c.forEach(x -> l.add(applyChildPropertiesFilter(session, childType, x))); return l; } else { - return applyChildPropertiesFilter(session, rawTypeMeta, o); + return applyChildPropertiesFilter(session, (ClassMeta<?>) rawTypeMeta, o); } } return o; diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanProxyInvocationHandler.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanProxyInvocationHandler.java index 9f5bb836db..be571d4962 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanProxyInvocationHandler.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanProxyInvocationHandler.java @@ -23,8 +23,6 @@ import static org.apache.juneau.commons.utils.Utils.*; import java.lang.reflect.*; import java.util.*; -import org.apache.juneau.json5.*; - /** * Provides an {@link InvocationHandler} for creating dynamic proxy instances of bean interfaces. * @@ -140,7 +138,7 @@ public class BeanProxyInvocationHandler<T> implements InvocationHandler { return Integer.valueOf(this.beanProps.hashCode()); if (mi.hasName("toString") && mi.getParameterCount() == 0) - return Json5Serializer.DEFAULT.toString(this.beanProps); + return Objects.toString(this.beanProps); var prop = meta.getGetterProps().get(method); if (nn(prop)) diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshalledPropertyPostProcessor.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshalledPropertyPostProcessor.java new file mode 100644 index 0000000000..c0f1332bc0 --- /dev/null +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshalledPropertyPostProcessor.java @@ -0,0 +1,156 @@ +/* + * 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.juneau; + +import static org.apache.juneau.commons.reflect.ReflectionUtils.*; +import static org.apache.juneau.commons.utils.ClassUtils.*; +import static org.apache.juneau.commons.utils.CollectionUtils.*; +import static org.apache.juneau.commons.utils.StringUtils.*; +import static org.apache.juneau.commons.utils.ThrowableUtils.*; +import static org.apache.juneau.commons.utils.Utils.*; + +import java.util.*; + +import org.apache.juneau.annotation.*; +import org.apache.juneau.commons.inject.*; +import org.apache.juneau.commons.reflect.*; +import org.apache.juneau.swap.*; +import org.apache.juneau.swaps.*; + +/** + * Marshalling-side post-processor that applies {@link MarshalledProp @MarshalledProp} and {@link Swap @Swap} + * annotation effects to a {@link BeanPropertyMeta.Builder} after {@link BeanPropertyMeta.Builder#validate validate()} + * runs successfully. + * + * <p> + * The reads here used to live inside {@link BeanPropertyMeta.Builder#validate}. They were lifted out as part of + * TODO-5 Step 8b-ii so the bean-modeling builder no longer references the marshalling-side + * {@link ObjectSwap}/{@link StringFormatSwap}/{@link Surrogate} types or the {@link MarshalledProp} annotation. + * + * <p> + * This post-processor mutates the following fields on the supplied builder when it runs: + * <ul> + * <li>{@code swap} — set to a {@link StringFormatSwap} (from {@link MarshalledProp#format()}) or a custom + * {@link ObjectSwap} (from {@link Swap}). + * <li>{@code properties} — set to the property-override list from {@link MarshalledProp#properties()}. + * <li>{@code dictionaryClasses} — appended with the {@link MarshalledProp#dictionary()} entries. + * <li>{@code typeMeta} — refreshed to the swap class meta when a swap is installed. + * </ul> + * + * <p> + * Only invoked from {@link BeanMeta#validateAndRegisterProperty} on the marshalling-side construction path + * (i.e. when {@link MarshallingContext} is non-null). The bean-modeling-only path + * ({@link BeanMeta#of(Class, BeanConfigContext)}) does not run this post-processor. + */ +final class MarshalledPropertyPostProcessor { + + private MarshalledPropertyPostProcessor() {} + + /** + * Applies {@code @MarshalledProp}/{@code @Swap} annotation effects to {@code b} using {@code bc} for + * {@link ClassMeta} resolution. + * + * @param bc The marshalling context. Must not be <jk>null</jk>. + * @param b The bean-property builder. Must not be <jk>null</jk>. + */ + static void process(MarshallingContext bc, BeanPropertyMeta.Builder b) { + var ap = bc.getAnnotationProvider(); + var bdClasses = new ArrayList<Class<?>>(); + + // innerField, getter, setter — same order as the original validate() loop. + if (nn(b.innerField)) { + ap.find(MarshalledProp.class, b.innerField).forEach(x -> { + var mp = x.inner(); + if (b.swap == null) + b.swap = marshalledPropSwap(x); + if (ne(mp.properties())) + b.properties = split(mp.properties()); + bdClasses.addAll(l(mp.dictionary())); + }); + ap.find(Swap.class, b.innerField).stream().findFirst().ifPresent(x -> b.swap = swapSwap(x)); + } + + if (nn(b.getter)) { + ap.find(MarshalledProp.class, b.getter).forEach(x -> { + var mp = x.inner(); + if (b.swap == null) + b.swap = marshalledPropSwap(x); + if (nn(b.properties) && ne(mp.properties())) + b.properties = split(mp.properties()); + bdClasses.addAll(l(mp.dictionary())); + }); + ap.find(Swap.class, b.getter).stream().forEach(x -> b.swap = swapSwap(x)); + } + + if (nn(b.setter)) { + ap.find(MarshalledProp.class, b.setter).forEach(x -> { + var mp = x.inner(); + if (b.swap == null) + b.swap = marshalledPropSwap(x); + if (nn(b.properties) && ne(mp.properties())) + b.properties = split(mp.properties()); + bdClasses.addAll(l(mp.dictionary())); + }); + ap.find(Swap.class, b.setter).stream().forEach(x -> b.swap = swapSwap(x)); + } + + if (! bdClasses.isEmpty()) { + var infos = new ArrayList<ClassInfo>(b.dictionaryClasses.size() + bdClasses.size()); + infos.addAll(b.dictionaryClasses); + bdClasses.forEach(c -> infos.add(info(c))); + b.dictionaryClasses = infos; + } + + // If a swap was installed and we have a rawTypeMeta, refresh typeMeta to the swap's swap-class meta. + // Matches the original behavior inside validate() which routed swap-class meta through bc.getClassMeta. + // Cast to ObjectSwap because b.swap is Object-typed (BeanPropertyMeta.Builder lives in the bean-modeling + // layer and cannot reference ObjectSwap directly). + if (nn(b.swap) && nn(b.rawTypeMeta)) + b.typeMeta = bc.getClassMeta(((ObjectSwap) b.swap).getSwapClass()); + } + + private static ObjectSwap marshalledPropSwap(AnnotationInfo<MarshalledProp> ai) { + var p = ai.inner(); + if (! p.format().isEmpty()) + return BeanInstantiator.of(ObjectSwap.class).type(StringFormatSwap.class).addBean(String.class, p.format()).run(); + return null; + } + + @SuppressWarnings({ + "java:S112" // throws RuntimeException intentional - callback/lifecycle method for swap initialization + }) + private static ObjectSwap swapSwap(AnnotationInfo<Swap> ai) { + var s = ai.inner(); + var c = s.value(); + if (isVoid(c)) + c = s.impl(); + if (isVoid(c)) + return null; + var ci = info(c); + if (ci.isAssignableTo(ObjectSwap.class)) { + var ps = BeanInstantiator.of(ObjectSwap.class).type(ci).run(); + if (nn(ps.forMediaTypes())) + throw unsupportedOp("TODO - Media types on swaps not yet supported on bean properties."); + if (nn(ps.withTemplate())) + throw unsupportedOp("TODO - Templates on swaps not yet supported on bean properties."); + return ps; + } + if (ci.isAssignableTo(Surrogate.class)) + throw unsupportedOp("TODO - Surrogate swaps not yet supported on bean properties."); + throw rex("Invalid class used in @Swap annotation. Must be a subclass of ObjectSwap or Surrogate. {0}", cn(c)); + } +} diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java index 91073ef242..be8e32c3f0 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java @@ -4184,7 +4184,7 @@ public class MarshallingContext extends Context implements ConversionFinder { return (in, memberOf, session, args) -> { try { var bs = beanSessionOrDefault(session); - return bs.newBeanMap(toMeta.inner()).load(in.toString()).getBean(); + return BeanMapLoader.load(bs.newBeanMap(toMeta.inner()), in.toString()).getBean(); } catch (Exception e) { throw rex(e); } @@ -4312,7 +4312,7 @@ public class MarshallingContext extends Context implements ConversionFinder { return (in, memberOf, session, args) -> { try { var bs = beanSessionOrDefault(session); - return bs.newBeanMap(toMeta.inner()).load(in.toString()).getBean(); + return BeanMapLoader.load(bs.newBeanMap(toMeta.inner()), in.toString()).getBean(); } catch (Exception e) { throw rex(e); } diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java index 0b5358bdec..66e50ee20d 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java @@ -1290,11 +1290,13 @@ public class MarshallingSession extends ContextSession implements ConverterSessi */ @Override /* BeanSession */ public final Object convertToType(Object value, Object targetType) { + if (targetType == null) + return convertToType(value, (ClassMeta<?>) null); if (targetType instanceof ClassMeta<?> cm) return convertToType(value, cm); if (targetType instanceof Class<?> c) return convertToType(value, c); - throw illegalArg("Unsupported targetType for convertToType: {0}", targetType == null ? "null" : targetType.getClass().getName()); + throw illegalArg("Unsupported targetType for convertToType: {0}", targetType.getClass().getName()); } /** @@ -1310,11 +1312,13 @@ public class MarshallingSession extends ContextSession implements ConverterSessi */ @Override /* BeanSession */ public final Object convertToMemberType(Object outer, Object value, Object targetType) { + if (targetType == null) + return convertToMemberType(outer, value, (ClassMeta<?>) null); if (targetType instanceof ClassMeta<?> cm) return convertToMemberType(outer, value, cm); if (targetType instanceof Class<?> c) return convertToMemberType(outer, value, c); - throw illegalArg("Unsupported targetType for convertToMemberType: {0}", targetType == null ? "null" : targetType.getClass().getName()); + throw illegalArg("Unsupported targetType for convertToMemberType: {0}", targetType.getClass().getName()); } } \ No newline at end of file diff --git a/juneau-utest/src/test/java/org/apache/juneau/Annotations_Test.java b/juneau-utest/src/test/java/org/apache/juneau/Annotations_Test.java index 0b2ee38fbb..ce35d6cb22 100755 --- a/juneau-utest/src/test/java/org/apache/juneau/Annotations_Test.java +++ b/juneau-utest/src/test/java/org/apache/juneau/Annotations_Test.java @@ -34,7 +34,7 @@ class Annotations_Test extends TestBase { var bc = MarshallingContext.DEFAULT; // Basic test - var bm = bc.newBeanMap(Person1.class).load("{age:21,name:'foobar'}"); + var bm = BeanMapLoader.load(bc.newBeanMap(Person1.class), "{age:21,name:'foobar'}"); assertBean(bm.getBean(), "name,age", "foobar,21"); bm.put("age", 65); @@ -57,7 +57,7 @@ class Annotations_Test extends TestBase { var bc = MarshallingContext.DEFAULT; // Basic test - var bm = bc.newBeanMap(Person2.class).load("{age:21,name:'foobar'}"); + var bm = BeanMapLoader.load(bc.newBeanMap(Person2.class), "{age:21,name:'foobar'}"); assertBean(bm.getBean(), "name,age", "foobar,21"); bm.put("age", 65); @@ -80,7 +80,7 @@ class Annotations_Test extends TestBase { var bc = MarshallingContext.DEFAULT; // Basic test - var bm = bc.newBeanMap(Person3.class).load("{age:21,name:'foobar'}"); + var bm = BeanMapLoader.load(bc.newBeanMap(Person3.class), "{age:21,name:'foobar'}"); assertBean(bm.getBean(), "name,age", "foobar,21"); bm.put("age", 65); @@ -103,7 +103,7 @@ class Annotations_Test extends TestBase { var bc = MarshallingContext.DEFAULT.copy().applyAnnotations(PersonConfig.class).build(); // Basic test - var bm = bc.newBeanMap(Person4.class).load("{age:21,name:'foobar'}"); + var bm = BeanMapLoader.load(bc.newBeanMap(Person4.class), "{age:21,name:'foobar'}"); assertBean(bm.getBean(), "name,age", "foobar,21"); bm.put("age", 65); @@ -131,7 +131,7 @@ class Annotations_Test extends TestBase { var bc = MarshallingContext.DEFAULT; // Make sure only public fields are detected - var bm = bc.newBeanMap(A.class).load("{publicField:123}"); + var bm = BeanMapLoader.load(bc.newBeanMap(A.class), "{publicField:123}"); assertBean(bm, "publicField", "123"); } diff --git a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java index 28c8f62b55..021e82bfff 100755 --- a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java +++ b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java @@ -639,7 +639,7 @@ class BeanMap_Test extends TestBase { assertBean(t7, "enum1,enum2", "ONE,TWO"); // Use MarshallingContext to create bean instance. - m = MarshallingContext.DEFAULT.newBeanMap(H.class).load("{enum1:'TWO',enum2:'THREE'}"); + m = BeanMapLoader.load(MarshallingContext.DEFAULT.newBeanMap(H.class), "{enum1:'TWO',enum2:'THREE'}"); assertEquals("{_type:'H',enum1:'TWO',enum2:'THREE'}", serializer.serialize(m.getBean())); t7 = m.getBean(); assertBean(t7, "enum1,enum2", "TWO,THREE"); @@ -921,7 +921,7 @@ class BeanMap_Test extends TestBase { // testPropertyNameFactoryDashedLC1 //==================================================================================================== @Test void a18_propertyNameFactoryDashedLC1() { - var m = bc.newBeanMap(P1.class).load("{'foo':1,'bar-baz':2,'bing-boo-url':3}"); + var m = BeanMapLoader.load(bc.newBeanMap(P1.class), "{'foo':1,'bar-baz':2,'bing-boo-url':3}"); assertBean(m, "foo,bar-baz,bing-boo-url", "1,2,3"); assertBean(m.getBean(), "foo,barBaz,bingBooURL", "1,2,3"); m.put("foo", 4); @@ -936,7 +936,7 @@ class BeanMap_Test extends TestBase { } @Test void a19_propertyNameFactoryDashedLC1_usingConfig() { - var m = bc.copy().applyAnnotations(P1cConfig.class).build().newBeanMap(P1c.class).load("{'foo':1,'bar-baz':2,'bing-boo-url':3}"); + var m = BeanMapLoader.load(bc.copy().applyAnnotations(P1cConfig.class).build().newBeanMap(P1c.class), "{'foo':1,'bar-baz':2,'bing-boo-url':3}"); assertBean(m, "foo,bar-baz,bing-boo-url", "1,2,3"); assertBean(m.getBean(), "foo,barBaz,bingBooURL", "1,2,3"); m.put("foo", 4); @@ -959,7 +959,7 @@ class BeanMap_Test extends TestBase { //==================================================================================================== @Test void a20_propertyNameFactoryDashedLC2() { var bc2 = MarshallingContext.DEFAULT; - var m = bc2.newBeanMap(P2.class).load("{'foo-bar':1,'baz-bing':2}"); + var m = BeanMapLoader.load(bc2.newBeanMap(P2.class), "{'foo-bar':1,'baz-bing':2}"); assertBean(m, "foo-bar,baz-bing", "1,2"); assertBean(m.getBean(), "fooBar,bazBING", "1,2"); m.put("foo-bar", 3); diff --git a/todo/TODO-5-bean-runtime-types-to-commons.md b/todo/TODO-5-bean-runtime-types-to-commons.md index fd6b4ee8e1..5cd96bef5e 100644 --- a/todo/TODO-5-bean-runtime-types-to-commons.md +++ b/todo/TODO-5-bean-runtime-types-to-commons.md @@ -99,22 +99,36 @@ Known limitations of the commons-side path (acceptable for Step 6, scoped for la - **`Delegate<T>` moved** from `org.apache.juneau` to `org.apache.juneau.commons.bean` via `git mv`. The interface's `getClassMeta()` method now returns `BeanTypeInfo<T>` instead of `ClassMeta<T>`. All five in-tree `Delegate` implementations (`BeanMap`, `DelegateBeanMap`, `DelegateList`, `DelegateMap`, `FilteredKeyMap`) keep their `ClassMeta<T>` return types via Java covariant returns (no source changes needed in those classes). Five marshalling-side files that used the raw-typed `(Del [...] - **Build/test verification.** `python3 scripts/test.py --full` passed clean (juneau + juneau-rest + juneau-microservice + examples + utests, ~70k tests). `cd juneau-core/juneau-commons && mvn clean compile` passes standalone — `juneau-commons` still compiles without depending on `juneau-marshall`. `ReadLints` clean on all modified files. - [ ] **Step 8b-ii** — Continuation of the bean-runtime move. Remaining work to physically relocate the 8 target files (`BeanMap`, `BeanMapEntry`, `BeanMeta`, `BeanMetaFiltered`, `BeanPropertyMeta`, `BeanPropertyValue`, `BeanPropertyConsumer`, `BeanProxyInvocationHandler`) into `commons.bean`. The SPI seams from 8b-i are in place; this step is the field-retype + cross-module-leakage cleanup pass: - - **`BeanPropertyMeta.rawTypeMeta` / `typeMeta`** — retype from `ClassMeta<?>` to `BeanTypeInfo<?>`. `BeanPropertyMeta.getClassMeta()` return type widens to `BeanTypeInfo<?>`. The ~70 marshalling-side call sites that call `pMeta.getClassMeta().<method>` mostly work unchanged (inherited `ClassInfo`/`ClassInfoTyped` methods + new `BeanTypeInfo` abstract methods are sufficient). The few sites that call `ClassMeta`-only methods (e.g. `getSerializedClassMeta(this)`, `getMarshallingContext() [...] - - **`BeanPropertyMeta.swap`** — retype from `ObjectSwap` to `Object`. Internal references (`setPropertyValue`'s defensive double-unswap check, `properties()` debug method, Javadocs) cast or remove the check (it's belt-and-braces — the install-time `writeTransform` already normalizes the value before this point). - - **`BeanPropertyMeta.Builder.bc`** — retype from `MarshallingContext` to `Object`. `Builder.bc.resolveClassMeta(...)`, `Builder.bc.getClassMeta(...)`, `Builder.bc.object()` calls inside `validate(...)` lift out to a marshalling-side post-processor (or route through a `BeanSession`-style narrow SPI). The bean-modeling-side path already short-circuits when `bc == null`, so the lift-out is mostly about giving marshalling-side callers a place to install type metadata after the commons-sid [...] - - **`@MarshalledProp` annotation reads** inside `BeanPropertyMeta.Builder.validate(...)` — lift out to a marshalling-side post-processor. After `Builder.build()` returns, the marshalling layer reads `@MarshalledProp` annotations off the property's getter/setter/field and post-processes the just-built `BeanPropertyMeta` to install the swap transforms / dictionary classes / property override list. - - **`BeanMeta.beanRegistry`** and **`propertyBeanRegistries`** — retype from `BeanRegistry` to `BeanRegistryLookup`. `BeanMeta.getBeanRegistry()` and `getPropertyBeanRegistry(...)` return types narrow to `BeanRegistry` via casts (only concrete impl in-tree), OR the public getter return types widen to `BeanRegistryLookup` and the 3 callers (ParserSession/SerializerSession/XmlParserSession) cast at their call sites. The `findBeanRegistry()` helper either: (a) stays in `BeanMeta` with `Ob [...] - - **`BeanMeta` constructor signatures** — the `protected BeanMeta(ClassMeta<T>, BeanFilter, String[], ClassInfo)` constructor takes a `ClassMeta` that the bean-modeling layer should not know about. Three options: (a) keep this constructor on `BeanMeta` typed as `Object`/`BeanTypeInfo`, marshalling-side callers cast; (b) move it to a marshalling-side factory helper (e.g. `ClassMeta.findBeanMeta()` was the only original caller) and delete it from `BeanMeta`; (c) introduce a marshalling-s [...] - - **`BeanMetaFiltered.super(...)`** call — uses the old `(ClassMeta, MarshalledFilter, String[], ClassInfo)` BeanMeta constructor. After Step 8b-ii's constructor refactor, this call site needs to either inline a private helper-build path or route through the new commons-side `BeanMeta.of(Class, BeanConfigContext)` factory. - - **`BeanMap.load(Reader, ReaderParser)`** and **`BeanMap.load(String)`** — these use `JsonMap.ofText(...)` / `JsonMap.ofJson(...)` which are marshalling-side. Move both methods to a marshalling-side helper class (e.g. `BeanMapLoader`); the static methods take a `BeanMap<T>` + `Reader`/`String` and call `putAll(JsonMap.ofText(...))`. Updates the JsonMap-aware Javadoc references on `BeanMap`. - - **`BeanMap.getBean()`** uses `Json5Serializer.DEFAULT.toString(...)` for an error message — replace with `Arrays.toString(...)` or simple class-name iteration. - - **`BeanProxyInvocationHandler.toString()`** uses `Json5Serializer.DEFAULT.toString(...)` — replace with `Objects.toString(...)` or a hand-rolled "{prop1: val1, prop2: val2}" formatter. - - **`JsonMap` / `JsonList` constructions** inside `BeanPropertyMeta.add(...)` and `BeanPropertyMeta.setPropertyValue(...)` — replace with `LinkedHashMap` / `ArrayList` (the only reason `JsonMap`/`JsonList` were used is for the `MarshallingSession`-aware member-type conversion; after Step 3 those conversions go through `BeanSession.convertToMemberType(...)` directly, so plain JDK collections work). - - **`BeanInstantiator.of(Collection.class)/of(Map.class)` calls** inside `setPropertyValue` — `BeanInstantiator` is already in `commons.inject`, but the `type(rawTypeMeta)` overload may need updating to take `BeanTypeInfo<?>` rather than `ClassMeta<?>`. - - **`ParseException`** imports — `BeanMap.load(...)` and `BeanPropertyMeta.set` throw `ParseException` which lives in `org.apache.juneau.parser`. After the load() methods move to a marshalling-side helper, the only remaining `ParseException` site is `BeanPropertyMeta.set` where it's caught from `session.convertToMemberType(...)` and re-thrown as a `BeanRuntimeException` (which is the desired bean-modeling-side exception). The catch block already wraps it; just delete the `throws ParseE [...] - - **`Surrogate.class`** reference inside `BeanPropertyMeta.Builder.swapSwap(...)` — already gated by an `unsupportedOp("TODO - Surrogate swaps not yet supported on bean properties.")` throw. Either move `Surrogate` to commons (it's a marker class), or skip the check entirely on the commons-side path (rely on the marshalling-side post-processor to do `@Swap`/`@Surrogate` validation). - - **`StringFormatSwap`** instantiation inside `BeanPropertyMeta.Builder.marshalledPropSwap(...)` — marshalling-side type. Move into the marshalling-side post-processor along with the `@MarshalledProp` annotation read (one location). - - **Final cleanup** — `git mv` the 8 files into `juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/`, update `package` declarations, repo-wide reference sweep (`import org.apache.juneau.BeanMap` → `import org.apache.juneau.commons.bean.BeanMap`, etc., plus `{@link …}` references in Javadoc), verify `cd juneau-core/juneau-commons && mvn clean compile` standalone passes, run `python3 scripts/test.py --full`. + + **Phase A status (uncommitted, working tree) — COMPLETE, build + full test green (49,912 tests pass).** + + Lift-out items landed: + - **Sub-item 12 (`throws ParseException` declaration on `set`)** — removed the `throws ParseException` from the private `setPropertyValue` helper inside `BeanPropertyMeta`. `ParseException` is a `RuntimeException` so the declaration was documentation-only; the catch in `set(...)` already wraps it via `bex(e2)`. + - **Sub-item 9 (`Json5Serializer.DEFAULT.toString(...)`)** — `BeanMap.getBean(true)` error message now uses `Arrays.toString(getClasses(args))`; `BeanProxyInvocationHandler.toString()` now uses `Objects.toString(this.beanProps)`. Both files dropped the `import org.apache.juneau.json5.*;`. The two remaining substring-based test assertions still match because both still produce a bracketed list of property/class names. + - **Sub-item 13 (`Surrogate.class` reference in `swapSwap`)** — moved out of `BeanPropertyMeta.Builder` along with the rest of the `@Swap` annotation logic (see sub-items 4 + 14 below). The new home is a marshalling-side post-processor that retains the `unsupportedOp("TODO - Surrogate swaps not yet supported on bean properties.")` guard. + - **Sub-items 4 + 14 (`@MarshalledProp` annotation reads + `StringFormatSwap` instantiation)** — created new marshalling-side helper `MarshalledPropertyPostProcessor` in `juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshalledPropertyPostProcessor.java`. It owns the `marshalledPropSwap(AnnotationInfo<MarshalledProp>)` and `swapSwap(AnnotationInfo<Swap>)` helpers that used to live as `private static` methods on `BeanPropertyMeta.Builder`, plus the `forEach(@MarshalledProp [...] + - **Sub-item 10 (`JsonList` / `JsonMap` → JDK collections)** — partial. The `propertyCache` allocations inside `BeanPropertyMeta.add(BeanMap,String,Object)` and `add(BeanMap,String,String,Object)` now use `new ArrayList<>()` / `new LinkedHashMap<>()` (test-invisible — they're internal caches for read-only beans). The `c = new JsonList(session)` / `map = new JsonMap(session)` fallback constructions in `add(...)` likewise flipped to `new ArrayList<>()` / `new LinkedHashMap<>()`. **Deferr [...] + - **Sub-item 8 (`BeanMap.load(...)` → `BeanMapLoader`)** — created `juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java` with `static <T> BeanMap<T> load(BeanMap<T> m, String input) throws ParseException` and `static <T> BeanMap<T> load(BeanMap<T> m, Reader r, ReaderParser p) throws ParseException, IOException`. Removed `BeanMap.load(Reader, ReaderParser)` and `BeanMap.load(String)` from `BeanMap.java`. Updated 2 call sites in `MarshallingContext.java` (`bs.n [...] + + **Phase B status (uncommitted, working tree) — COMPLETE, build + full test green (49,912 tests pass).** + + Field retypes landed (all use the "type the field against the commons.bean SPI seam; keep the public getter returning the marshalling-side type via a cast" pattern, so external callers don't need to change): + + - **Sub-item 2 (`BeanPropertyMeta.swap` → `Object`)** — both `Builder.swap` and `BeanPropertyMeta.swap` retyped. The single use site inside `setPropertyValue` (the defensive double-unswap check) keeps the `nn(swap) && value1 != null && ((ObjectSwap)swap).getSwapClass().isAssignableFrom(value1.getClass())` guard with a local cast. Marshalling-side consumers (`BeanMeta.installSwapAwareTransforms` reading `Builder.swap`, `MarshalledPropertyPostProcessor` reading `b.swap.getSwapClass()`) c [...] + - **Sub-item 3 (`BeanPropertyMeta.Builder.bc` → `Object`)** — both `Builder.bc` and `BeanPropertyMeta.bc` retyped. The few callers that need MarshallingContext-only methods (`Builder.rawMetaType(Class<?>)` calls `bc.getClassMeta(value)`, `applyChildPropertiesFilter` calls `bc.getBeanMeta(o.getClass())`, the post-constructor body calls `bc.getAnnotationProvider()`) cast back to `MarshallingContext` at the use site. `Builder.validate(MarshallingContext bc, ...)`'s parameter is unchanged [...] + - **Sub-item 5 (`BeanMeta.beanRegistry` / `propertyBeanRegistries` → `BeanRegistryLookup`)** — `Supplier<BeanRegistry>` → `Supplier<BeanRegistryLookup>`, `Map<BeanPropertyMeta,BeanRegistry>` → `Map<BeanPropertyMeta,BeanRegistryLookup>`. `findBeanRegistry()` return narrowed to `BeanRegistryLookup`. Public getters `getBeanRegistry()` and `getPropertyBeanRegistry(BeanPropertyMeta)` keep their `BeanRegistry` return types via internal `(BeanRegistry) beanRegistry.get()` / `(BeanRegistry) pr [...] + - **Sub-item 1 (`BeanPropertyMeta.rawTypeMeta` / `typeMeta` / `BeanMeta.classMeta` → `BeanTypeInfo<?>`)** — all three fields retyped (both Builder copies and the final `BeanPropertyMeta` instance fields). The public getters `BeanPropertyMeta.getClassMeta()` and `BeanMeta.getClassMeta()` keep their `ClassMeta<?>` / `ClassMeta<T>` return types via internal `(ClassMeta<?>) typeMeta` / `(ClassMeta<T>) classMeta` casts. Internal call sites that pass `rawTypeMeta` to a `ClassMeta`-typed para [...] + - **Sub-items 6 + 7 (`BeanMeta` constructor refactor)** — picked option (a) (typed against `BeanTypeInfo<T>`, marshalling-side body casts back). The protected `BeanMeta(ClassMeta<T>, BeanFilter, String[], ClassInfo)` constructor signature changed to `BeanMeta(BeanTypeInfo<T>, BeanFilter, String[], ClassInfo)`; the body casts `cm` back to `ClassMeta<T>` to call `getMarshallingContext()` / `getBeanConfigContext()`. `BeanMetaFiltered.super(...)` (passes `innerMeta.getClassMeta()` which st [...] + - **Sub-item 11 (`BeanInstantiator.type(rawTypeMeta)`)** — verified compatible. `BeanInstantiator.type(ClassInfo)` accepts any `ClassInfo` subtype; `BeanTypeInfo<T>` extends `ClassInfoTyped<T>` which extends `ClassInfo`, so `BeanInstantiator.of(Collection.class).type(rawTypeMeta).preferZeroArgConstructor().run()` compiles unchanged. No `BeanInstantiator` changes needed. + - **`MarshallingSession` bridge fix** — `convertToType(Object value, Object targetType)` and `convertToMemberType(Object outer, Object value, Object targetType)` now treat `targetType == null` as a `(ClassMeta<?>) null` dispatch so the typed `ClassMeta`-aware overloads can apply their `object()` fallback. Without this fix, the `getElementType()`/`getValueType()`/`getKeyType()` calls that legitimately return `null` for non-collection/non-map types throw `"Unsupported targetType for conv [...] + + Remaining sub-items (deferred to Phase C): + + - **Phase C — Public API widen.** `BeanPropertyMeta.getClassMeta()` → `BeanTypeInfo<?>`, `BeanMeta.getClassMeta()` → `BeanTypeInfo<T>` (or remove from the bean-modeling layer entirely), `BeanMeta.getBeanRegistry()`/`getPropertyBeanRegistry(...)` → `BeanRegistryLookup`. The ~30+ marshalling-side callers that currently use the `ClassMeta`-returning getters need an audit — most will just inherit `BeanTypeInfo`/`ClassInfo` methods; some will need explicit `(ClassMeta<?>) pMeta.getClassMeta [...] + - **Phase C — `validate(...)` body lift-out.** `Builder.validate(MarshallingContext bc, ...)` reads `bc.resolveClassMeta(...)` / `bc.object()`; those reads need to move to a marshalling-side post-processor (or a narrow `BeanSession` SPI) so the bean-modeling-side `validate` becomes pure (or split into a commons-side `validate()` and a marshalling-side `installTypeMetadata(...)`). + - **Phase C — Marshalling-side `BeanMeta` factory helper.** Move the body of `BeanMeta(BeanTypeInfo<T>, BeanFilter, String[], ClassInfo)` out to a marshalling-side helper that sets `classMeta` and `marshallingContext` via a setter on the commons-side `BeanMeta`. Alternatively, fold the marshalling-only fields (`classMeta`, `marshallingContext`) into a side-map keyed by `BeanMeta` instance. + - **Phase C — `JsonList`/`JsonMap` final cleanup (deferred from Phase A sub-item 10).** Flip `setPropertyValue`'s `new JsonList(valueList)` to `new ArrayList<>(valueList)`. This is test-observable: `BeanMap_Test.a05_arrayProperties`, `a06_arrayProperties_usingConfig`, `a09_beanPropertyAnnotation` assert that the resulting field's concrete class is `JsonList`. Either update the tests to expect `ArrayList` (preferred — the original `JsonList` choice was incidental, not contractual), or w [...] + - **Phase C sub-item 15 — Final cleanup.** `git mv` the 8 files into `juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/`, update `package` declarations, repo-wide reference sweep (`import org.apache.juneau.BeanMap` → `import org.apache.juneau.commons.bean.BeanMap`, etc., plus `{@link …}` references in Javadoc), verify `cd juneau-core/juneau-commons && mvn clean compile` standalone passes, run `python3 scripts/test.py --full`. - [ ] **Step 8c** — (optional) Cleanup pass for anything that comes up during 8b-ii: deprecated bridges, stale imports, package-info docs, etc. - [ ] **Step 9** — Reference sweep: 80–120 unique files (mostly inside `juneau-marshall`). Update imports, Javadoc `{@link …}` references, package-info docs. - [ ] **Step 10** — Update `juneau-docs` release notes / migration guide (`docs/pages/release-notes/9.5.0.md`, `## Package Moves` section) with the bean-runtime relocations.
