diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/apacheimpl/AvroRecyclerPools.java b/avro/src/main/java/tools/jackson/dataformat/avro/apacheimpl/AvroRecyclerPools.java index 627bac1f9..d85db06ec 100644 --- a/avro/src/main/java/tools/jackson/dataformat/avro/apacheimpl/AvroRecyclerPools.java +++ b/avro/src/main/java/tools/jackson/dataformat/avro/apacheimpl/AvroRecyclerPools.java @@ -7,7 +7,6 @@ import tools.jackson.core.util.RecyclerPool; import tools.jackson.core.util.RecyclerPool.BoundedPoolBase; import tools.jackson.core.util.RecyclerPool.ConcurrentDequePoolBase; -import tools.jackson.core.util.RecyclerPool.LockFreePoolBase; public final class AvroRecyclerPools { @@ -58,24 +57,6 @@ public static RecyclerPool newConcurrentDequePool() { return ConcurrentDequePool.construct(); } - /** - * Accessor for getting the shared/global {@link LockFreePool} instance. - * - * @return Globally shared instance of {@link LockFreePool}. - */ - public static RecyclerPool sharedLockFreePool() { - return LockFreePool.GLOBAL; - } - - /** - * Accessor for constructing a new, non-shared {@link LockFreePool} instance. - * - * @return Globally shared instance of {@link LockFreePool}. - */ - public static RecyclerPool newLockFreePool() { - return LockFreePool.construct(); - } - /** * Accessor for getting the shared/global {@link BoundedPool} instance. * @@ -195,42 +176,6 @@ protected Object readResolve() { } } - /** - * {@link RecyclerPool} implementation that uses - * a lock free linked list for recycling instances. - *

- * Pool is unbounded: see {@link RecyclerPool} for - * details on what this means. - */ - public static class LockFreePool extends LockFreePoolBase - { - private static final long serialVersionUID = 1L; - - protected static final LockFreePool GLOBAL = new LockFreePool(SERIALIZATION_SHARED); - - // // // Life-cycle (constructors, factory methods) - - protected LockFreePool(int serialization) { - super(serialization); - } - - public static LockFreePool construct() { - return new LockFreePool(SERIALIZATION_NON_SHARED); - } - - @Override - public ApacheCodecRecycler createPooled() { - return new ApacheCodecRecycler(); - } - - // // // JDK serialization support - - // Make sure to re-link to global/shared or non-shared. - protected Object readResolve() { - return _resolveToShared(GLOBAL).orElseGet(() -> construct()); - } - } - /** * {@link RecyclerPool} implementation that uses * a bounded queue ({@link ArrayBlockingQueue} for recycling instances. diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/schema/AvroSchemaGenerator.java b/avro/src/main/java/tools/jackson/dataformat/avro/schema/AvroSchemaGenerator.java index 53f7ab16e..2844d5a1a 100644 --- a/avro/src/main/java/tools/jackson/dataformat/avro/schema/AvroSchemaGenerator.java +++ b/avro/src/main/java/tools/jackson/dataformat/avro/schema/AvroSchemaGenerator.java @@ -31,4 +31,17 @@ public AvroSchemaGenerator disableLogicalTypes() { super.disableLogicalTypes(); return this; } + + @Override + public AvroSchemaGenerator enableWriteEnumAsString() { + super.enableWriteEnumAsString(); + return this; + } + + @Override + public AvroSchemaGenerator disableWriteEnumAsString() { + super.disableWriteEnumAsString(); + return this; + } + } diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/schema/EnumVisitor.java b/avro/src/main/java/tools/jackson/dataformat/avro/schema/EnumVisitor.java new file mode 100644 index 000000000..7840ca4ea --- /dev/null +++ b/avro/src/main/java/tools/jackson/dataformat/avro/schema/EnumVisitor.java @@ -0,0 +1,48 @@ +package tools.jackson.dataformat.avro.schema; + +import tools.jackson.databind.*; +import tools.jackson.databind.introspect.AnnotatedClass; +import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; + +import org.apache.avro.Schema; + +import java.util.ArrayList; +import java.util.Set; + +/** + * Specific visitor for Java Enum types that are to be exposed as + * Avro Enums. Used unless Java Enums are to be mapped to Avro Strings. + */ +public class EnumVisitor extends JsonStringFormatVisitor.Base + implements SchemaBuilder +{ + protected final SerializerProvider _provider; + protected final JavaType _type; + protected final DefinedSchemas _schemas; + + protected Set _enums; + + public EnumVisitor(SerializerProvider provider, DefinedSchemas schemas, JavaType t) { + _schemas = schemas; + _type = t; + _provider = provider; + } + + @Override + public void enumTypes(Set enums) { + _enums = enums; + } + + @Override + public Schema builtAvroSchema() { + if (_enums == null) { + throw new IllegalStateException("Possible enum values cannot be null"); + } + + AnnotatedClass annotations = _provider.introspectClassAnnotations(_type); + Schema s = AvroSchemaHelper.createEnumSchema(_provider.getConfig(), _type, + annotations, new ArrayList<>(_enums)); + _schemas.addSchema(_type, s); + return s; + } +} diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/schema/StringVisitor.java b/avro/src/main/java/tools/jackson/dataformat/avro/schema/StringVisitor.java index 841dd01ac..8ad4b7a47 100644 --- a/avro/src/main/java/tools/jackson/dataformat/avro/schema/StringVisitor.java +++ b/avro/src/main/java/tools/jackson/dataformat/avro/schema/StringVisitor.java @@ -1,7 +1,6 @@ package tools.jackson.dataformat.avro.schema; -import java.util.ArrayList; -import java.util.Set; +import java.util.*; import org.apache.avro.Schema; @@ -19,13 +18,9 @@ public class StringVisitor extends JsonStringFormatVisitor.Base { protected final SerializerProvider _provider; protected final JavaType _type; - protected final DefinedSchemas _schemas; - protected Set _enums; - - public StringVisitor(SerializerProvider provider, DefinedSchemas schemas, JavaType t) { - _schemas = schemas; - _type = t; + public StringVisitor(SerializerProvider provider, JavaType type) { + _type = type; _provider = provider; } @@ -36,7 +31,7 @@ public void format(JsonValueFormat format) { @Override public void enumTypes(Set enums) { - _enums = enums; + // Do nothing } @Override @@ -51,12 +46,6 @@ public Schema builtAvroSchema() { return AvroSchemaHelper.createUUIDSchema(); } AnnotatedClass annotations = _provider.introspectClassAnnotations(_type); - if (_enums != null) { - Schema s = AvroSchemaHelper.createEnumSchema(_provider.getConfig(), _type, - annotations, new ArrayList<>(_enums)); - _schemas.addSchema(_type, s); - return s; - } Schema schema = Schema.create(Schema.Type.STRING); // Stringable classes need to include the type if (AvroSchemaHelper.isStringable(annotations) && !_type.hasRawClass(String.class)) { diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java b/avro/src/main/java/tools/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java index e2b1e5a10..80b21a7f7 100644 --- a/avro/src/main/java/tools/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java +++ b/avro/src/main/java/tools/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java @@ -1,5 +1,7 @@ package tools.jackson.dataformat.avro.schema; +import java.time.temporal.Temporal; + import tools.jackson.core.JsonGenerator; import tools.jackson.databind.JavaType; @@ -10,8 +12,6 @@ import org.apache.avro.Schema; -import java.time.temporal.Temporal; - public class VisitorFormatWrapperImpl implements JsonFormatVisitorWrapper { @@ -19,11 +19,10 @@ public class VisitorFormatWrapperImpl protected final DefinedSchemas _schemas; - /** - * @since 2.13 - */ protected boolean _logicalTypesEnabled = false; + protected boolean _writeEnumAsString = false; + /** * Visitor used for resolving actual Schema, if structured type * (or one with complex configuration) @@ -97,6 +96,8 @@ public Schema getAvroSchema() { /** * Enables Avro schema with Logical Types generation. + * + * @since 2.13 */ public VisitorFormatWrapperImpl enableLogicalTypes() { _logicalTypesEnabled = true; @@ -105,6 +106,8 @@ public VisitorFormatWrapperImpl enableLogicalTypes() { /** * Disables Avro schema with Logical Types generation. + * + * @since 2.13 */ public VisitorFormatWrapperImpl disableLogicalTypes() { _logicalTypesEnabled = false; @@ -115,6 +118,31 @@ public boolean isLogicalTypesEnabled() { return _logicalTypesEnabled; } + /** + * Enable Java enum to Avro string mapping. + * + * @since 2.18 + */ + public VisitorFormatWrapperImpl enableWriteEnumAsString() { + _writeEnumAsString = true; + return this; + } + + /** + * Disable Java enum to Avro string mapping. + * + * @since 2.18 + */ + public VisitorFormatWrapperImpl disableWriteEnumAsString() { + _writeEnumAsString = false; + return this; + } + + // @since 2.18 + public boolean isWriteEnumAsStringEnabled() { + return _writeEnumAsString; + } + /* /********************************************************************** /* Callbacks @@ -169,7 +197,16 @@ public JsonStringFormatVisitor expectStringFormat(JavaType type) _valueSchema = s; return null; } - StringVisitor v = new StringVisitor(_provider, _schemas, type); + + // 06-Jun-2024: [dataformats-binary#494] Enums may be exposed either + // as native Avro Enums, or as Avro Strings: + if (type.isEnumType() && !isWriteEnumAsStringEnabled()) { + EnumVisitor v = new EnumVisitor(_provider, _schemas, type); + _builder = v; + return v; + } + + StringVisitor v = new StringVisitor(_provider, type); _builder = v; return v; } diff --git a/avro/src/test/java/tools/jackson/dataformat/avro/EnumTest.java b/avro/src/test/java/tools/jackson/dataformat/avro/EnumTest.java index da17d3884..ca6d90e05 100644 --- a/avro/src/test/java/tools/jackson/dataformat/avro/EnumTest.java +++ b/avro/src/test/java/tools/jackson/dataformat/avro/EnumTest.java @@ -2,7 +2,8 @@ public class EnumTest extends AvroTestBase { - protected final static String ENUM_SCHEMA_JSON = "{\n" + // gender as Avro enum + protected final static String ENUM_SCHEMA_JSON = "{\n" +"\"type\": \"record\",\n" +"\"name\": \"Employee\",\n" +"\"fields\": [\n" @@ -11,8 +12,16 @@ public class EnumTest extends AvroTestBase +"}\n" +"]}"; - protected enum Gender { M, F; } - + // gender as Avro string + protected final static String STRING_SCHEMA_JSON = "{" + +" \"type\": \"record\", " + +" \"name\": \"Employee\", " + +" \"fields\": [" + +" {\"name\": \"gender\", \"type\": \"string\"}" + +"]}"; + + protected enum Gender { M, F; } + protected static class Employee { public Gender gender; } @@ -21,9 +30,9 @@ protected static class EmployeeStr { public String gender; } - private final AvroMapper MAPPER = new AvroMapper(); - - public void testSimple() throws Exception + private final AvroMapper MAPPER = newMapper(); + + public void test_avroSchemaWithEnum_fromEnumValueToEnumValue() throws Exception { AvroSchema schema = MAPPER.schemaFrom(ENUM_SCHEMA_JSON); Employee input = new Employee(); @@ -31,6 +40,8 @@ public void testSimple() throws Exception byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input); assertNotNull(bytes); + // Enum Gender.M is encoded as bytes array: {0}, where DEC 0 is encoded long value 0, Gender.M ordinal value + // Enum Gender.F is encoded as bytes array: {2}, where DEC 2 is encoded long value 1, Gender.F ordinal value assertEquals(1, bytes.length); // measured to be current exp size // and then back @@ -40,7 +51,7 @@ public void testSimple() throws Exception assertEquals(Gender.F, output.gender); } - public void testEnumValueAsString() throws Exception + public void test_avroSchemaWithEnum_fromStringValueToEnumValue() throws Exception { AvroSchema schema = MAPPER.schemaFrom(ENUM_SCHEMA_JSON); EmployeeStr input = new EmployeeStr(); @@ -56,4 +67,55 @@ public void testEnumValueAsString() throws Exception assertNotNull(output); assertEquals(Gender.F, output.gender); } + + public void test_avroSchemaWithString_fromEnumValueToEnumValue() throws Exception + { + AvroSchema schema = MAPPER.schemaFrom(STRING_SCHEMA_JSON); + Employee input = new Employee(); + input.gender = Gender.F; + + byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input); + assertNotNull(bytes); + // Enum Gender.F as string is encoded as {2, 70} bytes array. + // Where + // - DEC 2, HEX 0x2, is a long value 1 written using variable-length zig-zag coding. + // It represents number of following characters in string "F" + // - DEC 70, HEX 0x46, is UTF-8 code for letter F + // + // Enum Gender.M as string is encoded as {2, 77} bytes array. + // Where + // - DEC 2, HEX 0x2, is a long value 1. It is number of following characters in string "M"), + // written using variable-length zig-zag coding. + // - DEC 77, HEX 0x4D, is UTF-8 code for letter M + // + // See https://avro.apache.org/docs/1.8.2/spec.html#Encodings + assertEquals(2, bytes.length); // measured to be current exp size + assertEquals(0x2, bytes[0]); + assertEquals(0x46, bytes[1]); + + // and then back + Employee output = MAPPER.readerFor(Employee.class).with(schema) + .readValue(bytes); + assertNotNull(output); + assertEquals(Gender.F, output.gender); + } + + // Not sure this test makes sense + public void test_avroSchemaWithString_fromStringValueToEnumValue() throws Exception + { + AvroSchema schema = MAPPER.schemaFrom(STRING_SCHEMA_JSON); + EmployeeStr input = new EmployeeStr(); + input.gender = "F"; + + byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input); + assertNotNull(bytes); + assertEquals(2, bytes.length); // measured to be current exp size + + // and then back + Employee output = MAPPER.readerFor(Employee.class).with(schema) + .readValue(bytes); + assertNotNull(output); + assertEquals(Gender.F, output.gender); + } + } diff --git a/avro/src/test/java/tools/jackson/dataformat/avro/schema/Enum_schemaCreationTest.java b/avro/src/test/java/tools/jackson/dataformat/avro/schema/Enum_schemaCreationTest.java new file mode 100644 index 000000000..0deb79e85 --- /dev/null +++ b/avro/src/test/java/tools/jackson/dataformat/avro/schema/Enum_schemaCreationTest.java @@ -0,0 +1,56 @@ +package tools.jackson.dataformat.avro.schema; + +import org.junit.Test; + +import tools.jackson.dataformat.avro.AvroMapper; +import tools.jackson.dataformat.avro.AvroTestBase; + +import org.apache.avro.Schema; +import org.apache.avro.specific.SpecificData; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Enum_schemaCreationTest extends AvroTestBase { + + static enum NumbersEnum { + ONE, TWO, THREE + } + + private final AvroMapper MAPPER = newMapper(); + + @Test + public void testJavaEnumToAvroEnum_test() throws Exception { + // GIVEN + AvroSchemaGenerator gen = new AvroSchemaGenerator(); + + // WHEN + MAPPER.acceptJsonFormatVisitor(NumbersEnum.class , gen); + Schema actualSchema = gen.getGeneratedSchema().getAvroSchema(); + + //System.out.println("schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo( Schema.Type.ENUM); + assertThat(actualSchema.getEnumSymbols()).containsExactlyInAnyOrder("ONE", "TWO", "THREE"); + } + + @Test + public void testJavaEnumToAvroString_test() throws Exception { + // GIVEN + AvroSchemaGenerator gen = new AvroSchemaGenerator() + .enableWriteEnumAsString(); + + // WHEN + MAPPER.acceptJsonFormatVisitor(NumbersEnum.class , gen); + Schema actualSchema = gen.getGeneratedSchema().getAvroSchema(); + + //System.out.println("schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo( Schema.Type.STRING); + + // When type is stringable then java-class property is addded. + assertThat(actualSchema.getProp(SpecificData.CLASS_PROP)).isNotEmpty(); + } + +} diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 9eb32cf43..9d8827c10 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -222,6 +222,9 @@ Michal Foksa (MichalFoksa@github) * Contributed #310: (avro) Avro schema generation: allow override namespace with new `@AvroNamespace` annotation (2.14.0) +* Contributed #494: Avro Schema generation: allow mapping Java Enum properties to + Avro String values + (2.18.0) Hunter Herman (hherman1@github) @@ -325,6 +328,11 @@ Yoann Vernageau (@yvrng) when source is an empty `InputStream` (2.17.1) -PJ Fanning (pjfanning@github) +PJ Fanning (@pjfanning) * Contributed #484: Rework synchronization in `ProtobufMapper` (2.18.0) + +Joachim Lous (@jlous) + * Requested #494: Avro Schema generation: allow mapping Java Enum properties to + Avro String values + (2.18.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 9b2c1f8f4..e6c0c73fc 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -18,6 +18,10 @@ Active maintainers: #484: (protobuf) Rework synchronization in `ProtobufMapper` (contributed by @pjfanning) +#494: Avro Schema generation: allow mapping Java Enum properties to + Avro String values + (requested by Joachim L) + (contributed by Michal F) 2.17.1 (04-May-2024)