/** * 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.avro.data; import java.io.IOException; import java.io.StringReader; import java.util.Iterator; import org.apache.avro.util.internal.JacksonUtils; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.JsonNodeFactory; import org.codehaus.jackson.node.LongNode; import org.codehaus.jackson.node.DoubleNode; import org.codehaus.jackson.node.TextNode; import org.codehaus.jackson.node.BooleanNode; import org.codehaus.jackson.node.NullNode; import org.codehaus.jackson.node.ArrayNode; import org.codehaus.jackson.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 { SCHEMA = Schema.parse (Json.class.getResourceAsStream("/org/apache/avro/data/Json.avsc")); } catch (IOException e) { throw new AvroRuntimeException(e); } } /** * {@link DatumWriter} for arbitrary Json data. * @deprecated use {@link ObjectWriter} */ @Deprecated public static class Writer implements DatumWriter<JsonNode> { @Override public void setSchema(Schema schema) { if (!SCHEMA.equals(schema)) throw new RuntimeException("Not the Json schema: "+schema); } @Override public void write(JsonNode datum, Encoder out) throws IOException { Json.write(datum, out); } } /** * {@link DatumReader} for arbitrary Json data. * @deprecated use {@link ObjectReader} */ @Deprecated public static class Reader implements DatumReader<JsonNode> { private Schema written; private ResolvingDecoder resolver; @Override public void setSchema(Schema schema) { this.written = SCHEMA.equals(written) ? null : schema; } @Override public JsonNode read(JsonNode reuse, Decoder in) throws IOException { if (written == null) // same schema return Json.read(in); // use a resolver to adapt alternate version of Json schema if (resolver == null) resolver = DecoderFactory.get().resolvingDecoder(written, SCHEMA, null); resolver.configure(in); JsonNode result = Json.read(resolver); resolver.drain(); return result; } } /** {@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.createJsonParser( new StringReader(s)))); } catch (JsonParseException e) { throw new RuntimeException(e); } 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. * @deprecated internal method */ @Deprecated public static void write(JsonNode node, Encoder out) throws IOException { switch(node.asToken()) { case VALUE_NUMBER_INT: out.writeIndex(JsonType.LONG.ordinal()); out.writeLong(node.getLongValue()); break; case VALUE_NUMBER_FLOAT: out.writeIndex(JsonType.DOUBLE.ordinal()); out.writeDouble(node.getDoubleValue()); break; case VALUE_STRING: out.writeIndex(JsonType.STRING.ordinal()); out.writeString(node.getTextValue()); 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.getFieldNames(); 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. * @deprecated internal method */ @Deprecated public 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.put(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)); } }