diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 27b85d6a0..9d3563e63 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -18,9 +18,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- java_version: ['17', '21', '24']
+ java_version: ['17', '21', '24', '25']
include:
- - java_version: '17'
+ # Release/deploy runs on 25 so the blackbird module (profile-gated
+ # to JDK 25+) is part of the deployed reactor.
+ - java_version: '25'
release_build: 'R'
env:
JAVA_OPTS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
diff --git a/blackbird-jpms-tests/pom.xml b/blackbird-jpms-tests/pom.xml
new file mode 100644
index 000000000..2d237365c
--- /dev/null
+++ b/blackbird-jpms-tests/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ tools.jackson.module
+ jackson-modules-base
+ 3.3.0-SNAPSHOT
+
+ jackson-module-blackbird-jpms-tests
+ Jackson module: Blackbird JPMS integration tests
+ jar
+ Module-path integration tests: a foreign named module that
+requires blackbird, exercising codec generation for its own non-public
+classes. Not published.
+
+
+ 25
+ 25
+ 25
+
+ true
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ true
+
+
+
+
+
+
+
+ tools.jackson.module
+ jackson-module-blackbird
+ ${project.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
diff --git a/blackbird-jpms-tests/src/main/java/blackbird/jpms/test/beans/Beans.java b/blackbird-jpms-tests/src/main/java/blackbird/jpms/test/beans/Beans.java
new file mode 100644
index 000000000..d83f90e4c
--- /dev/null
+++ b/blackbird-jpms-tests/src/main/java/blackbird/jpms/test/beans/Beans.java
@@ -0,0 +1,16 @@
+package blackbird.jpms.test.beans;
+
+import java.lang.invoke.MethodHandles;
+
+/** Hands tests this module's lookup and its non-public bean type. */
+public final class Beans {
+ private Beans() {}
+
+ public static MethodHandles.Lookup lookup() {
+ return MethodHandles.lookup();
+ }
+
+ public static Class> pkgBeanClass() {
+ return PkgBean.class;
+ }
+}
diff --git a/blackbird-jpms-tests/src/main/java/blackbird/jpms/test/beans/PkgBean.java b/blackbird-jpms-tests/src/main/java/blackbird/jpms/test/beans/PkgBean.java
new file mode 100644
index 000000000..73d5b2090
--- /dev/null
+++ b/blackbird-jpms-tests/src/main/java/blackbird/jpms/test/beans/PkgBean.java
@@ -0,0 +1,14 @@
+package blackbird.jpms.test.beans;
+
+// Deliberately package-private: the codec for this bean must be defined in
+// this module's package context, which only works when the generated class
+// can resolve its supertype from here.
+class PkgBean {
+ private int count;
+ private String name;
+
+ public int getCount() { return count; }
+ public void setCount(int v) { count = v; }
+ public String getName() { return name; }
+ public void setName(String v) { name = v; }
+}
diff --git a/blackbird-jpms-tests/src/main/java/module-info.java b/blackbird-jpms-tests/src/main/java/module-info.java
new file mode 100644
index 000000000..7310c03ad
--- /dev/null
+++ b/blackbird-jpms-tests/src/main/java/module-info.java
@@ -0,0 +1,14 @@
+// A foreign named module from Blackbird's point of view: it requires the
+// blackbird module and holds non-public beans plus a lookup of its own, the
+// exact arrangement a modular application has.
+module blackbird.jpms.test
+{
+ requires tools.jackson.databind;
+ requires tools.jackson.module.blackbird;
+
+ exports blackbird.jpms.test.beans;
+ // What a modular application must do for stock databind to construct
+ // non-public beans reflectively. Deliberately NOT opened to blackbird:
+ // the accelerated path uses the lookup this module hands over instead.
+ opens blackbird.jpms.test.beans to tools.jackson.databind;
+}
diff --git a/blackbird-jpms-tests/src/test/java/blackbird/jpms/test/beans/JpmsPackagePrivateTest.java b/blackbird-jpms-tests/src/test/java/blackbird/jpms/test/beans/JpmsPackagePrivateTest.java
new file mode 100644
index 000000000..fcdaa70e3
--- /dev/null
+++ b/blackbird-jpms-tests/src/test/java/blackbird/jpms/test/beans/JpmsPackagePrivateTest.java
@@ -0,0 +1,91 @@
+package blackbird.jpms.test.beans;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.module.blackbird.BlackbirdModule;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Runs on the module path as the named module blackbird.jpms.test, which
+ * requires blackbird: the arrangement a modular application has. A
+ * package-private bean of this module must accelerate when the application
+ * supplies its own lookup, because the generated codec - defined in this
+ * module's package context - can resolve its supertype from blackbird's
+ * exported internal package. Without a usable lookup the bean demotes to the
+ * stock path and behavior is unchanged.
+ *
+ * Engagement is asserted through the blackbird.debug.codegen diagnostic
+ * stream: byte-identical output makes generated and stock otherwise
+ * indistinguishable from out here.
+ */
+public class JpmsPackagePrivateTest
+{
+ static {
+ // Before any Blackbird class initializes: the factories cache this.
+ System.setProperty("blackbird.debug.codegen", "true");
+ }
+
+ private static final String DOC = "{\"count\":7,\"name\":\"x\"}";
+
+ @Test
+ public void packagePrivateBeanAcceleratesWithModuleLookup() throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new BlackbirdModule() {
+ private static final long serialVersionUID = 1L;
+ @Override
+ protected Supplier findLookupSupplier() {
+ return Beans::lookup;
+ }
+ })
+ .build();
+ String err = captureErr(() -> {
+ PkgBean bean = mapper.readValue(DOC, PkgBean.class);
+ assertEquals(7, bean.getCount());
+ assertEquals("x", bean.getName());
+ });
+ assertTrue(err.contains("BBReader_PkgBean"),
+ "expected a generated codec for PkgBean; diagnostics:\n" + err);
+ assertFalse(err.contains("null (gated)"),
+ "PkgBean was gated instead of accelerated; diagnostics:\n" + err);
+ }
+
+ @Test
+ public void defaultLookupDemotesToStock() throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new BlackbirdModule())
+ .build();
+ String err = captureErr(() -> {
+ PkgBean bean = mapper.readValue(DOC, PkgBean.class);
+ assertEquals(7, bean.getCount());
+ assertEquals("x", bean.getName());
+ });
+ // Blackbird's own lookup cannot reach this module's package, so the
+ // bean stays stock - correctly, and without an error.
+ assertFalse(err.contains("BBReader_PkgBean"), err);
+ }
+
+ private interface Body {
+ void run() throws Exception;
+ }
+
+ private static synchronized String captureErr(Body body) throws Exception {
+ PrintStream original = System.err;
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ System.setErr(new PrintStream(buffer, true, StandardCharsets.UTF_8));
+ try {
+ body.run();
+ } finally {
+ System.setErr(original);
+ }
+ return buffer.toString(StandardCharsets.UTF_8);
+ }
+}
diff --git a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/BlackbirdInjectionTestBase.java b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/BlackbirdInjectionTestBase.java
index 1f3575043..037d11cf8 100644
--- a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/BlackbirdInjectionTestBase.java
+++ b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/BlackbirdInjectionTestBase.java
@@ -1,8 +1,5 @@
package tools.jackson.module.blackbird.inject;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -11,25 +8,30 @@
import tools.jackson.databind.SerializationConfig;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.ValueSerializer;
-import tools.jackson.databind.deser.SettableBeanProperty;
import tools.jackson.databind.deser.ValueDeserializerModifier;
-import tools.jackson.databind.deser.bean.BeanDeserializer;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;
-import tools.jackson.databind.ser.BeanPropertyWriter;
import tools.jackson.databind.ser.ValueSerializerModifier;
import tools.jackson.module.blackbird.BlackbirdModule;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-// Test utilities for verifying that Blackbird's lambda-based injection pipeline
-// actually ran on a given POJO. Because Blackbird's optimized property and writer
-// classes (SettableIntProperty, etc.; IntPropertyWriter, etc.) are package-private
-// inside the blackbird module, these checks rely on reflection and simple-name
-// matching rather than compile-time type references. Mirrors the afterburner-tests
-// harness; see that module's README for the broader rationale.
+// Test utilities for verifying that Blackbird's codec generation engaged for a
+// given POJO loaded from the unnamed module (classpath). Blackbird installs a
+// per-bean placeholder (BBReaderPlaceholder / BBWriterPlaceholder) that
+// resolves to a generated hidden-class codec; the placeholder types are
+// package-private inside the blackbird module, so these checks match by simple
+// name and package rather than compile-time type references. Mirrors the
+// afterburner-tests harness; see that module's README for the broader
+// rationale.
abstract class BlackbirdInjectionTestBase
{
+ static {
+ // Strict mode: a codec-generation failure fails the test instead of
+ // demoting to the stock path (see CodegenFallbacks).
+ System.setProperty("tools.jackson.module.blackbird.failOnCodegenError", "true");
+ }
+
protected static Harness newHarness() {
return new Harness();
}
@@ -67,9 +69,11 @@ public ValueSerializer> modifySerializer(
});
}
};
+ // Modifiers run in reverse registration order, so the capture
+ // module registers first to observe what Blackbird installed.
this.mapper = JsonMapper.builder()
- .addModule(new BlackbirdModule())
.addModule(capture)
+ .addModule(new BlackbirdModule())
.build();
}
@@ -86,86 +90,20 @@ ValueSerializer> serFor(Class> cls) {
}
}
- /** Returns the `_propsByIndex` array from a bean deserializer. */
- protected static SettableBeanProperty[] propsOf(ValueDeserializer> deser) {
- if (!(deser instanceof BeanDeserializer)) {
- throw new AssertionError("not a BeanDeserializer: " + deser.getClass().getName());
- }
- return (SettableBeanProperty[]) reflectField(deser, "_propsByIndex");
+ /** True if Blackbird installed its deserializer codec for the captured value. */
+ protected static boolean isBlackbirdReader(ValueDeserializer> deser) {
+ return blackbirdClassChainIncludes(deser.getClass(), "BBReaderPlaceholder");
}
- /** Returns the BeanPropertyWriter[] from a bean serializer, as a list. */
- protected static List writersOf(ValueSerializer> ser) {
- BeanPropertyWriter[] arr = (BeanPropertyWriter[]) reflectField(ser, "_props");
- List out = new ArrayList<>(arr.length);
- for (BeanPropertyWriter w : arr) {
- out.add(w);
- }
- return out;
- }
-
- /** Walks the class chain of {@code instance} looking for a declared field
- * named {@code fieldName}. Picks the error message hypothesis based on the
- * class's package — databind/blackbird fields usually mean a rename,
- * anything else means the caller passed the wrong receiver. */
- protected static Object reflectField(Object instance, String fieldName) {
- Class> origClass = instance.getClass();
- Class> c = origClass;
- while (c != null) {
- try {
- Field f = c.getDeclaredField(fieldName);
- f.setAccessible(true);
- return f.get(instance);
- } catch (NoSuchFieldException ignore) {
- c = c.getSuperclass();
- } catch (IllegalAccessException e) {
- throw new AssertionError("cannot read field '" + fieldName + "' on "
- + origClass.getName(), e);
- }
- }
- String pkg = origClass.getPackageName();
- String hint;
- if (pkg.startsWith("tools.jackson.databind")
- || pkg.startsWith("tools.jackson.module.blackbird")) {
- hint = "databind or blackbird may have renamed or removed it;"
- + " update " + BlackbirdInjectionTestBase.class.getSimpleName()
- + " to match.";
- } else {
- hint = "this looks like the wrong receiver type — '" + fieldName
- + "' is an internal Jackson field and the caller passed an"
- + " instance of " + origClass.getName() + ".";
- }
- throw new AssertionError("field '" + fieldName + "' not found on "
- + origClass.getName() + " (walked up full class chain) — " + hint);
- }
-
- /** True if `prop`'s class chain contains Blackbird's OptimizedSettableBeanProperty. */
- protected static boolean isOptimizedProperty(SettableBeanProperty prop) {
- return blackbirdClassChainIncludes(prop.getClass(), "OptimizedSettableBeanProperty");
- }
-
- /** True if `writer`'s class chain contains Blackbird's OptimizedBeanPropertyWriter. */
- protected static boolean isOptimizedWriter(BeanPropertyWriter writer) {
- return blackbirdClassChainIncludes(writer.getClass(), "OptimizedBeanPropertyWriter");
- }
-
- /** Walks the superclass chain of {@code cls} looking for a class whose simple
- * name is {@code simpleName}. Used to recognize Blackbird's package-private
- * optimized types without importing them. */
- protected static boolean classChainIncludes(Class> cls, String simpleName) {
- Class> c = cls;
- while (c != null) {
- if (simpleName.equals(c.getSimpleName())) {
- return true;
- }
- c = c.getSuperclass();
- }
- return false;
+ /** True if Blackbird installed its serializer codec for the captured value. */
+ protected static boolean isBlackbirdWriter(ValueSerializer> ser) {
+ return blackbirdClassChainIncludes(ser.getClass(), "BBWriterPlaceholder");
}
- /** Like {@link #classChainIncludes} but additionally requires the matched
- * class to live inside a blackbird package. Guards against false positives
- * from unrelated classes that happen to share a simple name. */
+ /** Walks the superclass chain of {@code cls} looking for a class whose
+ * simple name is {@code simpleName} and that lives inside a blackbird
+ * package. Recognizes Blackbird's package-private types without importing
+ * them, and guards against unrelated classes sharing a simple name. */
protected static boolean blackbirdClassChainIncludes(Class> cls, String simpleName) {
Class> c = cls;
while (c != null) {
diff --git a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/CrossLoaderAccessTest.java b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/CrossLoaderAccessTest.java
deleted file mode 100644
index b775e7599..000000000
--- a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/CrossLoaderAccessTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package tools.jackson.module.blackbird.inject;
-
-import org.junit.jupiter.api.Test;
-
-import tools.jackson.databind.deser.SettableBeanProperty;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-// Documents a finding about Blackbird's CrossLoaderAccess code path:
-// on JDK 9+ with a bean in the unnamed module (classpath), the companion
-// `$$JacksonBlackbirdAccess` class is **never** defined.
-//
-// Why: BBDeserializerModifier.updateBuilder obtains a lookup via
-// MethodHandles.lookup() inside BlackbirdModule (full privilege on blackbird's
-// own class), then calls ReflectionHack.privateLookupIn(beanClass, lookup).
-// For an unnamed-module bean, privateLookupIn always succeeds and returns a
-// lookup with hasFullPrivilegeAccess() == true over the bean's package. Then
-// CrossLoaderAccess.grantAccess short-circuits on `hasFullAccess(lookup)` and
-// returns the lookup unchanged, skipping the slow path that would define a
-// $$JacksonBlackbirdAccess companion class via Lookup.defineClass(byte[]).
-//
-// That slow path exists for historical / edge-case scenarios (JDK 8 when
-// DEFINE_CLASS is null, or a future JDK change that lowers privileges on
-// privateLookupIn). It is not exercised by either Blackbird's in-tree test
-// suite (same-module case is also full-privilege) nor by this classpath
-// test module.
-//
-// This test pins the current behavior: the fast path must win, and the
-// companion class must not be introduced as a side effect of deserialization.
-// If Blackbird's CrossLoaderAccess logic changes such that the slow path
-// starts firing for classpath POJOs, this test will catch it — at which
-// point the maintainer should decide whether that's intended (and update the
-// assertion) or accidental (and revert).
-public class CrossLoaderAccessTest extends BlackbirdInjectionTestBase
-{
- public static class XLoaderBean {
- private int a;
- private String b;
- public int getA() { return a; }
- public void setA(int a) { this.a = a; }
- public String getB() { return b; }
- public void setB(String b) { this.b = b; }
- }
-
- private static final String COMPANION_CLASS_NAME =
- XLoaderBean.class.getPackage().getName() + ".$$JacksonBlackbirdAccess";
-
- @Test
- public void testFastPathWins_NoCompanionClassDefined() throws Exception
- {
- Harness h = newHarness();
-
- // Trigger Blackbird's modifier chain for XLoaderBean.
- XLoaderBean bean = h.mapper.readValue("{\"a\":1,\"b\":\"hi\"}", XLoaderBean.class);
- assertEquals(1, bean.getA());
- assertEquals("hi", bean.getB());
-
- // Optimization still ran — setters are Blackbird-optimized.
- SettableBeanProperty[] props = propsOf(h.deserFor(XLoaderBean.class));
- assertEquals(2, props.length);
- for (SettableBeanProperty p : props) {
- assertTrue(isOptimizedProperty(p),
- "XLoaderBean." + p.getName() + " not optimized: "
- + p.getClass().getName());
- }
-
- // But the $$JacksonBlackbirdAccess companion class must NOT have been
- // defined — CrossLoaderAccess.grantAccess short-circuits for a lookup
- // with hasFullPrivilegeAccess() == true, which is what privateLookupIn
- // returns for an unnamed-module target.
- assertFalse(companionClassExists(),
- "CrossLoaderAccess unexpectedly defined " + COMPANION_CLASS_NAME
- + " for an unnamed-module bean — the grantAccess fast path"
- + " should have short-circuited. If Blackbird has started"
- + " taking the slow path (e.g. because privateLookupIn"
- + " semantics changed), update this test.");
- }
-
- private static boolean companionClassExists() {
- try {
- Class.forName(COMPANION_CLASS_NAME, false,
- XLoaderBean.class.getClassLoader());
- return true;
- } catch (ClassNotFoundException e) {
- return false;
- }
- }
-}
diff --git a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/FieldAccessNotOptimizedTest.java b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/FieldAccessNotOptimizedTest.java
deleted file mode 100644
index f36ee1cf9..000000000
--- a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/FieldAccessNotOptimizedTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package tools.jackson.module.blackbird.inject;
-
-import org.junit.jupiter.api.Test;
-
-import tools.jackson.databind.deser.SettableBeanProperty;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-// Documents a known design limitation of Blackbird: direct public-field access
-// is NOT optimized. BBDeserializerModifier.nextProperty only handles properties
-// whose backing JDK member is a Method (setter); if it's a Field, the method
-// returns early and the property is left as a plain FieldProperty. Afterburner
-// optimizes both setter and field access; Blackbird deliberately doesn't.
-//
-// The point of this test is to pin that contract. If Blackbird ever grows
-// field-access support, this test will start failing — which is the correct
-// signal to update it.
-public class FieldAccessNotOptimizedTest extends BlackbirdInjectionTestBase
-{
- public static class FieldOnlyBean {
- public int intField;
- public long longField;
- public boolean boolField;
- public String stringField;
- }
-
- private final Harness h = newHarness();
-
- @Test
- public void testFieldPropsAreNotReplacedWithOptimizedVersions() throws Exception
- {
- // End-to-end deserialization must still work — Blackbird just delegates
- // to databind's plain FieldProperty for these.
- FieldOnlyBean bean = h.mapper.readValue(
- "{\"intField\":1,\"longField\":2,\"boolField\":true,\"stringField\":\"x\"}",
- FieldOnlyBean.class);
- assertEquals(1, bean.intField);
- assertEquals(2L, bean.longField);
- assertTrue(bean.boolField);
- assertEquals("x", bean.stringField);
-
- // None of the properties should be Blackbird-optimized.
- SettableBeanProperty[] props = propsOf(h.deserFor(FieldOnlyBean.class));
- assertEquals(4, props.length);
- for (SettableBeanProperty p : props) {
- assertFalse(isOptimizedProperty(p),
- "Blackbird unexpectedly optimized field property '" + p.getName()
- + "' (is " + p.getClass().getName() + "). If Blackbird"
- + " has grown field-access support, update this test to"
- + " assert the positive case instead.");
- }
- }
-}
diff --git a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/FieldAccessTest.java b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/FieldAccessTest.java
new file mode 100644
index 000000000..91365b579
--- /dev/null
+++ b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/FieldAccessTest.java
@@ -0,0 +1,37 @@
+package tools.jackson.module.blackbird.inject;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// Public-field access is generated: a bean whose properties are all public
+// fields engages a Blackbird codec that stores through putfield (read side) and
+// getfield (write side), the same as a setter/getter POJO. Values must match
+// stock databind.
+public class FieldAccessTest extends BlackbirdInjectionTestBase
+{
+ public static class FieldOnlyBean {
+ public int intField;
+ public long longField;
+ public boolean boolField;
+ public String stringField;
+ }
+
+ private final Harness h = newHarness();
+
+ @Test
+ public void testFieldBackedBeanEngagesCodec() throws Exception
+ {
+ FieldOnlyBean bean = h.mapper.readValue(
+ "{\"intField\":1,\"longField\":2,\"boolField\":true,\"stringField\":\"x\"}",
+ FieldOnlyBean.class);
+ assertEquals(1, bean.intField);
+ assertEquals(2L, bean.longField);
+ assertTrue(bean.boolField);
+ assertEquals("x", bean.stringField);
+
+ assertTrue(isBlackbirdReader(h.deserFor(FieldOnlyBean.class)),
+ "FieldOnlyBean did not engage a Blackbird codec: "
+ + h.deserFor(FieldOnlyBean.class).getClass().getName());
+ }
+}
diff --git a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SerializerInjectionTest.java b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SerializerInjectionTest.java
index 41f6ada69..431480fab 100644
--- a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SerializerInjectionTest.java
+++ b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SerializerInjectionTest.java
@@ -1,19 +1,15 @@
package tools.jackson.module.blackbird.inject;
-import java.util.List;
-
import org.junit.jupiter.api.Test;
-import tools.jackson.databind.ser.BeanPropertyWriter;
+import tools.jackson.databind.json.JsonMapper;
import static org.junit.jupiter.api.Assertions.*;
-// Verifies Blackbird's serializer-side optimization runs end-to-end for all
-// accessor-method specializations, and documents the parallel limitation with
-// the deserializer side: direct public-field writers are not optimized either.
-// BBSerializerModifier.createProperty skips properties whose backing JDK
-// member isn't an AnnotatedMethod (see BBSerializerModifier.java lines
-// 103-109), so public fields stay as plain BeanPropertyWriter instances.
+// Verifies Blackbird's serializer codec generation engages end-to-end on the
+// classpath and produces output identical to stock databind, for a getter POJO
+// and for a public-field POJO (whose values are read through a generated
+// getfield).
public class SerializerInjectionTest extends BlackbirdInjectionTestBase
{
public static class GetterSerPojo {
@@ -44,44 +40,32 @@ public FieldSerPojo() { }
}
@Test
- public void testAllGetterBasedWritersOptimized() throws Exception
+ public void testGetterPojoEngagesGeneratedWriter() throws Exception
{
Harness h = newHarness();
+ GetterSerPojo pojo = new GetterSerPojo(1, 2L, true, "x", java.util.Arrays.asList("a", "b"));
- String json = h.mapper.writeValueAsString(
- new GetterSerPojo(1, 2L, true, "x", java.util.Arrays.asList("a", "b")));
- assertTrue(json.contains("\"intProp\":1"), json);
- assertTrue(json.contains("\"longProp\":2"), json);
- assertTrue(json.contains("\"boolProp\":true"), json);
- assertTrue(json.contains("\"stringProp\":\"x\""), json);
- assertTrue(json.contains("\"objectProp\":[\"a\",\"b\"]"), json);
+ String json = h.mapper.writeValueAsString(pojo);
+ assertEquals(new JsonMapper().writeValueAsString(pojo), json,
+ "generated writer output differs from stock databind");
- List writers = writersOf(h.serFor(GetterSerPojo.class));
- assertEquals(5, writers.size(), "expected 5 writers, got " + writers);
- for (BeanPropertyWriter w : writers) {
- assertTrue(isOptimizedWriter(w),
- "ser writer '" + w.getName() + "' not optimized (is "
- + w.getClass().getName() + "); BBSerializerModifier"
- + " did not replace it with a Blackbird-generated writer");
- }
+ assertTrue(isBlackbirdWriter(h.serFor(GetterSerPojo.class)),
+ "GetterSerPojo did not engage a Blackbird writer codec: "
+ + h.serFor(GetterSerPojo.class).getClass().getName());
}
@Test
- public void testFieldBackedWriterNotOptimized() throws Exception
+ public void testFieldBackedWriterEngagesCodec() throws Exception
{
Harness h = newHarness();
+ FieldSerPojo pojo = new FieldSerPojo(42);
- // Serialization still works end-to-end, just not via a Blackbird-generated accessor.
- String json = h.mapper.writeValueAsString(new FieldSerPojo(42));
- assertTrue(json.contains("\"value\":42"), json);
+ String json = h.mapper.writeValueAsString(pojo);
+ assertEquals(new JsonMapper().writeValueAsString(pojo), json,
+ "field-backed output differs from stock databind");
- List writers = writersOf(h.serFor(FieldSerPojo.class));
- assertEquals(1, writers.size());
- BeanPropertyWriter w = writers.get(0);
- assertFalse(isOptimizedWriter(w),
- "Blackbird unexpectedly optimized a field-backed writer '" + w.getName()
- + "' (is " + w.getClass().getName() + "). If BBSerializerModifier"
- + " has grown field-access support, update this test to assert"
- + " the positive case instead.");
+ assertTrue(isBlackbirdWriter(h.serFor(FieldSerPojo.class)),
+ "FieldSerPojo did not engage a Blackbird writer codec: "
+ + h.serFor(FieldSerPojo.class).getClass().getName());
}
}
diff --git a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SetterOptimizationTest.java b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SetterOptimizationTest.java
index 93db3d552..05fa1f8d1 100644
--- a/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SetterOptimizationTest.java
+++ b/blackbird-tests/src/test/java/tools/jackson/module/blackbird/inject/SetterOptimizationTest.java
@@ -2,17 +2,12 @@
import org.junit.jupiter.api.Test;
-import tools.jackson.databind.deser.SettableBeanProperty;
-
import static org.junit.jupiter.api.Assertions.*;
-// End-to-end verification that Blackbird's setter optimization runs on POJOs
-// loaded from the unnamed module (classpath). The specializations tested here
-// correspond to BBDeserializerModifier's four primitive branches (int, long,
-// boolean, object/String) plus the object/non-String branch.
-//
-// Note: Blackbird does NOT optimize direct public-field access — only setter
-// methods. See FieldAccessNotOptimizedTest for the documented negative case.
+// End-to-end verification that Blackbird's codec generation engages for setter
+// POJOs loaded from the unnamed module (classpath). The beans cover the
+// generator's scalar kinds (int, long, boolean, String) plus a non-scalar
+// setter that rides the codec's stock-property arm.
public class SetterOptimizationTest extends BlackbirdInjectionTestBase
{
public static class IntSetterBean {
@@ -47,28 +42,28 @@ public static class ObjectSetterBean {
public void testIntSetter() throws Exception {
IntSetterBean b = h.mapper.readValue("{\"value\":42}", IntSetterBean.class);
assertEquals(42, b.getValue());
- assertAllPropsOptimized(IntSetterBean.class);
+ assertCodecEngaged(IntSetterBean.class);
}
@Test
public void testLongSetter() throws Exception {
LongSetterBean b = h.mapper.readValue("{\"value\":9999999999}", LongSetterBean.class);
assertEquals(9999999999L, b.getValue());
- assertAllPropsOptimized(LongSetterBean.class);
+ assertCodecEngaged(LongSetterBean.class);
}
@Test
public void testBooleanSetter() throws Exception {
BooleanSetterBean b = h.mapper.readValue("{\"value\":true}", BooleanSetterBean.class);
assertTrue(b.isValue());
- assertAllPropsOptimized(BooleanSetterBean.class);
+ assertCodecEngaged(BooleanSetterBean.class);
}
@Test
public void testStringSetter() throws Exception {
StringSetterBean b = h.mapper.readValue("{\"value\":\"hi\"}", StringSetterBean.class);
assertEquals("hi", b.getValue());
- assertAllPropsOptimized(StringSetterBean.class);
+ assertCodecEngaged(StringSetterBean.class);
}
@Test
@@ -76,17 +71,12 @@ public void testObjectSetter() throws Exception {
ObjectSetterBean b = h.mapper.readValue("{\"value\":[\"a\",\"b\"]}", ObjectSetterBean.class);
assertEquals(2, b.getValue().size());
assertEquals("a", b.getValue().get(0));
- assertAllPropsOptimized(ObjectSetterBean.class);
+ assertCodecEngaged(ObjectSetterBean.class);
}
- private void assertAllPropsOptimized(Class> cls) {
- SettableBeanProperty[] props = propsOf(h.deserFor(cls));
- assertTrue(props.length > 0, "no properties for " + cls.getSimpleName());
- for (SettableBeanProperty p : props) {
- assertTrue(isOptimizedProperty(p),
- cls.getSimpleName() + "." + p.getName() + " not optimized: "
- + p.getClass().getName()
- + " — BBDeserializerModifier did not install a SettableXProperty");
- }
+ private void assertCodecEngaged(Class> cls) {
+ assertTrue(isBlackbirdReader(h.deserFor(cls)),
+ cls.getSimpleName() + " did not engage a Blackbird codec: "
+ + h.deserFor(cls).getClass().getName());
}
}
diff --git a/blackbird/README.md b/blackbird/README.md
index 1d1c26ddc..d543416fb 100644
--- a/blackbird/README.md
+++ b/blackbird/README.md
@@ -1,38 +1,34 @@
# Jackson Blackbird
-_Upgrade your Afterburner for your Java 11+ Environment 🚀_
-
-The [Afterburner](https://github.com/FasterXML/jackson-modules-base/tree/master/afterburner)
-has long been your engine of choice for maximum Jackson performance.
-But in the brave new Java 11 world, the trusty Afterburner is showing its age.
-It uses horrifying bytecode manipulation and cracks `Unsafe.defineClass` which will
-[stop working soon](https://github.com/FasterXML/jackson-modules-base/issues/37).
-
-Blackbird is Afterburner Mk II - powered by 100% renewable Lambda based fuel.
-
-The [LambdaMetafactory](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/invoke/LambdaMetafactory.html)
-introduces a standard Java API for dynamically instantiating function objects.
-The current OpenJDK implementation generates anonymous classes in a somewhat similar fashion
-to the classic Afterburner. While the metafactory cannot generate comparably specialized
-implementations, we can write needed adapters as simple Java code and use the metafactory
-to create distinct call sites for every needed access path. This should allow each accessor to
-have a monomorphic call profile and easily inline for maximum performance.
-
-> **Warning**
-> The Java implementation of lambdas associates each generated lambda object strongly with the
-> ClassLoader of the target class. This means that each Blackbird instance will add dynamic accessors
-> to your classes that cannot be unloaded until the entire ClassLoader is garbage collected.
-> Therefore, Blackbird is not appropriate if you instantiate many ObjectMappers, since
-> you will eventually run out of class space and OOM.
+Blackbird accelerates Jackson databind with generated per-bean codecs. For
+each bean type it defines one hidden class with the JDK ClassFile API
+(JEP 484) and installs it as that bean's deserializer. The generated codec
+matches property names with the parser's `PropertyNameMatcher`, dispatches on
+the match index with a `tableswitch`, and reads each property with direct,
+monomorphic code. The JIT sees one inlinable region per bean type instead of
+the stock dispatch chain.
+
+This is the second Blackbird engine. The first (Jackson 2.x, and 3.x before
+this version) built accessor lambdas with `LambdaMetafactory` inside the stock
+deserializer loop. The current engine replaces the loop itself. Blackbird
+2.x continues to serve Jackson 2.x users.
+
+## Requirements
+
+- Jackson 3.x.
+- JDK 25 or later (the module is compiled with `--release 25`).
## Status
[](https://central.sonatype.com/artifact/tools.jackson.module/jackson-module-blackbird)
-Blackbird is new and not as mature as Afterburner, but has been tested and runs well.
-The code is written to fail-fast and explode on the tarmac rather than later at runtime.
+The module passes the full Blackbird test suite, and generated codecs are
+verified byte-identical to stock databind on the benchmark corpus.
-Blackbird passes all the original Afterburner tests (except a couple that didn't make sense anymore).
+Generated codecs unload with the mapper: the hidden classes are anchored only
+by the codec instances in the mapper's caches. The old engine's lambdas stayed
+alive with the target ClassLoader, so applications that created many mappers
+ran out of metaspace. That limitation is gone.
## Usage
@@ -47,9 +43,9 @@ Blackbird is available on Maven Central:
```
-### Registering module
+### Registering the module
-To use the Module in Jackson, simply register it with the ObjectMapper instance:
+Register the module with the mapper:
```java
ObjectMapper mapper = JsonMapper.builder()
@@ -57,46 +53,70 @@ ObjectMapper mapper = JsonMapper.builder()
.build();
```
-after which you just do data-binding as usual:
+then do data binding as usual:
```java
Value val = mapper.readValue(jsonSource, Value.class);
mapper.writeValue(new File("result.json"), val);
```
-If you're really brave and running with a modulepath, you may need to grant Blackbird access to your classes.
-This is done by calling the caller sensitive `MethodHandles.lookup()`. The module constructor allows you to customize
-which lookup instances are used for what classes.
-
-### What is optimized?
-
-Following things are optimized:
-
-* For serialization (POJOs to JSON):
- * Getter methods are inlined using generated lambdas instead of reflection
- * Serializers for small number of 'primitive' types (`int`, `long`, `boolean`, `String`) are replaced with lambda-specialized generators, instead of getting delegated to `JsonSerializer`s
-* For deserialization (JSON to POJOs):
- * Calls to default (no-argument) constructors are lambda-fied instead of using reflection
- * Calls to @JsonCreate factory methods and constructors with arguments get a slightly less efficient lambda based implementation
- - This is new in Blackbird; Afterburner never did this
- * Setter methods are called using lambdas instead of reflection
- * Deserializers for small number of 'primitive' types (`int`, `long`, `boolean`, `String`) are replaced with lambda-specialized parsers, instead of getting delegated to `JsonDeserializer`s
-
-### ... and what is not?
-
-* Any sort of direct field access. This may be possible to support someday but requires additional JDK support. Use methods instead.
-* Streaming parser/generator access
-* Tree Model: there isn't much that can be done at this level
+On the module path, grant Blackbird access to your classes by supplying a
+`MethodHandles.Lookup` from your own module. Override `findLookup()` or
+`findLookupSupplier()` on `BlackbirdModule` to customize which lookup is used
+for which class. Record deserialization needs a lookup with access to the
+record's canonical constructor, and non-public classes accelerate only with
+a lookup that can reach them (the codec for a non-public class is defined in
+that class's package context; without an eligible lookup the class stays on
+the stock path). The `tools.jackson.module.blackbird.internal` package is
+exported only so such codecs can resolve their supertypes; it is not API.
+
+## What is optimized?
+
+Deserialization (JSON to POJOs):
+
+- Setter-based POJOs: direct construction and direct setter calls.
+- Field-backed properties: a public non-final field is stored through a direct
+ putfield; a non-public field is reached through a lookup-derived handle, the
+ same as a non-public setter, so it needs a `Lookup` for that class. Old
+ Blackbird could not write fields at all. Final fields keep stock behavior.
+- Records: typed locals in canonical-constructor order and a single
+ constructor call, replacing the generic creator buffering. This is the
+ largest measured win.
+- Builder-based beans (for example Immutables types): fluent setter calls
+ inlined, build method called through a constant MethodHandle.
+- Scalar properties (`String`, `int`, `long`, `boolean`, enums) read with
+ direct parser calls. Nested beans call the child codec directly. Lists of
+ strings and of beans use inline loops.
+
+Serialization (POJOs to JSON): straight-line writers with pre-encoded name
+constants, direct getter or public-field reads, and inline list loops.
+Property writes go through per-type generated helpers sized to compile as
+standalone units, which avoids a C2 code-quality penalty for inlined copies of
+hot jackson-core methods inside looping writer bodies.
+
+Properties and beans outside this coverage keep stock behavior by
+construction. A property with a custom deserializer, non-default coercion or
+null handling, polymorphic typing, or injection routes through the stock
+property implementation inside the generated codec. Beans with features the
+generator does not cover keep the stock `BeanDeserializer` outright.
+
+## What is not?
+
+- Streaming parser and generator access.
+- The tree model.
+
+## Debugging
+
+With `-Dblackbird.debug.codegen=true`, the module prints each generation
+decision (engaged, or which gate demoted the bean) to standard error. With
+`-Dblackbird.debug.dumpDir=`, the module writes each generated class's
+bytes to that directory as `-reader.class` or
+`-writer.class`, for inspection with javap or a decompiler.
## Performance
-Blackbird has been lightly performance tested using `jmh` on openjdk 11 and 12.
-For reading and writing a moderately complex bean, Blackbird performance seems
-to be almost exactly on par with Afterburner - up to 20% better than vanilla Jackson in some cases.
-At that point the method invocation overhead disappears behind the heat generated
-from the long parser and UTF8 decoder.
-
-I used [jitwatch](https://github.com/AdoptOpenJDK/jitwatch) with `hsdis` to verify the generated code at a
-superficial level. The generated call sites do seem to be good targets for inlining, although unfortunately
-[jitwatch currently can't analyze lambdas very well](https://github.com/AdoptOpenJDK/jitwatch/issues/282) so
-more advanced analysis will have to come later.
+Measured with paired JMH runs on JDK 25/26, aarch64 and x86_64, against the
+previous Blackbird engine: setter POJOs +7-9%, record graphs +48-49%, builder
+beans +19-20%. Against vanilla databind, record graphs measure up to +124%.
+Record and builder acceleration is new; the previous engine left both on the
+stock path.
diff --git a/blackbird/pom.xml b/blackbird/pom.xml
index e673dd2e3..1fe7d448d 100644
--- a/blackbird/pom.xml
+++ b/blackbird/pom.xml
@@ -27,6 +27,11 @@ that uses LambdaMetafactory based code generation to replace reflection calls.
+ 25
+
+ 25
+ 25
tools/jackson/module/blackbird
${project.groupId}.blackbird
diff --git a/blackbird/src/main/java/module-info.java b/blackbird/src/main/java/module-info.java
index cbe9b3422..814b083a4 100644
--- a/blackbird/src/main/java/module-info.java
+++ b/blackbird/src/main/java/module-info.java
@@ -7,6 +7,9 @@
requires transitive tools.jackson.databind;
exports tools.jackson.module.blackbird;
+ // Internal supertypes of generated hidden classes; exported so codecs
+ // defined in user-module contexts can resolve them. Not API.
+ exports tools.jackson.module.blackbird.internal;
provides tools.jackson.databind.JacksonModule with
tools.jackson.module.blackbird.BlackbirdModule;
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/BlackbirdModule.java b/blackbird/src/main/java/tools/jackson/module/blackbird/BlackbirdModule.java
index 01e109943..e5e3a6499 100644
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/BlackbirdModule.java
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/BlackbirdModule.java
@@ -96,9 +96,8 @@ public void setupModule(SetupContext context)
return;
}
Function, Lookup> lookup = findLookup();
- CrossLoaderAccess openSesame = new CrossLoaderAccess();
- context.addDeserializerModifier(new BBDeserializerModifier(lookup, openSesame));
- context.addSerializerModifier(new BBSerializerModifier(lookup, openSesame));
+ context.addDeserializerModifier(new BBDeserializerModifier(lookup));
+ context.addSerializerModifier(new BBSerializerModifier(lookup));
}
@Override
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/CrossLoaderAccess.java b/blackbird/src/main/java/tools/jackson/module/blackbird/CrossLoaderAccess.java
deleted file mode 100644
index 2c064276d..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/CrossLoaderAccess.java
+++ /dev/null
@@ -1,184 +0,0 @@
-package tools.jackson.module.blackbird;
-
-import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-import java.util.function.UnaryOperator;
-
-class CrossLoaderAccess implements UnaryOperator {
- private static final MethodHandle DEFINE_CLASS, HAS_FULL_ACCESS;
-
- /**
- * @deprecated Since 3.2. Only referenced by {@link #accessClassIn}, which
- * is dead code on JDK 9+ for all known inputs. Scheduled for removal
- * together with the companion-class slow path.
- */
- @Deprecated(since = "3.2", forRemoval = true)
- private static final String CLASS_NAME = "$$JacksonBlackbirdAccess";
-
- // Pre-compiled Java 8 bytecode:
- // package ;
- // public class BlackbirdAccess {
- // public static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
- // }
-
- /**
- * @deprecated Since 3.2. See {@link #accessClassIn}.
- */
- @Deprecated(since = "3.2", forRemoval = true)
- private static final int[] HEADER = new int[] {
- 0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x34, 0x00, 0x1c, 0x0a, 0x00,
- 0x02, 0x00, 0x03, 0x07, 0x00, 0x04, 0x0c, 0x00, 0x05, 0x00, 0x06, 0x01,
- 0x00, 0x10, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f,
- 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x00, 0x06, 0x3c, 0x69, 0x6e,
- 0x69, 0x74, 0x3e, 0x01, 0x00, 0x03, 0x28, 0x29, 0x56, 0x0a, 0x00, 0x08,
- 0x00, 0x09, 0x07, 0x00, 0x0a, 0x0c, 0x00, 0x0b, 0x00, 0x0c, 0x01, 0x00,
- 0x1e, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69,
- 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64,
- 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x01, 0x00, 0x06, 0x6c, 0x6f,
- 0x6f, 0x6b, 0x75, 0x70, 0x01, 0x00, 0x29, 0x28, 0x29, 0x4c, 0x6a, 0x61,
- 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x76, 0x6f,
- 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x48, 0x61, 0x6e,
- 0x64, 0x6c, 0x65, 0x73, 0x24, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x3b,
- 0x09, 0x00, 0x0e, 0x00, 0x0f, 0x07, 0x00, 0x10, 0x0c, 0x00, 0x11, 0x00,
- 0x12, 0x01
- };
- /**
- * @deprecated Since 3.2. See {@link #accessClassIn}.
- */
- @Deprecated(since = "3.2", forRemoval = true)
- private static final int[] FOOTER = new int[] {
- 0x01, 0x00, 0x06, 0x4c, 0x4f, 0x4f, 0x4b, 0x55, 0x50, 0x01, 0x00, 0x27,
- 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69,
- 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64,
- 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x24, 0x4c, 0x6f, 0x6f, 0x6b,
- 0x75, 0x70, 0x3b, 0x01, 0x00, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x01, 0x00,
- 0x0f, 0x4c, 0x69, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x54,
- 0x61, 0x62, 0x6c, 0x65, 0x01, 0x00, 0x08, 0x3c, 0x63, 0x6c, 0x69, 0x6e,
- 0x69, 0x74, 0x3e, 0x01, 0x00, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x46, 0x69, 0x6c, 0x65, 0x01, 0x00, 0x14, 0x42, 0x6c, 0x61, 0x63, 0x6b,
- 0x62, 0x69, 0x72, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x2e, 0x6a,
- 0x61, 0x76, 0x61, 0x01, 0x00, 0x0c, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x43,
- 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x07, 0x00, 0x1a, 0x01, 0x00, 0x25,
- 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69, 0x6e,
- 0x76, 0x6f, 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x48,
- 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x24, 0x4c, 0x6f, 0x6f, 0x6b, 0x75,
- 0x70, 0x01, 0x00, 0x06, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x00, 0x21,
- 0x00, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x19, 0x00, 0x11,
- 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x05, 0x00, 0x06,
- 0x00, 0x01, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x01, 0x00, 0x01,
- 0x00, 0x00, 0x00, 0x05, 0x2a, 0xb7, 0x00, 0x01, 0xb1, 0x00, 0x00, 0x00,
- 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00,
- 0x01, 0x00, 0x08, 0x00, 0x15, 0x00, 0x06, 0x00, 0x01, 0x00, 0x13, 0x00,
- 0x00, 0x00, 0x1f, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xb8,
- 0x00, 0x07, 0xb3, 0x00, 0x0d, 0xb1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x14,
- 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02,
- 0x00, 0x16, 0x00, 0x00, 0x00, 0x02, 0x00, 0x17, 0x00, 0x18, 0x00, 0x00,
- 0x00, 0x0a, 0x00, 0x01, 0x00, 0x19, 0x00, 0x08, 0x00, 0x1b, 0x00, 0x19
- };
-
- static {
- MethodHandle defineClass = null, hasFullAccess = null;
- try {
- defineClass = MethodHandles.lookup().findVirtual(MethodHandles.Lookup.class, "defineClass",
- MethodType.methodType(Class.class, byte[].class));
- hasFullAccess = MethodHandles.lookup().findVirtual(MethodHandles.Lookup.class, "hasFullPrivilegeAccess",
- MethodType.methodType(boolean.class));
- } catch (ReflectiveOperationException ign) { }
- DEFINE_CLASS = defineClass;
- HAS_FULL_ACCESS = hasFullAccess;
- }
-
- @Override
- public MethodHandles.Lookup apply(MethodHandles.Lookup lookup) {
- try {
- return grantAccess(lookup);
- } catch (IOException | ReflectiveOperationException | RuntimeException e) {
- e.printStackTrace();
- return lookup;
- }
- }
-
- private static MethodHandles.Lookup grantAccess(MethodHandles.Lookup lookup) throws IOException, ReflectiveOperationException {
- // Fast path: on JDK 9+, privateLookupIn always yields a full-privilege
- // lookup for any bean class Blackbird can see, so this branch is taken
- // for every real-world call and the companion-class slow path below
- // is effectively unreachable. Verified by `blackbird-tests`
- // CrossLoaderAccessTest; see that test's javadoc for the full rationale.
- if (DEFINE_CLASS == null || hasFullAccess(lookup)) {
- return lookup;
- }
- // Legacy slow path, retained while we finish verifying it has no live
- // callers in any supported JDK / lookup configuration. Scheduled for
- // removal — see `accessClassIn` javadoc.
- return (MethodHandles.Lookup) accessClassIn(lookup).getField("LOOKUP").get(null);
- }
-
- private static boolean hasFullAccess(MethodHandles.Lookup lookup) {
- try {
- return HAS_FULL_ACCESS != null && (boolean) HAS_FULL_ACCESS.invokeExact(lookup);
- } catch (RuntimeException | Error e) {
- throw e;
- } catch (Throwable e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Defines a {@code $$JacksonBlackbirdAccess} companion class in the same
- * package as {@code lookup.lookupClass()} and returns it. The companion
- * class exposes a {@code public static final MethodHandles.Lookup LOOKUP}
- * field (initialized from a class-initializer calling
- * {@link MethodHandles#lookup()}), which callers then use as a
- * full-privilege lookup for operations in that package.
- *
- * @deprecated Since 3.2. This is legacy Java 8 / pre-{@code privateLookupIn}
- * fallback logic that is no longer reachable for any known input on
- * JDK 9+. The {@link #grantAccess} fast path always wins because
- * {@link MethodHandles#privateLookupIn} returns a lookup with
- * {@link MethodHandles.Lookup#hasFullPrivilegeAccess()} {@code == true}
- * for any bean class Blackbird can see. The
- * {@code jackson-module-blackbird-tests} module
- * {@code CrossLoaderAccessTest} pins this behavior. If a real live
- * caller for this slow path is identified before 4.0, remove the
- * {@link Deprecated} marker and keep the method; otherwise, delete
- * along with {@link #CLASS_NAME}, {@link #HEADER}, and {@link #FOOTER}.
- */
- @Deprecated(since = "3.2", forRemoval = true)
- private static Class> accessClassIn(MethodHandles.Lookup lookup) throws IOException, ReflectiveOperationException {
- Package pkg = lookup.lookupClass().getPackage();
- final String pkgName = pkg.getName();
- String accessClassName = pkgName.isEmpty() ? CLASS_NAME : (pkgName + "." + CLASS_NAME);
- ClassLoader lookupClassLoader = lookup.lookupClass().getClassLoader();
- try {
- return Class.forName(accessClassName, true, lookupClassLoader);
- } catch (ClassNotFoundException ign) { }
- synchronized (CrossLoaderAccess.class) {
- try {
- return Class.forName(accessClassName, true, lookupClassLoader);
- } catch (ClassNotFoundException ign) { }
- String fqcn = pkgName.isEmpty() ? CLASS_NAME
- : (pkgName.replace('.', '/') + "/" + CLASS_NAME);
-
- ByteArrayOutputStream classBytes = new ByteArrayOutputStream(HEADER.length + FOOTER.length + fqcn.length() + 16);
- DataOutputStream dataOut = new DataOutputStream(classBytes);
- for (int b : HEADER) {
- dataOut.writeByte(b);
- }
- dataOut.writeUTF(fqcn);
- for (int b : FOOTER) {
- dataOut.writeByte(b);
- }
- try {
- return (Class>) DEFINE_CLASS.invokeExact(lookup, classBytes.toByteArray());
- } catch (RuntimeException | Error | IOException | ReflectiveOperationException e) {
- throw e;
- } catch (Throwable e) {
- throw new RuntimeException(e);
- }
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/BeanReaderGenerator.java b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/BeanReaderGenerator.java
new file mode 100644
index 000000000..6cb59dee8
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/BeanReaderGenerator.java
@@ -0,0 +1,1180 @@
+package tools.jackson.module.blackbird.codegen;
+
+import java.lang.classfile.ClassFile;
+import java.lang.classfile.CodeBuilder;
+import java.lang.classfile.Label;
+import java.lang.classfile.attribute.MethodParameterInfo;
+import java.lang.classfile.attribute.MethodParametersAttribute;
+import java.lang.classfile.instruction.SwitchCase;
+import java.lang.constant.ClassDesc;
+import java.lang.constant.ConstantDescs;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodTypeDesc;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Optional;
+
+import tools.jackson.core.sym.PropertyNameMatcher;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.SettableBeanProperty;
+import tools.jackson.databind.deser.bean.BeanDeserializerBase;
+import tools.jackson.databind.util.ClassUtil;
+import tools.jackson.module.blackbird.internal.GeneratedReaderBase;
+
+/**
+ * Emits a hidden-class deserializer for one bean: a loop on nextNameMatch, a
+ * tableswitch on the property index, inlined scalar reads with direct setter
+ * calls for eligible properties, and the stock SettableBeanProperty (a named
+ * classData constant) for everything else. The matcher and the per-property
+ * payloads travel as a name-addressed classData map; the fallback
+ * deserializer is a constructor argument consumed by
+ * {@link GeneratedReaderBase}.
+ */
+public final class BeanReaderGenerator
+{
+ public enum Kind { STRING, INT, LONG, BOOLEAN, CHILD, STOCK }
+
+ // How a generated codec treats an active view. NONE: views cannot affect
+ // this bean (the stock (de)serializer would ignore them too), no view code.
+ // DELEGATE: an active view hands the whole call to the stock fallback
+ // (beans with more than 64 properties). MASK: the codec resolves a cached
+ // per-view visibility bitmask and each property arm tests its bit, so
+ // view-active calls keep the generated fast path.
+ public enum ViewStrategy { NONE, DELEGATE, MASK }
+
+ // setter applies to POJO and builder modes (null when setterHandle carries
+ // a non-public setter, or when field carries a public field, instead);
+ // type is the record component type in record mode and the child value
+ // type for CHILD; child is the linked generated codec for CHILD; field is
+ // set for a public field stored through putfield.
+ public record GenProp(String name, Kind kind, Method setter, SettableBeanProperty stock,
+ Class> type, GeneratedReaderBase child, MethodHandle setterHandle, Field field) {
+ public GenProp(String name, Kind kind, Method setter, SettableBeanProperty stock) {
+ this(name, kind, setter, stock, null, null, null, null);
+ }
+
+ public GenProp(String name, Kind kind, Method setter, SettableBeanProperty stock,
+ Class> type) {
+ this(name, kind, setter, stock, type, null, null, null);
+ }
+
+ public GenProp(String name, Kind kind, Method setter, SettableBeanProperty stock,
+ Class> type, GeneratedReaderBase child, MethodHandle setterHandle) {
+ this(name, kind, setter, stock, type, child, setterHandle, null);
+ }
+ }
+
+ private static final ClassDesc CD_JSON_PARSER = ClassDesc.of("tools.jackson.core.JsonParser");
+ private static final ClassDesc CD_JSON_TOKEN = ClassDesc.of("tools.jackson.core.JsonToken");
+ private static final ClassDesc CD_DESER_CONTEXT = ClassDesc.of("tools.jackson.databind.DeserializationContext");
+ private static final ClassDesc CD_NAME_MATCHER = ClassDesc.of("tools.jackson.core.sym.PropertyNameMatcher");
+ private static final ClassDesc CD_SETTABLE_PROP = ClassDesc.of("tools.jackson.databind.deser.SettableBeanProperty");
+ // Derived from the class literal rather than a name: the test build
+ // compiles main sources into target/test-classes through --patch-module,
+ // and javac emits only compile-time-referenced classes there. The class
+ // literal makes sure that GeneratedReaderBase.class is present in the test
+ // module, which shadows target/classes at run time. Every same-module
+ // class that generated code names only as a string needs such a
+ // compile-time reference.
+ private static final ClassDesc CD_BASE =
+ GeneratedReaderBase.class.describeConstable().orElseThrow();
+ private static final ClassDesc CD_BEAN_DESER_BASE = ClassDesc.of("tools.jackson.databind.deser.bean.BeanDeserializerBase");
+ private static final ClassDesc CD_ISE = ClassDesc.of("java.lang.IllegalStateException");
+
+ private static final MethodTypeDesc MTD_NEXT_NAME_MATCH =
+ MethodTypeDesc.of(ConstantDescs.CD_int, CD_NAME_MATCHER);
+ private static final MethodTypeDesc MTD_NEXT_TOKEN = MethodTypeDesc.of(CD_JSON_TOKEN);
+ private static final MethodTypeDesc MTD_GET_STRING = MethodTypeDesc.of(ConstantDescs.CD_String);
+ private static final MethodTypeDesc MTD_GET_INT = MethodTypeDesc.of(ConstantDescs.CD_int);
+ private static final MethodTypeDesc MTD_GET_LONG = MethodTypeDesc.of(ConstantDescs.CD_long);
+ private static final MethodTypeDesc MTD_GET_BOOLEAN = MethodTypeDesc.of(ConstantDescs.CD_boolean);
+ private static final MethodTypeDesc MTD_ASSIGN_CURRENT =
+ MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_Object);
+ private static final MethodTypeDesc MTD_DESERIALIZE_AND_SET = MethodTypeDesc.of(
+ ConstantDescs.CD_void, CD_JSON_PARSER, CD_DESER_CONTEXT, ConstantDescs.CD_Object);
+ private static final MethodTypeDesc MTD_DESERIALIZE =
+ MethodTypeDesc.of(ConstantDescs.CD_Object, CD_JSON_PARSER, CD_DESER_CONTEXT);
+ private static final MethodTypeDesc MTD_CTOR =
+ MethodTypeDesc.of(ConstantDescs.CD_void, CD_BEAN_DESER_BASE);
+ private static final ClassDesc CD_SET = java.util.Set.class.describeConstable().orElseThrow();
+ private static final MethodTypeDesc MTD_CTOR4 = MethodTypeDesc.of(ConstantDescs.CD_void,
+ CD_BEAN_DESER_BASE, ConstantDescs.CD_boolean, CD_SET, CD_SET);
+ private static final MethodTypeDesc MTD_HANDLE_UNKNOWN = MethodTypeDesc.of(
+ ConstantDescs.CD_void, CD_JSON_PARSER, CD_DESER_CONTEXT, ConstantDescs.CD_Object);
+ private static final MethodTypeDesc MTD_CHECK_SEEN = MethodTypeDesc.of(
+ ConstantDescs.CD_void, CD_DESER_CONTEXT, ConstantDescs.CD_long, ConstantDescs.CD_int);
+ private static final MethodTypeDesc MTD_GET_ACTIVE_VIEW =
+ MethodTypeDesc.of(ConstantDescs.CD_Class);
+ private static final MethodTypeDesc MTD_VIEW_MASK =
+ MethodTypeDesc.of(ConstantDescs.CD_long, ConstantDescs.CD_Class);
+ private static final MethodTypeDesc MTD_HIDDEN_VIEW = MethodTypeDesc.of(
+ ConstantDescs.CD_void, CD_JSON_PARSER, CD_DESER_CONTEXT, CD_SETTABLE_PROP);
+ private static final MethodTypeDesc MTD_VISIBLE_IN_VIEW =
+ MethodTypeDesc.of(ConstantDescs.CD_boolean, ConstantDescs.CD_Class);
+ private static final ClassDesc CD_EXCEPTION = ClassDesc.of("java.lang.Exception");
+ private static final MethodTypeDesc MTD_PROP_WRAP = MethodTypeDesc.of(
+ ClassDesc.of("java.lang.RuntimeException"), ClassDesc.of("java.lang.Throwable"),
+ ConstantDescs.CD_Object, CD_SETTABLE_PROP, CD_DESER_CONTEXT);
+
+ // Generated code resolves class-data entries by NAME through the
+ // classDataEntry bootstrap on the base class (the JDK classDataAt
+ // bootstrap rejects any condy name but "_"); see dataName for the naming.
+ private static final java.lang.constant.DirectMethodHandleDesc BSM_DATA_ENTRY =
+ ConstantDescs.ofConstantBootstrap(CD_BASE, "classDataEntry", ConstantDescs.CD_Object);
+
+ private BeanReaderGenerator() {}
+
+ static void ldcData(CodeBuilder cob, String name, ClassDesc type) {
+ cob.ldc(DynamicConstantDesc.ofNamed(BSM_DATA_ENTRY, name, type));
+ }
+
+ // Condy names are JVM unqualified names ('.', ';', '[', '/' forbidden) and
+ // JSON property names are arbitrary, so sanitize and dedupe.
+ static String dataName(Map data, String base) {
+ StringBuilder sb = new StringBuilder(base.length());
+ for (int i = 0; i < base.length(); i++) {
+ char c = base.charAt(i);
+ sb.append(c == '.' || c == ';' || c == '[' || c == '/' ? '$' : c);
+ }
+ String name = sb.toString();
+ String candidate = name;
+ for (int n = 2; data.containsKey(candidate); n++) {
+ candidate = name + "$" + n;
+ }
+ return candidate;
+ }
+
+ public static ValueDeserializer generate(Class> beanClass, List props,
+ PropertyNameMatcher matcher, BeanDeserializerBase fallback,
+ MethodHandles.Lookup defineLookup, ViewStrategy views, Ignorals ignorals,
+ int[] aliasArms)
+ throws ReflectiveOperationException {
+ return generate(beanClass, props, matcher, fallback, null, null, defineLookup, views,
+ ignorals, aliasArms);
+ }
+
+ // recordCtor non-null selects record mode: props are in canonical
+ // constructor order, values collect into typed locals, and the
+ // constructor MethodHandle (exact component signature) builds the value.
+ public static ValueDeserializer generate(Class> beanClass, List props,
+ PropertyNameMatcher matcher, BeanDeserializerBase fallback, MethodHandle recordCtor,
+ MethodHandles.Lookup defineLookup, ViewStrategy views, Ignorals ignorals,
+ int[] aliasArms)
+ throws ReflectiveOperationException {
+ return generate(beanClass, props, matcher, fallback, recordCtor, null, defineLookup, views,
+ ignorals, aliasArms);
+ }
+
+ // builderSupport non-null selects builder mode: values apply to a builder
+ // instance created by the stock ValueInstantiator, fluent setter returns
+ // replace the builder local, and the build MethodHandle (asType'd to
+ // (Object)Object) produces the value.
+ public record BuilderSupport(tools.jackson.databind.deser.ValueInstantiator instantiator,
+ MethodHandle buildMethod, Class> builderClass) {}
+
+ @SuppressWarnings("unchecked")
+ public static ValueDeserializer generate(Class> beanClass, List props,
+ PropertyNameMatcher matcher, BeanDeserializerBase fallback, MethodHandle recordCtor,
+ BuilderSupport builder, MethodHandles.Lookup defineLookup, ViewStrategy views,
+ Ignorals ignorals, int[] aliasArms)
+ throws ReflectiveOperationException {
+ Map classData = new LinkedHashMap<>();
+ classData.put("matcher", matcher);
+ // Tier-A properties also carry their SettableBeanProperty: their
+ // VALUE_NULL branch runs through it, since null handling is
+ // configuration-dependent per property.
+ String[] stockName = new String[props.size()];
+ String[] childName = new String[props.size()];
+ String[] setterName = new String[props.size()];
+ for (int i = 0; i < props.size(); i++) {
+ GenProp gp = props.get(i);
+ stockName[i] = dataName(classData, gp.name() + "Prop");
+ classData.put(stockName[i], gp.stock());
+ if (gp.child() != null) {
+ childName[i] = dataName(classData, gp.name() + "Codec");
+ classData.put(childName[i], gp.child());
+ }
+ if (gp.setterHandle() != null) {
+ setterName[i] = dataName(classData, gp.name() + "Setter");
+ classData.put(setterName[i], gp.setterHandle());
+ }
+ }
+ if (recordCtor != null) {
+ classData.put("constructor", recordCtor);
+ }
+ if (builder != null) {
+ classData.put("instantiator", builder.instantiator());
+ classData.put("buildMethod", builder.buildMethod());
+ }
+
+ // Non-public beans define in the bean's package context (the caller
+ // supplies a privateLookupIn of the bean class), which makes the
+ // generated new/invokevirtual/putfield instructions legal in-package.
+ // Public beans keep the module's own context.
+ MethodHandles.Lookup definer =
+ (defineLookup != null) ? defineLookup : MethodHandles.lookup();
+ byte[] bytes = buildClass(definer.lookupClass().getPackageName(), beanClass, props,
+ stockName, childName, setterName, recordCtor != null,
+ builder == null ? null : builder.builderClass(), views, aliasArms);
+ CodegenDump.dump(beanClass, "reader", bytes);
+ // No ClassOption.STRONG: the codec instance held by the mapper's
+ // deserializer cache anchors the class, so codecs unload with the
+ // mapper instead of pinning metaspace for the loader's lifetime.
+ MethodHandles.Lookup hidden;
+ try {
+ hidden = definer.defineHiddenClassWithClassData(
+ bytes, Map.copyOf(classData), true);
+ } catch (IllegalAccessException | SecurityException | LinkageError e) {
+ if (defineLookup == null) {
+ // Module-context defines never legitimately fail: a generator bug.
+ if (e instanceof IllegalAccessException iae) {
+ throw iae;
+ }
+ if (e instanceof RuntimeException re) {
+ throw re;
+ }
+ throw (LinkageError) e;
+ }
+ // Bean-context defines can fail where the bean's module does not
+ // read this module at all (the exported .internal package holds
+ // the supertype): an environment gate, stock path.
+ return null;
+ }
+ MethodHandle ctor = hidden.findConstructor(hidden.lookupClass(),
+ MethodType.methodType(void.class, BeanDeserializerBase.class,
+ boolean.class, Set.class, Set.class));
+ Ignorals ig = (ignorals == null) ? Ignorals.NONE : ignorals;
+ try {
+ return (ValueDeserializer) ctor.invoke(fallback,
+ ig.ignoreAllUnknown(), ig.ignorableProps(), ig.includableProps());
+ } catch (Throwable t) {
+ throw new IllegalStateException("cannot instantiate generated codec", t);
+ }
+ }
+
+ /** Ignoral configuration carried from the modifier into the codec. */
+ public record Ignorals(boolean ignoreAllUnknown, Set ignorableProps,
+ Set includableProps) {
+ public static final Ignorals NONE = new Ignorals(false, null, null);
+ }
+
+ private static byte[] buildClass(String targetPackage, Class> beanClass,
+ List props,
+ String[] stockName, String[] childName, String[] setterName,
+ boolean recordMode, Class> builderClass,
+ ViewStrategy views, int[] aliasArms) {
+ // A hidden class must be named in its define context's package.
+ ClassDesc thisClass = ClassDesc.of(
+ targetPackage + ".BBReader_" + beanClass.getSimpleName());
+ return ClassFile.of().build(thisClass, clb -> {
+ clb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_FINAL | ClassFile.ACC_SUPER);
+ clb.withSuperclass(CD_BASE);
+ clb.withMethod(ConstantDescs.INIT_NAME, MTD_CTOR4, ClassFile.ACC_PUBLIC,
+ mb -> mb.with(params("fallback", "ignoreAllUnknown", "ignorableProps",
+ "includableProps"))
+ .withCode(cob -> cob.aload(0).aload(1).iload(2).aload(3).aload(4)
+ .invokespecial(CD_BASE, ConstantDescs.INIT_NAME, MTD_CTOR4)
+ .return_()));
+ clb.withMethod("deserialize", MTD_DESERIALIZE, ClassFile.ACC_PUBLIC,
+ mb -> mb.with(params("p", "ctxt")).withCode(cob -> {
+ if (builderClass != null) {
+ buildBuilderDeserialize(cob, builderClass, props, stockName,
+ childName, views, aliasArms);
+ } else if (!recordMode) {
+ buildDeserialize(cob, beanClass, props, stockName, childName,
+ setterName, views, aliasArms);
+ } else {
+ buildRecordDeserialize(cob, beanClass, props, stockName, childName,
+ views, aliasArms);
+ }
+ }));
+ if (views == ViewStrategy.MASK) {
+ emitComputeViewMask(clb, props, stockName);
+ }
+ });
+ }
+
+ // Debug metadata: parameter names and named locals make dumped codecs
+ // (the blackbird.debug.dumpDir output) read like source in a decompiler;
+ // the verifier ignores both attributes.
+ static MethodParametersAttribute params(String... names) {
+ MethodParameterInfo[] infos = new MethodParameterInfo[names.length];
+ for (int i = 0; i < names.length; i++) {
+ infos[i] = MethodParameterInfo.ofParameter(Optional.of(names[i]), 0);
+ }
+ return MethodParametersAttribute.of(infos);
+ }
+
+ // Overrides GeneratedReaderBase._computeViewMask: bit i set when arm i's
+ // stock property is visible in the view, which keeps visibility semantics
+ // (matchers, default-view inclusion) exactly the stock ones.
+ private static void emitComputeViewMask(java.lang.classfile.ClassBuilder clb,
+ List props, String[] stockName) {
+ clb.withMethod("_computeViewMask", MTD_VIEW_MASK, ClassFile.ACC_PROTECTED,
+ mb -> mb.with(params("activeView")).withCode(cob -> {
+ final int maskSlot = 2;
+ Label scopeStart = cob.newBoundLabel();
+ cob.lconst_0().lstore(maskSlot);
+ for (int i = 0; i < props.size(); i++) {
+ Label skip = cob.newLabel();
+ ldcData(cob, stockName[i], CD_SETTABLE_PROP);
+ cob.aload(1);
+ cob.invokevirtual(CD_SETTABLE_PROP, "visibleInView", MTD_VISIBLE_IN_VIEW);
+ cob.ifeq(skip);
+ cob.lload(maskSlot);
+ cob.loadConstant(1L << i);
+ cob.lor();
+ cob.lstore(maskSlot);
+ cob.labelBinding(skip);
+ }
+ cob.lload(maskSlot).lreturn();
+ Label scopeEnd = cob.newBoundLabel();
+ cob.localVariable(1, "activeView", ConstantDescs.CD_Class, scopeStart, scopeEnd);
+ cob.localVariable(maskSlot, "mask", ConstantDescs.CD_long, scopeStart, scopeEnd);
+ }));
+ }
+
+ // MASK strategy: resolves the visibility bitmask for the call (all-ones
+ // when no view is active) into maskSlot.
+ private static void emitViewMask(CodeBuilder cob, int ctxt, int maskSlot) {
+ Label nullView = cob.newLabel();
+ Label haveMask = cob.newLabel();
+ cob.aload(ctxt).invokevirtual(CD_DESER_CONTEXT, "getActiveView", MTD_GET_ACTIVE_VIEW);
+ cob.dup();
+ cob.ifnull(nullView);
+ cob.aload(0);
+ cob.swap();
+ cob.invokevirtual(CD_BASE, "_viewMask", MTD_VIEW_MASK);
+ cob.lstore(maskSlot);
+ cob.goto_(haveMask);
+ cob.labelBinding(nullView);
+ cob.pop();
+ cob.loadConstant(-1L);
+ cob.lstore(maskSlot);
+ cob.labelBinding(haveMask);
+ }
+
+ // MASK strategy, start of each arm: a hidden property consumes its value
+ // with stock semantics (advance to the value token, then the base helper
+ // reports or skips) and continues the loop without touching the bean.
+ private static void emitArmViewCheck(CodeBuilder cob, int parser, int ctxt, int maskSlot,
+ int matcherSlot, int ixSlot, int armIndex, String stockName, Label loop) {
+ Label visible = cob.newLabel();
+ cob.lload(maskSlot);
+ cob.loadConstant(1L << armIndex);
+ cob.land();
+ cob.lconst_0();
+ cob.lcmp();
+ cob.ifne(visible);
+ cob.aload(0).aload(parser).aload(ctxt);
+ ldcData(cob, stockName, CD_SETTABLE_PROP);
+ cob.invokevirtual(CD_BASE, "_hiddenView", MTD_HIDDEN_VIEW);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(loop);
+ cob.labelBinding(visible);
+ }
+
+ private static void buildDeserialize(CodeBuilder cob, Class> beanClass,
+ List props, String[] stockName, String[] childName, String[] setterName,
+ ViewStrategy views, int[] aliasArms) {
+ final int parser = 1;
+ final int ctxt = 2;
+ final int beanSlot = 3;
+ final int matcherSlot = 4;
+ final int ixSlot = 5;
+ final int propSlot = 6;
+ final int excSlot = 7;
+ final int maskSlot = 8;
+ Label propHandler = cob.newLabel();
+
+ ClassDesc beanDesc = beanClass.describeConstable().orElseThrow();
+
+ Label scopeStart = cob.newBoundLabel();
+ Label delegate = emitEntryGuard(cob, parser, ctxt, views);
+ if (views == ViewStrategy.MASK) {
+ emitViewMask(cob, ctxt, maskSlot);
+ }
+
+ cob.new_(beanDesc).dup()
+ .invokespecial(beanDesc, ConstantDescs.INIT_NAME, ConstantDescs.MTD_void)
+ .astore(beanSlot);
+ cob.aload(parser).aload(beanSlot)
+ .invokevirtual(CD_JSON_PARSER, "assignCurrentValue", MTD_ASSIGN_CURRENT);
+
+ ldcData(cob, "matcher", CD_NAME_MATCHER);
+ cob.astore(matcherSlot);
+
+ Label loop = cob.newLabel();
+ Label switchPart = cob.newLabel();
+ Label endObject = cob.newLabel();
+ Label unknown = cob.newLabel();
+ Label oddToken = cob.newLabel();
+ Label defaultCase = cob.newLabel();
+
+ emitFirstMatch(cob, parser, matcherSlot, ixSlot);
+
+ cob.labelBinding(loop);
+ cob.iload(ixSlot).ifge(switchPart);
+ cob.iload(ixSlot).iconst_m1().if_icmpeq(endObject);
+ cob.iload(ixSlot).ldc(-2).if_icmpeq(unknown);
+ cob.goto_(oddToken);
+
+ cob.labelBinding(switchPart);
+ List cases = new ArrayList<>(props.size());
+ Label[] caseLabels = new Label[props.size()];
+ for (int i = 0; i < props.size(); i++) {
+ caseLabels[i] = cob.newLabel();
+ cases.add(SwitchCase.of(i, caseLabels[i]));
+ }
+ // Alias matcher indexes follow the primaries and share their arms.
+ int totalNames = props.size() + ((aliasArms == null) ? 0 : aliasArms.length);
+ for (int k = props.size(); k < totalNames; k++) {
+ cases.add(SwitchCase.of(k, caseLabels[aliasArms[k - props.size()]]));
+ }
+ cob.iload(ixSlot);
+ cob.tableswitch(0, totalNames - 1, defaultCase, cases);
+
+ for (int i = 0; i < props.size(); i++) {
+ cob.labelBinding(caseLabels[i]);
+ if (views == ViewStrategy.MASK) {
+ emitArmViewCheck(cob, parser, ctxt, maskSlot, matcherSlot, ixSlot, i,
+ stockName[i], loop);
+ }
+ Label armEnd = beginArm(cob, stockName[i], propSlot, propHandler);
+ GenProp prop = props.get(i);
+ switch (prop.kind()) {
+ case STRING -> emitScalar(cob, beanSlot, parser, ctxt, beanDesc, prop,
+ "getString", MTD_GET_STRING, ConstantDescs.CD_String,
+ stockName[i], setterName[i], "VALUE_STRING");
+ case INT -> emitScalar(cob, beanSlot, parser, ctxt, beanDesc, prop,
+ "getIntValue", MTD_GET_INT, ConstantDescs.CD_int,
+ stockName[i], setterName[i], "VALUE_NUMBER_INT");
+ case LONG -> emitScalar(cob, beanSlot, parser, ctxt, beanDesc, prop,
+ "getLongValue", MTD_GET_LONG, ConstantDescs.CD_long,
+ stockName[i], setterName[i], "VALUE_NUMBER_INT");
+ case BOOLEAN -> emitScalar(cob, beanSlot, parser, ctxt, beanDesc, prop,
+ "getBooleanValue", MTD_GET_BOOLEAN, ConstantDescs.CD_boolean,
+ stockName[i], setterName[i], null);
+ case CHILD -> {
+ Label childStock = cob.newLabel();
+ Label childDone = cob.newLabel();
+ ClassDesc childType = prop.type().describeConstable().orElseThrow();
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN);
+ cob.getstatic(CD_JSON_TOKEN, "START_OBJECT", CD_JSON_TOKEN);
+ cob.if_acmpne(childStock);
+ cob.aload(beanSlot);
+ emitChildCall(cob, parser, ctxt, childName[i]);
+ cob.checkcast(childType);
+ emitStore(cob, beanDesc, prop, childType);
+ cob.goto_(childDone);
+ cob.labelBinding(childStock);
+ emitStockSet(cob, parser, ctxt, beanSlot, stockName[i]);
+ cob.labelBinding(childDone);
+ }
+ case STOCK -> {
+ emitStockSet(cob, parser, ctxt, beanSlot, stockName[i]);
+ }
+ }
+ cob.labelBinding(armEnd);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(loop);
+ }
+
+ cob.labelBinding(defaultCase);
+ throwIse(cob, "bad property index");
+
+ cob.labelBinding(endObject);
+ cob.aload(beanSlot).areturn();
+
+ cob.labelBinding(unknown);
+ cob.aload(0).aload(parser).aload(ctxt);
+ cob.aload(beanSlot);
+ cob.invokevirtual(CD_BASE, "_handleUnknown", MTD_HANDLE_UNKNOWN);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(loop);
+
+ cob.labelBinding(oddToken);
+ cob.aload(0).aload(parser).aload(ctxt)
+ .invokevirtual(CD_BASE, "_unexpectedToken", MTD_DESERIALIZE)
+ .areturn();
+
+ emitPropertyHandler(cob, propHandler, ctxt, propSlot, excSlot, beanSlot);
+ emitDelegateTail(cob, parser, ctxt, delegate);
+
+ Label scopeEnd = cob.newBoundLabel();
+ cob.localVariable(parser, "p", CD_JSON_PARSER, scopeStart, scopeEnd);
+ cob.localVariable(ctxt, "ctxt", CD_DESER_CONTEXT, scopeStart, scopeEnd);
+ cob.localVariable(beanSlot, "bean", beanDesc, scopeStart, scopeEnd);
+ cob.localVariable(matcherSlot, "matcher", CD_NAME_MATCHER, scopeStart, scopeEnd);
+ cob.localVariable(ixSlot, "ix", ConstantDescs.CD_int, scopeStart, scopeEnd);
+ cob.localVariable(propSlot, "prop", CD_SETTABLE_PROP, scopeStart, scopeEnd);
+ cob.localVariable(excSlot, "e", CD_EXCEPTION, scopeStart, scopeEnd);
+ if (views == ViewStrategy.MASK) {
+ cob.localVariable(maskSlot, "viewMask", ConstantDescs.CD_long, scopeStart, scopeEnd);
+ }
+ }
+
+ private static final MethodTypeDesc MTD_PROP_DESERIALIZE =
+ MethodTypeDesc.of(ConstantDescs.CD_Object, CD_JSON_PARSER, CD_DESER_CONTEXT);
+ private static final ClassDesc CD_VALUE_INSTANTIATOR =
+ ClassDesc.of("tools.jackson.databind.deser.ValueInstantiator");
+ private static final MethodTypeDesc MTD_CREATE_DEFAULT =
+ MethodTypeDesc.of(ConstantDescs.CD_Object, CD_DESER_CONTEXT);
+ private static final MethodTypeDesc MTD_DESER_SET_RETURN = MethodTypeDesc.of(
+ ConstantDescs.CD_Object, CD_JSON_PARSER, CD_DESER_CONTEXT, ConstantDescs.CD_Object);
+ private static final MethodTypeDesc MTD_BUILD_INVOKE =
+ MethodTypeDesc.of(ConstantDescs.CD_Object, ConstantDescs.CD_Object);
+
+ private static void buildBuilderDeserialize(CodeBuilder cob, Class> builderClass,
+ List props, String[] stockName, String[] childName,
+ ViewStrategy views, int[] aliasArms) {
+ final int parser = 1;
+ final int ctxt = 2;
+ final int builderSlot = 3;
+ final int matcherSlot = 4;
+ final int ixSlot = 5;
+ final int propSlot = 6;
+ final int excSlot = 7;
+ final int maskSlot = 8;
+ Label propHandler = cob.newLabel();
+
+ ClassDesc builderDesc = builderClass.describeConstable().orElseThrow();
+
+ Label scopeStart = cob.newBoundLabel();
+ Label delegate = emitEntryGuard(cob, parser, ctxt, views);
+ if (views == ViewStrategy.MASK) {
+ emitViewMask(cob, ctxt, maskSlot);
+ }
+
+ ldcData(cob, "instantiator", CD_VALUE_INSTANTIATOR);
+ cob.aload(ctxt);
+ cob.invokevirtual(CD_VALUE_INSTANTIATOR, "createUsingDefault", MTD_CREATE_DEFAULT);
+ cob.checkcast(builderDesc);
+ cob.astore(builderSlot);
+
+ ldcData(cob, "matcher", CD_NAME_MATCHER);
+ cob.astore(matcherSlot);
+
+ Label loop = cob.newLabel();
+ Label switchPart = cob.newLabel();
+ Label endObject = cob.newLabel();
+ Label unknown = cob.newLabel();
+ Label oddToken = cob.newLabel();
+ Label defaultCase = cob.newLabel();
+
+ emitFirstMatch(cob, parser, matcherSlot, ixSlot);
+
+ cob.labelBinding(loop);
+ cob.iload(ixSlot).ifge(switchPart);
+ cob.iload(ixSlot).iconst_m1().if_icmpeq(endObject);
+ cob.iload(ixSlot).ldc(-2).if_icmpeq(unknown);
+ cob.goto_(oddToken);
+
+ cob.labelBinding(switchPart);
+ List cases = new ArrayList<>(props.size());
+ Label[] caseLabels = new Label[props.size()];
+ for (int i = 0; i < props.size(); i++) {
+ caseLabels[i] = cob.newLabel();
+ cases.add(SwitchCase.of(i, caseLabels[i]));
+ }
+ // Alias matcher indexes follow the primaries and share their arms.
+ int totalNames = props.size() + ((aliasArms == null) ? 0 : aliasArms.length);
+ for (int k = props.size(); k < totalNames; k++) {
+ cases.add(SwitchCase.of(k, caseLabels[aliasArms[k - props.size()]]));
+ }
+ cob.iload(ixSlot);
+ cob.tableswitch(0, totalNames - 1, defaultCase, cases);
+
+ for (int i = 0; i < props.size(); i++) {
+ cob.labelBinding(caseLabels[i]);
+ if (views == ViewStrategy.MASK) {
+ emitArmViewCheck(cob, parser, ctxt, maskSlot, matcherSlot, ixSlot, i,
+ stockName[i], loop);
+ }
+ Label armEnd = beginArm(cob, stockName[i], propSlot, propHandler);
+ GenProp prop = props.get(i);
+ switch (prop.kind()) {
+ case STRING -> emitBuilderScalar(cob, parser, ctxt, builderSlot, builderDesc, prop,
+ "getString", MTD_GET_STRING, ConstantDescs.CD_String, stockName[i],
+ "VALUE_STRING");
+ case INT -> emitBuilderScalar(cob, parser, ctxt, builderSlot, builderDesc, prop,
+ "getIntValue", MTD_GET_INT, ConstantDescs.CD_int, stockName[i],
+ "VALUE_NUMBER_INT");
+ case LONG -> emitBuilderScalar(cob, parser, ctxt, builderSlot, builderDesc, prop,
+ "getLongValue", MTD_GET_LONG, ConstantDescs.CD_long, stockName[i],
+ "VALUE_NUMBER_INT");
+ case BOOLEAN -> emitBuilderScalar(cob, parser, ctxt, builderSlot, builderDesc, prop,
+ "getBooleanValue", MTD_GET_BOOLEAN, ConstantDescs.CD_boolean, stockName[i],
+ null);
+ case CHILD -> {
+ Label childStock = cob.newLabel();
+ Label childDone = cob.newLabel();
+ ClassDesc childType = prop.type().describeConstable().orElseThrow();
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN);
+ cob.getstatic(CD_JSON_TOKEN, "START_OBJECT", CD_JSON_TOKEN);
+ cob.if_acmpne(childStock);
+ cob.aload(builderSlot);
+ emitChildCall(cob, parser, ctxt, childName[i]);
+ cob.checkcast(childType);
+ Class> childRet = prop.setter().getReturnType();
+ MethodTypeDesc childSetter = MethodTypeDesc.of(
+ childRet == void.class ? ConstantDescs.CD_void
+ : childRet.describeConstable().orElseThrow(),
+ childType);
+ cob.invokevirtual(builderDesc, prop.setter().getName(), childSetter);
+ if (childRet != void.class) {
+ cob.astore(builderSlot);
+ }
+ cob.goto_(childDone);
+ cob.labelBinding(childStock);
+ emitStockSetReturn(cob, parser, ctxt, builderSlot, builderDesc, stockName[i]);
+ cob.labelBinding(childDone);
+ }
+ case STOCK -> {
+ emitStockSetReturn(cob, parser, ctxt, builderSlot, builderDesc, stockName[i]);
+ }
+ }
+ cob.labelBinding(armEnd);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(loop);
+ }
+
+ cob.labelBinding(defaultCase);
+ throwIse(cob, "bad property index");
+
+ cob.labelBinding(endObject);
+ ldcData(cob, "buildMethod", ConstantDescs.CD_MethodHandle);
+ cob.aload(builderSlot);
+ cob.invokevirtual(ConstantDescs.CD_MethodHandle, "invokeExact", MTD_BUILD_INVOKE);
+ cob.areturn();
+
+ cob.labelBinding(unknown);
+ cob.aload(0).aload(parser).aload(ctxt);
+ cob.aload(builderSlot);
+ cob.invokevirtual(CD_BASE, "_handleUnknown", MTD_HANDLE_UNKNOWN);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(loop);
+
+ cob.labelBinding(oddToken);
+ cob.aload(0).aload(parser).aload(ctxt)
+ .invokevirtual(CD_BASE, "_unexpectedToken", MTD_DESERIALIZE)
+ .areturn();
+
+ emitPropertyHandler(cob, propHandler, ctxt, propSlot, excSlot, builderSlot);
+ emitDelegateTail(cob, parser, ctxt, delegate);
+
+ Label scopeEnd = cob.newBoundLabel();
+ cob.localVariable(parser, "p", CD_JSON_PARSER, scopeStart, scopeEnd);
+ cob.localVariable(ctxt, "ctxt", CD_DESER_CONTEXT, scopeStart, scopeEnd);
+ cob.localVariable(builderSlot, "builder", builderDesc, scopeStart, scopeEnd);
+ cob.localVariable(matcherSlot, "matcher", CD_NAME_MATCHER, scopeStart, scopeEnd);
+ cob.localVariable(ixSlot, "ix", ConstantDescs.CD_int, scopeStart, scopeEnd);
+ cob.localVariable(propSlot, "prop", CD_SETTABLE_PROP, scopeStart, scopeEnd);
+ cob.localVariable(excSlot, "e", CD_EXCEPTION, scopeStart, scopeEnd);
+ if (views == ViewStrategy.MASK) {
+ cob.localVariable(maskSlot, "viewMask", ConstantDescs.CD_long, scopeStart, scopeEnd);
+ }
+ }
+
+ private static void emitBuilderScalar(CodeBuilder cob, int parser, int ctxt, int builderSlot,
+ ClassDesc builderDesc, GenProp prop, String getter, MethodTypeDesc getterType,
+ ClassDesc valueDesc, String stockName, String expectedToken) {
+ Label useStock = cob.newLabel();
+ Label done = cob.newLabel();
+ emitExpectedTokenCheck(cob, parser, expectedToken, useStock);
+ cob.aload(builderSlot);
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, getter, getterType);
+ Class> ret = prop.setter().getReturnType();
+ MethodTypeDesc setterType = MethodTypeDesc.of(
+ ret == void.class ? ConstantDescs.CD_void : ret.describeConstable().orElseThrow(),
+ valueDesc);
+ cob.invokevirtual(builderDesc, prop.setter().getName(), setterType);
+ if (ret != void.class) {
+ cob.astore(builderSlot);
+ }
+ cob.goto_(done);
+ cob.labelBinding(useStock);
+ emitStockSetReturn(cob, parser, ctxt, builderSlot, builderDesc, stockName);
+ cob.labelBinding(done);
+ }
+
+ // Builder properties apply through deserializeSetAndReturn: fluent
+ // builders may return a replacement instance, which becomes the new
+ // builder local.
+ private static void emitStockSetReturn(CodeBuilder cob, int parser, int ctxt, int builderSlot,
+ ClassDesc builderDesc, String stockName) {
+ ldcData(cob, stockName, CD_SETTABLE_PROP);
+ cob.aload(parser).aload(ctxt).aload(builderSlot);
+ cob.invokevirtual(CD_SETTABLE_PROP, "deserializeSetAndReturn", MTD_DESER_SET_RETURN);
+ cob.checkcast(builderDesc);
+ cob.astore(builderSlot);
+ }
+
+ private static void buildRecordDeserialize(CodeBuilder cob, Class> beanClass,
+ List props, String[] stockName, String[] childName,
+ ViewStrategy views, int[] aliasArms) {
+ final int parser = 1;
+ final int ctxt = 2;
+
+ int next = 3;
+ int[] componentSlot = new int[props.size()];
+ for (int i = 0; i < props.size(); i++) {
+ componentSlot[i] = next;
+ Class> t = props.get(i).type();
+ next += (t == long.class || t == double.class) ? 2 : 1;
+ }
+ final int matcherSlot = next++;
+ final int ixSlot = next++;
+ final int seenSlot = next;
+ final int propSlot = next + 2;
+ final int excSlot = next + 3;
+ final int maskSlot = next + 4;
+ final int nullMaskSlot = next + 6;
+ Label propHandler = cob.newLabel();
+
+ ClassDesc recordDesc = beanClass.describeConstable().orElseThrow();
+
+ Label scopeStart = cob.newBoundLabel();
+ Label delegate = emitEntryGuard(cob, parser, ctxt, views);
+ if (views == ViewStrategy.MASK) {
+ emitViewMask(cob, ctxt, maskSlot);
+ }
+
+ for (int i = 0; i < props.size(); i++) {
+ Class> t = props.get(i).type();
+ if (t == long.class) {
+ cob.lconst_0().lstore(componentSlot[i]);
+ } else if (t == double.class) {
+ cob.dconst_0().dstore(componentSlot[i]);
+ } else if (t == float.class) {
+ cob.fconst_0().fstore(componentSlot[i]);
+ } else if (t.isPrimitive()) {
+ cob.iconst_0().istore(componentSlot[i]);
+ } else {
+ cob.aconst_null().astore(componentSlot[i]);
+ }
+ }
+ cob.lconst_0().lstore(seenSlot);
+
+ ldcData(cob, "matcher", CD_NAME_MATCHER);
+ cob.astore(matcherSlot);
+
+ Label loop = cob.newLabel();
+ Label switchPart = cob.newLabel();
+ Label endObject = cob.newLabel();
+ Label unknown = cob.newLabel();
+ Label oddToken = cob.newLabel();
+ Label defaultCase = cob.newLabel();
+
+ emitFirstMatch(cob, parser, matcherSlot, ixSlot);
+
+ cob.labelBinding(loop);
+ cob.iload(ixSlot).ifge(switchPart);
+ cob.iload(ixSlot).iconst_m1().if_icmpeq(endObject);
+ cob.iload(ixSlot).ldc(-2).if_icmpeq(unknown);
+ cob.goto_(oddToken);
+
+ cob.labelBinding(switchPart);
+ List cases = new ArrayList<>(props.size());
+ Label[] caseLabels = new Label[props.size()];
+ for (int i = 0; i < props.size(); i++) {
+ caseLabels[i] = cob.newLabel();
+ cases.add(SwitchCase.of(i, caseLabels[i]));
+ }
+ // Alias matcher indexes follow the primaries and share their arms.
+ int totalNames = props.size() + ((aliasArms == null) ? 0 : aliasArms.length);
+ for (int k = props.size(); k < totalNames; k++) {
+ cases.add(SwitchCase.of(k, caseLabels[aliasArms[k - props.size()]]));
+ }
+ cob.iload(ixSlot);
+ cob.tableswitch(0, totalNames - 1, defaultCase, cases);
+
+ for (int i = 0; i < props.size(); i++) {
+ cob.labelBinding(caseLabels[i]);
+ if (views == ViewStrategy.MASK) {
+ // A hidden component stays unseen, so required and
+ // missing-property reporting treat it exactly like an absent
+ // property, matching the stock creator path.
+ emitArmViewCheck(cob, parser, ctxt, maskSlot, matcherSlot, ixSlot, i,
+ stockName[i], loop);
+ }
+ Label armEnd = beginArm(cob, stockName[i], propSlot, propHandler);
+ GenProp prop = props.get(i);
+ Class> t = prop.type();
+ switch (prop.kind()) {
+ case STRING -> emitRecordScalar(cob, parser, ctxt, componentSlot[i], t,
+ "getString", MTD_GET_STRING, stockName[i], "VALUE_STRING");
+ case INT -> emitRecordScalar(cob, parser, ctxt, componentSlot[i], t,
+ "getIntValue", MTD_GET_INT, stockName[i], "VALUE_NUMBER_INT");
+ case LONG -> emitRecordScalar(cob, parser, ctxt, componentSlot[i], t,
+ "getLongValue", MTD_GET_LONG, stockName[i], "VALUE_NUMBER_INT");
+ case BOOLEAN -> emitRecordScalar(cob, parser, ctxt, componentSlot[i], t,
+ "getBooleanValue", MTD_GET_BOOLEAN, stockName[i], null);
+ case CHILD -> {
+ Label childStock = cob.newLabel();
+ Label childDone = cob.newLabel();
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN);
+ cob.getstatic(CD_JSON_TOKEN, "START_OBJECT", CD_JSON_TOKEN);
+ cob.if_acmpne(childStock);
+ emitChildCall(cob, parser, ctxt, childName[i]);
+ cob.checkcast(t.describeConstable().orElseThrow());
+ storeLocal(cob, t, componentSlot[i]);
+ cob.goto_(childDone);
+ cob.labelBinding(childStock);
+ emitStockValueToLocal(cob, parser, ctxt, componentSlot[i], t, stockName[i]);
+ cob.labelBinding(childDone);
+ }
+ case STOCK -> {
+ emitStockValueToLocal(cob, parser, ctxt, componentSlot[i], t, stockName[i]);
+ }
+ }
+ cob.labelBinding(armEnd);
+ cob.lload(seenSlot);
+ cob.loadConstant(1L << i);
+ cob.lor();
+ cob.lstore(seenSlot);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(loop);
+ }
+
+ cob.labelBinding(defaultCase);
+ throwIse(cob, "bad property index");
+
+ cob.labelBinding(endObject);
+ // Missing components: the cold helper mirrors PropertyValueBuffer's
+ // required / FAIL_ON_MISSING_CREATOR_PROPERTIES reporting.
+ long allSeen = (props.size() == 64) ? -1L : (1L << props.size()) - 1;
+ Label allPresent = cob.newLabel();
+ cob.lload(seenSlot);
+ cob.loadConstant(allSeen);
+ cob.lcmp();
+ cob.ifeq(allPresent);
+ cob.aload(0).aload(ctxt).lload(seenSlot);
+ cob.loadConstant(props.size());
+ cob.invokevirtual(CD_BASE, "_checkRecordSeen", MTD_CHECK_SEEN);
+ cob.labelBinding(allPresent);
+ // Reference components that ended null (missing or explicit) feed the
+ // cold FAIL_ON_NULL_CREATOR_PROPERTIES check, mirroring
+ // PropertyValueBuffer; primitives never do, matching stock defaults.
+ cob.lconst_0().lstore(nullMaskSlot);
+ for (int i = 0; i < props.size(); i++) {
+ if (props.get(i).type().isPrimitive()) {
+ continue;
+ }
+ Label nonNull = cob.newLabel();
+ cob.aload(componentSlot[i]);
+ cob.ifnonnull(nonNull);
+ cob.lload(nullMaskSlot);
+ cob.loadConstant(1L << i);
+ cob.lor();
+ cob.lstore(nullMaskSlot);
+ cob.labelBinding(nonNull);
+ }
+ Label noNulls = cob.newLabel();
+ cob.lload(nullMaskSlot);
+ cob.lconst_0();
+ cob.lcmp();
+ cob.ifeq(noNulls);
+ cob.aload(0).aload(ctxt).lload(nullMaskSlot);
+ cob.loadConstant(props.size());
+ cob.invokevirtual(CD_BASE, "_checkRecordNulls", MTD_CHECK_SEEN);
+ cob.labelBinding(noNulls);
+ ldcData(cob, "constructor", ConstantDescs.CD_MethodHandle);
+ ClassDesc[] paramDescs = new ClassDesc[props.size()];
+ for (int i = 0; i < props.size(); i++) {
+ Class> t = props.get(i).type();
+ paramDescs[i] = t.describeConstable().orElseThrow();
+ if (t == long.class) {
+ cob.lload(componentSlot[i]);
+ } else if (t == double.class) {
+ cob.dload(componentSlot[i]);
+ } else if (t == float.class) {
+ cob.fload(componentSlot[i]);
+ } else if (t.isPrimitive()) {
+ cob.iload(componentSlot[i]);
+ } else {
+ cob.aload(componentSlot[i]);
+ }
+ }
+ cob.invokevirtual(ConstantDescs.CD_MethodHandle, "invokeExact",
+ MethodTypeDesc.of(recordDesc, paramDescs));
+ cob.areturn();
+
+ cob.labelBinding(unknown);
+ cob.aload(0).aload(parser).aload(ctxt);
+ cob.aconst_null();
+ cob.invokevirtual(CD_BASE, "_handleUnknown", MTD_HANDLE_UNKNOWN);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(loop);
+
+ cob.labelBinding(oddToken);
+ cob.aload(0).aload(parser).aload(ctxt)
+ .invokevirtual(CD_BASE, "_unexpectedToken", MTD_DESERIALIZE)
+ .areturn();
+
+ emitPropertyHandler(cob, propHandler, ctxt, propSlot, excSlot, -1);
+ emitDelegateTail(cob, parser, ctxt, delegate);
+
+ Label scopeEnd = cob.newBoundLabel();
+ cob.localVariable(parser, "p", CD_JSON_PARSER, scopeStart, scopeEnd);
+ cob.localVariable(ctxt, "ctxt", CD_DESER_CONTEXT, scopeStart, scopeEnd);
+ for (int i = 0; i < props.size(); i++) {
+ cob.localVariable(componentSlot[i], props.get(i).name(),
+ props.get(i).type().describeConstable().orElseThrow(),
+ scopeStart, scopeEnd);
+ }
+ cob.localVariable(matcherSlot, "matcher", CD_NAME_MATCHER, scopeStart, scopeEnd);
+ cob.localVariable(ixSlot, "ix", ConstantDescs.CD_int, scopeStart, scopeEnd);
+ cob.localVariable(seenSlot, "seen", ConstantDescs.CD_long, scopeStart, scopeEnd);
+ cob.localVariable(propSlot, "prop", CD_SETTABLE_PROP, scopeStart, scopeEnd);
+ cob.localVariable(excSlot, "e", CD_EXCEPTION, scopeStart, scopeEnd);
+ if (views == ViewStrategy.MASK) {
+ cob.localVariable(maskSlot, "viewMask", ConstantDescs.CD_long, scopeStart, scopeEnd);
+ }
+ }
+
+ private static void emitRecordScalar(CodeBuilder cob, int parser, int ctxt, int slot,
+ Class> type, String getter, MethodTypeDesc getterType, String stockName,
+ String expectedToken) {
+ Label useStock = cob.newLabel();
+ Label done = cob.newLabel();
+ emitExpectedTokenCheck(cob, parser, expectedToken, useStock);
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, getter, getterType);
+ storeLocal(cob, type, slot);
+ cob.goto_(done);
+ cob.labelBinding(useStock);
+ emitStockValueToLocal(cob, parser, ctxt, slot, type, stockName);
+ cob.labelBinding(done);
+ }
+
+ // Reads the whole value through the stock property (parser positioned on
+ // the value token), then converts the boxed result to the component type.
+ private static void emitStockValueToLocal(CodeBuilder cob, int parser, int ctxt, int slot,
+ Class> type, String stockName) {
+ ldcData(cob, stockName, CD_SETTABLE_PROP);
+ cob.aload(parser).aload(ctxt);
+ cob.invokevirtual(CD_SETTABLE_PROP, "deserialize", MTD_PROP_DESERIALIZE);
+ if (type.isPrimitive()) {
+ Class> box = ClassUtil.wrapperType(type);
+ ClassDesc boxDesc = box.describeConstable().orElseThrow();
+ cob.checkcast(boxDesc);
+ cob.invokevirtual(boxDesc, type.getName() + "Value",
+ MethodTypeDesc.of(type.describeConstable().orElseThrow()));
+ } else {
+ cob.checkcast(type.describeConstable().orElseThrow());
+ }
+ storeLocal(cob, type, slot);
+ }
+
+ private static void storeLocal(CodeBuilder cob, Class> type, int slot) {
+ if (type == long.class) {
+ cob.lstore(slot);
+ } else if (type == double.class) {
+ cob.dstore(slot);
+ } else if (type == float.class) {
+ cob.fstore(slot);
+ } else if (type.isPrimitive()) {
+ cob.istore(slot);
+ } else {
+ cob.astore(slot);
+ }
+ }
+
+ // The null branch enters deserializeAndSet with the parser already on the
+ // VALUE_NULL token, which is the position that method expects. A
+ // non-public setter arrives as a classData MethodHandle instead of a
+ // direct call.
+ private static void emitScalar(CodeBuilder cob, int beanSlot, int parser, int ctxt,
+ ClassDesc beanDesc, GenProp prop, String getter, MethodTypeDesc getterType,
+ ClassDesc valueDesc, String stockName, String setterName, String expectedToken) {
+ Label useStock = cob.newLabel();
+ Label done = cob.newLabel();
+ emitExpectedTokenCheck(cob, parser, expectedToken, useStock);
+ if (setterName != null) {
+ ldcData(cob, setterName, ConstantDescs.CD_MethodHandle);
+ cob.aload(beanSlot);
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, getter, getterType);
+ cob.invokevirtual(ConstantDescs.CD_MethodHandle, "invokeExact",
+ MethodTypeDesc.of(ConstantDescs.CD_void, beanDesc, valueDesc));
+ } else {
+ cob.aload(beanSlot);
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, getter, getterType);
+ emitStore(cob, beanDesc, prop, valueDesc);
+ }
+ cob.goto_(done);
+ cob.labelBinding(useStock);
+ emitStockSet(cob, parser, ctxt, beanSlot, stockName);
+ cob.labelBinding(done);
+ }
+
+ // Stores a value already on the stack (receiver, then value) into the bean:
+ // a putfield for a public field, otherwise the direct setter call.
+ private static void emitStore(CodeBuilder cob, ClassDesc beanDesc, GenProp prop,
+ ClassDesc valueDesc) {
+ if (prop.field() != null) {
+ ClassDesc owner = prop.field().getDeclaringClass().describeConstable().orElseThrow();
+ cob.putfield(owner, prop.field().getName(), valueDesc);
+ } else {
+ invokeSetter(cob, beanDesc, prop, valueDesc);
+ }
+ }
+
+ private static void emitChildCall(CodeBuilder cob, int parser, int ctxt, String childName) {
+ ldcData(cob, childName, CD_BASE);
+ cob.aload(parser).aload(ctxt);
+ cob.invokevirtual(CD_BASE, "deserialize", MTD_DESERIALIZE);
+ }
+
+ private static void emitStockSet(CodeBuilder cob, int parser, int ctxt, int beanSlot,
+ String stockName) {
+ ldcData(cob, stockName, CD_SETTABLE_PROP);
+ cob.aload(parser).aload(ctxt).aload(beanSlot);
+ cob.invokevirtual(CD_SETTABLE_PROP, "deserializeAndSet", MTD_DESERIALIZE_AND_SET);
+ }
+
+ private static void invokeSetter(CodeBuilder cob, ClassDesc beanDesc, GenProp prop, ClassDesc valueDesc) {
+ MethodTypeDesc setter = prop.setter().getReturnType() == void.class
+ ? MethodTypeDesc.of(ConstantDescs.CD_void, valueDesc)
+ : MethodTypeDesc.of(prop.setter().getReturnType().describeConstable().orElseThrow(), valueDesc);
+ cob.invokevirtual(beanDesc, prop.setter().getName(), setter);
+ if (prop.setter().getReturnType() != void.class) {
+ cob.pop();
+ }
+ }
+
+ // Guards the generated loop's entry: anything the loop does not model - a
+ // stream positioned on neither START_OBJECT nor PROPERTY_NAME (the
+ // AsProperty polymorphic path hands subtype deserializers a stream on the
+ // property after the type id) and, under the DELEGATE strategy only, an
+ // active view - branches to the returned label, whose stock-delegation
+ // tail the caller emits at the end of the method so the main body
+ // decompiles un-nested. The MASK strategy keeps view-active calls on the
+ // generated path through the visibility bitmask.
+ private static Label emitEntryGuard(CodeBuilder cob, int parser, int ctxt,
+ ViewStrategy views) {
+ Label delegate = cob.newLabel();
+ Label entryOk = cob.newLabel();
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN);
+ cob.getstatic(CD_JSON_TOKEN, "START_OBJECT", CD_JSON_TOKEN);
+ cob.if_acmpeq(entryOk);
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN);
+ cob.getstatic(CD_JSON_TOKEN, "PROPERTY_NAME", CD_JSON_TOKEN);
+ cob.if_acmpne(delegate);
+ cob.labelBinding(entryOk);
+ if (views == ViewStrategy.DELEGATE) {
+ cob.aload(ctxt).invokevirtual(CD_DESER_CONTEXT, "getActiveView",
+ MTD_GET_ACTIVE_VIEW);
+ cob.ifnonnull(delegate);
+ }
+ return delegate;
+ }
+
+ private static void emitDelegateTail(CodeBuilder cob, int parser, int ctxt, Label delegate) {
+ cob.labelBinding(delegate);
+ cob.aload(0).getfield(CD_BASE, "_fallback", CD_BEAN_DESER_BASE);
+ cob.aload(parser).aload(ctxt);
+ cob.invokevirtual(CD_BEAN_DESER_BASE, "deserialize", MTD_DESERIALIZE);
+ cob.areturn();
+ }
+
+ // Advances to the value token and branches to useStock unless it is the
+ // token the inline read expects. Null, quoted scalars, and mismatched
+ // shapes all take the stock property, which owns coercion and null
+ // handling. expectedToken null selects the boolean pair.
+ private static void emitExpectedTokenCheck(CodeBuilder cob, int parser,
+ String expectedToken, Label useStock) {
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN);
+ if (expectedToken != null) {
+ cob.getstatic(CD_JSON_TOKEN, expectedToken, CD_JSON_TOKEN);
+ cob.if_acmpne(useStock);
+ } else {
+ Label isTrue = cob.newLabel();
+ Label fast = cob.newLabel();
+ cob.dup();
+ cob.getstatic(CD_JSON_TOKEN, "VALUE_TRUE", CD_JSON_TOKEN);
+ cob.if_acmpeq(isTrue);
+ cob.getstatic(CD_JSON_TOKEN, "VALUE_FALSE", CD_JSON_TOKEN);
+ cob.if_acmpne(useStock);
+ cob.goto_(fast);
+ cob.labelBinding(isTrue);
+ cob.pop();
+ cob.labelBinding(fast);
+ }
+ }
+
+ // Loads the arm's stock property into propSlot and opens its exception
+ // region: any Exception from a property arm is rethrown with the property
+ // reference prepended, like the stock loop's wrapAndThrow.
+ private static Label beginArm(CodeBuilder cob, String stockName, int propSlot, Label handler) {
+ ldcData(cob, stockName, CD_SETTABLE_PROP);
+ cob.astore(propSlot);
+ Label armStart = cob.newLabel();
+ Label armEnd = cob.newLabel();
+ cob.exceptionCatch(armStart, armEnd, handler, CD_EXCEPTION);
+ cob.labelBinding(armStart);
+ return armEnd;
+ }
+
+ // receiverSlot < 0 selects aconst_null (record mode: no instance yet).
+ private static void emitPropertyHandler(CodeBuilder cob, Label handler, int ctxt,
+ int propSlot, int excSlot, int receiverSlot) {
+ cob.labelBinding(handler);
+ cob.astore(excSlot);
+ cob.aload(0);
+ cob.aload(excSlot);
+ if (receiverSlot < 0) {
+ cob.aconst_null();
+ } else {
+ cob.aload(receiverSlot);
+ }
+ cob.aload(propSlot);
+ cob.aload(ctxt);
+ cob.invokevirtual(CD_BASE, "_propertyException", MTD_PROP_WRAP);
+ cob.athrow();
+ }
+
+ private static void nextNameMatch(CodeBuilder cob, int parser, int matcherSlot, int ixSlot) {
+ cob.aload(parser).aload(matcherSlot)
+ .invokevirtual(CD_JSON_PARSER, "nextNameMatchAndToken", MTD_NEXT_NAME_MATCH)
+ .istore(ixSlot);
+ }
+
+ // First match of the loop. The entry guard admits START_OBJECT and
+ // PROPERTY_NAME; a name entry matches the CURRENT name, mirroring stock
+ // BeanDeserializer's currentNameMatch loop head. The fused arms consume
+ // the current token, so a matched name entry advances to its value here;
+ // fusing the entry itself would double-advance (the current name is
+ // already consumed). Negative results stay on the name, as the unknown
+ // arm expects.
+ private static void emitFirstMatch(CodeBuilder cob, int parser, int matcherSlot, int ixSlot) {
+ Label nameEntry = cob.newLabel();
+ Label done = cob.newLabel();
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN);
+ cob.getstatic(CD_JSON_TOKEN, "PROPERTY_NAME", CD_JSON_TOKEN);
+ cob.if_acmpeq(nameEntry);
+ nextNameMatch(cob, parser, matcherSlot, ixSlot);
+ cob.goto_(done);
+ cob.labelBinding(nameEntry);
+ cob.aload(parser).aload(matcherSlot)
+ .invokevirtual(CD_JSON_PARSER, "currentNameMatch", MTD_NEXT_NAME_MATCH)
+ .istore(ixSlot);
+ cob.iload(ixSlot).iflt(done);
+ cob.aload(parser).invokevirtual(CD_JSON_PARSER, "nextToken", MTD_NEXT_TOKEN).pop();
+ cob.labelBinding(done);
+ }
+
+ private static void throwIse(CodeBuilder cob, String message) {
+ cob.new_(CD_ISE).dup().ldc(message)
+ .invokespecial(CD_ISE, ConstantDescs.INIT_NAME,
+ MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_String))
+ .athrow();
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/BeanWriterGenerator.java b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/BeanWriterGenerator.java
new file mode 100644
index 000000000..faf147514
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/BeanWriterGenerator.java
@@ -0,0 +1,452 @@
+package tools.jackson.module.blackbird.codegen;
+
+import java.lang.classfile.ClassFile;
+import java.lang.classfile.CodeBuilder;
+import java.lang.classfile.Label;
+import java.lang.classfile.attribute.MethodParametersAttribute;
+import java.lang.constant.ClassDesc;
+import java.lang.constant.ConstantDescs;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodTypeDesc;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import tools.jackson.core.SerializableString;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.ser.PropertyWriter;
+import tools.jackson.databind.ser.bean.BeanSerializerBase;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator.ViewStrategy;
+import tools.jackson.module.blackbird.internal.GeneratedWriterBase;
+
+/**
+ * Emits a hidden-class serializer for one bean: a straight-line sequence of
+ * writeName/write-value pairs with pre-encoded name constants for eligible
+ * properties and the stock PropertyWriter (a named classData constant) for
+ * everything else. Beans that declare views resolve a per-view visibility
+ * bitmask and each property tests its bit, so view-active writes stay on the
+ * generated path; beans with more than 64 properties delegate view-active
+ * calls to the stock serializer instead.
+ */
+public final class BeanWriterGenerator
+{
+ public enum WKind { STRING, INT, LONG, BOOLEAN, CHILD, STOCK }
+
+ // getter carries a public getter; field carries a public field read
+ // through getfield instead. Exactly one of the two is set for a generated
+ // (non-STOCK) property.
+ public record GenWProp(WKind kind, Method getter, PropertyWriter stock,
+ SerializableString name, GeneratedWriterBase child, Field field) {
+ public GenWProp(WKind kind, Method getter, PropertyWriter stock,
+ SerializableString name, GeneratedWriterBase child) {
+ this(kind, getter, stock, name, child, null);
+ }
+ }
+
+ private static final ClassDesc CD_JSON_GENERATOR = ClassDesc.of("tools.jackson.core.JsonGenerator");
+ private static final ClassDesc CD_SER_CONTEXT = ClassDesc.of("tools.jackson.databind.SerializationContext");
+ private static final ClassDesc CD_SERIALIZABLE_STRING = ClassDesc.of("tools.jackson.core.SerializableString");
+ private static final ClassDesc CD_PROPERTY_WRITER = ClassDesc.of("tools.jackson.databind.ser.PropertyWriter");
+ private static final ClassDesc CD_WRITER_BASE =
+ GeneratedWriterBase.class.describeConstable().orElseThrow();
+ private static final ClassDesc CD_BEAN_SER_BASE = ClassDesc.of("tools.jackson.databind.ser.bean.BeanSerializerBase");
+
+ private static final MethodTypeDesc MTD_SERIALIZE = MethodTypeDesc.of(ConstantDescs.CD_void,
+ ConstantDescs.CD_Object, CD_JSON_GENERATOR, CD_SER_CONTEXT);
+ private static final MethodTypeDesc MTD_WRITE_NAME =
+ MethodTypeDesc.of(CD_JSON_GENERATOR, CD_SERIALIZABLE_STRING);
+ private static final MethodTypeDesc MTD_WRITE_STRING =
+ MethodTypeDesc.of(CD_JSON_GENERATOR, ConstantDescs.CD_String);
+ private static final MethodTypeDesc MTD_WRITE_INT =
+ MethodTypeDesc.of(CD_JSON_GENERATOR, ConstantDescs.CD_int);
+ private static final MethodTypeDesc MTD_WRITE_LONG =
+ MethodTypeDesc.of(CD_JSON_GENERATOR, ConstantDescs.CD_long);
+ private static final MethodTypeDesc MTD_WRITE_BOOLEAN =
+ MethodTypeDesc.of(CD_JSON_GENERATOR, ConstantDescs.CD_boolean);
+ private static final MethodTypeDesc MTD_WRITE_NULL = MethodTypeDesc.of(CD_JSON_GENERATOR);
+ private static final MethodTypeDesc MTD_WRITE_START_OBJECT =
+ MethodTypeDesc.of(CD_JSON_GENERATOR, ConstantDescs.CD_Object);
+ private static final MethodTypeDesc MTD_WRITE_END = MethodTypeDesc.of(CD_JSON_GENERATOR);
+ private static final MethodTypeDesc MTD_SERIALIZE_AS_PROPERTY = MethodTypeDesc.of(
+ ConstantDescs.CD_void, ConstantDescs.CD_Object, CD_JSON_GENERATOR, CD_SER_CONTEXT);
+ private static final MethodTypeDesc MTD_CTOR =
+ MethodTypeDesc.of(ConstantDescs.CD_void, CD_BEAN_SER_BASE);
+ private static final MethodTypeDesc MTD_GET_ACTIVE_VIEW =
+ MethodTypeDesc.of(ConstantDescs.CD_Class);
+ private static final MethodTypeDesc MTD_VIEW_MASK =
+ MethodTypeDesc.of(ConstantDescs.CD_long, ConstantDescs.CD_Class);
+ private static final MethodTypeDesc MTD_PROP_VISIBLE = MethodTypeDesc.of(
+ ConstantDescs.CD_boolean, CD_PROPERTY_WRITER, ConstantDescs.CD_Class,
+ ConstantDescs.CD_boolean);
+
+ // Named class-data entries through the writer base's classDataEntry
+ // bootstrap; rationale in BeanReaderGenerator.
+ private static final java.lang.constant.DirectMethodHandleDesc BSM_DATA_ENTRY =
+ ConstantDescs.ofConstantBootstrap(CD_WRITER_BASE, "classDataEntry",
+ ConstantDescs.CD_Object);
+
+ private BeanWriterGenerator() {}
+
+ private static void ldcData(CodeBuilder cob, String name, ClassDesc type) {
+ cob.ldc(DynamicConstantDesc.ofNamed(BSM_DATA_ENTRY, name, type));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static ValueSerializer generate(Class> beanClass, List props,
+ BeanSerializerBase fallback, MethodHandles.Lookup defineLookup,
+ ViewStrategy views, boolean includeByDefault)
+ throws ReflectiveOperationException {
+ Map classData = new LinkedHashMap<>();
+ String[] stockName = new String[props.size()];
+ String[] nameName = new String[props.size()];
+ String[] childName = new String[props.size()];
+ for (int i = 0; i < props.size(); i++) {
+ GenWProp p = props.get(i);
+ String base = p.stock().getName();
+ stockName[i] = BeanReaderGenerator.dataName(classData, base + "Writer");
+ classData.put(stockName[i], p.stock());
+ if (p.name() != null) {
+ nameName[i] = BeanReaderGenerator.dataName(classData, base + "Name");
+ classData.put(nameName[i], p.name());
+ }
+ if (p.child() != null) {
+ childName[i] = BeanReaderGenerator.dataName(classData, base + "Codec");
+ classData.put(childName[i], p.child());
+ }
+ }
+
+ // Rationale in BeanReaderGenerator: non-public beans define in the
+ // bean's package context; a define failure there is an environment
+ // gate (possible only when the bean's module does not read blackbird).
+ MethodHandles.Lookup definer =
+ (defineLookup != null) ? defineLookup : MethodHandles.lookup();
+ byte[] bytes = buildClass(definer.lookupClass().getPackageName(), beanClass, props,
+ stockName, nameName, childName, views, includeByDefault);
+ CodegenDump.dump(beanClass, "writer", bytes);
+ MethodHandles.Lookup hidden;
+ try {
+ hidden = definer.defineHiddenClassWithClassData(
+ bytes, Map.copyOf(classData), true);
+ } catch (IllegalAccessException | SecurityException | LinkageError e) {
+ if (defineLookup == null) {
+ if (e instanceof IllegalAccessException iae) {
+ throw iae;
+ }
+ if (e instanceof RuntimeException re) {
+ throw re;
+ }
+ throw (LinkageError) e;
+ }
+ return null;
+ }
+ MethodHandle ctor = hidden.findConstructor(hidden.lookupClass(),
+ MethodType.methodType(void.class, BeanSerializerBase.class));
+ try {
+ return (ValueSerializer) ctor.invoke(fallback);
+ } catch (Throwable t) {
+ throw new IllegalStateException("cannot instantiate generated writer", t);
+ }
+ }
+
+ // C2 refuses to inline a hot method whose bytecode exceeds FreqInlineSize
+ // (default 325). The scalar helpers are padded past that limit so each
+ // compiles standalone: on aarch64, inlined copies of writeName and the
+ // NumberOutput digit paths inside a large generated body run 2-3x slower
+ // than their standalone compilations, which costs number-heavy shapes ~24%.
+ // Out-of-line helper calls measure at parity with the best inlined layout.
+ private static final int INLINE_PAD = 384;
+
+ private static final MethodTypeDesc MTD_HELP_STRING = MethodTypeDesc.of(ConstantDescs.CD_void,
+ CD_JSON_GENERATOR, CD_SERIALIZABLE_STRING, ConstantDescs.CD_String);
+ private static final MethodTypeDesc MTD_HELP_INT = MethodTypeDesc.of(ConstantDescs.CD_void,
+ CD_JSON_GENERATOR, CD_SERIALIZABLE_STRING, ConstantDescs.CD_int);
+ private static final MethodTypeDesc MTD_HELP_LONG = MethodTypeDesc.of(ConstantDescs.CD_void,
+ CD_JSON_GENERATOR, CD_SERIALIZABLE_STRING, ConstantDescs.CD_long);
+ private static final MethodTypeDesc MTD_HELP_BOOLEAN = MethodTypeDesc.of(ConstantDescs.CD_void,
+ CD_JSON_GENERATOR, CD_SERIALIZABLE_STRING, ConstantDescs.CD_boolean);
+ private static final MethodTypeDesc MTD_HELP_NAME = MethodTypeDesc.of(ConstantDescs.CD_void,
+ CD_JSON_GENERATOR, CD_SERIALIZABLE_STRING);
+
+ private static byte[] buildClass(String targetPackage, Class> beanClass,
+ List props,
+ String[] stockName, String[] nameName, String[] childName,
+ ViewStrategy views, boolean includeByDefault) {
+ // A hidden class must be named in its define context's package.
+ ClassDesc thisClass = ClassDesc.of(
+ targetPackage + ".BBWriter_" + beanClass.getSimpleName());
+ return ClassFile.of().build(thisClass, clb -> {
+ clb.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_FINAL | ClassFile.ACC_SUPER);
+ clb.withSuperclass(CD_WRITER_BASE);
+ clb.withMethod(ConstantDescs.INIT_NAME, MTD_CTOR, ClassFile.ACC_PUBLIC,
+ mb -> mb.with(BeanReaderGenerator.params("fallback"))
+ .withCode(cob -> cob.aload(0).aload(1)
+ .invokespecial(CD_WRITER_BASE, ConstantDescs.INIT_NAME, MTD_CTOR)
+ .return_()));
+ clb.withMethod("serialize", MTD_SERIALIZE, ClassFile.ACC_PUBLIC,
+ mb -> mb.with(BeanReaderGenerator.params("value", "g", "ctxt"))
+ .withCode(cob -> buildSerialize(cob, thisClass, beanClass, props,
+ stockName, nameName, childName, views)));
+ emitHelpers(clb, thisClass, props);
+ if (views == ViewStrategy.MASK) {
+ emitComputeViewMask(clb, props, stockName, includeByDefault);
+ }
+ });
+ }
+
+ private static void emitHelpers(java.lang.classfile.ClassBuilder clb, ClassDesc thisClass,
+ List props) {
+ boolean needName = false;
+ boolean[] kinds = new boolean[WKind.values().length];
+ for (GenWProp p : props) {
+ kinds[p.kind().ordinal()] = true;
+ if (p.kind() == WKind.CHILD) {
+ needName = true;
+ }
+ }
+ if (kinds[WKind.STRING.ordinal()]) {
+ emitPadded(clb, "$str", MTD_HELP_STRING, cob -> {
+ cob.aload(0).aload(1)
+ .invokevirtual(CD_JSON_GENERATOR, "writeName", MTD_WRITE_NAME).pop();
+ Label isNull = cob.newLabel();
+ Label done = cob.newLabel();
+ cob.aload(2).ifnull(isNull);
+ cob.aload(0).aload(2)
+ .invokevirtual(CD_JSON_GENERATOR, "writeString", MTD_WRITE_STRING).pop();
+ cob.goto_(done);
+ cob.labelBinding(isNull);
+ cob.aload(0).invokevirtual(CD_JSON_GENERATOR, "writeNull", MTD_WRITE_NULL).pop();
+ cob.labelBinding(done);
+ cob.return_();
+ });
+ }
+ if (kinds[WKind.INT.ordinal()]) {
+ emitPadded(clb, "$int", MTD_HELP_INT, cob -> cob.aload(0).aload(1)
+ .invokevirtual(CD_JSON_GENERATOR, "writeName", MTD_WRITE_NAME).pop()
+ .aload(0).iload(2)
+ .invokevirtual(CD_JSON_GENERATOR, "writeNumber", MTD_WRITE_INT).pop()
+ .return_());
+ }
+ if (kinds[WKind.LONG.ordinal()]) {
+ emitPadded(clb, "$long", MTD_HELP_LONG, cob -> cob.aload(0).aload(1)
+ .invokevirtual(CD_JSON_GENERATOR, "writeName", MTD_WRITE_NAME).pop()
+ .aload(0).lload(2)
+ .invokevirtual(CD_JSON_GENERATOR, "writeNumber", MTD_WRITE_LONG).pop()
+ .return_());
+ }
+ if (kinds[WKind.BOOLEAN.ordinal()]) {
+ emitPadded(clb, "$bool", MTD_HELP_BOOLEAN, cob -> cob.aload(0).aload(1)
+ .invokevirtual(CD_JSON_GENERATOR, "writeName", MTD_WRITE_NAME).pop()
+ .aload(0).iload(2)
+ .invokevirtual(CD_JSON_GENERATOR, "writeBoolean", MTD_WRITE_BOOLEAN).pop()
+ .return_());
+ }
+ if (needName) {
+ emitPadded(clb, "$name", MTD_HELP_NAME, cob -> cob.aload(0).aload(1)
+ .invokevirtual(CD_JSON_GENERATOR, "writeName", MTD_WRITE_NAME).pop()
+ .return_());
+ }
+ }
+
+ private static void emitPadded(java.lang.classfile.ClassBuilder clb, String name,
+ MethodTypeDesc type, java.util.function.Consumer body) {
+ MethodParametersAttribute helperParams = (type.parameterCount() == 2)
+ ? BeanReaderGenerator.params("g", "name")
+ : BeanReaderGenerator.params("g", "name", "value");
+ clb.withMethod(name, type, ClassFile.ACC_PRIVATE | ClassFile.ACC_STATIC,
+ mb -> mb.with(helperParams).withCode(cob -> {
+ for (int i = 0; i < INLINE_PAD; i++) {
+ cob.nop();
+ }
+ body.accept(cob);
+ }));
+ }
+
+ private static void buildSerialize(CodeBuilder cob, ClassDesc thisClass, Class> beanClass,
+ List props, String[] stockName, String[] nameName, String[] childName,
+ ViewStrategy views) {
+ final int gen = 2;
+ final int ctxt = 3;
+ final int beanSlot = 4;
+ final int refSlot = 5;
+ final int maskSlot = 6;
+
+ ClassDesc beanDesc = beanClass.describeConstable().orElseThrow();
+ final boolean itf = beanClass.isInterface();
+
+ Label scopeStart = cob.newBoundLabel();
+ Label delegate = null;
+ if (views == ViewStrategy.DELEGATE) {
+ // View-active calls (declared views with more than 64 properties,
+ // or the write-nothing inclusion-off case) go to the stock
+ // serializer, whose tail sits after the body so the main path
+ // decompiles un-nested.
+ delegate = cob.newLabel();
+ cob.aload(ctxt).invokevirtual(CD_SER_CONTEXT, "getActiveView", MTD_GET_ACTIVE_VIEW);
+ cob.ifnonnull(delegate);
+ } else if (views == ViewStrategy.MASK) {
+ Label nullView = cob.newLabel();
+ Label haveMask = cob.newLabel();
+ cob.aload(ctxt).invokevirtual(CD_SER_CONTEXT, "getActiveView", MTD_GET_ACTIVE_VIEW);
+ cob.dup();
+ cob.ifnull(nullView);
+ cob.aload(0);
+ cob.swap();
+ cob.invokevirtual(CD_WRITER_BASE, "_viewMask", MTD_VIEW_MASK);
+ cob.lstore(maskSlot);
+ cob.goto_(haveMask);
+ cob.labelBinding(nullView);
+ cob.pop();
+ cob.loadConstant(-1L);
+ cob.lstore(maskSlot);
+ cob.labelBinding(haveMask);
+ }
+
+ cob.aload(1).checkcast(beanDesc).astore(beanSlot);
+ cob.aload(gen).aload(beanSlot)
+ .invokevirtual(CD_JSON_GENERATOR, "writeStartObject", MTD_WRITE_START_OBJECT).pop();
+
+ for (int i = 0; i < props.size(); i++) {
+ GenWProp prop = props.get(i);
+ Label hidden = null;
+ if (views == ViewStrategy.MASK) {
+ hidden = cob.newLabel();
+ cob.lload(maskSlot);
+ cob.loadConstant(1L << i);
+ cob.land();
+ cob.lconst_0();
+ cob.lcmp();
+ cob.ifeq(hidden);
+ }
+ switch (prop.kind()) {
+ case INT -> emitScalar(cob, thisClass, gen, beanSlot, beanDesc, itf, prop,
+ nameName[i], "$int", MTD_HELP_INT, ConstantDescs.CD_int);
+ case LONG -> emitScalar(cob, thisClass, gen, beanSlot, beanDesc, itf, prop,
+ nameName[i], "$long", MTD_HELP_LONG, ConstantDescs.CD_long);
+ case BOOLEAN -> emitScalar(cob, thisClass, gen, beanSlot, beanDesc, itf, prop,
+ nameName[i], "$bool", MTD_HELP_BOOLEAN, ConstantDescs.CD_boolean);
+ case STRING -> emitScalar(cob, thisClass, gen, beanSlot, beanDesc, itf, prop,
+ nameName[i], "$str", MTD_HELP_STRING, ConstantDescs.CD_String);
+ case CHILD -> {
+ Class> childRaw = prop.field() != null
+ ? prop.field().getType() : prop.getter().getReturnType();
+ ClassDesc childType = childRaw.describeConstable().orElseThrow();
+ cob.aload(gen);
+ ldcData(cob, nameName[i], CD_SERIALIZABLE_STRING);
+ cob.invokestatic(thisClass, "$name", MTD_HELP_NAME);
+ cob.aload(beanSlot);
+ emitLoad(cob, itf, beanDesc, prop, childType);
+ cob.astore(refSlot);
+ final String childEntry = childName[i];
+ emitNullableRef(cob, gen, refSlot, () -> {
+ ldcData(cob, childEntry, CD_WRITER_BASE);
+ cob.aload(refSlot).aload(gen).aload(ctxt);
+ cob.invokevirtual(CD_WRITER_BASE, "serialize", MTD_SERIALIZE);
+ });
+ }
+ case STOCK -> {
+ ldcData(cob, stockName[i], CD_PROPERTY_WRITER);
+ cob.aload(beanSlot).aload(gen).aload(ctxt);
+ cob.invokevirtual(CD_PROPERTY_WRITER, "serializeAsProperty",
+ MTD_SERIALIZE_AS_PROPERTY);
+ }
+ }
+ if (hidden != null) {
+ cob.labelBinding(hidden);
+ }
+ }
+
+ cob.aload(gen).invokevirtual(CD_JSON_GENERATOR, "writeEndObject", MTD_WRITE_END).pop();
+ cob.return_();
+ if (delegate != null) {
+ cob.labelBinding(delegate);
+ cob.aload(0).getfield(CD_WRITER_BASE, "_fallback", CD_BEAN_SER_BASE);
+ cob.aload(1).aload(gen).aload(ctxt);
+ cob.invokevirtual(CD_BEAN_SER_BASE, "serialize", MTD_SERIALIZE);
+ cob.return_();
+ }
+
+ Label scopeEnd = cob.newBoundLabel();
+ cob.localVariable(1, "value", ConstantDescs.CD_Object, scopeStart, scopeEnd);
+ cob.localVariable(gen, "g", CD_JSON_GENERATOR, scopeStart, scopeEnd);
+ cob.localVariable(ctxt, "ctxt", CD_SER_CONTEXT, scopeStart, scopeEnd);
+ cob.localVariable(beanSlot, "bean", beanDesc, scopeStart, scopeEnd);
+ // Slot entries must stay within max_locals, so name the child slot
+ // only when some CHILD arm actually stores it.
+ if (props.stream().anyMatch(pr -> pr.kind() == WKind.CHILD)) {
+ cob.localVariable(refSlot, "child", ConstantDescs.CD_Object, scopeStart, scopeEnd);
+ }
+ if (views == ViewStrategy.MASK) {
+ cob.localVariable(maskSlot, "viewMask", ConstantDescs.CD_long, scopeStart, scopeEnd);
+ }
+ }
+
+ // Overrides GeneratedWriterBase._computeViewMask: bit i set when property
+ // i is visible in the view, per the same rule the stock factory uses to
+ // build the filtered writer array.
+ private static void emitComputeViewMask(java.lang.classfile.ClassBuilder clb,
+ List props, String[] stockName, boolean includeByDefault) {
+ clb.withMethod("_computeViewMask", MTD_VIEW_MASK, ClassFile.ACC_PROTECTED,
+ mb -> mb.with(BeanReaderGenerator.params("activeView")).withCode(cob -> {
+ final int maskSlot = 2;
+ Label scopeStart = cob.newBoundLabel();
+ cob.lconst_0().lstore(maskSlot);
+ for (int i = 0; i < props.size(); i++) {
+ Label skip = cob.newLabel();
+ ldcData(cob, stockName[i], CD_PROPERTY_WRITER);
+ cob.aload(1);
+ cob.loadConstant(includeByDefault ? 1 : 0);
+ cob.invokestatic(CD_WRITER_BASE, "_propVisible", MTD_PROP_VISIBLE);
+ cob.ifeq(skip);
+ cob.lload(maskSlot);
+ cob.loadConstant(1L << i);
+ cob.lor();
+ cob.lstore(maskSlot);
+ cob.labelBinding(skip);
+ }
+ cob.lload(maskSlot).lreturn();
+ Label scopeEnd = cob.newBoundLabel();
+ cob.localVariable(1, "activeView", ConstantDescs.CD_Class, scopeStart, scopeEnd);
+ cob.localVariable(maskSlot, "mask", ConstantDescs.CD_long, scopeStart, scopeEnd);
+ }));
+ }
+
+ private static void emitScalar(CodeBuilder cob, ClassDesc thisClass, int gen, int beanSlot,
+ ClassDesc beanDesc, boolean itf, GenWProp prop, String nameEntry, String helper,
+ MethodTypeDesc helperType, ClassDesc valDesc) {
+ cob.aload(gen);
+ ldcData(cob, nameEntry, CD_SERIALIZABLE_STRING);
+ cob.aload(beanSlot);
+ emitLoad(cob, itf, beanDesc, prop, valDesc);
+ cob.invokestatic(thisClass, helper, helperType);
+ }
+
+ // Loads the property value from the bean already on the stack: a getfield
+ // for a public field, otherwise the getter call.
+ private static void emitLoad(CodeBuilder cob, boolean itf, ClassDesc beanDesc,
+ GenWProp prop, ClassDesc valDesc) {
+ if (prop.field() != null) {
+ ClassDesc owner = prop.field().getDeclaringClass().describeConstable().orElseThrow();
+ cob.getfield(owner, prop.field().getName(), valDesc);
+ } else if (itf) {
+ cob.invokeinterface(beanDesc, prop.getter().getName(), MethodTypeDesc.of(valDesc));
+ } else {
+ cob.invokevirtual(beanDesc, prop.getter().getName(), MethodTypeDesc.of(valDesc));
+ }
+ }
+
+ private static void emitNullableRef(CodeBuilder cob, int gen, int slot, Runnable nonNull) {
+ Label isNull = cob.newLabel();
+ Label done = cob.newLabel();
+ cob.aload(slot).ifnull(isNull);
+ nonNull.run();
+ cob.goto_(done);
+ cob.labelBinding(isNull);
+ cob.aload(gen).invokevirtual(CD_JSON_GENERATOR, "writeNull", MTD_WRITE_NULL).pop();
+ cob.labelBinding(done);
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodecAccess.java b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodecAccess.java
new file mode 100644
index 000000000..6ad4ba0bd
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodecAccess.java
@@ -0,0 +1,56 @@
+package tools.jackson.module.blackbird.codegen;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Modifier;
+import java.util.function.Function;
+
+/**
+ * Access decisions shared by the deserializer and serializer factories: where
+ * a generated hidden class may be defined, and which members its bytecode may
+ * reference directly.
+ */
+public final class CodecAccess
+{
+ private CodecAccess() {}
+
+ /**
+ * Public beans keep the module's define context (returned as null);
+ * non-public beans need the bean's package context, reached through
+ * privateLookupIn with the user-supplied lookup (or the module's own,
+ * which suffices on the classpath where everything shares the unnamed
+ * module). No reachable context means the bean stays on the stock path.
+ */
+ public static MethodHandles.Lookup defineContext(Class> beanClass,
+ Function, MethodHandles.Lookup> lookups) {
+ if (Modifier.isPublic(beanClass.getModifiers())
+ || beanClass.getPackageName().isEmpty()) {
+ return null;
+ }
+ MethodHandles.Lookup caller = lookups.apply(beanClass);
+ if (caller == null) {
+ caller = MethodHandles.lookup();
+ }
+ try {
+ return MethodHandles.privateLookupIn(beanClass, caller);
+ } catch (IllegalAccessException | SecurityException e) {
+ return null;
+ }
+ }
+
+ /**
+ * A member the generated code may reference directly: public on a public
+ * class always, and otherwise a non-private member of a class in the same
+ * runtime package as the define context (anchor null = module context).
+ */
+ public static boolean directlyAccessible(int memberMods, Class> declaring,
+ Class> anchor) {
+ if (Modifier.isPublic(memberMods) && Modifier.isPublic(declaring.getModifiers())) {
+ return true;
+ }
+ return anchor != null
+ && !Modifier.isPrivate(memberMods)
+ && !Modifier.isPrivate(declaring.getModifiers())
+ && declaring.getPackageName().equals(anchor.getPackageName())
+ && declaring.getClassLoader() == anchor.getClassLoader();
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodegenDump.java b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodegenDump.java
new file mode 100644
index 000000000..ebcac291b
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodegenDump.java
@@ -0,0 +1,39 @@
+package tools.jackson.module.blackbird.codegen;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Debug aid: when the {@code blackbird.debug.dumpDir} system property names a
+ * directory, every generated class's bytes are written there as
+ * {@code -.class} before the hidden class is defined,
+ * for inspection with javap or a decompiler. A dump failure never affects
+ * generation.
+ */
+final class CodegenDump
+{
+ private static final Path DIR = resolveDir();
+
+ private CodegenDump() {}
+
+ private static Path resolveDir() {
+ String dir = System.getProperty("blackbird.debug.dumpDir");
+ return (dir == null) ? null : Path.of(dir);
+ }
+
+ static void dump(Class> beanClass, String kind, byte[] bytes) {
+ if (DIR == null) {
+ return;
+ }
+ try {
+ Files.createDirectories(DIR);
+ Files.write(DIR.resolve(beanClass.getName() + "-" + kind + ".class"), bytes);
+ } catch (IOException e) {
+ Logger.getLogger("tools.jackson.module.blackbird").log(Level.FINE,
+ "Generated-class dump failed for " + beanClass.getName(), e);
+ }
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodegenFallbacks.java b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodegenFallbacks.java
new file mode 100644
index 000000000..75d59e0d8
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/CodegenFallbacks.java
@@ -0,0 +1,63 @@
+package tools.jackson.module.blackbird.codegen;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Reports codec-generation fallbacks so demotion to the stock path never hides
+ * a problem silently. Gate failures are expected world diversity (exotic
+ * classes make reflective gate checks throw) and log a warning once per type.
+ * Generation failures are Blackbird bugs: the type passed every gate and the
+ * generator still failed. They log severe once per type, and with the
+ * {@code tools.jackson.module.blackbird.failOnCodegenError} system property
+ * set they are rethrown instead of demoted, which the Blackbird test suites
+ * enable so a masked generation bug fails the build.
+ */
+public final class CodegenFallbacks
+{
+ private static final Logger LOGGER = Logger.getLogger("tools.jackson.module.blackbird");
+
+ // Keyed by class name, not Class, so this static set never pins a foreign
+ // classloader (the leak class this module just fixed).
+ private static final Set REPORTED = ConcurrentHashMap.newKeySet();
+
+ public static final String FAIL_ON_ERROR_PROPERTY =
+ "tools.jackson.module.blackbird.failOnCodegenError";
+
+ private CodegenFallbacks() {}
+
+ public static void gateFailure(Class> beanClass, Throwable cause) {
+ if (REPORTED.add("gate:" + beanClass.getName())) {
+ LOGGER.log(Level.WARNING, "Blackbird gate check failed for {0}; the stock"
+ + " (de)serializer stays in place. Cause: {1}. Later reports for"
+ + " this type log at FINE.",
+ new Object[] { beanClass.getName(), cause });
+ } else {
+ LOGGER.log(Level.FINE, "Blackbird gate check failed for " + beanClass.getName(), cause);
+ }
+ }
+
+ public static void generationFailure(Class> beanClass, Throwable cause) {
+ if (Boolean.getBoolean(FAIL_ON_ERROR_PROPERTY)) {
+ if (cause instanceof RuntimeException re) {
+ throw re;
+ }
+ if (cause instanceof Error err) {
+ throw err;
+ }
+ throw new IllegalStateException(
+ "Blackbird codec generation failed for " + beanClass.getName(), cause);
+ }
+ if (REPORTED.add("gen:" + beanClass.getName())) {
+ LOGGER.log(Level.SEVERE, "Blackbird codec generation failed for "
+ + beanClass.getName() + "; the stock (de)serializer stays in place."
+ + " This is likely a jackson-module-blackbird bug; please report it.",
+ cause);
+ } else {
+ LOGGER.log(Level.FINE,
+ "Blackbird codec generation failed for " + beanClass.getName(), cause);
+ }
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBDeserializerModifier.java
index 7ab360529..4b89689e0 100644
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBDeserializerModifier.java
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBDeserializerModifier.java
@@ -1,282 +1,186 @@
package tools.jackson.module.blackbird.deser;
-import java.lang.invoke.LambdaConversionException;
-import java.lang.invoke.LambdaMetafactory;
-import java.lang.invoke.MethodHandle;
+import java.io.IOException;
+import java.io.ObjectInputStream;
import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-import java.lang.invoke.MethodHandles.Lookup;
-import java.lang.reflect.Member;
-import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
-import java.util.*;
-import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
-import java.util.function.ObjIntConsumer;
-import java.util.function.ObjLongConsumer;
-import java.util.function.UnaryOperator;
-
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.*;
-import tools.jackson.databind.deser.impl.MethodProperty;
-import tools.jackson.databind.deser.std.StdValueInstantiator;
-import tools.jackson.databind.introspect.*;
-import tools.jackson.databind.util.ClassUtil;
-import tools.jackson.module.blackbird.util.ReflectionHack;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonIncludeProperties;
+
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.DeserializationConfig;
+import tools.jackson.databind.MapperFeature;
+import tools.jackson.databind.PropertyName;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.ValueDeserializerModifier;
+import tools.jackson.databind.deser.BeanDeserializerBuilder;
+import tools.jackson.databind.deser.bean.BeanDeserializer;
+import tools.jackson.databind.deser.bean.BeanDeserializerBase;
+import tools.jackson.databind.deser.bean.BuilderBasedDeserializer;
+import tools.jackson.databind.introspect.AnnotatedMethod;
+import tools.jackson.databind.introspect.BeanPropertyDefinition;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator;
+import tools.jackson.module.blackbird.codegen.CodegenFallbacks;
+
+/**
+ * Wraps eligible stock bean deserializers in a placeholder that generates a
+ * per-bean codec at resolve time (ClassFile API hidden class). Every gate that
+ * fails leaves the stock deserializer in place, so behavior never changes for
+ * beans the generator does not fully understand.
+ */
public class BBDeserializerModifier extends ValueDeserializerModifier
{
private static final long serialVersionUID = 1L;
- private static final MethodHandle TRAMPOLINE, BOOLEAN_TRAMPOLINE, LONG_TRAMPOLINE, INT_TRAMPOLINE;
+ // Reserved for member access beyond public API (private setters, creators
+ // in non-exported packages); the v1 generator only touches public members.
+ private final Function, MethodHandles.Lookup> _lookups;
- static {
- try {
- TRAMPOLINE = MethodHandles.lookup().findStatic(BBDeserializerModifier.class, "trampoline",
- MethodType.methodType(void.class, BiFunction.class, Object.class, Object.class));
- BOOLEAN_TRAMPOLINE = MethodHandles.lookup().findStatic(BBDeserializerModifier.class, "booleanTrampoline",
- MethodType.methodType(void.class, ObjBooleanBiFunction.class, Object.class, boolean.class));
- LONG_TRAMPOLINE = MethodHandles.lookup().findStatic(BBDeserializerModifier.class, "longTrampoline",
- MethodType.methodType(void.class, ObjLongBiFunction.class, Object.class, long.class));
- INT_TRAMPOLINE = MethodHandles.lookup().findStatic(BBDeserializerModifier.class, "intTrampoline",
- MethodType.methodType(void.class, ObjIntBiFunction.class, Object.class, int.class));
- } catch (Exception e) {
- throw new ExceptionInInitializerError(e);
- }
- }
- private final Function, Lookup> _lookups;
- private final UnaryOperator _accessGrant;
+ // The build method is only reachable from updateBuilder; the factory calls
+ // updateBuilder and modifyDeserializer for the same bean back to back on
+ // one thread, so a ThreadLocal hands it across. Not final: readObject
+ // recreates it, since transient fields deserialize as null.
+ private transient ThreadLocal _pendingBuildMethod = new ThreadLocal<>();
- public BBDeserializerModifier(Function, MethodHandles.Lookup> lookups, UnaryOperator accessGrant)
- {
+ public BBDeserializerModifier(Function, MethodHandles.Lookup> lookups) {
_lookups = lookups;
- _accessGrant = accessGrant;
}
- /*
- /**********************************************************************
- /* BeanDeserializerModifier methods
- /**********************************************************************
- */
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ _pendingBuildMethod = new ThreadLocal<>();
+ }
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config,
- BeanDescription.Supplier beanDescRef, BeanDeserializerBuilder builder)
- {
- final Class> beanClass = beanDescRef.getBeanClass();
- MethodHandles.Lookup lookup = _lookups.apply(beanClass);
- if (lookup == null) {
- return builder;
- }
- try {
- lookup = ReflectionHack.privateLookupIn(beanClass, lookup);
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- }
- /* Hmmh. Can we access stuff from private classes?
- * Possibly, if we can use parent class loader.
- * (should probably skip all non-public?)
- */
- if (Modifier.isPrivate(beanClass.getModifiers())) { // TODO??
- return builder;
- }
- lookup = _accessGrant.apply(lookup);
- List> newProps = findOptimizableProperties(
- lookup, config, builder.getProperties());
- // and if we found any, create mutator proxy, replace property objects
- if (!newProps.isEmpty()) {
- for (OptimizedSettableBeanProperty> prop : newProps) {
- builder.addOrReplaceProperty(prop, true);
- }
- }
- // Second thing: see if we could (re)generate Creator(s):
- ValueInstantiator inst = builder.getValueInstantiator();
- /* Hmmh. Probably better to require exact default implementation
- * and not sub-class; chances are sub-class uses its own
- * construction anyway.
- */
- if (inst.getClass() == StdValueInstantiator.class) {
- // also, only override if using default creator (no-arg ctor, no-arg static factory)
- if (inst.canCreateUsingDefault() || inst.canCreateFromObjectWith()) {
- inst = new CreatorOptimizer(beanClass, lookup, (StdValueInstantiator) inst).createOptimized();
- if (inst != null) {
- builder.setValueInstantiator(inst);
- }
- }
- }
-
- // also: may want to replace actual BeanDeserializer as well? For this, need to replace builder
- // (but only if builder is the original standard one; don't want to break other impls)
- if (builder.getClass() == BeanDeserializerBuilder.class) {
- return new SuperSonicDeserializerBuilder(builder);
- }
+ BeanDescription.Supplier beanDescRef, BeanDeserializerBuilder builder) {
+ _pendingBuildMethod.set(builder.getBuildMethod());
return builder;
}
- /*
- /**********************************************************************
- /* Internal methods
- /**********************************************************************
- */
-
- protected List> findOptimizableProperties(
- Lookup lookup, DeserializationConfig config,
- Iterator propIterator)
+ @Override
+ public ValueDeserializer> modifyDeserializer(DeserializationConfig config,
+ BeanDescription.Supplier beanDescRef, ValueDeserializer> deserializer)
{
- ArrayList> newProps = new ArrayList>();
-
- // Ok, then, find any properties for which we could generate accessors
- while (propIterator.hasNext()) {
- try {
- nextProperty(propIterator.next(), lookup, newProps);
- } catch (Throwable e) {
- if (e instanceof Error) {
- throw (Error) e;
- }
- if (e instanceof RuntimeException) {
- throw (RuntimeException) e;
- }
- throw new RuntimeException(e);
- }
+ // Gate failures of any kind leave the stock deserializer in place. The
+ // reflective gates can throw for exotic classes (a bean from a foreign
+ // classloader with inconsistent InnerClasses metadata raises
+ // IncompatibleClassChangeError from getEnclosingClass), and an
+ // acceleration modifier must never break a bean stock databind handles.
+ try {
+ return doModify(config, beanDescRef, deserializer);
+ } catch (RuntimeException | LinkageError e) {
+ CodegenFallbacks.gateFailure(beanDescRef.getBeanClass(), e);
+ return deserializer;
}
- return newProps;
}
- @SuppressWarnings("unchecked")
- private void nextProperty(SettableBeanProperty prop,
- Lookup lookup,
- ArrayList> newProps) throws Throwable
+ private ValueDeserializer> doModify(DeserializationConfig config,
+ BeanDescription.Supplier beanDescRef, ValueDeserializer> deserializer)
{
- AnnotatedMember member = prop.getMember();
- Member jdkMember = member.getMember();
-
- // if we ever support virtual properties, this would be null, so check, skip
- if (jdkMember == null) {
- return;
+ AnnotatedMethod buildMethod = _pendingBuildMethod.get();
+ _pendingBuildMethod.remove();
+ boolean builderBased = deserializer.getClass() == BuilderBasedDeserializer.class
+ && buildMethod != null;
+ if (!builderBased && deserializer.getClass() != BeanDeserializer.class) {
+ return deserializer;
}
- // First: we can't access private fields or methods....
- if (Modifier.isPrivate(jdkMember.getModifiers())) {
- return;
+ if (Boolean.TRUE.equals(config.getDefaultMergeable())) {
+ return deserializer;
}
- // (although, interestingly enough, can seem to access private classes...)
-
- // 30-Jul-2012, tatu: [module-afterburner#6]: Needs to skip custom deserializers, if any.
- if (prop.hasValueDeserializer()) {
- if (!isDefaultDeserializer(prop.getValueDeserializer())) {
- return;
+ BeanDescription beanDesc = beanDescRef.get();
+ Class> beanClass = beanDesc.getBeanClass();
+ if (Modifier.isPrivate(beanClass.getModifiers())
+ || (beanClass.getEnclosingClass() != null
+ && !Modifier.isStatic(beanClass.getModifiers()))) {
+ return deserializer;
+ }
+ // Records and builder beans skip the constructor checks: they create
+ // through the canonical constructor and the instantiator. Creator
+ // strictness needs no gate - the generated record path enforces
+ // required, FAIL_ON_MISSING, and FAIL_ON_NULL creator semantics per
+ // call, and builder beans here have no creator properties (the
+ // factory requires the default-creating instantiator).
+ if (!builderBased && !beanClass.isRecord()) {
+ if (Modifier.isAbstract(beanClass.getModifiers())) {
+ return deserializer;
+ }
+ try {
+ if (Modifier.isPrivate(beanClass.getDeclaredConstructor().getModifiers())) {
+ return deserializer;
+ }
+ } catch (NoSuchMethodException e) {
+ return deserializer;
}
}
-
- MethodHandle setter;
- Class> type;
- if (jdkMember instanceof Method && prop instanceof MethodProperty) {
- final Method method = (Method) jdkMember;
- setter = lookup.unreflect(method);
- type = ((AnnotatedMethod) member).getRawParameterType(0);
- } else {
- return;
- //setter = lookup.unreflectGetter((Field) jdkMember);
+ if (beanDesc.findAnySetterAccessor() != null) {
+ return deserializer;
}
-
- if (type.isPrimitive()) {
- if (type == Integer.TYPE) {
- newProps.add(new SettableIntProperty(prop,
- createSetter(lookup, ObjIntConsumer.class, ObjIntBiFunction.class,
- INT_TRAMPOLINE, int.class, setter)));
- } else if (type == Long.TYPE) {
- newProps.add(new SettableLongProperty(prop,
- createSetter(lookup, ObjLongConsumer.class, ObjLongBiFunction.class,
- LONG_TRAMPOLINE, long.class, setter)));
- } else if (type == Boolean.TYPE) {
- newProps.add(new SettableBooleanProperty(prop,
- createSetter(lookup, ObjBooleanConsumer.class, ObjBooleanBiFunction.class,
- BOOLEAN_TRAMPOLINE, boolean.class, setter)));
+ // Injected values arrive outside the property loop, which the
+ // generated codec does not model.
+ Map injectables = beanDesc.findInjectables();
+ if (injectables != null && !injectables.isEmpty()) {
+ return deserializer;
+ }
+ // Ignored and included property sets ride into the codec, whose
+ // unknown arm consults them in the stock loop's exact order.
+ JsonIgnoreProperties.Value ignorals =
+ config.getDefaultPropertyIgnorals(beanClass, beanDesc.getClassInfo());
+ JsonIncludeProperties.Value inclusions =
+ config.getDefaultPropertyInclusions(beanClass, beanDesc.getClassInfo());
+ boolean ignoreAllUnknown = ignorals != null && ignorals.getIgnoreUnknown();
+ Set ignorable = new HashSet<>();
+ if (ignorals != null) {
+ ignorable.addAll(ignorals.getIgnored());
+ }
+ ignorable.addAll(beanDesc.getIgnoredPropertyNames());
+ Set includable = (inclusions == null) ? null : inclusions.getIncluded();
+ BeanReaderGenerator.Ignorals codecIgnorals =
+ (!ignoreAllUnknown && ignorable.isEmpty() && includable == null)
+ ? BeanReaderGenerator.Ignorals.NONE
+ : new BeanReaderGenerator.Ignorals(ignoreAllUnknown,
+ ignorable.isEmpty() ? null : Set.copyOf(ignorable),
+ includable);
+ // Effective case-insensitivity, the way the stock builder computes it:
+ // the per-class format override wins, the mapper feature is baseline.
+ Boolean formatCI = beanDescRef.findExpectedFormat(null)
+ .getFeature(JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
+ boolean caseInsensitive = (formatCI == null)
+ ? config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
+ : formatCI.booleanValue();
+ boolean declaresViews = config.getAnnotationIntrospector()
+ .findViews(config, beanDesc.getClassInfo()) != null;
+ Map> aliases = null;
+ for (BeanPropertyDefinition def : beanDesc.findProperties()) {
+ if (def.findViews() != null) {
+ declaresViews = true;
}
- } else {
- if (type == String.class) {
- newProps.add(new SettableStringProperty(prop,
- createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter)));
- } else {
- newProps.add(new SettableObjectProperty(prop,
- createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter)));
+ List defAliases = def.findAliases();
+ if (!defAliases.isEmpty()) {
+ if (aliases == null) {
+ aliases = new HashMap<>();
+ }
+ aliases.put(def.getName(), defAliases);
+ }
+ if (def.getPrimaryMember() != null
+ && config.getAnnotationIntrospector()
+ .findUnwrappingNameTransformer(config, def.getPrimaryMember()) != null) {
+ return deserializer;
+ }
+ if (def.getMetadata() != null && def.getMetadata().getMergeInfo() != null) {
+ return deserializer;
}
}
- }
-
- private T createSetter(Lookup lookup, Class iface, Class> thunkType, MethodHandle trampoline, Class> valueType, MethodHandle setter)
- throws Throwable, LambdaConversionException {
- if (setter.type().returnType() == void.class) {
- return iface.cast(LambdaMetafactory.metafactory(
- lookup,
- "accept",
- MethodType.methodType(iface),
- MethodType.methodType(void.class, Object.class, valueType),
- setter,
- setter.type())
- .getTarget().invoke());
- }
- Object builtThunk = LambdaMetafactory.metafactory(
- lookup,
- "apply",
- MethodType.methodType(thunkType),
- MethodType.methodType(Object.class, Object.class, valueType),
- setter,
- setter.type())
- .getTarget().invoke();
- return iface.cast(LambdaMetafactory.metafactory(
- MethodHandles.lookup(),
- "accept",
- MethodType.methodType(iface, thunkType),
- MethodType.methodType(void.class, Object.class, valueType),
- trampoline,
- MethodType.methodType(void.class, Object.class, valueType))
- .getTarget().invoke(builtThunk));
- }
-
- /**
- * Helper method used to check whether given deserializer is the default
- * deserializer implementation: this is necessary to avoid overriding other
- * kinds of deserializers.
- */
- protected boolean isDefaultDeserializer(ValueDeserializer> deser) {
- return ClassUtil.isJacksonStdImpl(deser)
- // 07-May-2018, tatu: Probably can't happen but just in case
- || (deser instanceof SuperSonicBeanDeserializer);
-
- }
-
- // These trampolines adapt BiFunction-like setters (i.e. returns a value)
- // to BiConsumer style through a generated thunk lambda.
-
- @FunctionalInterface
- public interface ObjIntBiFunction {
- Object apply(Object bean, int value);
- }
-
- @FunctionalInterface
- public interface ObjLongBiFunction {
- Object apply(Object bean, long value);
- }
-
- @FunctionalInterface
- public interface ObjBooleanBiFunction {
- Object apply(Object bean, boolean value);
- }
-
- static void intTrampoline(ObjIntBiFunction thunk, Object bean, int value) {
- thunk.apply(bean, value);
- }
-
- static void longTrampoline(ObjLongBiFunction thunk, Object bean, long value) {
- thunk.apply(bean, value);
- }
-
- static void booleanTrampoline(ObjBooleanBiFunction thunk, Object bean, boolean value) {
- thunk.apply(bean, value);
- }
-
- static void trampoline(BiFunction thunk, Object bean, Object value) {
- thunk.apply(bean, value);
+ return new BBReaderPlaceholder((BeanDeserializerBase) deserializer, _lookups,
+ builderBased ? buildMethod : null, declaresViews, codecIgnorals, aliases,
+ caseInsensitive);
}
}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderFactory.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderFactory.java
new file mode 100644
index 000000000..59d0769d0
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderFactory.java
@@ -0,0 +1,557 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.RecordComponent;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import tools.jackson.core.sym.PropertyNameMatcher;
+import tools.jackson.core.util.Named;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.MapperFeature;
+import tools.jackson.databind.PropertyName;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.CreatorProperty;
+import tools.jackson.databind.deser.SettableBeanProperty;
+import tools.jackson.databind.deser.bean.BeanDeserializerBase;
+import tools.jackson.databind.introspect.AnnotatedConstructor;
+import tools.jackson.databind.introspect.AnnotatedField;
+import tools.jackson.databind.introspect.AnnotatedMethod;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator.GenProp;
+import tools.jackson.module.blackbird.internal.GeneratedReaderBase;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator.Kind;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator.ViewStrategy;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator;
+import tools.jackson.module.blackbird.codegen.CodecAccess;
+import tools.jackson.module.blackbird.codegen.CodegenFallbacks;
+
+/**
+ * Builds a generated codec from a resolved stock bean deserializer, or returns
+ * null when the bean does not qualify. Qualification is conservative: every
+ * gate that cannot be verified cheaply demotes a property to the stock path or
+ * rejects the bean entirely, so semantics never drift from databind.
+ */
+final class BBReaderFactory
+{
+ private static final Set STOCK_SCALAR_DESERS = Set.of(
+ "tools.jackson.databind.deser.jdk.StringDeserializer",
+ "tools.jackson.databind.deser.jdk.NumberDeserializers$IntegerDeserializer",
+ "tools.jackson.databind.deser.jdk.NumberDeserializers$LongDeserializer",
+ "tools.jackson.databind.deser.jdk.NumberDeserializers$BooleanDeserializer");
+
+ private BBReaderFactory() {}
+
+ private static final boolean DEBUG = Boolean.getBoolean("blackbird.debug.codegen");
+
+ static ValueDeserializer tryGenerate(BeanDeserializerBase delegate,
+ DeserializationContext ctxt,
+ Function, MethodHandles.Lookup> lookups,
+ AnnotatedMethod buildMethod, boolean declaresViews,
+ BeanReaderGenerator.Ignorals ignorals, Map> aliases,
+ boolean caseInsensitive) {
+ try {
+ ValueDeserializer codec = generate(delegate, ctxt, lookups, buildMethod,
+ declaresViews, ignorals, aliases, caseInsensitive);
+ if (DEBUG) {
+ System.err.println("bbdebug tryGenerate " + delegate.handledType().getName()
+ + " -> " + (codec == null ? "null (gated)" : codec.getClass().getName()));
+ }
+ return codec;
+ } catch (Throwable t) {
+ if (DEBUG) {
+ System.err.println("bbdebug tryGenerate " + delegate.handledType().getName() + " threw:");
+ t.printStackTrace();
+ }
+ CodegenFallbacks.generationFailure(delegate.handledType(), t);
+ return null;
+ }
+ }
+
+ private static ValueDeserializer generate(BeanDeserializerBase delegate,
+ DeserializationContext ctxt,
+ Function, MethodHandles.Lookup> lookups,
+ AnnotatedMethod buildMethod, boolean declaresViews,
+ BeanReaderGenerator.Ignorals ignorals, Map> aliases,
+ boolean caseInsensitive)
+ throws ReflectiveOperationException {
+ // Deliberately no delegate.hasViews() gate: 3.x disables
+ // DEFAULT_VIEW_INCLUSION by default, which marks every bean as needing
+ // view processing; the generated codec instead checks
+ // ctxt.getActiveView() per call and delegates when a view is active.
+ if (delegate.getObjectIdReader(ctxt) != null) {
+ if (DEBUG) System.err.println("bbdebug gate: objectId");
+ return null;
+ }
+ Class> beanClass = delegate.handledType();
+ if (Modifier.isPrivate(beanClass.getModifiers())
+ || Modifier.isAbstract(beanClass.getModifiers())) {
+ if (DEBUG) System.err.println("bbdebug gate: class modifiers");
+ return null;
+ }
+ // Generated code refers to the bean class by name, which the hidden
+ // class resolves through this module's loader. A bean from a foreign
+ // classloader would resolve to a different (or no) class, so it stays
+ // on the stock path.
+ if (!visibleToGenerator(beanClass)) {
+ if (DEBUG) System.err.println("bbdebug gate: foreign classloader");
+ return null;
+ }
+ MethodHandles.Lookup defineLookup = CodecAccess.defineContext(beanClass, lookups);
+ boolean gatedContext = !Modifier.isPublic(beanClass.getModifiers());
+ if (gatedContext && defineLookup == null) {
+ if (DEBUG) System.err.println("bbdebug gate: define context");
+ return null;
+ }
+ if (buildMethod != null) {
+ return generateBuilder(delegate, ctxt, beanClass, lookups, buildMethod, defineLookup, declaresViews, ignorals, aliases, caseInsensitive);
+ }
+ if (beanClass.isRecord()) {
+ return generateRecord(delegate, ctxt, beanClass, lookups, defineLookup, declaresViews, ignorals, aliases, caseInsensitive);
+ }
+ if (!delegate.getValueInstantiator().canCreateUsingDefault()) {
+ if (DEBUG) System.err.println("bbdebug gate: instantiator");
+ return null;
+ }
+ // The generated POJO codec constructs with a direct `new`, which is
+ // only equivalent when the instantiator's default creator IS the
+ // no-arg constructor. A no-arg @JsonCreator factory (or a custom
+ // instantiator) also reports canCreateUsingDefault and must construct
+ // through the instantiator, so such beans stay on the stock path.
+ if (!(delegate.getValueInstantiator().getDefaultCreator()
+ instanceof AnnotatedConstructor)) {
+ if (DEBUG) System.err.println("bbdebug gate: default creator is not the constructor");
+ return null;
+ }
+ try {
+ if (!CodecAccess.directlyAccessible(
+ beanClass.getDeclaredConstructor().getModifiers(), beanClass,
+ defineLookup == null ? null : defineLookup.lookupClass())) {
+ if (DEBUG) System.err.println("bbdebug gate: ctor modifiers");
+ return null;
+ }
+ } catch (NoSuchMethodException e) {
+ if (DEBUG) System.err.println("bbdebug gate: no ctor");
+ return null;
+ }
+
+ List props = new ArrayList<>();
+ List names = new ArrayList<>();
+ for (Iterator it = delegate.properties(); it.hasNext(); ) {
+ SettableBeanProperty prop = it.next();
+ if (prop instanceof CreatorProperty
+ || prop.getMetadata().getMergeInfo() != null) {
+ if (DEBUG) System.err.println("bbdebug gate: prop " + prop.getName());
+ return null;
+ }
+ props.add(classify(prop, beanClass, lookups,
+ defineLookup == null ? null : defineLookup.lookupClass()));
+ names.add(Named.fromString(prop.getName()));
+ }
+ if (props.isEmpty()) {
+ if (DEBUG) System.err.println("bbdebug gate: no props");
+ return null;
+ }
+ int[] aliasArms = appendAliases(names, props, aliases);
+ PropertyNameMatcher matcher = matcher(ctxt, names, caseInsensitive);
+ return BeanReaderGenerator.generate(beanClass, props, matcher, delegate, defineLookup,
+ viewStrategy(ctxt, props, declaresViews), ignorals, aliasArms);
+ }
+
+ // Beans that declare no @JsonView anywhere (read from the property
+ // definitions at modify time; the resolved matchers cannot distinguish a
+ // declared view from the empty set that disabled DEFAULT_VIEW_INCLUSION
+ // forces onto every unannotated property) emit no per-arm view code:
+ // with inclusion on, views cannot affect them at all (NONE); with it off,
+ // an active view hides every property, so the degenerate view-active call
+ // DELEGATEs to stock and the no-view hot path stays free of mask tests.
+ // Declared views take MASK (the fast path under views) up to 64
+ // properties, DELEGATE beyond. Mask visibility is read per property from
+ // SettableBeanProperty.visibleInView, so semantics stay exactly stock.
+ private static ViewStrategy viewStrategy(DeserializationContext ctxt,
+ List props, boolean declaresViews) {
+ if (!declaresViews) {
+ return ctxt.isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)
+ ? ViewStrategy.NONE : ViewStrategy.DELEGATE;
+ }
+ return (props.size() > 64) ? ViewStrategy.DELEGATE : ViewStrategy.MASK;
+ }
+
+ // Builder-based beans: the stock ValueInstantiator creates the builder,
+ // properties apply to it (tier-A fluent setters must return void or the
+ // builder class exactly, or they demote to the stock path), and the build
+ // method - unreflected through the user lookup and asType'd to
+ // (Object)Object - produces the value.
+ private static ValueDeserializer generateBuilder(BeanDeserializerBase delegate,
+ DeserializationContext ctxt, Class> beanClass,
+ Function, MethodHandles.Lookup> lookups, AnnotatedMethod buildMethod,
+ MethodHandles.Lookup defineLookup, boolean declaresViews,
+ BeanReaderGenerator.Ignorals ignorals, Map> aliases,
+ boolean caseInsensitive)
+ throws ReflectiveOperationException {
+ Method build = buildMethod.getAnnotated();
+ Class> builderClass = build.getDeclaringClass();
+ if (!delegate.getValueInstantiator().canCreateUsingDefault()
+ || Modifier.isPrivate(builderClass.getModifiers())
+ || !visibleToGenerator(builderClass)
+ || Modifier.isPrivate(build.getModifiers())
+ || build.getParameterCount() != 0) {
+ if (DEBUG) System.err.println("bbdebug gate: builder shape");
+ return null;
+ }
+ // The generated code casts to and calls setters on the builder class,
+ // so a non-public builder needs a define context in its own package.
+ if (!Modifier.isPublic(builderClass.getModifiers())
+ && (defineLookup == null || !builderClass.getPackageName().equals(
+ defineLookup.lookupClass().getPackageName()))) {
+ defineLookup = CodecAccess.defineContext(builderClass, lookups);
+ if (defineLookup == null) {
+ if (DEBUG) System.err.println("bbdebug gate: builder context");
+ return null;
+ }
+ }
+ Class> anchor = defineLookup == null ? null : defineLookup.lookupClass();
+ MethodHandles.Lookup lookup =
+ (defineLookup != null) ? defineLookup : lookups.apply(builderClass);
+ if (lookup == null) {
+ if (DEBUG) System.err.println("bbdebug gate: builder lookup");
+ return null;
+ }
+ List props = new ArrayList<>();
+ List names = new ArrayList<>();
+ for (Iterator it = delegate.properties(); it.hasNext(); ) {
+ SettableBeanProperty prop = it.next();
+ if (prop instanceof CreatorProperty
+ || prop.getMetadata().getMergeInfo() != null) {
+ if (DEBUG) System.err.println("bbdebug gate: builder prop " + prop.getName());
+ return null;
+ }
+ props.add(classifyBuilder(prop, builderClass, anchor));
+ names.add(Named.fromString(prop.getName()));
+ }
+ if (props.isEmpty()) {
+ if (DEBUG) System.err.println("bbdebug gate: builder no props");
+ return null;
+ }
+ MethodHandle buildMH;
+ try {
+ buildMH = lookup.unreflect(build)
+ .asType(MethodType.methodType(Object.class, Object.class));
+ } catch (IllegalAccessException e) {
+ if (DEBUG) System.err.println("bbdebug gate: build method access");
+ return null;
+ }
+ int[] aliasArms = appendAliases(names, props, aliases);
+ PropertyNameMatcher matcher = matcher(ctxt, names, caseInsensitive);
+ return BeanReaderGenerator.generate(beanClass, props, matcher, delegate, null,
+ new BeanReaderGenerator.BuilderSupport(
+ delegate.getValueInstantiator(), buildMH, builderClass), defineLookup,
+ viewStrategy(ctxt, props, declaresViews), ignorals, aliasArms);
+ }
+
+ private static GenProp classifyBuilder(SettableBeanProperty prop, Class> builderClass,
+ Class> anchor) {
+ Class> raw = prop.getType().getRawClass();
+ Method setter = setterOf(prop, raw);
+ if (setter == null
+ || !CodecAccess.directlyAccessible(setter.getModifiers(), builderClass, anchor)
+ || setter.getDeclaringClass() != builderClass
+ || (setter.getReturnType() != void.class
+ && setter.getReturnType() != builderClass)) {
+ return stock(prop);
+ }
+ ValueDeserializer> valueDeser = prop.getValueDeserializer();
+ if (valueDeser instanceof GeneratedReaderBase child) {
+ return new GenProp(prop.getName(), Kind.CHILD, setter, prop, raw, child, null);
+ }
+ Kind kind = scalarKind(raw);
+ if (kind == null || valueDeser == null
+ || !STOCK_SCALAR_DESERS.contains(valueDeser.getClass().getName())) {
+ return stock(prop);
+ }
+ return new GenProp(prop.getName(), kind, setter, prop);
+ }
+
+ // Records collect components into typed locals and construct through the
+ // canonical constructor; every property must be a CreatorProperty whose
+ // creator index and type line up with the record components.
+ private static ValueDeserializer generateRecord(BeanDeserializerBase delegate,
+ DeserializationContext ctxt, Class> beanClass,
+ Function, MethodHandles.Lookup> lookups, MethodHandles.Lookup defineLookup,
+ boolean declaresViews, BeanReaderGenerator.Ignorals ignorals,
+ Map> aliases, boolean caseInsensitive)
+ throws ReflectiveOperationException {
+ if (!delegate.getValueInstantiator().canCreateFromObjectWith()) {
+ if (DEBUG) System.err.println("bbdebug gate: record instantiator");
+ return null;
+ }
+ // The generated code constructs through the canonical constructor. A
+ // @JsonCreator factory (or a custom instantiator) must construct
+ // through the instantiator instead, so such records stay on the stock
+ // path. An AnnotatedConstructor that also passes the per-component
+ // index and type match below is the canonical constructor: a second
+ // constructor with the same signature cannot exist.
+ if (!(delegate.getValueInstantiator().getWithArgsCreator()
+ instanceof AnnotatedConstructor)) {
+ if (DEBUG) System.err.println("bbdebug gate: record creator is not the constructor");
+ return null;
+ }
+ RecordComponent[] comps = beanClass.getRecordComponents();
+ // The seen-component mask in the generated code is one long.
+ if (comps.length > 64) {
+ if (DEBUG) System.err.println("bbdebug gate: record size");
+ return null;
+ }
+ SettableBeanProperty[] byIndex = new SettableBeanProperty[comps.length];
+ int count = 0;
+ for (Iterator it = delegate.properties(); it.hasNext(); ) {
+ SettableBeanProperty prop = it.next();
+ if (!(prop instanceof CreatorProperty)
+ || prop.getMetadata().getMergeInfo() != null
+ // A missing injectable component takes its value from the
+ // context, which the typed-locals loop does not model.
+ || prop.getInjectionDefinition() != null) {
+ if (DEBUG) System.err.println("bbdebug gate: record prop " + prop.getName());
+ return null;
+ }
+ int idx = prop.getCreatorIndex();
+ if (idx < 0 || idx >= comps.length || byIndex[idx] != null
+ || prop.getType().getRawClass() != comps[idx].getType()) {
+ if (DEBUG) System.err.println("bbdebug gate: record index " + prop.getName());
+ return null;
+ }
+ byIndex[idx] = prop;
+ count++;
+ }
+ if (count != comps.length || count == 0) {
+ if (DEBUG) System.err.println("bbdebug gate: record count");
+ return null;
+ }
+ MethodHandles.Lookup lookup =
+ (defineLookup != null) ? defineLookup : lookups.apply(beanClass);
+ if (lookup == null) {
+ if (DEBUG) System.err.println("bbdebug gate: record lookup");
+ return null;
+ }
+ Class>[] paramTypes = new Class>[comps.length];
+ List props = new ArrayList<>(comps.length);
+ List names = new ArrayList<>(comps.length);
+ for (int i = 0; i < comps.length; i++) {
+ paramTypes[i] = comps[i].getType();
+ SettableBeanProperty prop = byIndex[i];
+ ValueDeserializer> valueDeser = prop.getValueDeserializer();
+ if (valueDeser instanceof GeneratedReaderBase child) {
+ props.add(new GenProp(prop.getName(), Kind.CHILD, null, prop,
+ paramTypes[i], child, null));
+ names.add(Named.fromString(prop.getName()));
+ continue;
+ }
+ Kind kind = scalarKind(paramTypes[i]);
+ if (kind == null || valueDeser == null
+ || !STOCK_SCALAR_DESERS.contains(valueDeser.getClass().getName())) {
+ kind = Kind.STOCK;
+ }
+ props.add(new GenProp(prop.getName(), kind, null, prop, paramTypes[i]));
+ names.add(Named.fromString(prop.getName()));
+ }
+ MethodHandle recordCtor;
+ try {
+ recordCtor = lookup.findConstructor(beanClass,
+ MethodType.methodType(void.class, paramTypes));
+ } catch (IllegalAccessException e) {
+ // The supplied lookup cannot reach the canonical constructor: an
+ // environment gate, same as no lookup at all.
+ if (DEBUG) System.err.println("bbdebug gate: record ctor access");
+ return null;
+ }
+ int[] aliasArms = appendAliases(names, props, aliases);
+ PropertyNameMatcher matcher = matcher(ctxt, names, caseInsensitive);
+ return BeanReaderGenerator.generate(beanClass, props, matcher, delegate, recordCtor,
+ defineLookup, viewStrategy(ctxt, props, declaresViews), ignorals, aliasArms);
+ }
+
+ // Appends alias entries after the primary names, each mapping back to its
+ // primary arm - the same layout stock BeanPropertyMap.initMatcher builds -
+ // and returns the arm index per alias entry.
+ private static int[] appendAliases(List names, List props,
+ Map> aliases) {
+ if (aliases == null || aliases.isEmpty()) {
+ return null;
+ }
+ List arms = new ArrayList<>();
+ for (int i = 0; i < props.size(); i++) {
+ List propAliases = aliases.get(props.get(i).name());
+ if (propAliases == null) {
+ continue;
+ }
+ for (PropertyName alias : propAliases) {
+ names.add(Named.fromString(alias.getSimpleName()));
+ arms.add(i);
+ }
+ }
+ if (arms.isEmpty()) {
+ return null;
+ }
+ int[] out = new int[arms.size()];
+ for (int i = 0; i < out.length; i++) {
+ out[i] = arms.get(i);
+ }
+ return out;
+ }
+
+ // The matcher kind mirrors stock BeanPropertyMap.initMatcher: the
+ // case-insensitive variant when the mapper enables
+ // ACCEPT_CASE_INSENSITIVE_PROPERTIES, with the configured locale.
+ private static PropertyNameMatcher matcher(DeserializationContext ctxt, List names,
+ boolean caseInsensitive) {
+ if (caseInsensitive) {
+ return ctxt.tokenStreamFactory().constructCINameMatcher(names, true,
+ ctxt.getConfig().getLocale());
+ }
+ return ctxt.tokenStreamFactory().constructNameMatcher(names, true);
+ }
+
+ private static GenProp classify(SettableBeanProperty prop, Class> beanClass,
+ Function, MethodHandles.Lookup> lookups, Class> anchor) {
+ Class> raw = prop.getType().getRawClass();
+ ValueDeserializer> valueDeser = prop.getValueDeserializer();
+ Method setter = setterOf(prop, raw);
+ if (setter != null) {
+ return classifySetter(prop, beanClass, lookups, raw, valueDeser, setter, anchor);
+ }
+ Field field = fieldOf(prop, raw);
+ if (field != null) {
+ return classifyField(prop, beanClass, lookups, raw, valueDeser, field, anchor);
+ }
+ return stock(prop);
+ }
+
+ private static GenProp classifySetter(SettableBeanProperty prop, Class> beanClass,
+ Function, MethodHandles.Lookup> lookups, Class> raw,
+ ValueDeserializer> valueDeser, Method setter, Class> anchor) {
+ boolean direct = CodecAccess.directlyAccessible(setter.getModifiers(),
+ setter.getDeclaringClass(), anchor);
+ if (valueDeser instanceof GeneratedReaderBase child && direct) {
+ return new GenProp(prop.getName(), Kind.CHILD, setter, prop, raw, child, null);
+ }
+ Kind kind = scalarKind(raw);
+ if (kind == null || valueDeser == null
+ || !STOCK_SCALAR_DESERS.contains(valueDeser.getClass().getName())) {
+ return stock(prop);
+ }
+ if (direct) {
+ return new GenProp(prop.getName(), kind, setter, prop);
+ }
+ // Non-public setter: reach it through the user-supplied lookup; any
+ // access failure keeps the property on the stock path.
+ try {
+ MethodHandles.Lookup lookup = lookups.apply(beanClass);
+ if (lookup == null) {
+ return stock(prop);
+ }
+ MethodHandle mh = lookup.unreflect(setter)
+ .asType(MethodType.methodType(void.class, beanClass, raw));
+ return new GenProp(prop.getName(), kind, null, prop, raw, null, mh);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ return stock(prop);
+ }
+ }
+
+ // Field-backed properties store through a generated putfield (public,
+ // non-final field on a public declaring class) or, for a non-public field,
+ // an unreflected setter handle reached through the user lookup - the same
+ // fallback the non-public-setter path uses. Final fields stay on the stock
+ // path so their (stock-defined) behavior is preserved exactly.
+ private static GenProp classifyField(SettableBeanProperty prop, Class> beanClass,
+ Function, MethodHandles.Lookup> lookups, Class> raw,
+ ValueDeserializer> valueDeser, Field field, Class> anchor) {
+ if (Modifier.isFinal(field.getModifiers())) {
+ return stock(prop);
+ }
+ boolean direct = CodecAccess.directlyAccessible(field.getModifiers(),
+ field.getDeclaringClass(), anchor);
+ if (valueDeser instanceof GeneratedReaderBase child && direct) {
+ return new GenProp(prop.getName(), Kind.CHILD, null, prop, raw, child, null, field);
+ }
+ Kind kind = scalarKind(raw);
+ if (kind == null || valueDeser == null
+ || !STOCK_SCALAR_DESERS.contains(valueDeser.getClass().getName())) {
+ return stock(prop);
+ }
+ if (direct) {
+ return new GenProp(prop.getName(), kind, null, prop, raw, null, null, field);
+ }
+ try {
+ MethodHandles.Lookup lookup = lookups.apply(beanClass);
+ if (lookup == null) {
+ return stock(prop);
+ }
+ MethodHandle mh = lookup.unreflectSetter(field)
+ .asType(MethodType.methodType(void.class, beanClass, raw));
+ return new GenProp(prop.getName(), kind, null, prop, raw, null, mh);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ return stock(prop);
+ }
+ }
+
+ private static Field fieldOf(SettableBeanProperty prop, Class> raw) {
+ if (!(prop.getMember() instanceof AnnotatedField af)) {
+ return null;
+ }
+ Field field = af.getAnnotated();
+ if (field == null || Modifier.isStatic(field.getModifiers())
+ || field.getType() != raw) {
+ return null;
+ }
+ return field;
+ }
+
+ private static Method setterOf(SettableBeanProperty prop, Class> raw) {
+ if (!(prop.getMember() instanceof AnnotatedMethod am)) {
+ return null;
+ }
+ Method setter = am.getAnnotated();
+ if (setter == null || setter.getParameterCount() != 1
+ || setter.getParameterTypes()[0] != raw) {
+ return null;
+ }
+ return setter;
+ }
+
+ private static GenProp stock(SettableBeanProperty prop) {
+ return new GenProp(prop.getName(), Kind.STOCK, null, prop);
+ }
+
+ static boolean visibleToGenerator(Class> cls) {
+ try {
+ return Class.forName(cls.getName(), false,
+ GeneratedReaderBase.class.getClassLoader()) == cls;
+ } catch (ClassNotFoundException | LinkageError e) {
+ return false;
+ }
+ }
+
+ private static Kind scalarKind(Class> raw) {
+ if (raw == String.class) {
+ return Kind.STRING;
+ }
+ if (raw == int.class) {
+ return Kind.INT;
+ }
+ if (raw == long.class) {
+ return Kind.LONG;
+ }
+ if (raw == boolean.class) {
+ return Kind.BOOLEAN;
+ }
+ return null;
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderPlaceholder.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderPlaceholder.java
new file mode 100644
index 000000000..cfcb7d5a0
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderPlaceholder.java
@@ -0,0 +1,164 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.BeanProperty;
+import tools.jackson.databind.DeserializationConfig;
+import tools.jackson.databind.PropertyName;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.SettableBeanProperty;
+import tools.jackson.databind.deser.bean.BeanDeserializerBase;
+import tools.jackson.databind.introspect.AnnotatedMethod;
+import tools.jackson.databind.jsontype.TypeDeserializer;
+import tools.jackson.databind.type.LogicalType;
+import tools.jackson.databind.util.AccessPattern;
+import tools.jackson.databind.util.NameTransformer;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator;
+
+/**
+ * Stands in for a stock bean deserializer until resolution: the codec needs
+ * the resolved property list and the context's TokenStreamFactory, both of
+ * which exist only at resolve time. After resolve, contextualization hands out
+ * the generated codec directly, so steady-state call sites reference the codec
+ * with no extra hop; anything that keeps the placeholder still works through
+ * delegation.
+ */
+final class BBReaderPlaceholder extends ValueDeserializer
+{
+ private final BeanDeserializerBase _delegate;
+
+ private final Function, MethodHandles.Lookup> _lookups;
+
+ private final AnnotatedMethod _buildMethod;
+
+ // Whether the bean or any property declares @JsonView explicitly, read
+ // from the property definitions at modify time (the resolved properties
+ // cannot distinguish a declared view from the empty view set that
+ // disabled DEFAULT_VIEW_INCLUSION forces onto unannotated properties).
+ private final boolean _declaresViews;
+ private final BeanReaderGenerator.Ignorals _ignorals;
+ private final Map> _aliases;
+ private final boolean _caseInsensitive;
+
+ private volatile ValueDeserializer _codec;
+
+ BBReaderPlaceholder(BeanDeserializerBase delegate,
+ Function, MethodHandles.Lookup> lookups,
+ AnnotatedMethod buildMethod, boolean declaresViews,
+ BeanReaderGenerator.Ignorals ignorals, Map> aliases,
+ boolean caseInsensitive) {
+ _delegate = delegate;
+ _lookups = lookups;
+ _buildMethod = buildMethod;
+ _declaresViews = declaresViews;
+ _ignorals = ignorals;
+ _aliases = aliases;
+ _caseInsensitive = caseInsensitive;
+ }
+
+ @Override
+ public void resolve(DeserializationContext ctxt) {
+ if (Boolean.getBoolean("blackbird.debug.codegen")) {
+ System.err.println("bbdebug resolve " + _delegate.handledType().getName());
+ }
+ _delegate.resolve(ctxt);
+ _codec = BBReaderFactory.tryGenerate(_delegate, ctxt, _lookups, _buildMethod,
+ _declaresViews, _ignorals, _aliases, _caseInsensitive);
+ }
+
+ @Override
+ public ValueDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) {
+ ValueDeserializer> contextual = _delegate.createContextual(ctxt, property);
+ if (contextual != _delegate) {
+ return contextual;
+ }
+ ValueDeserializer codec = _codec;
+ return (codec != null) ? codec : this;
+ }
+
+ @Override
+ public Object deserialize(JsonParser p, DeserializationContext ctxt) {
+ ValueDeserializer codec = _codec;
+ return (codec != null) ? codec.deserialize(p, ctxt) : _delegate.deserialize(p, ctxt);
+ }
+
+ @Override
+ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) {
+ return _delegate.deserialize(p, ctxt, intoValue);
+ }
+
+ @Override
+ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
+ TypeDeserializer typeDeserializer) {
+ return _delegate.deserializeWithType(p, ctxt, typeDeserializer);
+ }
+
+ @Override
+ public Object getNullValue(DeserializationContext ctxt) {
+ return _delegate.getNullValue(ctxt);
+ }
+
+ @Override
+ public Object getEmptyValue(DeserializationContext ctxt) {
+ return _delegate.getEmptyValue(ctxt);
+ }
+
+ @Override
+ public Object getAbsentValue(DeserializationContext ctxt) {
+ return _delegate.getAbsentValue(ctxt);
+ }
+
+ @Override
+ public AccessPattern getNullAccessPattern() {
+ return _delegate.getNullAccessPattern();
+ }
+
+ @Override
+ public AccessPattern getEmptyAccessPattern() {
+ return _delegate.getEmptyAccessPattern();
+ }
+
+ @Override
+ public Collection getKnownPropertyNames() {
+ return _delegate.getKnownPropertyNames();
+ }
+
+ @Override
+ public SettableBeanProperty findBackReference(String refName) {
+ return _delegate.findBackReference(refName);
+ }
+
+ @Override
+ public Boolean supportsUpdate(DeserializationConfig config) {
+ return _delegate.supportsUpdate(config);
+ }
+
+ @Override
+ public Class> handledType() {
+ return _delegate.handledType();
+ }
+
+ @Override
+ public LogicalType logicalType() {
+ return _delegate.logicalType();
+ }
+
+ @Override
+ public boolean isCachable() {
+ return _delegate.isCachable();
+ }
+
+ // Unwrapping must produce the stock unwrapping variant, never the codec
+ // (rationale in GeneratedReaderBase.unwrappingDeserializer).
+ @Override
+ public ValueDeserializer unwrappingDeserializer(DeserializationContext ctxt,
+ NameTransformer unwrapper) {
+ return _delegate.unwrappingDeserializer(ctxt, unwrapper);
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/CreatorOptimizer.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/CreatorOptimizer.java
deleted file mode 100644
index 67911d35f..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/CreatorOptimizer.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.lang.invoke.LambdaMetafactory;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.reflect.AnnotatedElement;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Stream;
-
-import tools.jackson.databind.deser.ValueInstantiator;
-import tools.jackson.databind.deser.std.StdValueInstantiator;
-import tools.jackson.databind.introspect.AnnotatedWithParams;
-import tools.jackson.module.blackbird.util.Unchecked;
-
-import static java.lang.invoke.MethodHandles.lookup;
-import static java.lang.invoke.MethodType.methodType;
-
-/**
- * Helper class that tries to generate {@link ValueInstantiator} class
- * that calls constructors and/or factory methods directly, instead
- * of using Reflection.
- */
-public class CreatorOptimizer
-{
- protected final Class> _valueClass;
-
- protected final StdValueInstantiator _originalInstantiator;
-
- private final MethodHandles.Lookup _lookup;
-
- public CreatorOptimizer(Class> valueClass, MethodHandles.Lookup lookup, StdValueInstantiator orig)
- {
- _valueClass = valueClass;
- _originalInstantiator = orig;
- _lookup = lookup;
- }
-
- public ValueInstantiator createOptimized()
- {
- /* [Issue#11]: Need to avoid optimizing if we use delegate-based creators. */
- if (_originalInstantiator.canCreateUsingDelegate()) {
- return null;
- }
-
- if (_lookup == null) {
- return null;
- }
-
- AnnotatedWithParams argsCreator = _originalInstantiator.getWithArgsCreator();
- Function optimizedArgsCreator = null;
- if (argsCreator != null) {
- // The LambdaMetafactory requires a specifying interface, which is not possible to provide
- // for methods with arbitrary parameter lists. So we have to use a spread invoker instead,
- // which is not a valid target for the metafactory. Instead, we wrap it in a trampoline to
- // avoid creating megamorphic code paths and hope that code inlining covers up our reflective sins.
- MethodHandle argsCreatorHandle = directHandle(argsCreator.getAnnotated());
- if (argsCreatorHandle != null) {
- optimizedArgsCreator = Unchecked.supplier(() ->
- (Function) LambdaMetafactory.metafactory(
- _lookup,
- "apply",
- methodType(Function.class, MethodHandle.class),
- methodType(Object.class, Object.class),
- lookup().findStatic(
- CreatorOptimizer.class,
- "invokeTrampoline",
- methodType(Object.class, MethodHandle.class, Object[].class)),
- methodType(Object.class, Object[].class))
- .getTarget().invokeExact(
- argsCreatorHandle
- .asFixedArity()
- .asSpreader(
- Object[].class,
- argsCreatorHandle.type().parameterCount())
- .asType(methodType(Object.class, Object[].class))))
- .get();
- }
- }
-
- AnnotatedWithParams defaultCreator = _originalInstantiator.getDefaultCreator();
- Supplier> optimizedFactory = null;
- if (defaultCreator != null) {
- MethodHandle defaultCreatorHandle = directHandle(defaultCreator.getAnnotated());
- if (defaultCreatorHandle != null) {
- optimizedFactory = Unchecked.supplier(() ->
- (Supplier>) LambdaMetafactory.metafactory(
- _lookup,
- "get",
- methodType(Supplier.class),
- methodType(Object.class),
- defaultCreatorHandle,
- defaultCreatorHandle.type())
- .getTarget().invokeExact())
- .get();
- }
- }
- if (optimizedArgsCreator != null || optimizedFactory != null) {
- return new OptimizedValueInstantiator(_originalInstantiator, optimizedFactory, optimizedArgsCreator);
- }
- return null;
- }
-
- private MethodHandle directHandle(AnnotatedElement element) {
- return Stream.concat(
- Stream.of(element)
- .filter(Constructor.class::isInstance)
- .map(Constructor.class::cast)
- .filter(c -> !Modifier.isPrivate(c.getModifiers()))
- .flatMap(t -> {
- try {
- return Stream.of(_lookup.unreflectConstructor(t));
- } catch (IllegalAccessException e) {
- return Stream.empty();
- }
- }),
- Stream.of(element)
- .filter(Method.class::isInstance)
- .map(Method.class::cast)
- .filter(m -> {
- int mods = m.getModifiers();
- return Modifier.isStatic(mods) && !Modifier.isPrivate(mods);
- })
- .flatMap(t -> {
- try {
- return Stream.of(_lookup.unreflect(t));
- } catch (IllegalAccessException e) {
- return Stream.empty();
- }
- }))
- .findFirst().orElse(null);
- }
-
- public static Object invokeTrampoline(MethodHandle delegate, Object[] args) throws Throwable {
- return delegate.invokeExact(args);
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/ObjBooleanConsumer.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/ObjBooleanConsumer.java
deleted file mode 100644
index 43411f9e4..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/ObjBooleanConsumer.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-@FunctionalInterface
-public interface ObjBooleanConsumer {
- void accept(Object bean, boolean value);
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java
deleted file mode 100644
index 4fd8f642f..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import tools.jackson.core.*;
-
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.*;
-import tools.jackson.databind.deser.impl.NullsConstantProvider;
-import tools.jackson.databind.util.ClassUtil;
-
-/**
- * Base class for concrete type-specific {@link SettableBeanProperty}
- * implementations.
- */
-abstract class OptimizedSettableBeanProperty>
- extends SettableBeanProperty.Delegating
-{
- private static final long serialVersionUID = 1L;
-
- /**
- * @since 2.9
- */
- final protected boolean _skipNulls;
-
- /**
- * Marker that we set if mutator turns out to be broken in a systematic
- * way that we can handle by redirecting it back to standard one.
- */
- private volatile boolean broken = false;
-
- /*
- /**********************************************************************
- /* Life-cycle
- /**********************************************************************
- */
-
- protected OptimizedSettableBeanProperty(SettableBeanProperty src)
- {
- super(src);
- _skipNulls = NullsConstantProvider.isSkipper(_nullProvider);
- }
-
- // Base impl of `withName()` fine as-is:
- // public SettableBeanProperty withName(PropertyName name);
-
- // But value deserializer handling needs some more care
- @Override
- public final SettableBeanProperty withValueDeserializer(ValueDeserializer> deser) {
- SettableBeanProperty newDelegate = delegate.withValueDeserializer(deser);
- if (newDelegate == delegate) {
- return this;
- }
- if (!_isDefaultDeserializer(deser)) {
- return newDelegate;
- }
- return withDelegate(newDelegate);
- }
-
- @Override
- protected abstract SettableBeanProperty withDelegate(SettableBeanProperty d);
-
- /*
- /**********************************************************************
- /* Deserialization, regular
- /**********************************************************************
- */
-
- @Override
- public abstract void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object arg2)
- throws JacksonException;
-
- @Override
- public abstract void set(DeserializationContext ctxt, Object bean, Object value);
-
- /*
- /**********************************************************************
- /* Deserialization, builders
- /**********************************************************************
- */
-
- /* !!! 19-Feb-2012, tatu:
- *
- * We do not yet generate code for these methods: it would not be hugely
- * difficult to add them, but let's first see if anyone needs them...
- * (it is non-trivial, adds code etc, so not without cost).
- *
- * That is: we'll use Reflection fallback for Builder-based deserialization,
- * so it will not be significantly faster.
- */
-
- @Override
- public abstract Object deserializeSetAndReturn(JsonParser p,
- DeserializationContext ctxt, Object instance)
- throws JacksonException;
-
-
- @Override
- public Object setAndReturn(DeserializationContext ctxt, Object instance, Object value) {
- return delegate.setAndReturn(ctxt, instance, value);
- }
-
- /*
- /**********************************************************************
- /* Error handling
- /**********************************************************************
- */
-
- /**
- * Helper method called when an exception is throw from mutator, to figure
- * out what to do.
- */
- protected void _reportProblem(DeserializationContext ctxt, Object bean, Object value, Throwable e)
- throws JacksonException
- {
- if ((e instanceof IllegalAccessError)
- || (e instanceof SecurityException)) {
- synchronized (this) {
- // yes, double-locking, so not guaranteed; but all we want here is to reduce likelihood
- // of multiple logging of same underlying problem. Not guaranteed, just improved.
- if (!broken) {
- broken = true;
- String msg = String.format("Disabling Blackbird deserialization for %s (field %s; mutator %s), due to access error (type %s, message=%s)%n",
- bean.getClass(), _propName, getClass().getName(),
- e.getClass().getName(), e.getMessage());
- Logger.getLogger(getClass().getName()).log(Level.WARNING, msg, e);
- }
- }
- delegate.set(ctxt, bean, value);
- return;
- }
- if (e instanceof JacksonException) {
- throw (JacksonException) e;
- }
- if (e instanceof Error) {
- throw (Error) e;
- }
- if (e instanceof RuntimeException) {
- throw (RuntimeException) e;
- }
- // how could this ever happen?
- throw new RuntimeException(e);
- }
-
- /*
- /**********************************************************************
- /* Helper methods
- /**********************************************************************
- */
-
- /**
- * Helper method used to check whether given deserializer is the default
- * deserializer implementation: this is necessary to avoid overriding custom
- * deserializers.
- */
- protected boolean _isDefaultDeserializer(ValueDeserializer> deser) {
- return (deser == null)
- || (deser instanceof SuperSonicBeanDeserializer)
- || ClassUtil.isJacksonStdImpl(deser);
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/OptimizedValueInstantiator.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/OptimizedValueInstantiator.java
deleted file mode 100644
index 37c555c32..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/OptimizedValueInstantiator.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-import tools.jackson.core.JacksonException;
-import tools.jackson.databind.DeserializationContext;
-import tools.jackson.databind.deser.std.StdValueInstantiator;
-
-class OptimizedValueInstantiator extends StdValueInstantiator
-{
- private final Supplier> _optimizedDefaultCreator;
- private final Function _optimizedArgsCreator;
-
- protected OptimizedValueInstantiator(StdValueInstantiator original,
- Supplier> defaultCreator, Function argsCreator) {
- super(original);
- this._optimizedDefaultCreator = defaultCreator;
- this._optimizedArgsCreator = argsCreator;
- }
-
- @Override
- public boolean canCreateUsingDefault() {
- return _optimizedDefaultCreator != null || super.canCreateUsingDefault();
- }
-
- @Override
- public boolean canCreateFromObjectWith() {
- return _optimizedArgsCreator != null || super.canCreateFromObjectWith();
- }
-
- @Override
- public Object createUsingDefault(DeserializationContext ctxt) throws JacksonException {
- if (_optimizedDefaultCreator != null) {
- try {
- return _optimizedDefaultCreator.get();
- } catch (Exception e) {
- return ctxt.handleInstantiationProblem(_valueClass, null, e);
- }
- }
- return super.createUsingDefault(ctxt);
- }
-
- @Override
- public Object createFromObjectWith(DeserializationContext ctxt, Object[] args) throws JacksonException {
- if (_optimizedArgsCreator != null) {
- try {
- return _optimizedArgsCreator.apply(args);
- } catch (Exception e) {
- return ctxt.handleInstantiationProblem(_valueClass, args, e);
- }
- }
- return super.createFromObjectWith(ctxt, args);
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableBooleanProperty.java
deleted file mode 100644
index 0c69807f3..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableBooleanProperty.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import tools.jackson.core.*;
-
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.SettableBeanProperty;
-
-final class SettableBooleanProperty
- extends OptimizedSettableBeanProperty
-{
- private static final long serialVersionUID = 1L;
- private ObjBooleanConsumer _optimizedSetter;
-
- public SettableBooleanProperty(SettableBeanProperty src, ObjBooleanConsumer optimizedSetter)
- {
- super(src);
- _optimizedSetter = optimizedSetter;
- }
-
- @Override
- protected SettableBeanProperty withDelegate(SettableBeanProperty del) {
- return new SettableBooleanProperty(del, _optimizedSetter);
- }
-
- /*
- /**********************************************************************
- /* Deserialization
- /**********************************************************************
- */
-
- @Override
- public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean)
- throws JacksonException
- {
- boolean b;
- JsonToken t = p.currentToken();
- if (t == JsonToken.VALUE_TRUE) {
- b = true;
- } else if (t == JsonToken.VALUE_FALSE) {
- b = false;
- } else {
- delegate.deserializeAndSet(p, ctxt, bean);
- return;
- }
- try {
- _optimizedSetter.accept(bean, b);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, b, e);
- }
- }
-
- @Override
- public Object deserializeSetAndReturn(JsonParser p,
- DeserializationContext ctxt, Object instance)
- throws JacksonException
- {
- JsonToken t = p.currentToken();
- if (t == JsonToken.VALUE_TRUE) {
- return setAndReturn(ctxt, instance, Boolean.TRUE);
- }
- if (t == JsonToken.VALUE_FALSE) {
- return setAndReturn(ctxt, instance, Boolean.FALSE);
- }
- return delegate.deserializeSetAndReturn(p, ctxt, instance);
- }
-
- @Override
- public void set(DeserializationContext ctxt, Object bean, Object value)
- {
- // not optimal (due to boxing), but better than using reflection:
- final boolean b = ((Boolean) value).booleanValue();
- try {
- _optimizedSetter.accept(bean, b);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, b, e);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableIntProperty.java
deleted file mode 100644
index efbcc49f3..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableIntProperty.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.function.ObjIntConsumer;
-
-import tools.jackson.core.*;
-
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.SettableBeanProperty;
-
-final class SettableIntProperty
- extends OptimizedSettableBeanProperty
-{
- private static final long serialVersionUID = 1L;
- private ObjIntConsumer _optimizedSetter;
-
- public SettableIntProperty(SettableBeanProperty src, ObjIntConsumer optimizedSetter)
- {
- super(src);
- _optimizedSetter = optimizedSetter;
- }
-
- @Override
- protected SettableBeanProperty withDelegate(SettableBeanProperty del) {
- return new SettableIntProperty(del, _optimizedSetter);
- }
-
- /*
- /**********************************************************************
- /* Deserialization
- /**********************************************************************
- */
-
- @Override
- public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean)
- throws JacksonException
- {
- if (!p.isExpectedNumberIntToken()) {
- delegate.deserializeAndSet(p, ctxt, bean);
- return;
- }
- final int v = p.getIntValue();
- try {
- _optimizedSetter.accept(bean, v);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, v, e);
- }
- }
-
- @Override
- public Object deserializeSetAndReturn(JsonParser p,
- DeserializationContext ctxt, Object instance)
- throws JacksonException
- {
- if (p.isExpectedNumberIntToken()) {
- return setAndReturn(ctxt, instance, p.getIntValue());
- }
- return delegate.deserializeSetAndReturn(p, ctxt, instance);
- }
-
- @Override
- public void set(DeserializationContext ctxt, Object bean, Object value) {
- // not optimal (due to boxing), but better than using reflection:
- int v = ((Number) value).intValue();
- try {
- _optimizedSetter.accept(bean, v);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, v, e);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableLongProperty.java
deleted file mode 100644
index e66b4c134..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableLongProperty.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.function.ObjLongConsumer;
-
-import tools.jackson.core.*;
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.SettableBeanProperty;
-
-final class SettableLongProperty
- extends OptimizedSettableBeanProperty
-{
- private static final long serialVersionUID = 1L;
- private ObjLongConsumer _optimizedSetter;
-
- public SettableLongProperty(SettableBeanProperty src, ObjLongConsumer optimizedSetter)
- {
- super(src);
- _optimizedSetter = optimizedSetter;
- }
-
- @Override
- protected SettableBeanProperty withDelegate(SettableBeanProperty del) {
- return new SettableLongProperty(del, _optimizedSetter);
- }
-
- /*
- /**********************************************************************
- /* Deserialization
- /**********************************************************************
- */
-
- @Override
- public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean)
- throws JacksonException
- {
- if (!p.isExpectedNumberIntToken()) {
- delegate.deserializeAndSet(p, ctxt, bean);
- return;
- }
- final long v = p.getLongValue();
- try {
- _optimizedSetter.accept(bean, v);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, v, e);
- }
- }
-
- @Override
- public Object deserializeSetAndReturn(JsonParser p,
- DeserializationContext ctxt, Object instance)
- throws JacksonException
- {
- if (p.isExpectedNumberIntToken()) {
- return setAndReturn(ctxt, instance, p.getLongValue());
- }
- return delegate.deserializeSetAndReturn(p, ctxt, instance);
- }
-
- @Override
- public void set(DeserializationContext ctxt, Object bean, Object value)
- {
- // not optimal (due to boxing), but better than using reflection:
- final long v = ((Number) value).longValue();
- try {
- _optimizedSetter.accept(bean, v);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, v, e);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableObjectProperty.java
deleted file mode 100644
index cdca67f31..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableObjectProperty.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.function.BiConsumer;
-
-import tools.jackson.core.*;
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.SettableBeanProperty;
-
-final class SettableObjectProperty
- extends OptimizedSettableBeanProperty
-{
- private static final long serialVersionUID = 1L;
- private BiConsumer _optimizedSetter;
-
- public SettableObjectProperty(SettableBeanProperty src, BiConsumer optimizedSetter)
- {
- super(src);
- this._optimizedSetter = optimizedSetter;
- }
-
- @Override
- protected SettableBeanProperty withDelegate(SettableBeanProperty del) {
- return new SettableObjectProperty(del, _optimizedSetter);
- }
-
- /*
- /**********************************************************************
- /* Deserialization
- /**********************************************************************
- */
-
- @Override
- public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean)
- throws JacksonException
- {
- Object value;
- if (p.hasToken(JsonToken.VALUE_NULL)) {
- if (_skipNulls) {
- return;
- }
- value = _nullProvider.getNullValue(ctxt);
- } else if (_valueTypeDeserializer == null) {
- value = _valueDeserializer.deserialize(p, ctxt);
- // 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null
- if (value == null) {
- if (_skipNulls) {
- return;
- }
- value = _nullProvider.getNullValue(ctxt);
- }
- } else {
- value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
- }
- set(ctxt, bean, value);
- }
-
- @Override
- public Object deserializeSetAndReturn(JsonParser p,
- DeserializationContext ctxt, Object instance)
- throws JacksonException
- {
- Object value;
- if (p.hasToken(JsonToken.VALUE_NULL)) {
- if (_skipNulls) {
- return instance;
- }
- value = _nullProvider.getNullValue(ctxt);
- } else if (_valueTypeDeserializer == null) {
- value = _valueDeserializer.deserialize(p, ctxt);
- // 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null
- if (value == null) {
- if (_skipNulls) {
- return instance;
- }
- value = _nullProvider.getNullValue(ctxt);
- }
- } else {
- value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
- }
- return setAndReturn(ctxt, instance, value);
- }
-
- @Override
- public void set(DeserializationContext ctxt, Object bean, Object v)
- {
- try {
- _optimizedSetter.accept(bean, v);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, v, e);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableStringProperty.java
deleted file mode 100644
index 4af3fb878..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SettableStringProperty.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.function.BiConsumer;
-
-import tools.jackson.core.*;
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.SettableBeanProperty;
-
-final class SettableStringProperty
- extends OptimizedSettableBeanProperty
-{
- private static final long serialVersionUID = 1L;
- private final BiConsumer _optimizedSetter;
-
- public SettableStringProperty(SettableBeanProperty src, BiConsumer optimizedSetter)
- {
- super(src);
- _optimizedSetter = optimizedSetter;
- }
-
- @Override
- protected SettableBeanProperty withDelegate(SettableBeanProperty del) {
- return new SettableStringProperty(del, _optimizedSetter);
- }
-
- /*
- /**********************************************************************
- /* Deserialization
- /**********************************************************************
- */
-
- // Copied from StdDeserializer.StringDeserializer:
- @Override
- public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean)
- throws JacksonException
- {
- if (!p.hasToken(JsonToken.VALUE_STRING)) {
- delegate.deserializeAndSet(p, ctxt, bean);
- return;
- }
- set(ctxt, bean, p.getString());
- }
-
- @Override
- public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance)
- throws JacksonException
- {
- if (p.hasToken(JsonToken.VALUE_STRING)) {
- return setAndReturn(ctxt, instance, p.getString());
- }
- return delegate.deserializeSetAndReturn(p, ctxt, instance);
- }
-
- @Override
- public void set(DeserializationContext ctxt, Object bean, Object value) {
- try {
- _optimizedSetter.accept(bean, (String) value);
- } catch (Throwable e) {
- _reportProblem(ctxt, bean, value, e);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SuperSonicBeanDeserializer.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SuperSonicBeanDeserializer.java
deleted file mode 100644
index 6b7e34117..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SuperSonicBeanDeserializer.java
+++ /dev/null
@@ -1,342 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.*;
-
-import tools.jackson.core.*;
-import tools.jackson.core.io.SerializedString;
-
-import tools.jackson.databind.*;
-import tools.jackson.databind.deser.*;
-import tools.jackson.databind.deser.bean.BeanDeserializer;
-import tools.jackson.databind.deser.bean.BeanPropertyMap;
-import tools.jackson.databind.deser.bean.PropertyBasedCreator;
-import tools.jackson.databind.deser.impl.UnwrappedPropertyHandler;
-import tools.jackson.databind.util.NameTransformer;
-
-final class SuperSonicBeanDeserializer extends BeanDeserializer
-{
- /**
- * Names of properties being deserialized, in ordered they are
- * expected to have been written (as per serialization settings);
- * used for speculative order-based optimizations
- */
- protected final SerializedString[] _orderedPropertyNames;
-
- /**
- * Properties matching names in {@code _orderedPropertyNames},
- * assigned after resolution when property instances are finalized.
- */
- protected SettableBeanProperty[] _orderedProperties;
-
- /*
- /**********************************************************************
- /* Life-cycle, construction, initialization
- /**********************************************************************
- */
-
- public SuperSonicBeanDeserializer(BeanDeserializer src, List props)
- {
- super(src);
- final int len = props.size();
- _orderedPropertyNames = new SerializedString[len];
- for (int i = 0; i < len; ++i) {
- _orderedPropertyNames[i] = new SerializedString(props.get(i).getName());
- }
- }
-
- protected SuperSonicBeanDeserializer(SuperSonicBeanDeserializer src,
- UnwrappedPropertyHandler unwrapHandler, PropertyBasedCreator propertyBasedCreator,
- BeanPropertyMap renamedProperties, boolean ignoreAllUnknown)
- {
- super(src, unwrapHandler, propertyBasedCreator, renamedProperties, ignoreAllUnknown);
- _orderedPropertyNames = src._orderedPropertyNames;
- _orderedProperties = src._orderedProperties;
- }
-
- @Override
- public ValueDeserializer unwrappingDeserializer(DeserializationContext ctxt,
- NameTransformer transformer)
- {
- // 17-Dec-2020, tatu: Was like so:
-// return new SuperSonicBeanDeserializer(this, unwrapper);
- // but Afterburner 3.0 had this:
-
- // NOTE: copied verbatim from `BeanDeserializer`
-
- if (_currentlyTransforming == transformer) { // from [databind#383]
- return this;
- }
- _currentlyTransforming = transformer;
- try {
- UnwrappedPropertyHandler uwHandler = _unwrappedPropertyHandler;
- if (uwHandler != null) {
- uwHandler = uwHandler.renameAll(ctxt, transformer);
- }
- PropertyBasedCreator pbCreator = _propertyBasedCreator;
- if (pbCreator != null) {
- pbCreator = pbCreator.renameAll(ctxt, transformer);
- }
- return new SuperSonicBeanDeserializer(this, uwHandler, pbCreator,
- _beanProperties.renameAll(ctxt, transformer), true);
- } finally { _currentlyTransforming = null; }
- }
-
- // // Others, let's just leave as is; will not be optimized?
-
- //public BeanDeserializer withObjectIdReader(ObjectIdReader oir) {
-
- //public BeanDeserializer withIgnorableProperties(HashSet ignorableProps)
-
- //protected BeanDeserializerBase asArrayDeserializer()
-
- /*
- /**********************************************************************
- /* BenaDeserializer overrides
- /**********************************************************************
- */
-
- /**
- * This method is overridden as we need to know expected order of
- * properties.
- */
- @Override
- public void resolve(DeserializationContext ctxt)
- {
- super.resolve(ctxt);
- // Ok, now; need to find actual property instances to go with order
- // defined based on property names.
-
- // 20-Sep-2014, tatu: As per [afterburner#43], use of `JsonTypeInfo.As.EXTERNAL_PROPERTY`
- // will "hide" matching property, leading to no match below.
- // But since we don't use optimized path if that case, let's just bail out.
- if (_externalTypeIdHandler != null || _unwrappedPropertyHandler != null) {
- // should we assign empty array or... ?
- return;
- }
-
- int len = _orderedPropertyNames.length;
- ArrayList props = new ArrayList(len);
- int i = 0;
- for (; i < len; ++i) {
- SettableBeanProperty prop = _beanProperties.findDefinition(_orderedPropertyNames[i].toString());
- if (prop != null) {
- props.add(prop);
- }
- }
- // should usually get at least one property; let's for now consider it an error if not
- // (may need to revisit in future)
- if (i == 0) {
- throw new IllegalStateException("Blackbird internal error: BeanDeserializer for "
- +_beanType+" has no properties that match expected ordering (should have "+len+") -- can not create optimized deserializer");
- }
- _orderedProperties = props.toArray(new SettableBeanProperty[0]);
- }
-
- @Override
- public Object deserialize(JsonParser p, DeserializationContext ctxt)
- throws JacksonException
- {
- if (!_vanillaProcessing || (_objectIdReader != null)) {
- // should we ever get here? Just in case
- return super.deserialize(p, ctxt);
- }
- // common case first:
- if (!p.isExpectedStartObjectToken()) {
- return _deserializeOther(p, ctxt, p.currentToken());
- }
- if (_nonStandardCreation) {
- p.nextToken();
- if (_unwrappedPropertyHandler != null) {
- return deserializeWithUnwrapped(p, ctxt);
- }
- if (_externalTypeIdHandler != null) {
- return deserializeWithExternalTypeId(p, ctxt);
- }
- Object bean = deserializeFromObjectUsingNonDefault(p, ctxt);
- if (_injectables != null) {
- injectValues(ctxt, bean);
- }
- return bean;
- }
- final Object bean = _valueInstantiator.createUsingDefault(ctxt);
- // [databind#631]: Assign current value, to be accessible by custom serializers
- p.assignCurrentValue(bean);
- if (p.canReadObjectId()) {
- Object id = p.getObjectId();
- if (id != null) {
- _handleTypedObjectId(p, ctxt, bean, id);
- }
- }
- if (_injectables != null) {
- injectValues(ctxt, bean);
- }
- for (int i = 0, len = _orderedProperties.length; i < len; ++i) {
- SettableBeanProperty prop = _orderedProperties[i];
- if (!p.nextName(_orderedPropertyNames[i])) { // miss...
- if (p.currentToken() == JsonToken.END_OBJECT) {
- return bean;
- }
- // we likely point to PROPERTY_NAME, so can just call parent impl
- return super.deserialize(p, ctxt, bean);
- }
- p.nextToken(); // skip field, returns value token
- try {
- prop.deserializeAndSet(p, ctxt, bean);
- } catch (Exception e) {
- wrapAndThrow(e, bean, prop.getName(), ctxt);
- }
- }
- // also, need to ensure we get closing END_OBJECT...
- if (p.nextToken() != JsonToken.END_OBJECT) {
- return super.deserialize(p, ctxt, bean);
- }
- return bean;
- }
-
- // much of below is cut'n pasted from BeanSerializer
- @Override
- public final Object deserialize(JsonParser p, DeserializationContext ctxt, Object bean)
- throws JacksonException
- {
- // [modules-base#339]: Need to delegate to base implementation for
- // proper @JsonView handling during deserialization
- if (_needViewProcesing) {
- return super.deserialize(p, ctxt, bean);
- }
- // [databind#631]: Assign current value, to be accessible by custom deserializers
- p.assignCurrentValue(bean);
- if (_injectables != null) {
- injectValues(ctxt, bean);
- }
- if (_unwrappedPropertyHandler != null) {
- return deserializeWithUnwrapped(p, ctxt, bean);
- }
- if (_externalTypeIdHandler != null) {
- return deserializeWithExternalTypeId(p, ctxt, bean);
- }
- SettableBeanProperty prop = _orderedProperties[0];
- // First: verify that first name is expected
- if (p.isExpectedStartObjectToken()) {
- if (!p.nextName(_orderedPropertyNames[0])) {
- return super.deserialize(p, ctxt, bean);
- }
- } else if (!p.hasToken(JsonToken.PROPERTY_NAME)
- || !prop.getName().equals(p.currentName())) {
- // no, something funky, use base impl for special cases
- return super.deserialize(p, ctxt, bean);
- }
- p.nextToken();
- try {
- prop.deserializeAndSet(p, ctxt, bean);
- } catch (Exception e) {
- wrapAndThrow(e, bean, prop.getName(), ctxt);
- }
- // then rest of properties
- for (int i = 1, len = _orderedProperties.length; i < len; ++i) {
- if (!p.nextName(_orderedPropertyNames[i])) { // miss...
- if (p.hasToken(JsonToken.END_OBJECT)) {
- return bean;
- }
- // we likely point to PROPERTY_NAME, so can just call parent impl
- return super.deserialize(p, ctxt, bean);
- }
- prop = _orderedProperties[i];
- p.nextToken(); // skip field, returns value token
- try {
- prop.deserializeAndSet(p, ctxt, bean);
- } catch (Exception e) {
- wrapAndThrow(e, bean, prop.getName(), ctxt);
- }
- }
- // also, need to ensure we get closing END_OBJECT...
- if (p.nextToken() != JsonToken.END_OBJECT) {
- return super.deserialize(p, ctxt, bean);
- }
- return bean;
- }
-
- // much of below is cut'n pasted from BeanSerializer
- @Override
- public final Object deserializeFromObject(JsonParser p, DeserializationContext ctxt)
- throws JacksonException
- {
- // [modules-base#339]: Need to delegate to base implementation for
- // proper @JsonView handling during deserialization
- if (_needViewProcesing) {
- return super.deserializeFromObject(p, ctxt);
- }
- // See BeanDeserializer.deserializeFromObject [databind#622]
- // Allow Object Id references to come in as JSON Objects as well...
- if ((_objectIdReader != null) && _objectIdReader.maySerializeAsObject()) {
- if (p.hasTokenId(JsonTokenId.ID_PROPERTY_NAME)
- && _objectIdReader.isValidReferencePropertyName(p.currentName(), p)) {
- return deserializeFromObjectId(p, ctxt);
- }
- }
- if (_nonStandardCreation) {
- if (_unwrappedPropertyHandler != null) {
- return deserializeWithUnwrapped(p, ctxt);
- }
- if (_externalTypeIdHandler != null) {
- return deserializeWithExternalTypeId(p, ctxt);
- }
- Object bean = deserializeFromObjectUsingNonDefault(p, ctxt);
- if (_injectables != null) {
- injectValues(ctxt, bean);
- }
- return bean;
- }
- final Object bean = _valueInstantiator.createUsingDefault(ctxt);
- // [databind#631]: Assign current value, to be accessible by custom serializers
- p.assignCurrentValue(bean);
- if (p.canReadObjectId()) {
- Object id = p.getObjectId();
- if (id != null) {
- _handleTypedObjectId(p, ctxt, bean, id);
- }
- }
- if (_injectables != null) {
- injectValues(ctxt, bean);
- }
- SettableBeanProperty prop = _orderedProperties[0];
- // First: verify that first name is expected
- if (p.isExpectedStartObjectToken()) {
- if (!p.nextName(_orderedPropertyNames[0])) {
- return super.deserialize(p, ctxt, bean);
- }
- } else if (!p.hasToken(JsonToken.PROPERTY_NAME)
- || !prop.getName().equals(p.currentName())) {
- return super.deserialize(p, ctxt, bean);
- }
- // and deserialize
- p.nextToken();
- try {
- prop.deserializeAndSet(p, ctxt, bean);
- } catch (Exception e) {
- wrapAndThrow(e, bean, prop.getName(), ctxt);
- }
-
- // then rest of properties
- for (int i = 1, len = _orderedProperties.length; i < len; ++i) {
- prop = _orderedProperties[i];
- if (!p.nextName(_orderedPropertyNames[i])) { // miss...
- if (p.hasToken(JsonToken.END_OBJECT)) {
- return bean;
- }
- // we likely point to PROPERTY_NAME, so can just call parent impl
- return super.deserialize(p, ctxt, bean);
- }
- p.nextToken(); // skip field, returns value token
- try {
- prop.deserializeAndSet(p, ctxt, bean);
- } catch (Exception e) {
- wrapAndThrow(e, bean, prop.getName(), ctxt);
- }
- }
- // also, need to ensure we get closing END_OBJECT...
- if (p.nextToken() != JsonToken.END_OBJECT) {
- return super.deserialize(p, ctxt, bean);
- }
- return bean;
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SuperSonicDeserializerBuilder.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SuperSonicDeserializerBuilder.java
deleted file mode 100644
index e56cf341c..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/SuperSonicDeserializerBuilder.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package tools.jackson.module.blackbird.deser;
-
-import java.util.*;
-
-import tools.jackson.databind.ValueDeserializer;
-import tools.jackson.databind.deser.*;
-import tools.jackson.databind.deser.bean.BeanDeserializer;
-
-class SuperSonicDeserializerBuilder extends BeanDeserializerBuilder
-{
- public SuperSonicDeserializerBuilder(BeanDeserializerBuilder base) {
- super(base);
- }
-
- @Override
- public ValueDeserializer> build()
- {
- BeanDeserializer deser = (BeanDeserializer) super.build();
- // only create custom one, if existing one is standard deserializer;
- if (deser.getClass() == BeanDeserializer.class) {
- BeanDeserializer beanDeser = (BeanDeserializer) deser;
- Iterator it = getProperties();
- // also: only build custom one for non-empty beans:
- if (it.hasNext()) {
- // So let's find actual order of properties, necessary for optimal access
- ArrayList props = new ArrayList();
- do {
- props.add(it.next());
- } while (it.hasNext());
- return new SuperSonicBeanDeserializer(beanDeser, props);
- }
- }
- return deser;
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedReaderBase.java b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedReaderBase.java
new file mode 100644
index 000000000..050d4e751
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedReaderBase.java
@@ -0,0 +1,318 @@
+package tools.jackson.module.blackbird.internal;
+
+import java.lang.constant.ConstantDescs;
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.DeserializationConfig;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.SettableBeanProperty;
+import tools.jackson.databind.exc.IgnoredPropertyException;
+import tools.jackson.databind.util.IgnorePropertiesUtil;
+import tools.jackson.databind.deser.bean.BeanDeserializerBase;
+import tools.jackson.databind.jsontype.TypeDeserializer;
+import tools.jackson.databind.type.LogicalType;
+import tools.jackson.databind.util.AccessPattern;
+import tools.jackson.databind.util.ClassUtil;
+import tools.jackson.databind.util.NameTransformer;
+
+/**
+ * Superclass for generated codecs: everything except the plain-object
+ * deserialize loop forwards to the stock deserializer the codec replaced, so
+ * typed (polymorphic) entry, null handling, and update-value reads keep stock
+ * semantics.
+ */
+public abstract class GeneratedReaderBase extends ValueDeserializer
+{
+ protected final BeanDeserializerBase _fallback;
+
+ // Ignoral configuration mirrored from the stock deserializer's build:
+ // consulted by _handleUnknown exactly the way the stock loop consults
+ // _ignoreAllUnknown / _ignorableProps / _includableProps.
+ private final boolean _ignoreAllUnknown;
+ private final Set _ignorableProps;
+ private final Set _includableProps;
+
+ protected GeneratedReaderBase(BeanDeserializerBase fallback) {
+ this(fallback, false, null, null);
+ }
+
+ protected GeneratedReaderBase(BeanDeserializerBase fallback, boolean ignoreAllUnknown,
+ Set ignorableProps, Set includableProps) {
+ _fallback = fallback;
+ _ignoreAllUnknown = ignoreAllUnknown;
+ _ignorableProps = ignorableProps;
+ _includableProps = includableProps;
+ }
+
+ @Override
+ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
+ TypeDeserializer typeDeserializer) {
+ return _fallback.deserializeWithType(p, ctxt, typeDeserializer);
+ }
+
+ @Override
+ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) {
+ return _fallback.deserialize(p, ctxt, intoValue);
+ }
+
+ @Override
+ public Object getNullValue(DeserializationContext ctxt) {
+ return _fallback.getNullValue(ctxt);
+ }
+
+ @Override
+ public Object getEmptyValue(DeserializationContext ctxt) {
+ return _fallback.getEmptyValue(ctxt);
+ }
+
+ @Override
+ public Object getAbsentValue(DeserializationContext ctxt) {
+ return _fallback.getAbsentValue(ctxt);
+ }
+
+ @Override
+ public AccessPattern getNullAccessPattern() {
+ return _fallback.getNullAccessPattern();
+ }
+
+ @Override
+ public AccessPattern getEmptyAccessPattern() {
+ return _fallback.getEmptyAccessPattern();
+ }
+
+ @Override
+ public Collection getKnownPropertyNames() {
+ return _fallback.getKnownPropertyNames();
+ }
+
+ @Override
+ public SettableBeanProperty findBackReference(String refName) {
+ return _fallback.findBackReference(refName);
+ }
+
+ @Override
+ public Boolean supportsUpdate(DeserializationConfig config) {
+ return _fallback.supportsUpdate(config);
+ }
+
+ @Override
+ public Class> handledType() {
+ return _fallback.handledType();
+ }
+
+ @Override
+ public LogicalType logicalType() {
+ return _fallback.logicalType();
+ }
+
+ @Override
+ public boolean isCachable() {
+ return _fallback.isCachable();
+ }
+
+ // Unwrapping must produce the stock unwrapping variant: the generated loop
+ // reads one JSON object, which is not the shape an unwrapped value has.
+ @Override
+ public ValueDeserializer unwrappingDeserializer(DeserializationContext ctxt,
+ NameTransformer unwrapper) {
+ return _fallback.unwrappingDeserializer(ctxt, unwrapper);
+ }
+
+ // Called by generated code when a property name was expected but the
+ // stream holds something else; reports through the context the way the
+ // stock deserializer does.
+ protected final Object _unexpectedToken(JsonParser p, DeserializationContext ctxt) {
+ return ctxt.handleUnexpectedToken(_fallback.handledType(), p);
+ }
+
+ // Called from the generated exception handler that covers every property
+ // arm: mirrors the stock loop's wrapAndThrow so property errors carry the
+ // reference path. Always throws; the return type only satisfies the
+ // generated athrow.
+ protected final RuntimeException _propertyException(Throwable t, Object beanOrNull,
+ SettableBeanProperty prop, DeserializationContext ctxt) {
+ Object ref = (beanOrNull == null) ? _fallback.handledType() : beanOrNull;
+ throw _fallback.wrapAndThrow(t, ref, prop.getName(), ctxt);
+ }
+
+ // Called by generated code for a name the matcher does not know: the
+ // stock loop's exact order - ignore-all skips silently, an explicitly
+ // ignored (or not-included) name goes through ignored-property handling,
+ // and everything else runs the problem handlers and honors
+ // FAIL_ON_UNKNOWN_PROPERTIES through the context. beanOrBuilder is null
+ // in record mode, where no instance exists yet.
+ protected final void _handleUnknown(JsonParser p, DeserializationContext ctxt,
+ Object beanOrBuilder) {
+ String name = p.currentName();
+ p.nextToken();
+ if (_ignoreAllUnknown) {
+ p.skipChildren();
+ return;
+ }
+ Object ref = (beanOrBuilder == null) ? _fallback.handledType() : beanOrBuilder;
+ if (IgnorePropertiesUtil.shouldIgnore(name, _ignorableProps, _includableProps)) {
+ if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)) {
+ throw IgnoredPropertyException.from(p, ref, name,
+ _fallback.getKnownPropertyNames());
+ }
+ p.skipChildren();
+ return;
+ }
+ ctxt.handleUnknownProperty(p, _fallback, ref, name);
+ }
+
+ // Called by generated record codecs when the document ended with unseen
+ // components; mirrors PropertyValueBuffer's required and
+ // FAIL_ON_MISSING_CREATOR_PROPERTIES reporting.
+ protected final void _checkRecordSeen(DeserializationContext ctxt, long seen, int count) {
+ for (int i = 0; i < count; i++) {
+ if ((seen & (1L << i)) != 0) {
+ continue;
+ }
+ SettableBeanProperty prop = _creatorPropByIndex(i);
+ if (prop == null) {
+ continue;
+ }
+ if (prop.isRequired()) {
+ ctxt.reportInputMismatch(prop,
+ "Missing required creator property '%s' (index %d)",
+ prop.getName(), i);
+ }
+ if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES)) {
+ ctxt.reportInputMismatch(prop,
+ "Missing creator property '%s' (index %d); `DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES` enabled",
+ prop.getName(), i);
+ }
+ }
+ }
+
+ // Called by generated record codecs when any reference-typed component is
+ // null at construction (missing or explicit null): mirrors
+ // PropertyValueBuffer's FAIL_ON_NULL_CREATOR_PROPERTIES reporting.
+ protected final void _checkRecordNulls(DeserializationContext ctxt, long nullMask, int count) {
+ if (!ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) {
+ return;
+ }
+ for (int i = 0; i < count; i++) {
+ if ((nullMask & (1L << i)) == 0) {
+ continue;
+ }
+ SettableBeanProperty prop = _creatorPropByIndex(i);
+ if (prop != null) {
+ ctxt.reportInputMismatch(prop,
+ "Null value for creator property '%s' (index %d); `DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES` enabled",
+ prop.getName(), i);
+ }
+ }
+ }
+
+ private SettableBeanProperty _creatorPropByIndex(int index) {
+ for (Iterator it = _fallback.properties(); it.hasNext(); ) {
+ SettableBeanProperty prop = it.next();
+ if (prop.getCreatorIndex() == index) {
+ return prop;
+ }
+ }
+ return null;
+ }
+
+ /*
+ /**********************************************************************
+ /* Support for generated code
+ /**********************************************************************
+ */
+
+ // Constant bootstrap for generated code: resolves one named entry of the
+ // hidden class's class data, a Map built at generation time. The JDK's
+ // classDataAt bootstrap requires the condy name to be "_", so named
+ // entries need this owner, which generated classes can always resolve
+ // (the package is exported exactly for their supertype needs). A condy
+ // links once and then constant-folds the same as classDataAt; the names
+ // exist for dump readability and to remove positional-index bookkeeping
+ // from the generators.
+ public static Object classDataEntry(MethodHandles.Lookup lookup, String name, Class> type)
+ throws IllegalAccessException {
+ Map, ?> data = MethodHandles.classData(lookup, ConstantDescs.DEFAULT_NAME, Map.class);
+ Object value = data.get(name);
+ if (value == null) {
+ throw new IllegalStateException("no class data entry named " + name);
+ }
+ return type.cast(value);
+ }
+
+ /*
+ /**********************************************************************
+ /* View support: per-view visibility masks
+ /**********************************************************************
+ */
+
+ private static final Class>[] NO_VIEWS = new Class>[0];
+ private static final long[] NO_MASKS = new long[0];
+
+ // Copy-on-write cache of view -> arm-visibility bitmask. View sets are
+ // small and stable per application, so lookups are a reference scan.
+ // Instance state only: the cached view classes unload with the codec and
+ // its mapper.
+ private volatile Class>[] _maskViews = NO_VIEWS;
+ private volatile long[] _masks = NO_MASKS;
+
+ // Called by generated code once per view-active call.
+ protected final long _viewMask(Class> view) {
+ Class>[] views = _maskViews;
+ for (int i = 0; i < views.length; i++) {
+ if (views[i] == view) {
+ return _masks[i];
+ }
+ }
+ return _addViewMask(view);
+ }
+
+ private synchronized long _addViewMask(Class> view) {
+ Class>[] views = _maskViews;
+ for (int i = 0; i < views.length; i++) {
+ if (views[i] == view) {
+ return _masks[i];
+ }
+ }
+ long mask = _computeViewMask(view);
+ Class>[] newViews = new Class>[views.length + 1];
+ long[] newMasks = new long[views.length + 1];
+ System.arraycopy(views, 0, newViews, 0, views.length);
+ System.arraycopy(_masks, 0, newMasks, 0, views.length);
+ newViews[views.length] = view;
+ newMasks[views.length] = mask;
+ _masks = newMasks;
+ _maskViews = newViews;
+ return mask;
+ }
+
+ // Overridden by generated codecs that filter per view: bit i reports
+ // whether property arm i is visible in the given view. Codecs that
+ // delegate view-active calls whole (more than 64 properties) never call
+ // the mask machinery.
+ protected long _computeViewMask(Class> view) {
+ throw new UnsupportedOperationException("codec has no view mask");
+ }
+
+ // Called by generated code for an arm hidden in the active view, with the
+ // parser advanced to the value token; mirrors the stock loop's
+ // handleUnexpectedView (message included), then consumes the value.
+ protected final void _hiddenView(JsonParser p, DeserializationContext ctxt,
+ SettableBeanProperty prop) {
+ if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_UNEXPECTED_VIEW_PROPERTIES)) {
+ ctxt.reportInputMismatch(_fallback.handledType(),
+ "Input mismatch while deserializing %s. Property '%s' is not part of current active view '%s'"
+ + " (disable 'DeserializationFeature.FAIL_ON_UNEXPECTED_VIEW_PROPERTIES' to allow)",
+ ClassUtil.nameOf(_fallback.handledType()), prop.getName(),
+ ctxt.getActiveView().getName());
+ }
+ p.skipChildren();
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedWriterBase.java b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedWriterBase.java
new file mode 100644
index 000000000..ad2d47e30
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedWriterBase.java
@@ -0,0 +1,155 @@
+package tools.jackson.module.blackbird.internal;
+
+import java.lang.constant.ConstantDescs;
+import java.lang.invoke.MethodHandles;
+import java.util.Map;
+
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.jsontype.TypeSerializer;
+import tools.jackson.databind.ser.BeanPropertyWriter;
+import tools.jackson.databind.ser.PropertyWriter;
+import tools.jackson.databind.ser.bean.BeanSerializerBase;
+import tools.jackson.databind.util.NameTransformer;
+
+/**
+ * Superclass for generated serializers: everything except the plain-object
+ * serialize path forwards to the stock serializer the codec replaced.
+ */
+public abstract class GeneratedWriterBase extends ValueSerializer
+{
+ protected final BeanSerializerBase _fallback;
+
+ protected GeneratedWriterBase(BeanSerializerBase fallback) {
+ _fallback = fallback;
+ }
+
+ @Override
+ public void serializeWithType(Object value, JsonGenerator g, SerializationContext ctxt,
+ TypeSerializer typeSer) {
+ _fallback.serializeWithType(value, g, ctxt, typeSer);
+ }
+
+ @Override
+ public boolean isEmpty(SerializationContext ctxt, Object value) {
+ return _fallback.isEmpty(ctxt, value);
+ }
+
+ @Override
+ public boolean usesObjectId() {
+ return _fallback.usesObjectId();
+ }
+
+ @Override
+ public Class> handledType() {
+ return _fallback.handledType();
+ }
+
+ // Unwrapping must produce the stock unwrapping variant: the generated
+ // writer emits one JSON object, which is not the shape an unwrapped value
+ // has.
+ @Override
+ public ValueSerializer unwrappingSerializer(NameTransformer unwrapper) {
+ return _fallback.unwrappingSerializer(unwrapper);
+ }
+
+ @Override
+ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType type) {
+ _fallback.acceptJsonFormatVisitor(visitor, type);
+ }
+
+ /*
+ /**********************************************************************
+ /* Support for generated code
+ /**********************************************************************
+ */
+
+ // Constant bootstrap for generated code: resolves one named entry of the
+ // hidden class's class data, a Map built at generation time. The JDK's
+ // classDataAt bootstrap requires the condy name to be "_", so named
+ // entries need this owner, which generated classes can always resolve
+ // (the package is exported exactly for their supertype needs). A condy
+ // links once and then constant-folds the same as classDataAt; the names
+ // exist for dump readability and to remove positional-index bookkeeping
+ // from the generators.
+ public static Object classDataEntry(MethodHandles.Lookup lookup, String name, Class> type)
+ throws IllegalAccessException {
+ Map, ?> data = MethodHandles.classData(lookup, ConstantDescs.DEFAULT_NAME, Map.class);
+ Object value = data.get(name);
+ if (value == null) {
+ throw new IllegalStateException("no class data entry named " + name);
+ }
+ return type.cast(value);
+ }
+
+ /*
+ /**********************************************************************
+ /* View support: per-view visibility masks
+ /**********************************************************************
+ */
+
+ private static final Class>[] NO_VIEWS = new Class>[0];
+ private static final long[] NO_MASKS = new long[0];
+
+ // Copy-on-write cache of view -> property-visibility bitmask; same shape
+ // and rationale as GeneratedReaderBase. Instance state only, so cached view
+ // classes unload with the writer and its mapper.
+ private volatile Class>[] _maskViews = NO_VIEWS;
+ private volatile long[] _masks = NO_MASKS;
+
+ protected final long _viewMask(Class> view) {
+ Class>[] views = _maskViews;
+ for (int i = 0; i < views.length; i++) {
+ if (views[i] == view) {
+ return _masks[i];
+ }
+ }
+ return _addViewMask(view);
+ }
+
+ private synchronized long _addViewMask(Class> view) {
+ Class>[] views = _maskViews;
+ for (int i = 0; i < views.length; i++) {
+ if (views[i] == view) {
+ return _masks[i];
+ }
+ }
+ long mask = _computeViewMask(view);
+ Class>[] newViews = new Class>[views.length + 1];
+ long[] newMasks = new long[views.length + 1];
+ System.arraycopy(views, 0, newViews, 0, views.length);
+ System.arraycopy(_masks, 0, newMasks, 0, views.length);
+ newViews[views.length] = view;
+ newMasks[views.length] = mask;
+ _masks = newMasks;
+ _maskViews = newViews;
+ return mask;
+ }
+
+ // Overridden by generated writers that filter per view: bit i reports
+ // whether property i is visible in the given view.
+ protected long _computeViewMask(Class> view) {
+ throw new UnsupportedOperationException("writer has no view mask");
+ }
+
+ // Visibility of one property in a view, matching the rule the stock
+ // factory uses to build the filtered writer array: no view annotations
+ // means DEFAULT_VIEW_INCLUSION decides (captured at generation time),
+ // otherwise any declared view assignable from the active view.
+ protected static boolean _propVisible(PropertyWriter w, Class> view,
+ boolean includeByDefault) {
+ Class>[] views = (w instanceof BeanPropertyWriter bpw) ? bpw.getViews() : null;
+ if (views == null || views.length == 0) {
+ return includeByDefault;
+ }
+ for (Class> v : views) {
+ if (v.isAssignableFrom(view)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/internal/package-info.java b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/package-info.java
new file mode 100644
index 000000000..11a8124ec
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Internal supertypes of Blackbird's generated hidden classes. This package
+ * is exported only because a hidden codec defined in a user module's context
+ * must resolve its supertype from there; it is internal API with no
+ * compatibility guarantees, and user code must not extend or call these
+ * types.
+ */
+package tools.jackson.module.blackbird.internal;
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBSerializerModifier.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBSerializerModifier.java
index 4644a0aee..1d19bd837 100644
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBSerializerModifier.java
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBSerializerModifier.java
@@ -1,168 +1,65 @@
package tools.jackson.module.blackbird.ser;
-import java.lang.invoke.LambdaMetafactory;
-import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodHandles.Lookup;
-import java.lang.reflect.Member;
-import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
-import java.util.*;
import java.util.function.Function;
-import java.util.function.ToIntFunction;
-import java.util.function.ToLongFunction;
-import java.util.function.UnaryOperator;
-
-import tools.jackson.databind.*;
-import tools.jackson.databind.annotation.JacksonStdImpl;
-import tools.jackson.databind.introspect.AnnotatedMember;
-import tools.jackson.databind.introspect.AnnotatedMethod;
-import tools.jackson.databind.ser.*;
-import tools.jackson.module.blackbird.util.ReflectionHack;
-import tools.jackson.module.blackbird.util.Unchecked;
-
-import static java.lang.invoke.MethodType.*;
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.SerializationConfig;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.ser.BeanSerializer;
+import tools.jackson.databind.ser.UnrolledBeanSerializer;
+import tools.jackson.databind.ser.ValueSerializerModifier;
+import tools.jackson.databind.ser.bean.BeanSerializerBase;
+import tools.jackson.module.blackbird.codegen.CodegenFallbacks;
+
+/**
+ * Wraps eligible stock bean serializers in a placeholder that generates a
+ * per-bean writer at resolve time. Every gate that fails leaves the stock
+ * serializer in place.
+ */
public class BBSerializerModifier extends ValueSerializerModifier
{
private static final long serialVersionUID = 1L;
- private final Function, Lookup> _lookups;
- private final UnaryOperator _accessGrant;
+ // Reserved for member access beyond public API; the v1 writer generator
+ // only touches public getters.
+ private final Function, MethodHandles.Lookup> _lookups;
- public BBSerializerModifier(Function, MethodHandles.Lookup> lookups, UnaryOperator accessGrant)
- {
+ public BBSerializerModifier(Function, MethodHandles.Lookup> lookups) {
_lookups = lookups;
- _accessGrant = accessGrant;
}
@Override
- public List changeProperties(SerializationConfig config,
- BeanDescription.Supplier beanDescRef, List beanProperties)
+ public ValueSerializer> modifySerializer(SerializationConfig config,
+ BeanDescription.Supplier beanDescRef, ValueSerializer> serializer)
{
- final Class> beanClass = beanDescRef.getBeanClass();
-
- /* Hmmh. Can we access stuff from private classes?
- * Possibly, if we can use parent class loader.
- * (should probably skip all non-public?)
- */
- if (Modifier.isPrivate(beanClass.getModifiers())) { // TODO?
- return beanProperties;
+ // Gate failures of any kind leave the stock serializer in place. The
+ // reflective gates can throw for exotic classes (a bean from a foreign
+ // classloader with inconsistent InnerClasses metadata raises
+ // IncompatibleClassChangeError from getEnclosingClass), and an
+ // acceleration modifier must never break a bean stock databind handles.
+ try {
+ return doModify(beanDescRef, serializer);
+ } catch (RuntimeException | LinkageError e) {
+ CodegenFallbacks.gateFailure(beanDescRef.getBeanClass(), e);
+ return serializer;
}
-
- findProperties(beanClass, config, beanProperties);
- return beanProperties;
}
- protected void findProperties(Class> beanClass,
- SerializationConfig config, List beanProperties)
+ private ValueSerializer> doModify(BeanDescription.Supplier beanDescRef,
+ ValueSerializer> serializer)
{
- MethodHandles.Lookup lookup = _lookups.apply(beanClass);
- if (lookup == null) {
- return;
- }
-
- ListIterator it = beanProperties.listIterator();
- while (it.hasNext()) {
- Unchecked.runnable(() ->
- createProperty(it, ReflectionHack.privateLookupIn(beanClass, lookup), config))
- .run();
- }
- }
-
- protected void createProperty(ListIterator it, Lookup lookup, SerializationConfig config) throws Throwable {
- BeanPropertyWriter bpw = it.next();
- AnnotatedMember member = bpw.getMember();
-
- Member jdkMember = member.getMember();
- // 11-Sep-2015, tatu: Let's skip virtual members (related to #57)
- if (jdkMember == null) {
- return;
- }
- // We can't access private fields or methods, skip:
- if (Modifier.isPrivate(jdkMember.getModifiers())) {
- return;
- }
- // (although, interestingly enough, can seem to access private classes...)
-
- // 30-Jul-2012, tatu: [#6]: Needs to skip custom serializers, if any.
- if (bpw.hasSerializer() && !SerializerUtil.isDefaultSerializer(bpw.getSerializer())) {
- return;
- }
- // [#9]: also skip unwrapping stuff...
- if (bpw.isUnwrapping()) {
- return;
+ if (serializer.getClass() != BeanSerializer.class
+ && serializer.getClass() != UnrolledBeanSerializer.class) {
+ return serializer;
}
-
- if (!bpw.getClass().isAnnotationPresent(JacksonStdImpl.class)) {
- return;
- }
-
- Class> type = bpw.getMember().getRawType();
- MethodHandle getter;
- if (member instanceof AnnotatedMethod) {
- getter = lookup.unreflect((Method) member.getMember());
- } else {
- // TODO: currently VarHandles aren't considered direct MH
- return;
- //getter = lookup.unreflectGetter((Field) member.getMember());
- }
- lookup = _accessGrant.apply(lookup);
-
- if (type.isPrimitive()) {
- if (type == Integer.TYPE) {
- ToIntFunction accessor = (ToIntFunction) LambdaMetafactory.metafactory(
- lookup,
- "applyAsInt",
- methodType(ToIntFunction.class),
- methodType(int.class, Object.class),
- getter,
- getter.type())
- .getTarget().invokeExact();
- it.set(new IntPropertyWriter(bpw, accessor, null));
- } else if (type == Long.TYPE) {
- ToLongFunction accessor = (ToLongFunction) LambdaMetafactory.metafactory(
- lookup,
- "applyAsLong",
- methodType(ToLongFunction.class),
- methodType(long.class, Object.class),
- getter,
- getter.type())
- .getTarget().invokeExact();
- it.set(new LongPropertyWriter(bpw, accessor, null));
- } else if (type == Boolean.TYPE) {
- ToBooleanFunction accessor = (ToBooleanFunction) LambdaMetafactory.metafactory(
- lookup,
- "applyAsBoolean",
- methodType(ToBooleanFunction.class),
- methodType(boolean.class, Object.class),
- getter,
- getter.type())
- .getTarget().invokeExact();
- it.set(new BooleanPropertyWriter(bpw, accessor, null));
- }
- } else {
- if (type == String.class) {
- Function accessor = (Function) LambdaMetafactory.metafactory(
- lookup,
- "apply",
- methodType(Function.class),
- methodType(Object.class, Object.class),
- getter,
- getter.type())
- .getTarget().invokeExact();
- it.set(new StringPropertyWriter(bpw, accessor, null));
- } else {
- Function accessor = (Function) LambdaMetafactory.metafactory(
- lookup,
- "apply",
- methodType(Function.class),
- methodType(Object.class, Object.class),
- getter,
- getter.type())
- .getTarget().invokeExact();
- it.set(new ObjectPropertyWriter(bpw, accessor, null));
- }
+ Class> beanClass = beanDescRef.getBeanClass();
+ if (Modifier.isPrivate(beanClass.getModifiers())
+ || (beanClass.getEnclosingClass() != null
+ && !Modifier.isStatic(beanClass.getModifiers()))) {
+ return serializer;
}
+ return new BBWriterPlaceholder((BeanSerializerBase) serializer, _lookups);
}
}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBWriterFactory.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBWriterFactory.java
new file mode 100644
index 000000000..6d704ecf3
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBWriterFactory.java
@@ -0,0 +1,215 @@
+package tools.jackson.module.blackbird.ser;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.introspect.AnnotatedField;
+import tools.jackson.databind.introspect.AnnotatedMethod;
+import tools.jackson.databind.ser.BeanPropertyWriter;
+import tools.jackson.databind.ser.PropertyWriter;
+import tools.jackson.databind.ser.bean.BeanSerializerBase;
+import tools.jackson.databind.MapperFeature;
+import tools.jackson.module.blackbird.codegen.BeanReaderGenerator.ViewStrategy;
+import tools.jackson.module.blackbird.codegen.BeanWriterGenerator;
+import tools.jackson.module.blackbird.codegen.CodecAccess;
+import tools.jackson.module.blackbird.codegen.BeanWriterGenerator.GenWProp;
+import tools.jackson.module.blackbird.codegen.BeanWriterGenerator.WKind;
+import tools.jackson.module.blackbird.internal.GeneratedWriterBase;
+import tools.jackson.module.blackbird.codegen.CodegenFallbacks;
+
+/**
+ * Builds a generated serializer from a resolved stock bean serializer, or
+ * returns null when the bean does not qualify. Same philosophy as the
+ * deserializer side: anything not cheaply verifiable rides the stock
+ * PropertyWriter from generated code.
+ */
+final class BBWriterFactory
+{
+ private static final Set STOCK_SCALAR_SERS = Set.of(
+ "tools.jackson.databind.ser.jdk.StringSerializer",
+ "tools.jackson.databind.ser.jdk.NumberSerializers$IntegerSerializer",
+ "tools.jackson.databind.ser.jdk.NumberSerializers$LongSerializer",
+ "tools.jackson.databind.ser.jdk.BooleanSerializer");
+
+ private BBWriterFactory() {}
+
+ static ValueSerializer tryGenerate(BeanSerializerBase delegate,
+ SerializationContext ctxt, Function, MethodHandles.Lookup> lookups) {
+ try {
+ return generate(delegate, ctxt, lookups);
+ } catch (Throwable t) {
+ CodegenFallbacks.generationFailure(delegate.handledType(), t);
+ return null;
+ }
+ }
+
+ private static ValueSerializer generate(BeanSerializerBase delegate,
+ SerializationContext ctxt, Function, MethodHandles.Lookup> lookups)
+ throws ReflectiveOperationException {
+ if (delegate.usesObjectId() || delegate.getFilterId() != null) {
+ return null;
+ }
+ Class> beanClass = delegate.handledType();
+ if (Modifier.isPrivate(beanClass.getModifiers())) {
+ return null;
+ }
+ if (!visibleToGenerator(beanClass)) {
+ return null;
+ }
+ MethodHandles.Lookup defineLookup = CodecAccess.defineContext(beanClass, lookups);
+ if (!Modifier.isPublic(beanClass.getModifiers()) && defineLookup == null) {
+ return null;
+ }
+ Class> anchor = defineLookup == null ? null : defineLookup.lookupClass();
+ List props = new ArrayList<>();
+ for (Iterator it = delegate.properties(); it.hasNext(); ) {
+ PropertyWriter writer = it.next();
+ props.add(classify(writer, beanClass, anchor));
+ }
+ if (props.isEmpty()) {
+ return null;
+ }
+ boolean includeByDefault = ctxt.isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION);
+ return BeanWriterGenerator.generate(beanClass, props, delegate, defineLookup,
+ viewStrategy(props, includeByDefault), includeByDefault);
+ }
+
+ // Matches the stock filtered-writer-array rule. No property declares a
+ // view: with default inclusion on, stock ignores views entirely (NONE, no
+ // view code); with it off, an active view hides every property, so the
+ // rare view-active call DELEGATEs to stock's write-nothing path and the
+ // no-view hot path stays free of per-property tests. Properties with
+ // views take MASK (fast path under views) up to 64 properties, DELEGATE
+ // beyond. Visibility is read per property from BeanPropertyWriter.getViews.
+ private static ViewStrategy viewStrategy(List props, boolean includeByDefault) {
+ boolean viewsFound = false;
+ for (GenWProp p : props) {
+ if (p.stock() instanceof BeanPropertyWriter bpw
+ && bpw.getViews() != null && bpw.getViews().length > 0) {
+ viewsFound = true;
+ break;
+ }
+ }
+ if (!viewsFound) {
+ return includeByDefault ? ViewStrategy.NONE : ViewStrategy.DELEGATE;
+ }
+ return (props.size() > 64) ? ViewStrategy.DELEGATE : ViewStrategy.MASK;
+ }
+
+ private static GenWProp classify(PropertyWriter writer, Class> beanClass,
+ Class> anchor) {
+ if (writer.getClass() != BeanPropertyWriter.class) {
+ return stock(writer);
+ }
+ BeanPropertyWriter bpw = (BeanPropertyWriter) writer;
+ if (bpw.willSuppressNulls() || SuppressionProbe.hasSuppressableValue(bpw)) {
+ return stock(writer);
+ }
+ ValueSerializer valueSer = bpw.getSerializer();
+ if (bpw.getMember() instanceof AnnotatedMethod am) {
+ return classifyGetter(writer, beanClass, bpw, am.getAnnotated(), valueSer, anchor);
+ }
+ if (bpw.getMember() instanceof AnnotatedField af) {
+ return classifyField(writer, bpw, af.getAnnotated(), valueSer, anchor);
+ }
+ return stock(writer);
+ }
+
+ private static GenWProp classifyGetter(PropertyWriter writer, Class> beanClass,
+ BeanPropertyWriter bpw, Method getter, ValueSerializer valueSer,
+ Class> anchor) {
+ if (getter == null || getter.getParameterCount() != 0
+ || !CodecAccess.directlyAccessible(getter.getModifiers(),
+ getter.getDeclaringClass(), anchor)
+ || getter.getDeclaringClass() != beanClass) {
+ return stock(writer);
+ }
+ if (valueSer instanceof GeneratedWriterBase child
+ && !getter.getReturnType().isPrimitive()) {
+ return new GenWProp(WKind.CHILD, getter, writer, bpw.getSerializedName(), child);
+ }
+ WKind kind = scalarKind(getter.getReturnType());
+ if (kind == null || valueSer == null
+ || !STOCK_SCALAR_SERS.contains(valueSer.getClass().getName())) {
+ return stock(writer);
+ }
+ return new GenWProp(kind, getter, writer, bpw.getSerializedName(), null);
+ }
+
+ // Public fields (any finality - reads are unrestricted) load through a
+ // generated getfield. Non-public fields have no generated read path on the
+ // serializer side and stay on the stock writer, the same limitation
+ // non-public getters have.
+ private static GenWProp classifyField(PropertyWriter writer, BeanPropertyWriter bpw,
+ Field field, ValueSerializer valueSer, Class> anchor) {
+ if (field == null || Modifier.isStatic(field.getModifiers())
+ || !CodecAccess.directlyAccessible(field.getModifiers(),
+ field.getDeclaringClass(), anchor)) {
+ return stock(writer);
+ }
+ Class> raw = field.getType();
+ if (valueSer instanceof GeneratedWriterBase child && !raw.isPrimitive()) {
+ return new GenWProp(WKind.CHILD, null, writer, bpw.getSerializedName(), child, field);
+ }
+ WKind kind = scalarKind(raw);
+ if (kind == null || valueSer == null
+ || !STOCK_SCALAR_SERS.contains(valueSer.getClass().getName())) {
+ return stock(writer);
+ }
+ return new GenWProp(kind, null, writer, bpw.getSerializedName(), null, field);
+ }
+
+ // BeanPropertyWriter keeps its include filter in a protected field with no
+ // accessor; the copy constructor carries it into this probe, whose own
+ // inherited field is readable. Custom includes (JsonInclude.Include.CUSTOM
+ // and friends) must ride the stock writer.
+ private static final class SuppressionProbe extends BeanPropertyWriter {
+ private SuppressionProbe(BeanPropertyWriter base) {
+ super(base);
+ }
+
+ static boolean hasSuppressableValue(BeanPropertyWriter base) {
+ return new SuppressionProbe(base)._suppressableValue != null;
+ }
+ }
+
+ private static GenWProp stock(PropertyWriter writer) {
+ return new GenWProp(WKind.STOCK, null, writer, null, null);
+ }
+
+ // Rationale in BBReaderFactory.visibleToGenerator: generated code refers to
+ // the bean class by name, resolved through this module's loader.
+ private static boolean visibleToGenerator(Class> cls) {
+ try {
+ return Class.forName(cls.getName(), false,
+ GeneratedWriterBase.class.getClassLoader()) == cls;
+ } catch (ClassNotFoundException | LinkageError e) {
+ return false;
+ }
+ }
+
+ private static WKind scalarKind(Class> raw) {
+ if (raw == String.class) {
+ return WKind.STRING;
+ }
+ if (raw == int.class) {
+ return WKind.INT;
+ }
+ if (raw == long.class) {
+ return WKind.LONG;
+ }
+ if (raw == boolean.class) {
+ return WKind.BOOLEAN;
+ }
+ return null;
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBWriterPlaceholder.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBWriterPlaceholder.java
new file mode 100644
index 000000000..147841118
--- /dev/null
+++ b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BBWriterPlaceholder.java
@@ -0,0 +1,93 @@
+package tools.jackson.module.blackbird.ser;
+
+import java.lang.invoke.MethodHandles;
+import java.util.function.Function;
+
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.databind.BeanProperty;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.jsontype.TypeSerializer;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.ser.bean.BeanSerializerBase;
+import tools.jackson.databind.util.NameTransformer;
+
+/**
+ * Stands in for a stock bean serializer until resolution, then hands out the
+ * generated writer through contextualization; anything that keeps the
+ * placeholder still works through delegation.
+ */
+final class BBWriterPlaceholder extends ValueSerializer
+{
+ private final BeanSerializerBase _delegate;
+
+ private final Function, MethodHandles.Lookup> _lookups;
+
+ private volatile ValueSerializer _codec;
+
+ BBWriterPlaceholder(BeanSerializerBase delegate,
+ Function, MethodHandles.Lookup> lookups) {
+ _delegate = delegate;
+ _lookups = lookups;
+ }
+
+ @Override
+ public void resolve(SerializationContext ctxt) {
+ _delegate.resolve(ctxt);
+ _codec = BBWriterFactory.tryGenerate(_delegate, ctxt, _lookups);
+ }
+
+ @Override
+ public ValueSerializer> createContextual(SerializationContext ctxt, BeanProperty property) {
+ ValueSerializer> contextual = _delegate.createContextual(ctxt, property);
+ if (contextual != _delegate) {
+ return contextual;
+ }
+ ValueSerializer codec = _codec;
+ return (codec != null) ? codec : this;
+ }
+
+ @Override
+ public void serialize(Object value, JsonGenerator g, SerializationContext ctxt) {
+ ValueSerializer codec = _codec;
+ if (codec != null) {
+ codec.serialize(value, g, ctxt);
+ } else {
+ _delegate.serialize(value, g, ctxt);
+ }
+ }
+
+ @Override
+ public void serializeWithType(Object value, JsonGenerator g, SerializationContext ctxt,
+ TypeSerializer typeSer) {
+ _delegate.serializeWithType(value, g, ctxt, typeSer);
+ }
+
+ @Override
+ public boolean isEmpty(SerializationContext ctxt, Object value) {
+ return _delegate.isEmpty(ctxt, value);
+ }
+
+ @Override
+ public boolean usesObjectId() {
+ return _delegate.usesObjectId();
+ }
+
+ @Override
+ public Class> handledType() {
+ return _delegate.handledType();
+ }
+
+ // Unwrapping must produce the stock unwrapping variant, never the codec
+ // (rationale in GeneratedWriterBase.unwrappingSerializer).
+ @Override
+ public ValueSerializer unwrappingSerializer(NameTransformer unwrapper) {
+ return _delegate.unwrappingSerializer(unwrapper);
+ }
+
+ @Override
+ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType type) {
+ _delegate.acceptJsonFormatVisitor(visitor, type);
+ }
+}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BooleanPropertyWriter.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BooleanPropertyWriter.java
deleted file mode 100644
index 7ebdf15b2..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/BooleanPropertyWriter.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.ValueSerializer;
-import tools.jackson.databind.PropertyName;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ser.BeanPropertyWriter;
-
-final class BooleanPropertyWriter
- extends OptimizedBeanPropertyWriter
-{
- private static final long serialVersionUID = 1L;
-
- private final boolean _suppressableSet;
- private final boolean _suppressableBoolean;
-
- private final ToBooleanFunction _acc;
-
- public BooleanPropertyWriter(BeanPropertyWriter src, ToBooleanFunction acc, ValueSerializer ser) {
- super(src, ser);
- _acc = acc;
-
- if (_suppressableValue instanceof Boolean) {
- _suppressableBoolean = ((Boolean)_suppressableValue).booleanValue();
- _suppressableSet = true;
- } else {
- _suppressableBoolean = false;
- _suppressableSet = false;
- }
- }
-
- protected BooleanPropertyWriter(BooleanPropertyWriter base, PropertyName name) {
- super(base, name);
- _suppressableSet = base._suppressableSet;
- _suppressableBoolean = base._suppressableBoolean;
- _acc = base._acc;
- }
-
- @Override
- protected BeanPropertyWriter _new(PropertyName newName) {
- return new BooleanPropertyWriter(this, newName);
- }
-
- @Override
- public BeanPropertyWriter withSerializer(ValueSerializer ser) {
- return new BooleanPropertyWriter(this, _acc, ser);
- }
-
- /*
- /**********************************************************************
- /* Overrides
- /**********************************************************************
- */
-
- @Override
- public final void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsProperty(bean, g, ctxt);
- return;
- }
- boolean value;
- try {
- value = _acc.applyAsBoolean(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, false);
- return;
- }
- if (!_suppressableSet || _suppressableBoolean != value) {
- g.writeName(_fastName);
- g.writeBoolean(value);
- }
- }
-
- @Override
- public final void serializeAsElement(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsElement(bean, g, ctxt);
- return;
- }
- boolean value;
- try {
- value = _acc.applyAsBoolean(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, true);
- return;
- }
- if (!_suppressableSet || _suppressableBoolean != value) {
- g.writeBoolean(value);
- } else { // important: MUST output a placeholder
- serializeAsOmittedElement(bean, g, ctxt);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/IntPropertyWriter.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/IntPropertyWriter.java
deleted file mode 100644
index 078d80035..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/IntPropertyWriter.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-import java.util.function.ToIntFunction;
-
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.ValueSerializer;
-import tools.jackson.databind.PropertyName;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ser.BeanPropertyWriter;
-
-final class IntPropertyWriter
- extends OptimizedBeanPropertyWriter
-{
- private static final long serialVersionUID = 1L;
-
- private final int _suppressableInt;
- private final boolean _suppressableIntSet;
-
- private final ToIntFunction _acc;
-
- public IntPropertyWriter(BeanPropertyWriter src, ToIntFunction acc, ValueSerializer ser) {
- super(src, ser);
- _acc = acc;
-
- if (_suppressableValue instanceof Integer) {
- _suppressableInt = (Integer)_suppressableValue;
- _suppressableIntSet = true;
- } else {
- _suppressableInt = 0;
- _suppressableIntSet = false;
- }
- }
-
- protected IntPropertyWriter(IntPropertyWriter base, PropertyName name) {
- super(base, name);
- _suppressableInt = base._suppressableInt;
- _suppressableIntSet = base._suppressableIntSet;
- _acc = base._acc;
- }
-
- @Override
- protected BeanPropertyWriter _new(PropertyName newName) {
- return new IntPropertyWriter(this, newName);
- }
-
- @Override
- public BeanPropertyWriter withSerializer(ValueSerializer ser) {
- return new IntPropertyWriter(this, _acc, ser);
- }
-
- /*
- /**********************************************************************
- /* Overrides
- /**********************************************************************
- */
-
- @Override
- public final void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsProperty(bean, g, ctxt);
- return;
- }
- int value;
- try {
- value = _acc.applyAsInt(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, false);
- return;
- }
- if (!_suppressableIntSet || _suppressableInt != value) {
- g.writeName(_fastName);
- g.writeNumber(value);
- }
- }
-
- @Override
- public final void serializeAsElement(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsElement(bean, g, ctxt);
- return;
- }
- int value;
- try {
- value = _acc.applyAsInt(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, true);
- return;
- }
- if (!_suppressableIntSet || _suppressableInt != value) {
- g.writeNumber(value);
- } else { // important: MUST output a placeholder
- serializeAsOmittedElement(bean, g, ctxt);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/LongPropertyWriter.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/LongPropertyWriter.java
deleted file mode 100644
index 02db15de6..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/LongPropertyWriter.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-import java.util.function.ToLongFunction;
-
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.ValueSerializer;
-import tools.jackson.databind.PropertyName;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ser.BeanPropertyWriter;
-
-final class LongPropertyWriter
- extends OptimizedBeanPropertyWriter
-{
- private static final long serialVersionUID = 1L;
-
- private final long _suppressableLong;
- private final boolean _suppressableSet;
-
- private final ToLongFunction _acc;
-
- public LongPropertyWriter(BeanPropertyWriter src, ToLongFunction acc, ValueSerializer ser) {
- super(src, ser);
- _acc = acc;
-
- if (_suppressableValue instanceof Long) {
- _suppressableLong = (Long)_suppressableValue;
- _suppressableSet = true;
- } else {
- _suppressableLong = 0L;
- _suppressableSet = false;
- }
- }
-
- protected LongPropertyWriter(LongPropertyWriter base, PropertyName name) {
- super(base, name);
- _suppressableSet = base._suppressableSet;
- _suppressableLong = base._suppressableLong;
- _acc = base._acc;
- }
-
- @Override
- protected BeanPropertyWriter _new(PropertyName newName) {
- return new LongPropertyWriter(this, newName);
- }
-
- @Override
- public BeanPropertyWriter withSerializer(ValueSerializer ser) {
- return new LongPropertyWriter(this, _acc, ser);
- }
-
- /*
- /**********************************************************************
- /* Overrides
- /**********************************************************************
- */
-
- @Override
- public final void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsProperty(bean, g, ctxt);
- return;
- }
- long value;
- try {
- value = _acc.applyAsLong(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, false);
- return;
- }
- if (!_suppressableSet || _suppressableLong != value) {
- g.writeName(_fastName);
- g.writeNumber(value);
- }
- }
-
- @Override
- public final void serializeAsElement(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsElement(bean, g, ctxt);
- return;
- }
- long value;
- try {
- value = _acc.applyAsLong(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, false);
- return;
- }
- if (!_suppressableSet || _suppressableLong != value) {
- g.writeNumber(value);
- } else { // important: MUST output a placeholder
- serializeAsOmittedElement(bean, g, ctxt);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/ObjectPropertyWriter.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/ObjectPropertyWriter.java
deleted file mode 100644
index 2a0a63c6f..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/ObjectPropertyWriter.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-import java.util.function.Function;
-
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.ValueSerializer;
-import tools.jackson.databind.PropertyName;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ser.BeanPropertyWriter;
-import tools.jackson.databind.ser.impl.PropertySerializerMap;
-
-final class ObjectPropertyWriter
- extends OptimizedBeanPropertyWriter
-{
- private static final long serialVersionUID = 1L;
- private final Function _acc;
-
- public ObjectPropertyWriter(BeanPropertyWriter src, Function acc, ValueSerializer ser) {
- super(src, ser);
- _acc = acc;
- }
-
- protected ObjectPropertyWriter(ObjectPropertyWriter base, PropertyName name) {
- super(base, name);
- _acc = base._acc;
- }
-
- @Override
- protected BeanPropertyWriter _new(PropertyName newName) {
- return new ObjectPropertyWriter(this, newName);
- }
-
- @Override
- public BeanPropertyWriter withSerializer(ValueSerializer ser) {
- return new ObjectPropertyWriter(this, _acc, ser);
- }
-
- /*
- /**********************************************************************
- /* Overrides
- /**********************************************************************
- */
-
- @Override
- public final void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsProperty(bean, g, ctxt);
- return;
- }
- Object value;
- try {
- value = _acc.apply(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, false);
- return;
- }
- // Null (etc) handling; copied from super-class impl
- if (value == null) {
- // 20-Jun-2022, tatu: Defer checking of null, see [databind#3481]
- if ((_suppressableValue != null)
- && ctxt.includeFilterSuppressNulls(_suppressableValue)) {
- return;
- }
- if (_nullSerializer != null) {
- g.writeName(_fastName);
- _nullSerializer.serialize(null, g, ctxt);
- }
- return;
- }
- ValueSerializer ser = _serializer;
- if (ser == null) {
- Class> cls = value.getClass();
- PropertySerializerMap map = _dynamicSerializers;
- ser = map.serializerFor(cls);
- if (ser == null) {
- ser = _findAndAddDynamic(map, cls, ctxt);
- }
- }
- if (_suppressableValue != null) {
- if (MARKER_FOR_EMPTY == _suppressableValue) {
- if (ser.isEmpty(ctxt, value)) {
- return;
- }
- } else if (_suppressableValue.equals(value)) {
- return;
- }
- }
- if (value == bean) {
- // three choices: exception; handled by call; or pass-through
- if (_handleSelfReference(bean, g, ctxt, ser)) {
- return;
- }
- }
- g.writeName(_fastName);
- if (_typeSerializer == null) {
- ser.serialize(value, g, ctxt);
- } else {
- ser.serializeWithType(value, g, ctxt, _typeSerializer);
- }
- }
-
- @Override
- public final void serializeAsElement(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsElement(bean, g, ctxt);
- return;
- }
- Object value;
- try {
- value = _acc.apply(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, true);
- return;
- }
- // Null (etc) handling; copied from super-class impl
- if (value == null) {
- if (_nullSerializer != null) {
- _nullSerializer.serialize(null, g, ctxt);
- } else if (_suppressNulls) {
- serializeAsOmittedElement(bean, g, ctxt);
- } else {
- ctxt.defaultSerializeNullValue(g);
- }
- return;
- }
- ValueSerializer ser = _serializer;
- if (ser == null) {
- Class> cls = value.getClass();
- PropertySerializerMap map = _dynamicSerializers;
- ser = map.serializerFor(cls);
- if (ser == null) {
- ser = _findAndAddDynamic(map, cls, ctxt);
- }
- }
- if (_suppressableValue != null) {
- if (MARKER_FOR_EMPTY == _suppressableValue) {
- if (ser.isEmpty(ctxt, value)) {
- serializeAsOmittedElement(bean, g, ctxt);
- return;
- }
- } else if (_suppressableValue.equals(value)) {
- serializeAsOmittedElement(bean, g, ctxt);
- return;
- }
- }
- if (value == bean) {
- // three choices: exception; handled by call; or pass-through
- if (_handleSelfReference(bean, g, ctxt, ser)) {
- return;
- }
- }
- if (_typeSerializer == null) {
- ser.serialize(value, g, ctxt);
- } else {
- ser.serializeWithType(value, g, ctxt, _typeSerializer);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/OptimizedBeanPropertyWriter.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/OptimizedBeanPropertyWriter.java
deleted file mode 100644
index ada422a1e..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/OptimizedBeanPropertyWriter.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.core.SerializableString;
-
-import tools.jackson.databind.*;
-import tools.jackson.databind.jsontype.TypeSerializer;
-import tools.jackson.databind.ser.BeanPropertyWriter;
-
-/**
- * Intermediate base class that is used for concrete
- * per-type implementations
- */
-@SuppressWarnings("serial")
-abstract class OptimizedBeanPropertyWriter>
- extends BeanPropertyWriter
-{
- /**
- * Locally stored version of efficiently serializable name.
- * Used to work around earlier problems with typing between
- * interface, implementation
- */
- protected final SerializableString _fastName;
-
- protected final BeanPropertyWriter fallbackWriter;
- // Not volatile to prevent overhead, worst case is we trip the exception a few extra times
- protected boolean broken = false;
-
- protected OptimizedBeanPropertyWriter(BeanPropertyWriter src, ValueSerializer ser)
- {
- super(src);
- this.fallbackWriter = unwrapFallbackWriter(src);
- // either use the passed on serializer or the original one
- _serializer = (ser != null) ? ser : src.getSerializer();
- _fastName = src.getSerializedName();
- }
-
- protected OptimizedBeanPropertyWriter(OptimizedBeanPropertyWriter> base, PropertyName name) {
- super(base, name);
- // 24-Oct-2017, tatu: Should this be changed too?
- fallbackWriter = base.fallbackWriter;
- _serializer = base._serializer;
- _fastName = getSerializedName();
- }
-
- private BeanPropertyWriter unwrapFallbackWriter(BeanPropertyWriter srcIn)
- {
- while (srcIn instanceof OptimizedBeanPropertyWriter) {
- srcIn = ((OptimizedBeanPropertyWriter>)srcIn).fallbackWriter;
- }
- return srcIn;
- }
-
- // Ensure it gets defined
- @Override
- protected abstract BeanPropertyWriter _new(PropertyName newName);
-
- @Override
- public void assignTypeSerializer(TypeSerializer typeSer) {
- super.assignTypeSerializer(typeSer);
- if (fallbackWriter != null) {
- fallbackWriter.assignTypeSerializer(typeSer);
- }
- // 04-Oct-2015, tatu: Should we handle this wrt [module-afterburner#59]?
- // Seems unlikely, as String/long/int/boolean are final types; and for
- // basic 'Object' we delegate to deserializer as expected
- }
-
- @Override
- public void assignSerializer(ValueSerializer ser) {
- super.assignSerializer(ser);
- if (fallbackWriter != null) {
- fallbackWriter.assignSerializer(ser);
- }
- // 04-Oct-2015, tatu: To fix [module-afterburner#59], need to disable use of
- // fully optimized variant
- if (!SerializerUtil.isDefaultSerializer(ser)) {
- broken = true;
- }
- }
-
- @Override
- public void assignNullSerializer(ValueSerializer nullSer) {
- super.assignNullSerializer(nullSer);
- if (fallbackWriter != null) {
- fallbackWriter.assignNullSerializer(nullSer);
- }
- }
-
- public abstract BeanPropertyWriter withSerializer(ValueSerializer ser);
-
- @Override
- public abstract void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt) throws Exception;
-
- @Override
- public abstract void serializeAsElement(Object bean, JsonGenerator g, SerializationContext ctxt) throws Exception;
-
- // note: synchronized used to try to minimize race conditions; also, should NOT
- // be a performance problem
- protected synchronized void _handleProblem(Object bean, JsonGenerator g, SerializationContext ctxt,
- Throwable t, boolean element) throws Exception
- {
- if ((t instanceof IllegalAccessError)
- || (t instanceof SecurityException)) {
- _reportProblem(bean, t);
- if (element) {
- fallbackWriter.serializeAsElement(bean, g, ctxt);
- } else {
- fallbackWriter.serializeAsProperty(bean, g, ctxt);
- }
- return;
- }
- if (t instanceof Error) {
- throw (Error) t;
- }
- throw (Exception) t;
- }
-
- protected void _reportProblem(Object bean, Throwable e)
- {
- broken = true;
- String msg = String.format("Disabling Blackbird serialization for %s (field %s; mutator %s), due to access error (type %s, message=%s)%n",
- bean.getClass(), _name, getClass().getName(),
- e.getClass().getName(), e.getMessage());
- Logger.getLogger(OptimizedBeanPropertyWriter.class.getName()).log(Level.WARNING, msg, e);
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/SerializerUtil.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/SerializerUtil.java
deleted file mode 100644
index 74aa75657..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/SerializerUtil.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-import tools.jackson.databind.ValueSerializer;
-import tools.jackson.databind.ser.std.ToStringSerializer;
-import tools.jackson.databind.util.ClassUtil;
-
-/**
- * Helper class that contains utility methods needed by various other classes
- * in this package.
- *
- * @since 2.12
- */
-class SerializerUtil
-{
- /**
- * Helper method used to check whether given serializer is the default
- * serializer implementation: this is necessary to avoid overriding other
- * kinds of serializers.
- */
- public static boolean isDefaultSerializer(ValueSerializer> ser)
- {
- if (ser == null) {
- return true;
- }
- if (ClassUtil.isJacksonStdImpl(ser)) {
- // 20-Nov-2020, tatu: As per [modules-base#117], need to consider
- // one standard serializer that should not be replaced...
- if (ser instanceof ToStringSerializer) {
- return false;
- }
- return true;
- }
- return false;
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/StringPropertyWriter.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/StringPropertyWriter.java
deleted file mode 100644
index 33b274019..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/StringPropertyWriter.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-import java.util.function.Function;
-
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.ValueSerializer;
-import tools.jackson.databind.PropertyName;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ser.BeanPropertyWriter;
-
-final class StringPropertyWriter
- extends OptimizedBeanPropertyWriter
-{
- private static final long serialVersionUID = 1L;
-
- private final Function _acc;
-
- public StringPropertyWriter(BeanPropertyWriter src, Function acc, ValueSerializer ser) {
- super(src, ser);
- _acc = acc;
- }
-
- protected StringPropertyWriter(StringPropertyWriter base, PropertyName name) {
- super(base, name);
- _acc = base._acc;
- }
-
- @Override
- protected BeanPropertyWriter _new(PropertyName newName) {
- return new StringPropertyWriter(this, newName);
- }
-
- @Override
- public BeanPropertyWriter withSerializer(ValueSerializer ser) {
- return new StringPropertyWriter(this, _acc, ser);
- }
-
- /*
- /**********************************************************************
- /* Overrides
- /**********************************************************************
- */
-
- @Override
- public final void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsProperty(bean, g, ctxt);
- return;
- }
- String value;
- try {
- value = _acc.apply(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, false);
- return;
- }
- // Null (etc) handling; copied from super-class impl
- if (value == null) {
- // 20-Jun-2022, tatu: Defer checking of null, see [databind#3481]
- if ((_suppressableValue != null)
- && ctxt.includeFilterSuppressNulls(_suppressableValue)) {
- return;
- }
- if (_nullSerializer != null) {
- g.writeName(_fastName);
- _nullSerializer.serialize(null, g, ctxt);
- }
- return;
- }
- if (_suppressableValue != null) {
- if (MARKER_FOR_EMPTY == _suppressableValue) {
- if (value.length() == 0) {
- return;
- }
- } else if (_suppressableValue.equals(value)) {
- return;
- }
- }
- g.writeName(_fastName);
- g.writeString(value);
- }
-
- @Override
- public final void serializeAsElement(Object bean, JsonGenerator g, SerializationContext ctxt)
- throws Exception
- {
- if (broken) {
- fallbackWriter.serializeAsElement(bean, g, ctxt);
- return;
- }
-
- String value;
- try {
- value = _acc.apply(bean);
- } catch (Throwable t) {
- _handleProblem(bean, g, ctxt, t, true);
- return;
- }
- // Null (etc) handling; copied from super-class impl
- if (value == null) {
- if (_suppressNulls) {
- serializeAsOmittedElement(bean, g, ctxt);
- } else {
- ctxt.defaultSerializeNullValue(g);
- }
- return;
- }
- if (_suppressableValue != null) {
- if (MARKER_FOR_EMPTY == _suppressableValue) {
- if (value.length() == 0) {
- serializeAsOmittedElement(bean, g, ctxt);
- return;
- }
- } else if (_suppressableValue.equals(value)) {
- serializeAsOmittedElement(bean, g, ctxt);
- return;
- }
- }
- g.writeString(value);
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/ToBooleanFunction.java b/blackbird/src/main/java/tools/jackson/module/blackbird/ser/ToBooleanFunction.java
deleted file mode 100644
index be50ab935..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/ser/ToBooleanFunction.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package tools.jackson.module.blackbird.ser;
-
-@FunctionalInterface
-public interface ToBooleanFunction {
- boolean applyAsBoolean(Object bean);
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/util/CheckedFunction.java b/blackbird/src/main/java/tools/jackson/module/blackbird/util/CheckedFunction.java
deleted file mode 100644
index 53d45863c..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/util/CheckedFunction.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (c), Data Geekery GmbH, contact@datageekery.com
- *
- * Licensed 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 tools.jackson.module.blackbird.util;
-
-@FunctionalInterface
-public interface CheckedFunction {
- T apply(X x) throws Throwable;
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/util/CheckedSupplier.java b/blackbird/src/main/java/tools/jackson/module/blackbird/util/CheckedSupplier.java
deleted file mode 100644
index c6b3c8b39..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/util/CheckedSupplier.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (c), Data Geekery GmbH, contact@datageekery.com
- *
- * Licensed 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 tools.jackson.module.blackbird.util;
-
-@FunctionalInterface
-public interface CheckedSupplier {
- T get() throws Throwable;
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/util/ReflectionHack.java b/blackbird/src/main/java/tools/jackson/module/blackbird/util/ReflectionHack.java
deleted file mode 100644
index 2817f384e..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/util/ReflectionHack.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package tools.jackson.module.blackbird.util;
-
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodHandles.Lookup;
-import java.lang.reflect.Constructor;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import tools.jackson.module.blackbird.BlackbirdModule;
-
-import java.lang.invoke.MethodType;
-
-/**
- * Allow private access to fields across using a JVM version-appropriate strategy
- * without having a compile-time dependency on Java 9+.
- *
- * This entire class can be replaced by a direct call to JDK 9+
- * {@link MethodHandles}{@code .privateLookupIn}
- * once Jackson targets Java >= 9.
- */
-public class ReflectionHack {
- @SuppressWarnings("synthetic-access")
- public static Lookup privateLookupIn(Class> lookup, MethodHandles.Lookup orig) throws IllegalAccessException {
- if (Java9Up.FACTORY != null) {
- return Java9Up.privateLookupIn(lookup, orig);
- }
- return Java8.privateLookupIn(lookup);
- }
-
- static class Java9Up {
- static final MethodHandle FACTORY;
-
- static {
- FACTORY = init();
- }
-
- private static MethodHandle init() {
- try {
- return MethodHandles.lookup().findStatic(
- MethodHandles.class,
- "privateLookupIn",
- MethodType.methodType(Lookup.class, Class.class, MethodHandles.Lookup.class));
- } catch (ReflectiveOperationException e) {
- Logger l = Logger.getLogger(BlackbirdModule.class.getName());
- // 02-Nov-2022, tatu: as per [modules-base#187] only log exception at trace
- l.log(Level.WARNING,
- "Unable to find Java 9+ MethodHandles.privateLookupIn. Blackbird is not performing optimally!");
- l.log(Level.FINE,
- "Failure reason for `MethodHandles.privateLookupIn()` not being found: "+e.getMessage(), e);
- return null;
- }
- }
-
- public static Lookup privateLookupIn(Class> lookup, Lookup orig) {
- try {
- return (Lookup) FACTORY.invokeExact(lookup, orig);
- } catch (ReflectiveOperationException e) {
- return orig;
- } catch (Throwable t) {
- throw Sneaky.throwAnyway(t);
- }
- }
- }
-
- static class Java8 {
- private static final Constructor FACTORY;
-
- static {
- try {
- FACTORY = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class);
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(
- "Unable to use private Lookup constructor",
- e);
- }
-
- if (!FACTORY.isAccessible()) {
- FACTORY.setAccessible(true);
- }
- }
-
- public static Lookup privateLookupIn(Class> lookup) {
- return Unchecked.function(FACTORY::newInstance).apply(lookup);
- }
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/util/Sneaky.java b/blackbird/src/main/java/tools/jackson/module/blackbird/util/Sneaky.java
deleted file mode 100644
index 75cdd6713..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/util/Sneaky.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (c), Data Geekery GmbH, contact@datageekery.com
- *
- * Licensed 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 tools.jackson.module.blackbird.util;
-
-import java.lang.reflect.InvocationTargetException;
-
-public class Sneaky {
- private Sneaky() {
- throw new RuntimeException();
- }
-
- /**
- * Will always throw an exception, so the caller should also always throw the dummy return value to make sure the control flow remains clear.
- */
- public static RuntimeException throwAnyway(Throwable t) {
- if (t instanceof InterruptedException) {
- Thread.currentThread().interrupt();
- }
-
- if (t instanceof InvocationTargetException) {
- throw throwAnyway(t.getCause());
- }
-
- throwEvadingChecks(t);
-
- // never reached
- return null;
- }
-
- @SuppressWarnings("unchecked")
- private static void throwEvadingChecks(Throwable throwable) throws E {
- throw (E) throwable;
- }
-}
diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/util/Unchecked.java b/blackbird/src/main/java/tools/jackson/module/blackbird/util/Unchecked.java
deleted file mode 100644
index 76ea1186e..000000000
--- a/blackbird/src/main/java/tools/jackson/module/blackbird/util/Unchecked.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (c), Data Geekery GmbH, contact@datageekery.com
- *
- * Licensed 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 tools.jackson.module.blackbird.util;
-
-import java.util.concurrent.Callable;
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-public class Unchecked {
- private Unchecked() {
- throw new RuntimeException();
- }
-
- @SuppressWarnings("PMD.DoNotUseThreads")
- public static Runnable runnable(CheckedRunnable checkedRunnable) {
- return () -> {
- try {
- checkedRunnable.run();
- } catch (Throwable t) {
- throw Sneaky.throwAnyway(t);
- }
- };
- }
-
- public static Supplier supplier(CheckedSupplier checkedSupplier) {
- return () -> {
- try {
- return checkedSupplier.get();
- } catch (Throwable t) {
- throw Sneaky.throwAnyway(t);
- }
- };
- }
-
- public static Function function(CheckedFunction checkedFunction) {
- return x -> {
- try {
- return checkedFunction.apply(x);
- } catch (Throwable t) {
- throw Sneaky.throwAnyway(t);
- }
- };
- }
-
- public interface SneakyCallable extends Callable {
- @Override
- T call(); // no 'throws Exception'
- }
-
- public interface CheckedRunnable {
- void run() throws Throwable;
- }
-}
diff --git a/blackbird/src/test/java/module-info.java b/blackbird/src/test/java/module-info.java
index bd51578dc..c87883b4f 100644
--- a/blackbird/src/test/java/module-info.java
+++ b/blackbird/src/test/java/module-info.java
@@ -36,7 +36,6 @@
opens tools.jackson.module.blackbird.roundtrip;
opens tools.jackson.module.blackbird.ser;
opens tools.jackson.module.blackbird.ser.filter;
- opens tools.jackson.module.blackbird.tofix;
opens tools.jackson.module.blackbird.testutil;
opens tools.jackson.module.blackbird.testutil.failure;
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/BlackbirdTestBase.java b/blackbird/src/test/java/tools/jackson/module/blackbird/BlackbirdTestBase.java
index f2bbc4c87..62ce7fb2e 100644
--- a/blackbird/src/test/java/tools/jackson/module/blackbird/BlackbirdTestBase.java
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/BlackbirdTestBase.java
@@ -11,6 +11,7 @@
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.module.blackbird.testutil.NoCheckSubTypeValidator;
+import tools.jackson.module.blackbird.codegen.CodegenFallbacks;
import static org.junit.jupiter.api.Assertions.*;
@@ -213,6 +214,13 @@ protected static JsonMapper.Builder blackbirdMapperBuilder() {
return mapperBuilder();
}
+ static {
+ // The suite runs strict: a codec-generation failure fails the test
+ // instead of demoting to the stock path, so the lenient production
+ // fallback cannot mask a generator bug here.
+ System.setProperty(CodegenFallbacks.FAIL_ON_ERROR_PROPERTY, "true");
+ }
+
protected static JsonMapper.Builder mapperBuilder() {
return JsonMapper.builder()
.polymorphicTypeValidator(new NoCheckSubTypeValidator())
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecCompatibilityTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecCompatibilityTest.java
new file mode 100644
index 000000000..087770a0b
--- /dev/null
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecCompatibilityTest.java
@@ -0,0 +1,261 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.DeserializationProblemHandler;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.exc.UnrecognizedPropertyException;
+import tools.jackson.module.blackbird.BlackbirdTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Behavior-parity regression tests against stock databind: quoted-scalar
+ * coercion, unknown-property handling under per-call config and problem
+ * handlers, record required and missing components, {@code @JsonUnwrapped}
+ * with codec-eligible children, PROPERTY_NAME entry, and modifier
+ * serializability. Every case compares against a vanilla mapper.
+ */
+public class BBCodecCompatibilityTest extends BlackbirdTestBase
+{
+ public static class ScalarBean {
+ public int i;
+ public long l;
+ public boolean b;
+ public String s;
+ }
+
+ public record ReqRecord(@JsonProperty(required = true) String name, int count) { }
+
+ public record PlainRecord(String name, int count) { }
+
+ public static class Child {
+ public int value;
+ public String tag;
+ }
+
+ public static class UnwrappedParent {
+ public int a;
+ @JsonUnwrapped
+ public Child child;
+ }
+
+ public static class PrefixedParent {
+ public int a;
+ @JsonUnwrapped(prefix = "foo.")
+ public Child child;
+ }
+
+ @JsonIgnoreProperties({ "skipMe" })
+ public static class IgnoralBean {
+ public int i;
+ }
+
+ private final ObjectMapper mapper = newObjectMapper();
+ private final ObjectMapper vanilla = newVanillaJSONMapper();
+
+ @Test
+ public void testQuotedScalarCoercion() throws Exception {
+ String doc = a2q("{'i':'123','l':'456','b':'true','s':'x'}");
+ ScalarBean exp = vanilla.readValue(doc, ScalarBean.class);
+ ScalarBean act = mapper.readValue(doc, ScalarBean.class);
+ assertEquals(exp.i, act.i);
+ assertEquals(exp.l, act.l);
+ assertEquals(exp.b, act.b);
+ assertEquals(exp.s, act.s);
+ }
+
+ @Test
+ public void testFloatToIntCoercion() throws Exception {
+ String doc = a2q("{'i':1.5,'l':2.5}");
+ ScalarBean exp = vanilla.readValue(doc, ScalarBean.class);
+ ScalarBean act = mapper.readValue(doc, ScalarBean.class);
+ assertEquals(exp.i, act.i);
+ assertEquals(exp.l, act.l);
+ }
+
+ @Test
+ public void testScalarFromNumberToken() throws Exception {
+ // String property fed a number token: stock coerces, so must we.
+ String doc = a2q("{'s':42,'b':true,'i':7}");
+ ScalarBean exp = vanilla.readValue(doc, ScalarBean.class);
+ ScalarBean act = mapper.readValue(doc, ScalarBean.class);
+ assertEquals(exp.s, act.s);
+ assertEquals(exp.b, act.b);
+ assertEquals(exp.i, act.i);
+ }
+
+ @Test
+ public void testPerCallFailOnUnknownProperties() throws Exception {
+ String doc = a2q("{'i':1,'nope':{'x':2},'s':'y'}");
+ // Default (disabled): unknown skips, other properties land.
+ ScalarBean ok = mapper.readValue(doc, ScalarBean.class);
+ assertEquals(1, ok.i);
+ assertEquals("y", ok.s);
+ // Per-call enable must fail, exactly like vanilla.
+ assertThrows(UnrecognizedPropertyException.class, () -> vanilla
+ .readerFor(ScalarBean.class)
+ .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .readValue(doc));
+ assertThrows(UnrecognizedPropertyException.class, () -> mapper
+ .readerFor(ScalarBean.class)
+ .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .readValue(doc));
+ }
+
+ @Test
+ public void testProblemHandlerSeesUnknownProperty() throws Exception {
+ List seen = new ArrayList<>();
+ ObjectMapper m = mapperBuilder().addHandler(new DeserializationProblemHandler() {
+ @Override
+ public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p,
+ ValueDeserializer> deserializer, Object beanOrClass, String propertyName) {
+ seen.add(propertyName);
+ p.skipChildren();
+ return true;
+ }
+ }).build();
+ ScalarBean bean = m.readValue(a2q("{'i':1,'mystery':[1,2]}"), ScalarBean.class);
+ assertEquals(1, bean.i);
+ assertEquals(List.of("mystery"), seen);
+ }
+
+ @Test
+ public void testIgnoredPropertiesKeepStockSemantics() throws Exception {
+ String doc = a2q("{'i':1,'skipMe':{'x':2}}");
+ IgnoralBean exp = vanilla.readerFor(IgnoralBean.class)
+ .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).readValue(doc);
+ IgnoralBean act = mapper.readerFor(IgnoralBean.class)
+ .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).readValue(doc);
+ assertEquals(exp.i, act.i);
+ assertThrows(UnrecognizedPropertyException.class, () -> mapper
+ .readerFor(IgnoralBean.class)
+ .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .readValue(a2q("{'i':1,'other':2}")));
+ }
+
+ @Test
+ public void testRequiredRecordComponentMissing() throws Exception {
+ String doc = a2q("{'count':3}");
+ MismatchedInputException expEx = assertThrows(MismatchedInputException.class,
+ () -> vanilla.readValue(doc, ReqRecord.class));
+ MismatchedInputException actEx = assertThrows(MismatchedInputException.class,
+ () -> mapper.readValue(doc, ReqRecord.class));
+ assertTrue(expEx.getMessage().contains("Missing required creator property 'name'"),
+ expEx.getMessage());
+ assertTrue(actEx.getMessage().contains("Missing required creator property 'name'"),
+ actEx.getMessage());
+ // Present case unaffected.
+ assertEquals(vanilla.readValue(a2q("{'name':'x','count':3}"), ReqRecord.class),
+ mapper.readValue(a2q("{'name':'x','count':3}"), ReqRecord.class));
+ }
+
+ @Test
+ public void testPerCallFailOnMissingCreatorProperties() throws Exception {
+ String doc = a2q("{'name':'x'}");
+ // Default: missing component takes its default value.
+ assertEquals(vanilla.readValue(doc, PlainRecord.class),
+ mapper.readValue(doc, PlainRecord.class));
+ assertThrows(MismatchedInputException.class, () -> vanilla
+ .readerFor(PlainRecord.class)
+ .with(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES)
+ .readValue(doc));
+ assertThrows(MismatchedInputException.class, () -> mapper
+ .readerFor(PlainRecord.class)
+ .with(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES)
+ .readValue(doc));
+ }
+
+ @Test
+ public void testUnwrappedChildWithCodec() throws Exception {
+ UnwrappedParent p = new UnwrappedParent();
+ p.a = 1;
+ p.child = new Child();
+ p.child.value = 3;
+ p.child.tag = "t";
+ String expJson = vanilla.writeValueAsString(p);
+ assertEquals(expJson, mapper.writeValueAsString(p));
+
+ UnwrappedParent exp = vanilla.readValue(expJson, UnwrappedParent.class);
+ UnwrappedParent act = mapper.readValue(expJson, UnwrappedParent.class);
+ assertNotNull(act.child);
+ assertEquals(exp.a, act.a);
+ assertEquals(exp.child.value, act.child.value);
+ assertEquals(exp.child.tag, act.child.tag);
+ }
+
+ @Test
+ public void testPrefixedUnwrappedChildWithCodec() throws Exception {
+ PrefixedParent p = new PrefixedParent();
+ p.a = 1;
+ p.child = new Child();
+ p.child.value = 3;
+ p.child.tag = "t";
+ String expJson = vanilla.writeValueAsString(p);
+ assertEquals(expJson, mapper.writeValueAsString(p));
+
+ PrefixedParent exp = vanilla.readValue(expJson, PrefixedParent.class);
+ PrefixedParent act = mapper.readValue(expJson, PrefixedParent.class);
+ assertNotNull(act.child);
+ assertEquals(exp.a, act.a);
+ assertEquals(exp.child.value, act.child.value);
+ assertEquals(exp.child.tag, act.child.tag);
+ }
+
+ @Test
+ public void testPropertyNameEntry() throws Exception {
+ String doc = a2q("{'i':7,'l':8,'b':true,'s':'y'}");
+ ScalarBean exp;
+ try (JsonParser p = vanilla.createParser(doc)) {
+ p.nextToken();
+ p.nextToken();
+ exp = vanilla.readValue(p, ScalarBean.class);
+ }
+ ScalarBean act;
+ try (JsonParser p = mapper.createParser(doc)) {
+ p.nextToken();
+ p.nextToken();
+ act = mapper.readValue(p, ScalarBean.class);
+ }
+ assertEquals(exp.i, act.i);
+ assertEquals(exp.l, act.l);
+ assertEquals(exp.b, act.b);
+ assertEquals(exp.s, act.s);
+ }
+
+ @Test
+ public void testMapperUsableAfterJdkSerialization() throws Exception {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
+ out.writeObject(newObjectMapper());
+ }
+ ObjectMapper revived;
+ try (ObjectInputStream in = new ObjectInputStream(
+ new ByteArrayInputStream(bytes.toByteArray()))) {
+ revived = (ObjectMapper) in.readObject();
+ }
+ // Building a deserializer runs the modifier's updateBuilder, which
+ // uses the transient ThreadLocal that deserialization nulled out.
+ ScalarBean bean = revived.readValue(a2q("{'i':5,'s':'z'}"), ScalarBean.class);
+ assertEquals(5, bean.i);
+ assertEquals("z", bean.s);
+ assertEquals(newObjectMapper().writeValueAsString(bean),
+ revived.writeValueAsString(bean));
+ }
+}
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecCreatorTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecCreatorTest.java
new file mode 100644
index 000000000..f0c6e60cb
--- /dev/null
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecCreatorTest.java
@@ -0,0 +1,61 @@
+package tools.jackson.module.blackbird.deser;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.module.blackbird.BlackbirdTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// The value instantiator's default creator is not always the no-arg
+// constructor: a no-arg @JsonCreator factory takes its place, and databind
+// then constructs through the factory. Generated codecs must construct the
+// same way, not through a direct `new` that bypasses the factory.
+public class BBCodecCreatorTest extends BlackbirdTestBase
+{
+ public static class FactoryBean {
+ public String name;
+ public boolean viaFactory;
+
+ public FactoryBean() { }
+
+ @JsonCreator
+ public static FactoryBean create() {
+ FactoryBean bean = new FactoryBean();
+ bean.viaFactory = true;
+ return bean;
+ }
+ }
+
+ public record FactoryRec(String name, int count) {
+ @JsonCreator
+ public static FactoryRec create(@JsonProperty("name") String name,
+ @JsonProperty("count") int count) {
+ return new FactoryRec(name + "!", count);
+ }
+ }
+
+ @Test
+ public void testRecordFactoryCreatorIsUsed() throws Exception {
+ ObjectMapper vanilla = newVanillaJSONMapper();
+ FactoryRec expected = vanilla.readValue("{\"name\":\"a\",\"count\":2}", FactoryRec.class);
+
+ FactoryRec actual = newBlackbirdMapper().readValue("{\"name\":\"a\",\"count\":2}", FactoryRec.class);
+ assertEquals(expected, actual,
+ "record construction path differs from stock (creator is the factory)");
+ }
+
+ @Test
+ public void testNoArgFactoryCreatorIsUsed() throws Exception {
+ ObjectMapper vanilla = newVanillaJSONMapper();
+ FactoryBean expected = vanilla.readValue("{\"name\":\"a\"}", FactoryBean.class);
+
+ FactoryBean actual = newBlackbirdMapper().readValue("{\"name\":\"a\"}", FactoryBean.class);
+ assertEquals("a", actual.name);
+ assertEquals(expected.viaFactory, actual.viaFactory,
+ "construction path differs from stock (default creator is the factory)");
+ }
+}
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecDemotionTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecDemotionTest.java
new file mode 100644
index 000000000..f8c90f087
--- /dev/null
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecDemotionTest.java
@@ -0,0 +1,162 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonMerge;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.DeserializationConfig;
+import tools.jackson.databind.InjectableValues;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.ValueDeserializerModifier;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.module.blackbird.BlackbirdModule;
+import tools.jackson.module.blackbird.BlackbirdTestBase;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Documented demotions: beans whose features the generator does not model
+ * (object ids, per-property merge, any-setter, injected values) stay on the
+ * stock deserializer and behave exactly like vanilla. These pins keep the
+ * demotions deliberate; if the generator grows support, the engagement
+ * assertion flips.
+ */
+public class BBCodecDemotionTest extends BlackbirdTestBase
+{
+ @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
+ public static class IdBean {
+ public int value;
+ public IdBean next;
+ }
+
+ public static class MergeTarget {
+ public int a = 1;
+ public int b = 2;
+ }
+
+ public static class MergeBean {
+ @JsonMerge
+ public MergeTarget child = new MergeTarget();
+ }
+
+ public static class AnySetterBean {
+ public int known;
+ public Map rest = new LinkedHashMap<>();
+
+ @JsonAnySetter
+ public void any(String name, Object value) {
+ rest.put(name, value);
+ }
+ }
+
+ public static class InjectBean {
+ public int i;
+ @JacksonInject("who")
+ public String who;
+ }
+
+ private final ObjectMapper vanilla = newVanillaJSONMapper();
+
+ private static ObjectMapper capturingMapper(Map, ValueDeserializer>> seen) {
+ SimpleModule capture = new SimpleModule("capture") {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public void setupModule(SetupContext ctxt) {
+ super.setupModule(ctxt);
+ ctxt.addDeserializerModifier(new ValueDeserializerModifier() {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public ValueDeserializer> modifyDeserializer(DeserializationConfig cfg,
+ BeanDescription.Supplier ref, ValueDeserializer> d) {
+ seen.put(ref.getBeanClass(), d);
+ return d;
+ }
+ });
+ }
+ };
+ return JsonMapper.builder()
+ .addModule(capture)
+ .addModule(new BlackbirdModule())
+ .build();
+ }
+
+ // Stock comes in two shapes: the modifier demoted (raw stock captured),
+ // or the factory gated at resolve time (placeholder captured, codec never
+ // generated). Both mean every call runs the stock deserializer.
+ private static void assertStock(Map, ValueDeserializer>> seen, Class> cls)
+ throws Exception {
+ ValueDeserializer> d = seen.get(cls);
+ assertNotNull(d, "no deserializer captured for " + cls);
+ if (d instanceof BBReaderPlaceholder placeholder) {
+ java.lang.reflect.Field f = BBReaderPlaceholder.class.getDeclaredField("_codec");
+ f.setAccessible(true);
+ assertNull(f.get(placeholder),
+ cls.getSimpleName() + " unexpectedly generated a codec; if support"
+ + " was added deliberately, flip this pin to assert engagement");
+ }
+ }
+
+ @Test
+ public void testObjectIdBeanStaysStock() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'@id':1,'value':5,'next':1}");
+ IdBean v = vanilla.readValue(doc, IdBean.class);
+ IdBean m = mapper.readValue(doc, IdBean.class);
+ assertStock(seen, IdBean.class);
+ assertEquals(v.value, m.value);
+ assertSame(m, m.next);
+ }
+
+ @Test
+ public void testMergePropertyBeanStaysStock() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'child':{'a':9}}");
+ MergeBean v = vanilla.readValue(doc, MergeBean.class);
+ MergeBean m = mapper.readValue(doc, MergeBean.class);
+ assertStock(seen, MergeBean.class);
+ assertEquals(v.child.a, m.child.a);
+ assertEquals(v.child.b, m.child.b);
+ assertEquals(2, m.child.b);
+ }
+
+ @Test
+ public void testAnySetterBeanStaysStock() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'known':1,'x':'a','y':2}");
+ AnySetterBean v = vanilla.readValue(doc, AnySetterBean.class);
+ AnySetterBean m = mapper.readValue(doc, AnySetterBean.class);
+ assertStock(seen, AnySetterBean.class);
+ assertEquals(v.known, m.known);
+ assertEquals(v.rest, m.rest);
+ assertEquals(2, m.rest.size());
+ }
+
+ @Test
+ public void testInjectableBeanStaysStock() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper base = capturingMapper(seen);
+ InjectableValues inject =
+ new InjectableValues.Std().addValue("who", "injected");
+ String doc = a2q("{'i':7}");
+ InjectBean v = vanilla.reader(inject).forType(InjectBean.class).readValue(doc);
+ InjectBean m = base.reader(inject).forType(InjectBean.class).readValue(doc);
+ assertStock(seen, InjectBean.class);
+ assertEquals(v.i, m.i);
+ assertEquals(v.who, m.who);
+ assertEquals("injected", m.who);
+ }
+}
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecEngagementTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecEngagementTest.java
new file mode 100644
index 000000000..a36443989
--- /dev/null
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecEngagementTest.java
@@ -0,0 +1,220 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.exc.MismatchedInputException;
+
+import tools.jackson.module.blackbird.BlackbirdTestBase;
+import tools.jackson.databind.annotation.JsonDeserialize;
+import tools.jackson.databind.annotation.JsonPOJOBuilder;
+import tools.jackson.databind.deser.ValueDeserializerModifier;
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.module.blackbird.BlackbirdModule;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class BBCodecEngagementTest extends BlackbirdTestBase
+{
+ public static class PublicBean {
+ private String name;
+ private int count;
+ private long total;
+ private boolean active;
+ private List tags;
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ public int getCount() { return count; }
+ public void setCount(int count) { this.count = count; }
+ public long getTotal() { return total; }
+ public void setTotal(long total) { this.total = total; }
+ public boolean isActive() { return active; }
+ public void setActive(boolean active) { this.active = active; }
+ public List getTags() { return tags; }
+ public void setTags(List tags) { this.tags = tags; }
+ }
+
+ public record PublicRec(String name, int count, long total, boolean active, List tags) {}
+
+ @JsonDeserialize(builder = BuilderBean.Builder.class)
+ public static class BuilderBean {
+ private final String name;
+ private final int count;
+ private final List tags;
+
+ private BuilderBean(String name, int count, List tags) {
+ this.name = name;
+ this.count = count;
+ this.tags = tags;
+ }
+
+ public String getName() { return name; }
+ public int getCount() { return count; }
+ public List getTags() { return tags; }
+
+ @JsonPOJOBuilder(withPrefix = "")
+ public static class Builder {
+ private String name;
+ private int count;
+ private List tags;
+
+ public Builder name(String name) { this.name = name; return this; }
+ public Builder count(int count) { this.count = count; return this; }
+ public Builder tags(List tags) { this.tags = tags; return this; }
+ public BuilderBean build() { return new BuilderBean(name, count, tags); }
+ }
+ }
+
+ public static class PublicParent {
+ private String label;
+ private PublicBean child;
+ private PublicRec rec;
+
+ public String getLabel() { return label; }
+ public void setLabel(String label) { this.label = label; }
+ public PublicBean getChild() { return child; }
+ public void setChild(PublicBean child) { this.child = child; }
+ public PublicRec getRec() { return rec; }
+ public void setRec(PublicRec rec) { this.rec = rec; }
+ }
+
+ private final ObjectMapper MAPPER = newObjectMapper();
+
+ @Test
+ public void testNestedChildCodecs() throws Exception {
+ String doc = "{\"label\":\"p\",\"child\":{\"name\":\"c\",\"count\":2},"
+ + "\"rec\":{\"name\":\"r\",\"count\":5,\"total\":6,\"active\":true,\"tags\":[]}}";
+ PublicParent parent = MAPPER.readValue(doc, PublicParent.class);
+ assertEquals("p", parent.getLabel());
+ assertEquals("c", parent.getChild().getName());
+ assertEquals(2, parent.getChild().getCount());
+ assertEquals(new PublicRec("r", 5, 6L, true, List.of()), parent.getRec());
+
+ PublicParent nulls = MAPPER.readValue("{\"child\":null,\"rec\":null}", PublicParent.class);
+ assertNull(nulls.getChild());
+ assertNull(nulls.getRec());
+
+ PublicParent vanillaParent = newVanillaJSONMapper().readValue(doc, PublicParent.class);
+ assertEquals(vanillaParent.getRec(), parent.getRec());
+ assertEquals(vanillaParent.getChild().getName(), parent.getChild().getName());
+ }
+
+ @Test
+ public void testGeneratedRecordCodecValues() throws Exception {
+ PublicRec rec = MAPPER.readValue(
+ "{\"name\":\"a\",\"count\":3,\"total\":9000000000,\"active\":true,\"tags\":[\"x\"]}",
+ PublicRec.class);
+ assertEquals(new PublicRec("a", 3, 9000000000L, true, List.of("x")), rec);
+
+ PublicRec reversed = MAPPER.readValue(
+ "{\"tags\":[\"x\"],\"active\":true,\"total\":9000000000,\"count\":3,\"name\":\"a\"}",
+ PublicRec.class);
+ assertEquals(rec, reversed);
+ }
+
+ @Test
+ public void testGeneratedRecordCodecNullsUnknownsMissing() throws Exception {
+ ObjectMapper vanilla = newVanillaJSONMapper();
+ String doc = "{\"name\":null,\"junk\":{\"a\":[1]},\"count\":3}";
+ assertEquals(vanilla.readValue(doc, PublicRec.class),
+ MAPPER.readValue(doc, PublicRec.class));
+ String primNull = "{\"count\":null,\"name\":\"n\"}";
+ assertThrows(MismatchedInputException.class,
+ () -> vanilla.readValue(primNull, PublicRec.class));
+ assertThrows(MismatchedInputException.class,
+ () -> MAPPER.readValue(primNull, PublicRec.class));
+ }
+
+ @Test
+ public void testGeneratedBuilderCodecValues() throws Exception {
+ String doc = "{\"name\":\"b\",\"junk\":[{}],\"count\":7,\"tags\":[\"t\"]}";
+ BuilderBean bean = MAPPER.readValue(doc, BuilderBean.class);
+ assertEquals("b", bean.getName());
+ assertEquals(7, bean.getCount());
+ assertEquals(List.of("t"), bean.getTags());
+ BuilderBean vanillaBean = newVanillaJSONMapper().readValue(doc, BuilderBean.class);
+ assertEquals(vanillaBean.getName(), bean.getName());
+ assertEquals(vanillaBean.getCount(), bean.getCount());
+ assertEquals(vanillaBean.getTags(), bean.getTags());
+ }
+
+ // A non-object where a bean is expected reports MismatchedInputException
+ // from the generated codecs, same as stock.
+ @Test
+ public void testOddTokenMatchesStock() throws Exception {
+ for (Class> type : new Class>[] {
+ PublicBean.class, PublicRec.class, BuilderBean.class }) {
+ assertThrows(MismatchedInputException.class,
+ () -> MAPPER.readValue("[1]", type));
+ assertThrows(MismatchedInputException.class,
+ () -> newVanillaJSONMapper().readValue("[1]", type));
+ }
+ }
+
+ @Test
+ public void testEngineEngagesForAllShapes() throws Exception {
+ java.util.Map, tools.jackson.databind.ValueDeserializer>> seen =
+ new java.util.concurrent.ConcurrentHashMap<>();
+ tools.jackson.databind.deser.ValueDeserializerModifier capture =
+ new ValueDeserializerModifier() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public tools.jackson.databind.ValueDeserializer> modifyDeserializer(
+ tools.jackson.databind.DeserializationConfig config,
+ BeanDescription.Supplier beanDescRef,
+ tools.jackson.databind.ValueDeserializer> deserializer) {
+ seen.put(beanDescRef.getBeanClass(), deserializer);
+ return deserializer;
+ }
+ };
+ ObjectMapper m = JsonMapper.builder()
+ .addModule(new SimpleModule("capture")
+ .setDeserializerModifier(capture))
+ .addModule(new BlackbirdModule())
+ .build();
+ m.readValue("{\"name\":\"a\"}", PublicBean.class);
+ m.readValue("{\"name\":\"a\"}", PublicRec.class);
+ m.readValue("{\"name\":\"a\"}", BuilderBean.class);
+ assertEquals("BBReaderPlaceholder",
+ seen.get(PublicBean.class).getClass().getSimpleName());
+ assertEquals("BBReaderPlaceholder",
+ seen.get(PublicRec.class).getClass().getSimpleName());
+ // Builder flow keys the modifier by the builder class.
+ assertEquals("BBReaderPlaceholder",
+ seen.get(BuilderBean.Builder.class).getClass().getSimpleName());
+ }
+
+ @Test
+ public void testGeneratedCodecValues() throws Exception {
+ PublicBean bean = MAPPER.readValue(
+ "{\"name\":\"a\",\"count\":3,\"total\":9000000000,\"active\":true,\"tags\":[\"x\",\"y\"]}",
+ PublicBean.class);
+ assertEquals("a", bean.getName());
+ assertEquals(3, bean.getCount());
+ assertEquals(9000000000L, bean.getTotal());
+ assertTrue(bean.isActive());
+ assertEquals(List.of("x", "y"), bean.getTags());
+ }
+
+ @Test
+ public void testGeneratedCodecNullsAndUnknowns() throws Exception {
+ PublicBean bean = MAPPER.readValue(
+ "{\"name\":null,\"junk\":{\"deep\":[1,2]},\"active\":false}",
+ PublicBean.class);
+ assertNull(bean.getName());
+ assertEquals(0, bean.getCount());
+ assertFalse(bean.isActive());
+ assertNull(bean.getTags());
+ assertThrows(MismatchedInputException.class,
+ () -> MAPPER.readValue("{\"count\":null}", PublicBean.class));
+ assertThrows(MismatchedInputException.class,
+ () -> newVanillaJSONMapper().readValue("{\"count\":null}", PublicBean.class));
+ }
+
+}
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecFieldTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecFieldTest.java
new file mode 100644
index 000000000..17af6a427
--- /dev/null
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecFieldTest.java
@@ -0,0 +1,195 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.DeserializationConfig;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ValueDeserializer;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import tools.jackson.databind.deser.ValueDeserializerModifier;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+
+import tools.jackson.module.blackbird.BlackbirdModule;
+import tools.jackson.module.blackbird.BlackbirdTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// Field-backed properties: the generator stores through putfield / reads
+// through getfield for public fields, and reaches a non-public field through a
+// lookup-derived handle. Behavior must match stock databind on every path.
+public class BBCodecFieldTest extends BlackbirdTestBase
+{
+ @JsonPropertyOrder({ "name", "count", "total", "active", "tags", "child" })
+ public static class FieldBean {
+ public String name;
+ public int count;
+ public long total;
+ public boolean active;
+ public List tags;
+ public FieldChild child;
+ }
+
+ public static class FieldChild {
+ public String label;
+ }
+
+ public static class FinalFieldBean {
+ public final String name;
+ public int count;
+
+ public FinalFieldBean() {
+ this("", 0);
+ }
+
+ @JsonCreator
+ public FinalFieldBean(
+ @JsonProperty("name") String name,
+ @JsonProperty("count") int count) {
+ this.name = name;
+ this.count = count;
+ }
+ }
+
+ public static class MixedBean {
+ public String field;
+ private int viaSetter;
+
+ public int getViaSetter() { return viaSetter; }
+ public void setViaSetter(int v) { viaSetter = v; }
+ }
+
+ public static class PrivateFieldBean {
+ @JsonProperty("name")
+ private String name;
+ @JsonProperty("count")
+ private int count;
+
+ public String name() { return name; }
+ public int count() { return count; }
+ }
+
+ private final ObjectMapper MAPPER = newObjectMapper();
+ private final ObjectMapper VANILLA = newVanillaJSONMapper();
+
+ @Test
+ public void testPublicFieldValues() throws Exception {
+ String json = a2q("{'name':'a','count':3,'total':9000000000,'active':true,"
+ + "'tags':['x','y'],'child':{'label':'c'}}");
+ FieldBean bean = MAPPER.readValue(json, FieldBean.class);
+ assertEquals("a", bean.name);
+ assertEquals(3, bean.count);
+ assertEquals(9000000000L, bean.total);
+ assertTrue(bean.active);
+ assertEquals(List.of("x", "y"), bean.tags);
+ assertEquals("c", bean.child.label);
+ }
+
+ @Test
+ public void testPublicFieldReversedOrder() throws Exception {
+ String json = a2q("{'child':{'label':'c'},'tags':['x'],'active':true,"
+ + "'total':7,'count':2,'name':'a'}");
+ FieldBean bean = MAPPER.readValue(json, FieldBean.class);
+ FieldBean vanilla = VANILLA.readValue(json, FieldBean.class);
+ assertEquals(vanilla.name, bean.name);
+ assertEquals(vanilla.count, bean.count);
+ assertEquals(vanilla.child.label, bean.child.label);
+ }
+
+ @Test
+ public void testPublicFieldNullsAndUnknowns() throws Exception {
+ String json = a2q("{'name':null,'extra':{'deep':1},'count':5,"
+ + "'tags':null,'child':null}");
+ FieldBean bean = MAPPER.readValue(json, FieldBean.class);
+ assertNull(bean.name);
+ assertEquals(5, bean.count);
+ assertNull(bean.tags);
+ assertNull(bean.child);
+ }
+
+ @Test
+ public void testNullToPrimitiveFieldThrows() {
+ assertThrows(MismatchedInputException.class,
+ () -> MAPPER.readValue(a2q("{'count':null}"), FieldBean.class));
+ }
+
+ @Test
+ public void testPublicFieldRoundTripMatchesVanilla() throws Exception {
+ FieldBean bean = MAPPER.readValue(a2q("{'name':'a','count':3,'total':7,"
+ + "'active':true,'tags':['x','y'],'child':{'label':'c'}}"), FieldBean.class);
+ assertEquals(VANILLA.writeValueAsString(bean), MAPPER.writeValueAsString(bean));
+ }
+
+ @Test
+ public void testFinalFieldMatchesVanilla() throws Exception {
+ String json = a2q("{'name':'a','count':3}");
+ FinalFieldBean bean = MAPPER.readValue(json, FinalFieldBean.class);
+ assertEquals("a", bean.name);
+ assertEquals(3, bean.count);
+ assertEquals(VANILLA.writeValueAsString(bean), MAPPER.writeValueAsString(bean));
+ }
+
+ @Test
+ public void testMixedFieldAndSetter() throws Exception {
+ MixedBean bean = MAPPER.readValue(a2q("{'field':'a','viaSetter':7}"), MixedBean.class);
+ assertEquals("a", bean.field);
+ assertEquals(7, bean.getViaSetter());
+ assertEquals(VANILLA.writeValueAsString(bean), MAPPER.writeValueAsString(bean));
+ }
+
+ @Test
+ public void testPrivateFieldWithLookupAccelerates() throws Exception {
+ Supplier here = MethodHandles::lookup;
+ ObjectMapper m = JsonMapper.builder()
+ .addModule(new BlackbirdModule() {
+ @Override
+ protected Supplier findLookupSupplier() {
+ return here;
+ }
+ })
+ .build();
+ PrivateFieldBean bean = m.readValue(a2q("{'name':'a','count':3}"), PrivateFieldBean.class);
+ assertEquals("a", bean.name());
+ assertEquals(3, bean.count());
+ }
+
+ @Test
+ public void testPrivateFieldWithoutLookupStillWorks() throws Exception {
+ // The default lookup cannot reach the private field, so the property
+ // demotes to the stock path; the value must still round-trip.
+ PrivateFieldBean bean = MAPPER.readValue(a2q("{'name':'a','count':3}"),
+ PrivateFieldBean.class);
+ assertEquals("a", bean.name());
+ assertEquals(3, bean.count());
+ }
+
+ @Test
+ public void testFieldBeanEngagesCodec() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ValueDeserializerModifier capture = new ValueDeserializerModifier() {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public ValueDeserializer> modifyDeserializer(DeserializationConfig config,
+ BeanDescription.Supplier ref, ValueDeserializer> deser) {
+ seen.put(ref.getBeanClass(), deser);
+ return deser;
+ }
+ };
+ ObjectMapper m = JsonMapper.builder()
+ .addModule(new SimpleModule("capture").setDeserializerModifier(capture))
+ .addModule(new BlackbirdModule())
+ .build();
+ m.readValue(a2q("{'name':'a'}"), FieldBean.class);
+ assertEquals("BBReaderPlaceholder", seen.get(FieldBean.class).getClass().getSimpleName());
+ }
+}
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecIgnoralsTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecIgnoralsTest.java
new file mode 100644
index 000000000..e704cba3d
--- /dev/null
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecIgnoralsTest.java
@@ -0,0 +1,198 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonIncludeProperties;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.DeserializationConfig;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.ValueDeserializerModifier;
+import tools.jackson.databind.exc.IgnoredPropertyException;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.module.blackbird.BlackbirdModule;
+import tools.jackson.module.blackbird.BlackbirdTestBase;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Ignoral parity: beans with {@code @JsonIgnoreProperties},
+ * {@code @JsonIncludeProperties}, or {@code @JsonIgnore} now engage codecs,
+ * and the unknown arm consults the ignoral sets in the stock loop's exact
+ * order. Every case compares against vanilla.
+ */
+public class BBCodecIgnoralsTest extends BlackbirdTestBase
+{
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class LenientBean {
+ public int i;
+ public String s;
+ }
+
+ @JsonIgnoreProperties({ "secret" })
+ public static class IgnoredSetBean {
+ public int i;
+ public String s;
+ }
+
+ @JsonIncludeProperties({ "i" })
+ public static class IncludedSetBean {
+ public int i;
+ public String s;
+ }
+
+ public static class JsonIgnoreBean {
+ public int i;
+ @JsonIgnore
+ public String hidden;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record LenientRecord(String name, int count) { }
+
+ private final ObjectMapper vanilla = newVanillaJSONMapper();
+
+ private static String message(Exception e) {
+ String m = e.getMessage();
+ int nl = m.indexOf('\n');
+ return (nl < 0) ? m : m.substring(0, nl);
+ }
+
+ private static ObjectMapper capturingMapper(Map, ValueDeserializer>> seen) {
+ SimpleModule capture = new SimpleModule("capture") {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public void setupModule(SetupContext ctxt) {
+ super.setupModule(ctxt);
+ ctxt.addDeserializerModifier(new ValueDeserializerModifier() {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public ValueDeserializer> modifyDeserializer(DeserializationConfig cfg,
+ BeanDescription.Supplier ref, ValueDeserializer> d) {
+ seen.put(ref.getBeanClass(), d);
+ return d;
+ }
+ });
+ }
+ };
+ return JsonMapper.builder()
+ .addModule(capture)
+ .addModule(new BlackbirdModule())
+ .build();
+ }
+
+ private static void assertEngaged(Map, ValueDeserializer>> seen, Class> cls) {
+ ValueDeserializer> d = seen.get(cls);
+ assertNotNull(d, "no deserializer captured for " + cls);
+ assertEquals("BBReaderPlaceholder", d.getClass().getSimpleName(),
+ cls.getSimpleName() + " did not engage a codec");
+ }
+
+ @Test
+ public void testIgnoreUnknownSkips() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'i':1,'nope':{'deep':[1,2]},'s':'x','more':true}");
+ LenientBean v = vanilla.readValue(doc, LenientBean.class);
+ LenientBean m = mapper.readValue(doc, LenientBean.class);
+ assertEngaged(seen, LenientBean.class);
+ assertEquals(v.i, m.i);
+ assertEquals(v.s, m.s);
+ }
+
+ @Test
+ public void testIgnoreUnknownBeatsStrictPerCall() throws Exception {
+ // Stock skips silently under ignoreUnknown even when
+ // FAIL_ON_UNKNOWN_PROPERTIES is enabled per call.
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'i':1,'nope':true}");
+ LenientBean v = vanilla.reader(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .forType(LenientBean.class).readValue(doc);
+ LenientBean m = mapper.reader(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .forType(LenientBean.class).readValue(doc);
+ assertEngaged(seen, LenientBean.class);
+ assertEquals(v.i, m.i);
+ }
+
+ @Test
+ public void testIgnoredSetSkipsAndFailOnIgnored() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'i':1,'secret':'boo','s':'x'}");
+ IgnoredSetBean v = vanilla.readValue(doc, IgnoredSetBean.class);
+ IgnoredSetBean m = mapper.readValue(doc, IgnoredSetBean.class);
+ assertEngaged(seen, IgnoredSetBean.class);
+ assertEquals(v.i, m.i);
+ assertEquals(v.s, m.s);
+
+ IgnoredPropertyException ve = assertThrows(IgnoredPropertyException.class,
+ () -> vanilla.reader(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
+ .forType(IgnoredSetBean.class).readValue(doc));
+ IgnoredPropertyException me = assertThrows(IgnoredPropertyException.class,
+ () -> mapper.reader(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
+ .forType(IgnoredSetBean.class).readValue(doc));
+ assertEquals(message(ve), message(me));
+ }
+
+ @Test
+ public void testIncludedSetIgnoresOthers() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'i':1,'s':'x'}");
+ IncludedSetBean v = vanilla.readValue(doc, IncludedSetBean.class);
+ IncludedSetBean m = mapper.readValue(doc, IncludedSetBean.class);
+ assertEngaged(seen, IncludedSetBean.class);
+ assertEquals(v.i, m.i);
+ assertEquals(v.s, m.s);
+ assertNull(m.s);
+ }
+
+ @Test
+ public void testJsonIgnoredNameInDocument() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'i':1,'hidden':'boo'}");
+ JsonIgnoreBean v = vanilla.readValue(doc, JsonIgnoreBean.class);
+ JsonIgnoreBean m = mapper.readValue(doc, JsonIgnoreBean.class);
+ assertEngaged(seen, JsonIgnoreBean.class);
+ assertEquals(v.i, m.i);
+ assertNull(m.hidden);
+ }
+
+ @Test
+ public void testUnknownStillReportedWithIgnoredSet() throws Exception {
+ // An unknown name outside the ignored set still runs the stock
+ // unknown handling (throws under per-call FAIL_ON_UNKNOWN).
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'i':1,'other':true}");
+ Exception ve = assertThrows(Exception.class,
+ () -> vanilla.reader(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .forType(IgnoredSetBean.class).readValue(doc));
+ Exception me = assertThrows(Exception.class,
+ () -> mapper.reader(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .forType(IgnoredSetBean.class).readValue(doc));
+ assertEngaged(seen, IgnoredSetBean.class);
+ assertEquals(ve.getClass(), me.getClass());
+ assertEquals(message(ve), message(me));
+ }
+
+ @Test
+ public void testIgnoreUnknownRecord() throws Exception {
+ Map, ValueDeserializer>> seen = new ConcurrentHashMap<>();
+ ObjectMapper mapper = capturingMapper(seen);
+ String doc = a2q("{'name':'a','junk':[1,{'x':2}],'count':3}");
+ assertEquals(vanilla.readValue(doc, LenientRecord.class),
+ mapper.readValue(doc, LenientRecord.class));
+ assertEngaged(seen, LenientRecord.class);
+ }
+}
diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecMatcherTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecMatcherTest.java
new file mode 100644
index 000000000..75a285cfe
--- /dev/null
+++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecMatcherTest.java
@@ -0,0 +1,171 @@
+package tools.jackson.module.blackbird.deser;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.DeserializationConfig;
+import tools.jackson.databind.MapperFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.deser.ValueDeserializerModifier;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.module.blackbird.BlackbirdModule;
+import tools.jackson.module.blackbird.BlackbirdTestBase;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Name-matching parity: case-insensitive mappers use the core CI matcher
+ * instead of demoting, and property aliases map to the same generated arm the
+ * way stock BeanPropertyMap appends them. Every case compares against vanilla.
+ */
+public class BBCodecMatcherTest extends BlackbirdTestBase
+{
+ public static class AliasBean {
+ @JsonAlias({ "identifier", "the_id" })
+ public int id;
+ public String name;
+ }
+
+ public record AliasRecord(@JsonAlias("nm") @JsonProperty(required = true) String name,
+ int count) { }
+
+ public static class PlainBean {
+ public int count;
+ public String label;
+ }
+
+ @JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
+ public static class FormatCIBean {
+ public int count;
+ public String label;
+ }
+
+ private final ObjectMapper vanilla = newVanillaJSONMapper();
+
+ private static ObjectMapper capturingMapper(Map, ValueDeserializer>> seen,
+ boolean caseInsensitive) {
+ SimpleModule capture = new SimpleModule("capture") {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public void setupModule(SetupContext ctxt) {
+ super.setupModule(ctxt);
+ ctxt.addDeserializerModifier(new ValueDeserializerModifier() {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public ValueDeserializer> modifyDeserializer(DeserializationConfig cfg,
+ BeanDescription.Supplier ref, ValueDeserializer> d) {
+ seen.put(ref.getBeanClass(), d);
+ return d;
+ }
+ });
+ }
+ };
+ JsonMapper.Builder b =
+ JsonMapper.builder()
+ .addModule(capture)
+ .addModule(new BlackbirdModule());
+ if (caseInsensitive) {
+ b = b.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
+ }
+ return b.build();
+ }
+
+ private static void assertEngaged(Map, ValueDeserializer>> seen, Class> cls) {
+ ValueDeserializer> d = seen.get(cls);
+ assertNotNull(d, "no deserializer captured for " + cls);
+ assertEquals("BBReaderPlaceholder", d.getClass().getSimpleName(),
+ cls.getSimpleName() + " did not engage a codec");
+ }
+
+ @Test
+ public void testCaseInsensitiveMapper() throws Exception {
+ Map