diff --git a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/MapConsumer.java b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/MapConsumer.java index 6223650ff2..aa9ea88d9d 100644 --- a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/MapConsumer.java +++ b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/MapConsumer.java @@ -27,7 +27,6 @@ import org.apache.arrow.memory.BufferAllocator; import org.apache.arrow.vector.complex.MapVector; import org.apache.arrow.vector.complex.impl.UnionMapWriter; -import org.apache.arrow.vector.util.ObjectMapperFactory; /** * Consumer which consume map type values from {@link ResultSet}. Write the data into {@link @@ -36,7 +35,7 @@ public class MapConsumer extends BaseConsumer { private final UnionMapWriter writer; - private final ObjectMapper objectMapper = ObjectMapperFactory.newObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); private final TypeReference> typeReference = new TypeReference>() {}; private int currentRow; diff --git a/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/JdbcToArrowTestHelper.java b/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/JdbcToArrowTestHelper.java index 74b1ca34d7..aba9909e89 100644 --- a/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/JdbcToArrowTestHelper.java +++ b/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/JdbcToArrowTestHelper.java @@ -56,7 +56,6 @@ import org.apache.arrow.vector.types.pojo.Schema; import org.apache.arrow.vector.util.JsonStringArrayList; import org.apache.arrow.vector.util.JsonStringHashMap; -import org.apache.arrow.vector.util.ObjectMapperFactory; import org.apache.arrow.vector.util.Text; /** @@ -295,7 +294,7 @@ public static void assertMapVectorValues( public static Map[] getMapValues(String[] values, String dataType) { String[] dataArr = getValues(values, dataType); Map[] maps = new Map[dataArr.length]; - ObjectMapper objectMapper = ObjectMapperFactory.newObjectMapper(); + ObjectMapper objectMapper = new ObjectMapper(); TypeReference> typeReference = new TypeReference>() {}; for (int idx = 0; idx < dataArr.length; idx++) { String jsonString = dataArr[idx].replace("|", ","); diff --git a/vector/pom.xml b/vector/pom.xml index 9b40e8820c..2e8fbd05bd 100644 --- a/vector/pom.xml +++ b/vector/pom.xml @@ -45,23 +45,6 @@ under the License. com.fasterxml.jackson.core jackson-core - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - commons-codec - commons-codec - 1.22.0 - org.apache.arrow arrow-memory-netty diff --git a/vector/src/main/codegen/templates/ArrowType.java b/vector/src/main/codegen/templates/ArrowType.java index b428f09155..af8fc4fa92 100644 --- a/vector/src/main/codegen/templates/ArrowType.java +++ b/vector/src/main/codegen/templates/ArrowType.java @@ -23,6 +23,8 @@ import com.google.flatbuffers.FlatBufferBuilder; +import java.io.IOException; +import java.util.Locale; import java.util.Objects; import org.apache.arrow.flatbuf.Type; @@ -32,25 +34,12 @@ import org.apache.arrow.vector.FieldVector; import org.apache.arrow.vector.ValueVector; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonGenerator; /** * Arrow types * Source code generated using FreeMarker template ${.template_name} **/ -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "name") -@JsonSubTypes({ -<#list arrowTypes.types as type> - @JsonSubTypes.Type(value = ArrowType.${type.name?remove_ending("_")}.class, name = "${type.name?remove_ending("_")?lower_case}"), - -}) public abstract class ArrowType { public static abstract class PrimitiveType extends ArrowType { @@ -93,13 +82,48 @@ private ArrowTypeID(byte flatbufType) { } } - @JsonIgnore public abstract ArrowTypeID getTypeID(); - @JsonIgnore public abstract boolean isComplex(); public abstract int getType(FlatBufferBuilder builder); public abstract T accept(ArrowTypeVisitor visitor); + /** The lower-case type name used as the JSON discriminator for this type. */ + String getNameAsString() { + return getTypeID().name().toLowerCase(Locale.ROOT); + } + + /** Serializes this type to JSON, as a {@code {"name": ..., ...}} object. */ + void writeJson(JsonGenerator generator) throws IOException { + generator.writeStartObject(); + generator.writeStringField("name", getNameAsString()); + writeJsonFields(generator); + generator.writeEndObject(); + } + + /** Writes the type-specific fields (everything except the discriminator) to JSON. */ + void writeJsonFields(JsonGenerator generator) throws IOException { + } + + /** Constructs an ArrowType from its JSON discriminator name and parsed fields. */ + static ArrowType getArrowType(String name, java.util.Map fields) { + switch (name) { + <#list arrowTypes.types as type> + <#assign typeName = type.name?remove_ending("_")> + case "${type.name?remove_ending("_")?lower_case}": + <#if type.name == "Decimal"> + return new Decimal( + JsonValues.asInt(fields.get("precision")), + JsonValues.asInt(fields.get("scale")), + fields.get("bitWidth") == null ? 128 : JsonValues.asInt(fields.get("bitWidth"))); + <#else> + return new ${typeName}(<#list type.fields as field><#if field.valueType??>JsonValues.parseEnum(${field.valueType}.class, fields.get("${field.name}"))<#elseif field.type == "int">JsonValues.asInt(fields.get("${field.name}"))<#elseif field.type == "boolean">JsonValues.asBoolean(fields.get("${field.name}"))<#elseif field.type == "int[]">JsonValues.asIntArray(fields.get("${field.name}"))<#else>JsonValues.asString(fields.get("${field.name}"))<#if field_has_next>, ); + + + default: + throw new IllegalArgumentException("Unknown type: " + name); + } + } + /** * to visit the ArrowTypes * @@ -170,11 +194,10 @@ public static class ${name} extends <#if type.complex>ComplexType<#else>Primitiv <#if type.name == "Decimal"> // Needed to support golden file integration tests. - @JsonCreator public static Decimal createDecimal( - @JsonProperty("precision") int precision, - @JsonProperty("scale") int scale, - @JsonProperty("bitWidth") Integer bitWidth) { + int precision, + int scale, + Integer bitWidth) { return new Decimal(precision, scale, bitWidth == null ? 128 : bitWidth); } @@ -192,13 +215,11 @@ public Decimal(int precision, int scale) { this(precision, scale, 128); } - <#else> - @JsonCreator public ${type.name}( <#list type.fields as field> <#assign fieldType = field.valueType!field.type> - @JsonProperty("${field.name}") ${fieldType} ${field.name}<#if field_has_next>, + ${fieldType} ${field.name}<#if field_has_next>, ) { <#list type.fields as field> @@ -212,6 +233,29 @@ public Decimal(int precision, int scale) { return ${field.name}; } + + @Override + void writeJsonFields(JsonGenerator generator) throws IOException { + <#list type.fields as field> + <#if field.valueType??> + generator.writeStringField("${field.name}", ${field.name} == null ? null : ${field.name}.name()); + <#elseif field.type == "int"> + generator.writeNumberField("${field.name}", ${field.name}); + <#elseif field.type == "boolean"> + generator.writeBooleanField("${field.name}", ${field.name}); + <#elseif field.type == "int[]"> + generator.writeArrayFieldStart("${field.name}"); + if (${field.name} != null) { + for (int value : ${field.name}) { + generator.writeNumber(value); + } + } + generator.writeEndArray(); + <#else> + generator.writeStringField("${field.name}", ${field.name}); + + + } @Override @@ -312,6 +356,11 @@ public int getType(FlatBufferBuilder builder) { return storageType().getType(builder); } + @Override + void writeJson(JsonGenerator generator) throws IOException { + storageType().writeJson(generator); + } + public String toString() { return "ExtensionType(" + extensionName() + ", " + storageType().toString() + ")"; } diff --git a/vector/src/main/java/module-info.java b/vector/src/main/java/module-info.java index 8ba1b3579e..c2748d92de 100644 --- a/vector/src/main/java/module-info.java +++ b/vector/src/main/java/module-info.java @@ -35,18 +35,11 @@ exports org.apache.arrow.vector.util; exports org.apache.arrow.vector.validate; - opens org.apache.arrow.vector.types.pojo to - com.fasterxml.jackson.databind; - - requires com.fasterxml.jackson.annotation; requires com.fasterxml.jackson.core; - requires com.fasterxml.jackson.databind; - requires com.fasterxml.jackson.datatype.jsr310; requires flatbuffers.java; requires jdk.unsupported; requires org.apache.arrow.format; requires org.apache.arrow.memory.core; - requires org.apache.commons.codec; requires org.slf4j; uses org.apache.arrow.vector.compression.CompressionCodec.Factory; diff --git a/vector/src/main/java/org/apache/arrow/vector/extension/OpaqueType.java b/vector/src/main/java/org/apache/arrow/vector/extension/OpaqueType.java index 780a4ee659..f901dc824a 100644 --- a/vector/src/main/java/org/apache/arrow/vector/extension/OpaqueType.java +++ b/vector/src/main/java/org/apache/arrow/vector/extension/OpaqueType.java @@ -16,10 +16,12 @@ */ package org.apache.arrow.vector.extension; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.io.StringWriter; import java.util.Collections; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -72,6 +74,7 @@ */ public class OpaqueType extends ArrowType.ExtensionType { private static final AtomicBoolean registered = new AtomicBoolean(false); + private static final JsonFactory JSON_FACTORY = new JsonFactory(); public static final String EXTENSION_NAME = "arrow.opaque"; private final ArrowType storageType; private final String typeName; @@ -128,41 +131,85 @@ public boolean extensionEquals(ExtensionType other) { @Override public String serialize() { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode object = mapper.createObjectNode(); - object.put("type_name", typeName); - object.put("vendor_name", vendorName); - try { - return mapper.writeValueAsString(object); - } catch (JsonProcessingException e) { + try (StringWriter writer = new StringWriter(); + JsonGenerator generator = JSON_FACTORY.createGenerator(writer)) { + generator.writeStartObject(); + generator.writeStringField("type_name", typeName); + generator.writeStringField("vendor_name", vendorName); + generator.writeEndObject(); + generator.flush(); + return writer.toString(); + } catch (IOException e) { throw new RuntimeException("Could not serialize " + this, e); } } @Override public ArrowType deserialize(ArrowType storageType, String serializedData) { - ObjectMapper mapper = new ObjectMapper(); - JsonNode object; + java.util.Map object; try { - object = mapper.readTree(serializedData); - } catch (JsonProcessingException e) { + object = parseObject(serializedData); + } catch (IOException e) { throw new InvalidExtensionMetadataException("Extension metadata is invalid", e); } - JsonNode typeName = object.get("type_name"); - JsonNode vendorName = object.get("vendor_name"); - if (typeName == null) { + if (object == null) { + throw new InvalidExtensionMetadataException("Extension metadata is invalid"); + } + if (!object.containsKey("type_name")) { throw new InvalidExtensionMetadataException("typeName is missing"); } - if (vendorName == null) { + if (!object.containsKey("vendor_name")) { throw new InvalidExtensionMetadataException("vendorName is missing"); } - if (!typeName.isTextual()) { + Object typeName = object.get("type_name"); + Object vendorName = object.get("vendor_name"); + if (!(typeName instanceof String)) { throw new InvalidExtensionMetadataException("typeName should be string, was " + typeName); } - if (!vendorName.isTextual()) { + if (!(vendorName instanceof String)) { throw new InvalidExtensionMetadataException("vendorName should be string, was " + vendorName); } - return new OpaqueType(storageType, typeName.asText(), vendorName.asText()); + return new OpaqueType(storageType, (String) typeName, (String) vendorName); + } + + /** Parses a JSON object into a generic map, or returns null if the root is not an object. */ + private static java.util.Map parseObject(String serializedData) + throws IOException { + try (JsonParser parser = JSON_FACTORY.createParser(serializedData)) { + if (parser.nextToken() != JsonToken.START_OBJECT) { + return null; + } + java.util.Map result = new java.util.LinkedHashMap<>(); + while (parser.nextToken() != JsonToken.END_OBJECT) { + String name = parser.currentName(); + result.put(name, readScalarOrSkip(parser)); + } + return result; + } + } + + private static Object readScalarOrSkip(JsonParser parser) throws IOException { + JsonToken token = parser.nextToken(); + switch (token) { + case VALUE_STRING: + return parser.getValueAsString(); + case VALUE_NUMBER_INT: + return parser.getLongValue(); + case VALUE_NUMBER_FLOAT: + return parser.getDoubleValue(); + case VALUE_TRUE: + return Boolean.TRUE; + case VALUE_FALSE: + return Boolean.FALSE; + case VALUE_NULL: + return null; + case START_OBJECT: + case START_ARRAY: + parser.skipChildren(); + return new Object(); + default: + return null; + } } @Override diff --git a/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileReader.java b/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileReader.java index e4bab7eb80..50160ece73 100644 --- a/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileReader.java +++ b/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileReader.java @@ -29,13 +29,11 @@ import static org.apache.arrow.vector.BufferLayout.BufferType.VARIADIC_DATA_BUFFERS; import static org.apache.arrow.vector.BufferLayout.BufferType.VIEWS; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser.Feature; import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.MappingJsonFactory; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; import java.math.BigDecimal; @@ -46,6 +44,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HexFormat; import java.util.List; import java.util.Map; import java.util.Objects; @@ -81,8 +80,6 @@ import org.apache.arrow.vector.types.pojo.Schema; import org.apache.arrow.vector.util.DecimalUtility; import org.apache.arrow.vector.util.DictionaryUtility; -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Hex; /** * A reader for JSON files that translates them into vectors. This reader is used for integration @@ -108,11 +105,7 @@ public JsonFileReader(File inputFile, BufferAllocator allocator) throws JsonParseException, IOException { super(); this.allocator = allocator; - MappingJsonFactory jsonFactory = - new MappingJsonFactory( - new ObjectMapper() - // ignore case for enums - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true)); + JsonFactory jsonFactory = new JsonFactory(); this.parser = jsonFactory.createParser(inputFile); // Allow reading NaN for floating point values this.parser.configure(Feature.ALLOW_NON_NUMERIC_NUMBERS, true); @@ -294,7 +287,7 @@ List readVariadicBuffers(BufferAllocator allocator, int variadicBuffer parser.nextToken(); final byte[] value; - String variadicStr = parser.readValueAs(String.class); + String variadicStr = parser.getValueAsString(); if (variadicStr == null) { value = new byte[0]; } else { @@ -390,7 +383,7 @@ protected ArrowBuf read(BufferAllocator allocator, int count) throws IOException for (int i = 0; i < count; i++) { parser.nextToken(); - BitVectorHelper.setValidityBit(buf, i, parser.readValueAs(Boolean.class) ? 1 : 0); + BitVectorHelper.setValidityBit(buf, i, parser.getValueAsBoolean() ? 1 : 0); } buf.writerIndex(bufferSize); @@ -606,7 +599,7 @@ protected ArrowBuf read(BufferAllocator allocator, int count) throws IOException for (int i = 0; i < count; i++) { parser.nextToken(); - BigDecimal decimalValue = new BigDecimal(parser.readValueAs(String.class)); + BigDecimal decimalValue = new BigDecimal(parser.getValueAsString()); DecimalUtility.writeBigDecimalToArrowBuf( decimalValue, buf, i, DecimalVector.TYPE_WIDTH); } @@ -625,7 +618,7 @@ protected ArrowBuf read(BufferAllocator allocator, int count) throws IOException for (int i = 0; i < count; i++) { parser.nextToken(); - BigDecimal decimalValue = new BigDecimal(parser.readValueAs(String.class)); + BigDecimal decimalValue = new BigDecimal(parser.getValueAsString()); DecimalUtility.writeBigDecimalToArrowBuf( decimalValue, buf, i, Decimal256Vector.TYPE_WIDTH); } @@ -640,7 +633,7 @@ ArrowBuf readBinaryValues(BufferAllocator allocator, int count) throws IOExcepti long bufferSize = 0L; for (int i = 0; i < count; i++) { parser.nextToken(); - final byte[] value = decodeHexSafe(parser.readValueAs(String.class)); + final byte[] value = decodeHexSafe(parser.getValueAsString()); values.add(value); bufferSize += value.length; } @@ -952,8 +945,8 @@ private void readFromJsonIntoVector(Field field, FieldVector vector) throws IOEx private byte[] decodeHexSafe(String hexString) throws IOException { try { - return Hex.decodeHex(hexString.toCharArray()); - } catch (DecoderException e) { + return HexFormat.of().parseHex(hexString); + } catch (IllegalArgumentException e) { throw new IOException("Unable to decode hex string: " + hexString, e); } } @@ -972,7 +965,23 @@ private T readNextField(String expectedFieldName, Class c) throws IOException, JsonParseException { nextFieldIs(expectedFieldName); parser.nextToken(); - return parser.readValueAs(c); + return readValueAs(c); + } + + @SuppressWarnings("unchecked") + private T readValueAs(Class c) throws IOException { + if (c == String.class) { + return (T) parser.getValueAsString(); + } else if (c == Integer.class) { + return (T) Integer.valueOf(parser.getIntValue()); + } else if (c == Long.class) { + return (T) Long.valueOf(parser.getLongValue()); + } else if (c == Boolean.class) { + return (T) Boolean.valueOf(parser.getBooleanValue()); + } else if (c == Schema.class) { + return (T) Schema.fromJson(parser); + } + throw new UnsupportedOperationException("Cannot read JSON value of type " + c); } private void nextFieldIs(String expectedFieldName) throws IOException, JsonParseException { diff --git a/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileWriter.java b/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileWriter.java index 68700fe6af..44060424ee 100644 --- a/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileWriter.java +++ b/vector/src/main/java/org/apache/arrow/vector/ipc/JsonFileWriter.java @@ -19,10 +19,10 @@ import static org.apache.arrow.vector.BufferLayout.BufferType.*; import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter.NopIndenter; -import com.fasterxml.jackson.databind.MappingJsonFactory; import java.io.File; import java.io.IOException; import java.math.BigDecimal; @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.HexFormat; import java.util.List; import java.util.Set; import org.apache.arrow.memory.ArrowBuf; @@ -82,7 +83,6 @@ import org.apache.arrow.vector.types.pojo.Schema; import org.apache.arrow.vector.util.DecimalUtility; import org.apache.arrow.vector.util.DictionaryUtility; -import org.apache.commons.codec.binary.Hex; /** * A writer that converts binary Vectors into an internal, unstable JSON format suitable @@ -123,7 +123,7 @@ public JsonFileWriter(File outputFile) throws IOException { /** Constructs a new writer that will output to outputFile with the given options. */ public JsonFileWriter(File outputFile, JSONWriteConfig config) throws IOException { - MappingJsonFactory jsonFactory = new MappingJsonFactory(); + JsonFactory jsonFactory = new JsonFactory(); this.generator = jsonFactory.createGenerator(outputFile, JsonEncoding.UTF8); if (config.pretty) { DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); @@ -147,7 +147,8 @@ public void start(Schema schema, DictionaryProvider provider) throws IOException Schema updatedSchema = new Schema(fields, schema.getCustomMetadata()); generator.writeStartObject(); - generator.writeObjectField("schema", updatedSchema); + generator.writeFieldName("schema"); + updatedSchema.serialize(generator); // Write all dictionaries that were used if (!dictionaryIdsUsed.isEmpty()) { @@ -164,7 +165,7 @@ private void writeDictionaryBatches( generator.writeArrayFieldStart("dictionaries"); for (Long id : dictionaryIdsUsed) { generator.writeStartObject(); - generator.writeObjectField("id", id); + generator.writeNumberField("id", id); generator.writeFieldName("data"); Dictionary dictionary = provider.lookup(id); @@ -190,7 +191,7 @@ public void write(VectorSchemaRoot recordBatch) throws IOException { private void writeBatch(VectorSchemaRoot recordBatch) throws IOException { generator.writeStartObject(); { - generator.writeObjectField("count", recordBatch.getRowCount()); + generator.writeNumberField("count", recordBatch.getRowCount()); generator.writeArrayFieldStart("columns"); for (Field field : recordBatch.getSchema().getFields()) { FieldVector vector = recordBatch.getVector(field); @@ -220,9 +221,9 @@ private void writeFromVectorIntoJson(Field field, FieldVector vector) throws IOE generator.writeStartObject(); { - generator.writeObjectField("name", field.getName()); + generator.writeStringField("name", field.getName()); int valueCount = vector.getValueCount(); - generator.writeObjectField("count", valueCount); + generator.writeNumberField("count", valueCount); for (int v = 0; v < vectorTypes.size(); v++) { BufferType bufferType = vectorTypes.get(v); @@ -375,7 +376,7 @@ private void writeValueToViewGenerator( viewBuffer.getInt( ((long) index * elementSize) + lengthWidth + prefixWidth + bufIndexWidth); generator.writeFieldName("PREFIX_HEX"); - generator.writeString(Hex.encodeHexString(prefix)); + generator.writeString(HexFormat.of().formatHex(prefix)); generator.writeFieldName("BUFFER_INDEX"); generator.writeObject(bufferIndex); generator.writeFieldName("OFFSET"); @@ -385,7 +386,7 @@ private void writeValueToViewGenerator( if (vector.getMinorType() == MinorType.VIEWVARCHAR) { generator.writeString(new String(b, "UTF-8")); } else { - generator.writeString(Hex.encodeHexString(b)); + generator.writeString(HexFormat.of().formatHex(b)); } } generator.writeEndObject(); @@ -403,7 +404,7 @@ private void writeValueToDataBufferGenerator( byte[] result = new byte[(int) dataBuf.writerIndex()]; dataBuf.getBytes(0, result); if (result != null) { - generator.writeString(Hex.encodeHexString(result)); + generator.writeString(HexFormat.of().formatHex(result)); } } } @@ -527,16 +528,16 @@ private void writeValueToGenerator( break; case INTERVALDAY: generator.writeStartObject(); - generator.writeObjectField("days", IntervalDayVector.getDays(buffer, index)); - generator.writeObjectField( + generator.writeNumberField("days", IntervalDayVector.getDays(buffer, index)); + generator.writeNumberField( "milliseconds", IntervalDayVector.getMilliseconds(buffer, index)); generator.writeEndObject(); break; case INTERVALMONTHDAYNANO: generator.writeStartObject(); - generator.writeObjectField("months", IntervalMonthDayNanoVector.getMonths(buffer, index)); - generator.writeObjectField("days", IntervalMonthDayNanoVector.getDays(buffer, index)); - generator.writeObjectField( + generator.writeNumberField("months", IntervalMonthDayNanoVector.getMonths(buffer, index)); + generator.writeNumberField("days", IntervalMonthDayNanoVector.getDays(buffer, index)); + generator.writeNumberField( "nanoseconds", IntervalMonthDayNanoVector.getNanoseconds(buffer, index)); generator.writeEndObject(); break; @@ -547,14 +548,14 @@ private void writeValueToGenerator( { Preconditions.checkNotNull(offsetBuffer); String hexString = - Hex.encodeHexString(BaseVariableWidthVector.get(buffer, offsetBuffer, index)); + HexFormat.of().formatHex(BaseVariableWidthVector.get(buffer, offsetBuffer, index)); generator.writeObject(hexString); break; } case FIXEDSIZEBINARY: int byteWidth = ((FixedSizeBinaryVector) vector).getByteWidth(); String fixedSizeHexString = - Hex.encodeHexString(FixedSizeBinaryVector.get(buffer, index, byteWidth)); + HexFormat.of().formatHex(FixedSizeBinaryVector.get(buffer, index, byteWidth)); generator.writeObject(fixedSizeHexString); break; case VARCHAR: diff --git a/vector/src/main/java/org/apache/arrow/vector/types/pojo/DictionaryEncoding.java b/vector/src/main/java/org/apache/arrow/vector/types/pojo/DictionaryEncoding.java index b5e5d8c3ae..6966422e1e 100644 --- a/vector/src/main/java/org/apache/arrow/vector/types/pojo/DictionaryEncoding.java +++ b/vector/src/main/java/org/apache/arrow/vector/types/pojo/DictionaryEncoding.java @@ -16,9 +16,9 @@ */ package org.apache.arrow.vector.types.pojo; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import java.io.IOException; +import java.util.Map; import java.util.Objects; import org.apache.arrow.vector.types.pojo.ArrowType.Int; @@ -37,11 +37,7 @@ public class DictionaryEncoding { * @param indexType (nullable). The integer type to use for indexing in the dictionary. Defaults * to a signed 32 bit integer. */ - @JsonCreator - public DictionaryEncoding( - @JsonProperty("id") long id, - @JsonProperty("isOrdered") boolean ordered, - @JsonProperty("indexType") Int indexType) { + public DictionaryEncoding(long id, boolean ordered, Int indexType) { this.id = id; this.ordered = ordered; this.indexType = indexType == null ? new Int(32, true) : indexType; @@ -51,7 +47,6 @@ public long getId() { return id; } - @JsonGetter("isOrdered") public boolean isOrdered() { return ordered; } @@ -60,6 +55,32 @@ public Int getIndexType() { return indexType; } + /** Serializes this encoding to JSON. */ + void serialize(JsonGenerator generator) throws IOException { + generator.writeStartObject(); + generator.writeNumberField("id", id); + generator.writeBooleanField("isOrdered", ordered); + generator.writeFieldName("indexType"); + indexType.writeJson(generator); + generator.writeEndObject(); + } + + /** Builds a DictionaryEncoding from a parsed JSON object. */ + @SuppressWarnings("unchecked") + static DictionaryEncoding fromJson(Map object) { + Object indexTypeObj = object.get("indexType"); + Int indexType = null; + if (indexTypeObj != null) { + Map indexTypeMap = (Map) indexTypeObj; + indexType = + (Int) ArrowType.getArrowType(JsonValues.asString(indexTypeMap.get("name")), indexTypeMap); + } + return new DictionaryEncoding( + JsonValues.asLong(object.get("id")), + JsonValues.asBoolean(object.get("isOrdered")), + indexType); + } + @Override public String toString() { return "DictionaryEncoding[id=" + id + ",ordered=" + ordered + ",indexType=" + indexType + "]"; diff --git a/vector/src/main/java/org/apache/arrow/vector/types/pojo/Field.java b/vector/src/main/java/org/apache/arrow/vector/types/pojo/Field.java index d65ef1bee7..31709bfd45 100644 --- a/vector/src/main/java/org/apache/arrow/vector/types/pojo/Field.java +++ b/vector/src/main/java/org/apache/arrow/vector/types/pojo/Field.java @@ -21,12 +21,9 @@ import static org.apache.arrow.vector.types.pojo.ArrowType.getTypeForField; import static org.apache.arrow.vector.types.pojo.Schema.convertMetadata; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; import com.google.flatbuffers.FlatBufferBuilder; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -76,15 +73,33 @@ private Field( this(name, new FieldType(nullable, type, dictionary, metadata), children); } - @JsonCreator - private Field( - @JsonProperty("name") String name, - @JsonProperty("nullable") boolean nullable, - @JsonProperty("type") ArrowType type, - @JsonProperty("dictionary") DictionaryEncoding dictionary, - @JsonProperty("children") List children, - @JsonProperty("metadata") List> metadata) { - this(name, new FieldType(nullable, type, dictionary, convertMetadata(metadata)), children); + /** Builds a Field from a parsed JSON object. */ + @SuppressWarnings("unchecked") + static Field fromJson(Map object) { + String name = JsonValues.asString(object.get("name")); + boolean nullable = JsonValues.asBoolean(object.get("nullable")); + + Map typeObject = (Map) object.get("type"); + ArrowType type = + ArrowType.getArrowType(JsonValues.asString(typeObject.get("name")), typeObject); + + DictionaryEncoding dictionary = null; + Object dictionaryObject = object.get("dictionary"); + if (dictionaryObject != null) { + dictionary = DictionaryEncoding.fromJson((Map) dictionaryObject); + } + + List children = new ArrayList<>(); + Object childrenObject = object.get("children"); + if (childrenObject != null) { + for (Object child : (List) childrenObject) { + children.add(fromJson((Map) child)); + } + } + + Map metadata = + convertMetadata((List>) object.get("metadata")); + return new Field(name, nullable, type, dictionary, children, metadata); } /** @@ -240,12 +255,10 @@ public ArrowType getType() { return fieldType.getType(); } - @JsonIgnore public FieldType getFieldType() { return fieldType; } - @JsonInclude(Include.NON_NULL) public DictionaryEncoding getDictionary() { return fieldType.getDictionary(); } @@ -254,17 +267,38 @@ public List getChildren() { return children; } - @JsonIgnore public Map getMetadata() { return fieldType.getMetadata(); } - @JsonProperty("metadata") - @JsonInclude(Include.NON_EMPTY) List> getMetadataForJson() { return convertMetadata(getMetadata()); } + /** Serializes this field to JSON. */ + void serialize(JsonGenerator generator) throws IOException { + generator.writeStartObject(); + generator.writeStringField("name", name); + generator.writeBooleanField("nullable", isNullable()); + generator.writeFieldName("type"); + getType().writeJson(generator); + DictionaryEncoding dictionary = getDictionary(); + if (dictionary != null) { + generator.writeFieldName("dictionary"); + dictionary.serialize(generator); + } + generator.writeArrayFieldStart("children"); + for (Field child : children) { + child.serialize(generator); + } + generator.writeEndArray(); + List> metadata = getMetadataForJson(); + if (metadata != null && !metadata.isEmpty()) { + Schema.serializeMetadata(generator, metadata); + } + generator.writeEndObject(); + } + @Override public int hashCode() { return Objects.hash(name, isNullable(), getType(), getDictionary(), getMetadata(), children); diff --git a/vector/src/main/java/org/apache/arrow/vector/types/pojo/JsonValues.java b/vector/src/main/java/org/apache/arrow/vector/types/pojo/JsonValues.java new file mode 100644 index 0000000000..e6dd1229de --- /dev/null +++ b/vector/src/main/java/org/apache/arrow/vector/types/pojo/JsonValues.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.vector.types.pojo; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Helpers for reading a JSON document into a generic tree of {@link Map}, {@link List}, {@link + * String}, {@link Long}, {@link Double}, {@link Boolean} and {@code null} values, using only + * Jackson's streaming API (jackson-core). Used by the POJO schema (de)serialization to avoid a + * dependency on jackson-databind. + */ +final class JsonValues { + + private JsonValues() {} + + /** + * Reads the value at the parser's current token into a generic Java object. After this call the + * parser is positioned on the last token of the value (e.g. END_OBJECT / END_ARRAY / the scalar + * token), matching the behavior of {@code ObjectMapper.readValue}. + */ + static Object readValue(JsonParser parser) throws IOException { + JsonToken token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + switch (token) { + case START_OBJECT: + return readObject(parser); + case START_ARRAY: + return readArray(parser); + case VALUE_STRING: + return parser.getValueAsString(); + case VALUE_NUMBER_INT: + return parser.getLongValue(); + case VALUE_NUMBER_FLOAT: + return parser.getDoubleValue(); + case VALUE_TRUE: + return Boolean.TRUE; + case VALUE_FALSE: + return Boolean.FALSE; + case VALUE_NULL: + return null; + default: + throw new IOException("Unexpected JSON token: " + token); + } + } + + private static Map readObject(JsonParser parser) throws IOException { + Map result = new LinkedHashMap<>(); + while (parser.nextToken() != JsonToken.END_OBJECT) { + String name = parser.currentName(); + parser.nextToken(); + result.put(name, readValue(parser)); + } + return result; + } + + private static List readArray(JsonParser parser) throws IOException { + List result = new ArrayList<>(); + while (parser.nextToken() != JsonToken.END_ARRAY) { + result.add(readValue(parser)); + } + return result; + } + + static boolean asBoolean(Object value) { + return Boolean.TRUE.equals(value); + } + + static int asInt(Object value) { + return ((Number) value).intValue(); + } + + static long asLong(Object value) { + return ((Number) value).longValue(); + } + + static String asString(Object value) { + return value == null ? null : value.toString(); + } + + static int[] asIntArray(Object value) { + if (value == null) { + return null; + } + List list = (List) value; + int[] result = new int[list.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = ((Number) list.get(i)).intValue(); + } + return result; + } + + /** + * Resolves an enum value by name, case-insensitively (matching the previous Jackson behavior). + */ + static > E parseEnum(Class clazz, Object value) { + String name = asString(value); + for (E constant : clazz.getEnumConstants()) { + if (constant.name().equalsIgnoreCase(name)) { + return constant; + } + } + throw new IllegalArgumentException("No enum constant " + clazz.getName() + "." + name); + } +} diff --git a/vector/src/main/java/org/apache/arrow/vector/types/pojo/Schema.java b/vector/src/main/java/org/apache/arrow/vector/types/pojo/Schema.java index 293f1499df..8bb4910c8b 100644 --- a/vector/src/main/java/org/apache/arrow/vector/types/pojo/Schema.java +++ b/vector/src/main/java/org/apache/arrow/vector/types/pojo/Schema.java @@ -18,22 +18,18 @@ import static org.apache.arrow.vector.types.pojo.Field.convertField; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.google.flatbuffers.FlatBufferBuilder; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.StringWriter; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; @@ -74,13 +70,42 @@ public static Field findField(List fields, String name) { static final String METADATA_KEY = "key"; static final String METADATA_VALUE = "value"; - private static final ObjectMapper mapper = new ObjectMapper(); - private static final ObjectWriter writer = mapper.writerWithDefaultPrettyPrinter(); - private static final ObjectReader reader = mapper.readerFor(Schema.class); + private static final JsonFactory JSON_FACTORY = new JsonFactory(); private static final boolean LITTLE_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN; + /** Parses a Schema from its JSON representation. */ + @SuppressWarnings("unchecked") public static Schema fromJSON(String json) throws IOException { - return reader.readValue(Preconditions.checkNotNull(json)); + Preconditions.checkNotNull(json); + try (JsonParser parser = JSON_FACTORY.createParser(json)) { + parser.nextToken(); + Map object = (Map) JsonValues.readValue(parser); + return fromJson(object); + } + } + + /** + * Reads a Schema from the current position of the given JSON parser. The parser must be + * positioned at the schema's opening object token. + */ + @SuppressWarnings("unchecked") + public static Schema fromJson(JsonParser parser) throws IOException { + return fromJson((Map) JsonValues.readValue(parser)); + } + + /** Builds a Schema from a parsed JSON object. */ + @SuppressWarnings("unchecked") + static Schema fromJson(Map object) { + List fields = new ArrayList<>(); + Object fieldsObject = object.get("fields"); + if (fieldsObject != null) { + for (Object field : (List) fieldsObject) { + fields.add(Field.fromJson((Map) field)); + } + } + Map metadata = + convertMetadata((List>) object.get("metadata")); + return new Schema(fields, metadata); } /** @@ -101,14 +126,41 @@ public static Schema deserialize(ByteBuffer buffer) { * @return The deserialized schema. */ public static Schema deserializeMessage(ByteBuffer buffer) { - ByteBufferBackedInputStream stream = new ByteBufferBackedInputStream(buffer); - try (ReadChannel channel = new ReadChannel(Channels.newChannel(stream))) { + try (ReadChannel channel = new ReadChannel(byteBufferChannel(buffer))) { return MessageSerializer.deserializeSchema(channel); } catch (IOException ex) { throw new RuntimeException(ex); } } + private static ReadableByteChannel byteBufferChannel(ByteBuffer buffer) { + return new ReadableByteChannel() { + private boolean open = true; + + @Override + public int read(ByteBuffer dst) { + if (!buffer.hasRemaining()) { + return -1; + } + int count = Math.min(dst.remaining(), buffer.remaining()); + for (int i = 0; i < count; i++) { + dst.put(buffer.get()); + } + return count; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public void close() { + open = false; + } + }; + } + /** Converts a flatbuffer schema to its POJO representation. */ public static Schema convertSchema(org.apache.arrow.flatbuf.Schema schema) { List fields = new ArrayList<>(); @@ -141,14 +193,6 @@ public Schema(Iterable fields, Map metadata) { metadata == null ? Collections.emptyMap() : Collections2.immutableMapCopy(metadata)); } - /** Constructor used for JSON deserialization. */ - @JsonCreator - private Schema( - @JsonProperty("fields") Iterable fields, - @JsonProperty("metadata") List> metadata) { - this(fields, convertMetadata(metadata)); - } - /** * Private constructor to bypass automatic collection copy. * @@ -190,13 +234,10 @@ public List getFields() { return fields; } - @JsonIgnore public Map getCustomMetadata() { return metadata; } - @JsonProperty("metadata") - @JsonInclude(Include.NON_EMPTY) List> getCustomMetadataForJson() { return convertMetadata(getCustomMetadata()); } @@ -214,14 +255,47 @@ public Field findField(String name) { /** Returns the JSON string representation of this schema. */ public String toJson() { - try { - return writer.writeValueAsString(this); - } catch (JsonProcessingException e) { + try (StringWriter stringWriter = new StringWriter(); + JsonGenerator generator = JSON_FACTORY.createGenerator(stringWriter)) { + generator.setPrettyPrinter(new DefaultPrettyPrinter()); + serialize(generator); + generator.flush(); + return stringWriter.toString(); + } catch (IOException e) { // this should not happen throw new RuntimeException(e); } } + /** Serializes this schema to JSON. */ + /** Serializes this schema as a JSON object using the given generator. */ + public void serialize(JsonGenerator generator) throws IOException { + generator.writeStartObject(); + generator.writeArrayFieldStart("fields"); + for (Field field : fields) { + field.serialize(generator); + } + generator.writeEndArray(); + List> jsonMetadata = getCustomMetadataForJson(); + if (jsonMetadata != null && !jsonMetadata.isEmpty()) { + serializeMetadata(generator, jsonMetadata); + } + generator.writeEndObject(); + } + + /** Writes a list of key/value metadata entries to JSON as a {@code "metadata"} array. */ + static void serializeMetadata(JsonGenerator generator, List> metadata) + throws IOException { + generator.writeArrayFieldStart("metadata"); + for (Map entry : metadata) { + generator.writeStartObject(); + generator.writeStringField(METADATA_KEY, entry.get(METADATA_KEY)); + generator.writeStringField(METADATA_VALUE, entry.get(METADATA_VALUE)); + generator.writeEndObject(); + } + generator.writeEndArray(); + } + /** Adds this schema to the builder returning the size of the builder after adding. */ public int getSchema(FlatBufferBuilder builder) { int[] fieldOffsets = new int[fields.size()]; diff --git a/vector/src/main/java/org/apache/arrow/vector/util/JsonStringArrayList.java b/vector/src/main/java/org/apache/arrow/vector/util/JsonStringArrayList.java index 119d1dfc23..0c154eedfa 100644 --- a/vector/src/main/java/org/apache/arrow/vector/util/JsonStringArrayList.java +++ b/vector/src/main/java/org/apache/arrow/vector/util/JsonStringArrayList.java @@ -16,8 +16,6 @@ */ package org.apache.arrow.vector.util; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; /** @@ -28,7 +26,9 @@ */ public class JsonStringArrayList extends ArrayList { - private static final ObjectMapper MAPPER = ObjectMapperFactory.newObjectMapper(); + // Pinned to the value implicitly generated before the jackson-databind removal, to preserve + // Java serialization compatibility for instances persisted by earlier versions. + private static final long serialVersionUID = -339831253105369637L; public JsonStringArrayList() { super(); @@ -40,10 +40,6 @@ public JsonStringArrayList(int size) { @Override public final String toString() { - try { - return MAPPER.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Cannot serialize array list to JSON string", e); - } + return JsonStringSerializer.serialize(this); } } diff --git a/vector/src/main/java/org/apache/arrow/vector/util/JsonStringHashMap.java b/vector/src/main/java/org/apache/arrow/vector/util/JsonStringHashMap.java index 109df55153..f28113f01c 100644 --- a/vector/src/main/java/org/apache/arrow/vector/util/JsonStringHashMap.java +++ b/vector/src/main/java/org/apache/arrow/vector/util/JsonStringHashMap.java @@ -16,8 +16,6 @@ */ package org.apache.arrow.vector.util; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.LinkedHashMap; /** @@ -29,14 +27,12 @@ */ public class JsonStringHashMap extends LinkedHashMap { - private static final ObjectMapper MAPPER = ObjectMapperFactory.newObjectMapper(); + // Pinned to the value implicitly generated before the jackson-databind removal, to preserve + // Java serialization compatibility for instances persisted by earlier versions. + private static final long serialVersionUID = -7486716413378679089L; @Override public final String toString() { - try { - return MAPPER.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Cannot serialize hash map to JSON string", e); - } + return JsonStringSerializer.serialize(this); } } diff --git a/vector/src/main/java/org/apache/arrow/vector/util/JsonStringSerializer.java b/vector/src/main/java/org/apache/arrow/vector/util/JsonStringSerializer.java new file mode 100644 index 0000000000..03ffe081c6 --- /dev/null +++ b/vector/src/main/java/org/apache/arrow/vector/util/JsonStringSerializer.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.vector.util; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import java.io.IOException; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.temporal.TemporalUnit; +import java.util.Base64; +import java.util.Map; +import org.apache.arrow.vector.PeriodDuration; + +/** + * Serializes arbitrary values to a compact JSON string using only Jackson's streaming API + * (jackson-core). This replaces the previous use of jackson-databind for the {@code toString()} + * representations of {@link JsonStringHashMap} and {@link JsonStringArrayList}. + */ +final class JsonStringSerializer { + + private static final JsonFactory FACTORY = new JsonFactory(); + + private JsonStringSerializer() {} + + /** Serializes the given value to a compact JSON string. */ + static String serialize(Object value) { + try (StringWriter writer = new StringWriter(); + JsonGenerator generator = FACTORY.createGenerator(writer)) { + writeValue(generator, value); + generator.flush(); + return writer.toString(); + } catch (IOException e) { + throw new IllegalStateException("Cannot serialize value to JSON string", e); + } + } + + private static void writeValue(JsonGenerator generator, Object value) throws IOException { + if (value == null) { + generator.writeNull(); + } else if (value instanceof Map) { + generator.writeStartObject(); + for (Map.Entry entry : ((Map) value).entrySet()) { + generator.writeFieldName(String.valueOf(entry.getKey())); + writeValue(generator, entry.getValue()); + } + generator.writeEndObject(); + } else if (value instanceof Iterable) { + generator.writeStartArray(); + for (Object element : (Iterable) value) { + writeValue(generator, element); + } + generator.writeEndArray(); + } else if (value instanceof byte[]) { + generator.writeString(Base64.getEncoder().encodeToString((byte[]) value)); + } else if (value instanceof Boolean) { + generator.writeBoolean((Boolean) value); + } else if (value instanceof BigDecimal) { + generator.writeNumber((BigDecimal) value); + } else if (value instanceof BigInteger) { + generator.writeNumber((BigInteger) value); + } else if (value instanceof Double) { + generator.writeNumber((Double) value); + } else if (value instanceof Float) { + generator.writeNumber((Float) value); + } else if (value instanceof Number) { + generator.writeNumber(((Number) value).longValue()); + } else if (value instanceof LocalDateTime) { + // ---- BEGIN java.time legacy-compatibility block ---- + // The former toString() path used a jackson-databind ObjectMapper configured with + // JavaTimeModule (see the deleted ObjectMapperFactory). With WRITE_DATES_AS_TIMESTAMPS + // enabled (the default), that mapper rendered java.time values in a Jackson-specific + // numeric form rather than ISO-8601. The three branches below (LocalDateTime, Duration, + // PeriodDuration) reproduce that exact legacy output so toString() is byte-for-byte + // unchanged for struct/map/list vectors containing temporal children (timestamp, date, + // time, duration, interval-day, interval-month-day-nano). + // + // TO REVERT to cleaner native ISO-8601 output: delete this entire block (down to the + // matching END marker) plus the writeLocalDateTime/writeDuration/writePeriodDuration + // helpers below and their now-unused imports. The generic fallback then emits + // value.toString(): LocalDateTime -> "2021-01-02T03:04:05", Duration -> "PT1M30S", + // PeriodDuration -> "P1Y2M3D PT...". RISK: this only changes the human-readable + // toString()/contentToString() representation used for debugging, logging and display. + // It does NOT affect vector data, the IPC binary format, or the JSON file wire format + // (JsonFileWriter emits raw epoch numbers directly, not via this class). The risk is + // limited to any downstream code or test that string-matches the legacy toString() of + // temporal values inside complex vectors. + writeLocalDateTime(generator, (LocalDateTime) value); + } else if (value instanceof Duration) { + writeDuration(generator, (Duration) value); + } else if (value instanceof PeriodDuration) { + writePeriodDuration(generator, (PeriodDuration) value); + // ---- END java.time legacy-compatibility block ---- + } else if (value instanceof CharSequence || value instanceof Character) { + generator.writeString(value.toString()); + } else { + generator.writeString(value.toString()); + } + } + + /** + * Reproduces JavaTimeModule's timestamp serialization of {@link LocalDateTime} as an array {@code + * [year, month, day, hour, minute(, second(, nano))]}, where the second is omitted when both + * second and nano are zero, and the nano is omitted when zero. Part of the legacy java.time + * compatibility block in {@link #writeValue}; see that block's comment to revert. + */ + private static void writeLocalDateTime(JsonGenerator generator, LocalDateTime value) + throws IOException { + generator.writeStartArray(); + generator.writeNumber(value.getYear()); + generator.writeNumber(value.getMonthValue()); + generator.writeNumber(value.getDayOfMonth()); + generator.writeNumber(value.getHour()); + generator.writeNumber(value.getMinute()); + if (value.getSecond() != 0 || value.getNano() != 0) { + generator.writeNumber(value.getSecond()); + if (value.getNano() != 0) { + generator.writeNumber(value.getNano()); + } + } + generator.writeEndArray(); + } + + /** + * Reproduces JavaTimeModule's timestamp serialization of {@link Duration} as a decimal number of + * seconds with nanosecond precision (e.g. {@code 90.000000000}, {@code -1.500000000}), with the + * special case {@code 0.0} for a zero duration. Part of the legacy java.time compatibility block + * in {@link #writeValue}; see that block's comment to revert. + */ + private static void writeDuration(JsonGenerator generator, Duration value) throws IOException { + long seconds = value.getSeconds(); + int nanos = value.getNano(); + if (seconds == 0 && nanos == 0) { + generator.writeNumber(BigDecimal.valueOf(0, 1)); + } else { + generator.writeNumber(BigDecimal.valueOf(seconds).add(BigDecimal.valueOf(nanos, 9))); + } + } + + /** + * Reproduces the former ObjectMapper's (bean) serialization of {@link PeriodDuration} as {@code + * {"period": "P1Y2M3D", "duration": , "units": ["YEARS", ...]}}. The {@code units} array + * reflects {@link PeriodDuration#getUnits()} and is emitted only to preserve the exact legacy + * output. Part of the legacy java.time compatibility block in {@link #writeValue}; see that + * block's comment to revert. + */ + private static void writePeriodDuration(JsonGenerator generator, PeriodDuration value) + throws IOException { + generator.writeStartObject(); + generator.writeStringField("period", value.getPeriod().toString()); + generator.writeFieldName("duration"); + writeDuration(generator, value.getDuration()); + generator.writeArrayFieldStart("units"); + for (TemporalUnit unit : value.getUnits()) { + generator.writeString(((Enum) unit).name()); + } + generator.writeEndArray(); + generator.writeEndObject(); + } +} diff --git a/vector/src/main/java/org/apache/arrow/vector/util/ObjectMapperFactory.java b/vector/src/main/java/org/apache/arrow/vector/util/ObjectMapperFactory.java deleted file mode 100644 index 5d004ef36b..0000000000 --- a/vector/src/main/java/org/apache/arrow/vector/util/ObjectMapperFactory.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.arrow.vector.util; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -/** A {@link ObjectMapper} factory to read/write JSON. */ -public final class ObjectMapperFactory { - - private ObjectMapperFactory() {} - - /** Creates a new {@link ObjectMapper} instance. */ - public static ObjectMapper newObjectMapper() { - return JsonMapper.builder().addModule(new JavaTimeModule()).build(); - } -} diff --git a/vector/src/main/java/org/apache/arrow/vector/util/Text.java b/vector/src/main/java/org/apache/arrow/vector/util/Text.java index 35d810abbb..c10055bda6 100644 --- a/vector/src/main/java/org/apache/arrow/vector/util/Text.java +++ b/vector/src/main/java/org/apache/arrow/vector/util/Text.java @@ -16,11 +16,6 @@ */ package org.apache.arrow.vector.util; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; import java.io.DataInput; import java.io.IOException; import java.nio.ByteBuffer; @@ -39,7 +34,6 @@ * A simplified byte wrapper similar to Hadoop's Text class without all the dependencies. Lifted * from Hadoop 2.7.1 */ -@JsonSerialize(using = Text.TextSerializer.class) public class Text extends ReusableByteArray { private static ThreadLocal ENCODER_FACTORY = @@ -858,19 +852,4 @@ public static int utf8Length(String string) { } return size; } - - /** JSON serializer for {@link Text}. */ - public static class TextSerializer extends StdSerializer { - - public TextSerializer() { - super(Text.class); - } - - @Override - public void serialize( - Text text, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) - throws IOException, JsonGenerationException { - jsonGenerator.writeString(text.toString()); - } - } }