blob: ca73cc32870ad26251a9138311830a001c3d77a7 [file] [log] [blame]
/*
* 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
*
* https://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.avro.data;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import org.apache.avro.util.internal.JacksonUtils;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.avro.Schema;
import org.apache.avro.AvroRuntimeException;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.Decoder;
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.ResolvingDecoder;
/** Utilities for reading and writing arbitrary Json data in Avro format. */
public class Json {
private Json() {
} // singleton: no public ctor
static final JsonFactory FACTORY = new JsonFactory();
static final ObjectMapper MAPPER = new ObjectMapper(FACTORY);
/** The schema for Json data. */
public static final Schema SCHEMA;
static {
try {
try (InputStream in = Json.class.getResourceAsStream("/org/apache/avro/data/Json.avsc")) {
SCHEMA = new Schema.Parser().parse(in);
}
} catch (IOException e) {
throw new AvroRuntimeException(e);
}
}
/**
* {@link DatumWriter} for arbitrary Json data using the object model described
* in {@link org.apache.avro.JsonProperties}.
*/
public static class ObjectWriter implements DatumWriter<Object> {
@Override
public void setSchema(Schema schema) {
if (!SCHEMA.equals(schema))
throw new RuntimeException("Not the Json schema: " + schema);
}
@Override
public void write(Object datum, Encoder out) throws IOException {
Json.writeObject(datum, out);
}
}
/**
* {@link DatumReader} for arbitrary Json data using the object model described
* in {@link org.apache.avro.JsonProperties}.
*/
public static class ObjectReader implements DatumReader<Object> {
private Schema written;
private ResolvingDecoder resolver;
@Override
public void setSchema(Schema schema) {
this.written = SCHEMA.equals(written) ? null : schema;
}
@Override
public Object read(Object reuse, Decoder in) throws IOException {
if (written == null) // same schema
return Json.readObject(in);
// use a resolver to adapt alternate version of Json schema
if (resolver == null)
resolver = DecoderFactory.get().resolvingDecoder(written, SCHEMA, null);
resolver.configure(in);
Object result = Json.readObject(resolver);
resolver.drain();
return result;
}
}
/**
* Parses a JSON string and converts it to the object model described in
* {@link org.apache.avro.JsonProperties}.
*/
public static Object parseJson(String s) {
try {
return JacksonUtils.toObject(MAPPER.readTree(FACTORY.createParser(s)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Converts an instance of the object model described in
* {@link org.apache.avro.JsonProperties} to a JSON string.
*/
public static String toString(Object datum) {
return JacksonUtils.toJsonNode(datum).toString();
}
/** Note: this enum must be kept aligned with the union in Json.avsc. */
private enum JsonType {
LONG, DOUBLE, STRING, BOOLEAN, NULL, ARRAY, OBJECT
}
/**
* Write Json data as Avro data.
*/
private static void write(JsonNode node, Encoder out) throws IOException {
switch (node.asToken()) {
case VALUE_NUMBER_INT:
out.writeIndex(JsonType.LONG.ordinal());
out.writeLong(node.longValue());
break;
case VALUE_NUMBER_FLOAT:
out.writeIndex(JsonType.DOUBLE.ordinal());
out.writeDouble(node.doubleValue());
break;
case VALUE_STRING:
out.writeIndex(JsonType.STRING.ordinal());
out.writeString(node.textValue());
break;
case VALUE_TRUE:
out.writeIndex(JsonType.BOOLEAN.ordinal());
out.writeBoolean(true);
break;
case VALUE_FALSE:
out.writeIndex(JsonType.BOOLEAN.ordinal());
out.writeBoolean(false);
break;
case VALUE_NULL:
out.writeIndex(JsonType.NULL.ordinal());
out.writeNull();
break;
case START_ARRAY:
out.writeIndex(JsonType.ARRAY.ordinal());
out.writeArrayStart();
out.setItemCount(node.size());
for (JsonNode element : node) {
out.startItem();
write(element, out);
}
out.writeArrayEnd();
break;
case START_OBJECT:
out.writeIndex(JsonType.OBJECT.ordinal());
out.writeMapStart();
out.setItemCount(node.size());
Iterator<String> i = node.fieldNames();
while (i.hasNext()) {
out.startItem();
String name = i.next();
out.writeString(name);
write(node.get(name), out);
}
out.writeMapEnd();
break;
default:
throw new AvroRuntimeException(node.asToken() + " unexpected: " + node);
}
}
/**
* Read Json data from Avro data.
*/
private static JsonNode read(Decoder in) throws IOException {
switch (JsonType.values()[in.readIndex()]) {
case LONG:
return new LongNode(in.readLong());
case DOUBLE:
return new DoubleNode(in.readDouble());
case STRING:
return new TextNode(in.readString());
case BOOLEAN:
return in.readBoolean() ? BooleanNode.TRUE : BooleanNode.FALSE;
case NULL:
in.readNull();
return NullNode.getInstance();
case ARRAY:
ArrayNode array = JsonNodeFactory.instance.arrayNode();
for (long l = in.readArrayStart(); l > 0; l = in.arrayNext())
for (long i = 0; i < l; i++)
array.add(read(in));
return array;
case OBJECT:
ObjectNode object = JsonNodeFactory.instance.objectNode();
for (long l = in.readMapStart(); l > 0; l = in.mapNext())
for (long i = 0; i < l; i++)
object.set(in.readString(), read(in));
return object;
default:
throw new AvroRuntimeException("Unexpected Json node type");
}
}
private static void writeObject(Object datum, Encoder out) throws IOException {
write(JacksonUtils.toJsonNode(datum), out);
}
private static Object readObject(Decoder in) throws IOException {
return JacksonUtils.toObject(read(in));
}
}