Java tutorial
/** * (c) Copyright 2012 WibiData, Inc. * * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * 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 org.kiji.schema.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; import java.util.List; import java.util.Map; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.avro.Schema; import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.generic.IndexedRecord; import org.apache.avro.io.EncoderFactory; import org.apache.avro.io.JsonEncoder; import org.apache.avro.specific.SpecificDatumWriter; import org.apache.hadoop.hbase.util.Bytes; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ArrayNode; import org.codehaus.jackson.node.JsonNodeFactory; import org.codehaus.jackson.node.ObjectNode; import org.codehaus.jackson.util.DefaultPrettyPrinter; import org.kiji.annotations.ApiAudience; /** * Encode an Avro record into JSON. */ @ApiAudience.Private public final class ToJson { /** Utility class cannot be instantiated. */ private ToJson() { } private static final JsonFactory JSON_FACTORY = new JsonFactory(); private static final JsonNodeFactory JSON_NODE_FACTORY = JsonNodeFactory.instance; /** * Serializes a Java Avro value into JSON. * * When serializing records, fields whose value matches the fields' default value are omitted. * * @param value the Java value to serialize. * @param schema Avro schema of the value to serialize. * @return the value encoded as a JSON tree. * @throws IOException on error. */ public static JsonNode toJsonNode(Object value, Schema schema) throws IOException { switch (schema.getType()) { case NULL: return JSON_NODE_FACTORY.nullNode(); case BOOLEAN: return JSON_NODE_FACTORY.booleanNode((Boolean) value); case DOUBLE: return JSON_NODE_FACTORY.numberNode((Double) value); case FLOAT: return JSON_NODE_FACTORY.numberNode((Float) value); case INT: return JSON_NODE_FACTORY.numberNode((Integer) value); case LONG: return JSON_NODE_FACTORY.numberNode((Long) value); case STRING: return JSON_NODE_FACTORY.textNode((String) value); case ENUM: // Enums are represented as strings: @SuppressWarnings("rawtypes") final Enum enumValue = (Enum) value; return JSON_NODE_FACTORY.textNode(enumValue.toString()); case BYTES: case FIXED: // TODO Bytes are represented as strings... throw new RuntimeException("toJsonNode(byte array) not implemented"); case ARRAY: { final ArrayNode jsonArray = JSON_NODE_FACTORY.arrayNode(); @SuppressWarnings("unchecked") final Iterable<Object> javaArray = (Iterable<Object>) value; for (Object element : javaArray) { jsonArray.add(toJsonNode(element, schema.getElementType())); } return jsonArray; } case MAP: { final ObjectNode jsonObject = JSON_NODE_FACTORY.objectNode(); @SuppressWarnings("unchecked") final Map<String, Object> javaMap = (Map<String, Object>) value; for (Map.Entry<String, Object> entry : javaMap.entrySet()) { jsonObject.put(entry.getKey(), toJsonNode(entry.getValue(), schema.getValueType())); } return jsonObject; } case RECORD: { final ObjectNode jsonObject = JSON_NODE_FACTORY.objectNode(); final IndexedRecord record = (IndexedRecord) value; if (!record.getSchema().equals(schema)) { throw new IOException(String.format("Avro schema specifies record type '%s' but got '%s'.", schema.getFullName(), record.getSchema().getFullName())); } for (Schema.Field field : schema.getFields()) { final Object fieldValue = record.get(field.pos()); final JsonNode fieldNode = toJsonNode(fieldValue, field.schema()); // Outputs the field only if its value differs from the field's default: if ((field.defaultValue() == null) || !fieldNode.equals(field.defaultValue())) { jsonObject.put(field.name(), fieldNode); } } return jsonObject; } case UNION: return toUnionJsonNode(value, schema); default: throw new RuntimeException(String.format("Unexpected schema type '%s'.", schema)); } } /** * Encodes an Avro union into a JSON node. * * @param value an Avro union to encode. * @param schema schema of the union to encode. * @return the encoded value as a JSON node. * @throws IOException on error. */ private static JsonNode toUnionJsonNode(Object value, Schema schema) throws IOException { Preconditions.checkArgument(schema.getType() == Schema.Type.UNION); final Schema optionalType = AvroUtils.getOptionalType(schema); if (null != optionalType) { return (null == value) ? JSON_NODE_FACTORY.nullNode() : toJsonNode(value, optionalType); } final Map<Schema.Type, List<Schema>> typeMap = Maps.newEnumMap(Schema.Type.class); for (Schema type : schema.getTypes()) { List<Schema> typeList = typeMap.get(type.getType()); if (null == typeList) { typeList = Lists.newArrayList(); typeMap.put(type.getType(), typeList); } typeList.add(type); } // null is shortened as an immediate JSON null: if (null == value) { if (!typeMap.containsKey(Schema.Type.NULL)) { throw new IOException(String.format("Avro schema specifies '%s' but got 'null'.", schema)); } return JSON_NODE_FACTORY.nullNode(); } final ObjectNode union = JSON_NODE_FACTORY.objectNode(); for (Schema type : schema.getTypes()) { try { final JsonNode actualNode = toJsonNode(value, type); union.put(type.getFullName(), actualNode); return union; } catch (IOException ioe) { // This type was not the correct union case, ignore... } } throw new IOException(String.format("Unable to encode '%s' as union '%s'.", value, schema)); } /** * Encodes an Avro value into a JSON string. * * Fields with default values are omitted. * * @param value Avro value to encode. * @param schema Avro schema of the value. * @return Pretty string representation of the JSON-encoded value. * @throws IOException on error. */ public static String toJsonString(Object value, Schema schema) throws IOException { final JsonNode node = ToJson.toJsonNode(value, schema); final StringWriter stringWriter = new StringWriter(); final JsonGenerator generator = JSON_FACTORY.createJsonGenerator(stringWriter); // We have disabled this because we used unions to represent row key formats // in the table layout. This is a HACK and needs a better solution. // TODO: Find better solution. https://jira.kiji.org/browse/SCHEMA-174 //generator.disable(Feature.QUOTE_FIELD_NAMES); generator.setPrettyPrinter(new DefaultPrettyPrinter()); final ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(generator, node); return stringWriter.toString(); } /** * Encodes an Avro record into JSON. * * @param record Avro record to encode. * @return Pretty JSON representation of the record. * @throws IOException on error. */ public static String toJsonString(IndexedRecord record) throws IOException { final Schema schema = record.getSchema(); return toJsonString(record, schema); } /** * Standard Avro/JSON encoder. * * @param value Avro value to encode. * @param schema Avro schema of the value. * @return JSON-encoded value. * @throws IOException on error. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public static String toAvroJsonString(Object value, Schema schema) throws IOException { try { final ByteArrayOutputStream jsonOutputStream = new ByteArrayOutputStream(); final JsonEncoder jsonEncoder = EncoderFactory.get().jsonEncoder(schema, jsonOutputStream); final GenericDatumWriter writer = new GenericDatumWriter(schema); writer.write(value, jsonEncoder); jsonEncoder.flush(); return Bytes.toString(jsonOutputStream.toByteArray()); } catch (IOException ioe) { throw new RuntimeException("Internal error: " + ioe); } } /** * Standard Avro/JSON encoder. * * @param record Avro record to encode. * @return JSON-encoded value. * @throws IOException on error. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public static String toAvroJsonString(IndexedRecord record) throws IOException { final Schema schema = record.getSchema(); try { final ByteArrayOutputStream jsonOutputStream = new ByteArrayOutputStream(); final JsonEncoder jsonEncoder = EncoderFactory.get().jsonEncoder(schema, jsonOutputStream); final SpecificDatumWriter writer = new SpecificDatumWriter(record.getClass()); writer.write(record, jsonEncoder); jsonEncoder.flush(); return Bytes.toString(jsonOutputStream.toByteArray()); } catch (IOException ioe) { throw new RuntimeException("Internal error: " + ioe); } } }