/* Copyright (c) 2012 LinkedIn Corp. 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 com.linkedin.data.codec; import com.linkedin.data.ByteString; import com.linkedin.data.Data; import com.linkedin.data.DataComplex; import com.linkedin.data.DataList; import com.linkedin.data.DataMap; import com.linkedin.data.collections.CheckedUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayDeque; import java.util.Deque; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonLocation; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.PrettyPrinter; /** * A JSON codec that uses Jackson for serialization and de-serialization. * * @author slim */ public class JacksonDataCodec implements TextDataCodec { public JacksonDataCodec() { this(new JsonFactory()); } public JacksonDataCodec(JsonFactory jsonFactory) { _jsonFactory = jsonFactory; _jsonFactory.disable(JsonFactory.Feature.INTERN_FIELD_NAMES); setAllowComments(true); } public void setAllowComments(boolean allowComments) { _jsonFactory.configure(JsonParser.Feature.ALLOW_COMMENTS, allowComments); _allowComments = allowComments; } public PrettyPrinter getPrettyPrinter() { return _prettyPrinter; } public void setPrettyPrinter(PrettyPrinter prettyPrinter) { _prettyPrinter = prettyPrinter; } @Override public String getStringEncoding() { return _jsonEncoding.getJavaName(); } @Override public byte[] mapToBytes(DataMap map) throws IOException { return objectToBytes(map); } @Override public String mapToString(DataMap map) throws IOException { return objectToString(map); } @Override public byte[] listToBytes(DataList list) throws IOException { return objectToBytes(list); } @Override public String listToString(DataList list) throws IOException { return objectToString(list); } protected byte[] objectToBytes(Object object) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(_defaultBufferSize); writeObject(object, createJsonGenerator(out)); return out.toByteArray(); } protected String objectToString(Object object) throws IOException { StringWriter out = new StringWriter(_defaultBufferSize); writeObject(object, createJsonGenerator(out)); return out.toString(); } @Override public DataMap bytesToMap(byte[] input) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(input); return parser.parse(jsonParser, DataMap.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } @Override public DataMap stringToMap(String input) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(input); return parser.parse(jsonParser, DataMap.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } @Override public DataList bytesToList(byte[] input) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(input); return parser.parse(jsonParser, DataList.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } @Override public DataList stringToList(String input) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(input); return parser.parse(jsonParser, DataList.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } @Override public void writeMap(DataMap map, OutputStream out) throws IOException { writeObject(map, createJsonGenerator(out)); } @Override public void writeMap(DataMap map, Writer out) throws IOException { writeObject(map, createJsonGenerator(out)); } @Override public void writeList(DataList list, OutputStream out) throws IOException { writeObject(list, createJsonGenerator(out)); } @Override public void writeList(DataList list, Writer out) throws IOException { writeObject(list, createJsonGenerator(out)); } protected JsonGenerator createJsonGenerator(OutputStream out) throws IOException { final JsonGenerator generator = _jsonFactory.createGenerator(out, _jsonEncoding); if (_prettyPrinter != null) { generator.setPrettyPrinter(_prettyPrinter); } return generator; } protected JsonGenerator createJsonGenerator(Writer out) throws IOException { final JsonGenerator generator = _jsonFactory.createGenerator(out); if (_prettyPrinter != null) { generator.setPrettyPrinter(_prettyPrinter); } return generator; } protected void writeObject(Object object, JsonGenerator generator) throws IOException { try { JsonTraverseCallback callback = new JsonTraverseCallback(generator); Data.traverse(object, callback); generator.flush(); } catch (IOException e) { throw e; } finally { try { generator.close(); } catch (IOException e) { // TODO: use Java 7 try-with-resources statement and Throwable.getSuppressed() } } } @Override public DataMap readMap(InputStream in) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(in); return parser.parse(jsonParser, DataMap.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } @Override public DataMap readMap(Reader in) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(in); return parser.parse(jsonParser, DataMap.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } @Override public DataList readList(InputStream in) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(in); return parser.parse(jsonParser, DataList.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } @Override public DataList readList(Reader in) throws IOException { final Parser parser = new Parser(); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(in); return parser.parse(jsonParser, DataList.class); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } /** * Reads an {@link InputStream} and parses its contents into a list of Data objects. * * @param in provides the {@link InputStream} * @param mesg provides the {@link StringBuilder} to store validation error messages, * such as duplicate keys in the same {@link DataMap}. * @param locationMap provides where to store the mapping of a Data object * to its location in the in the {@link InputStream}. may be * {@code null} if this mapping is not needed by the caller. * This map should usually be an {@link IdentityHashMap}. * @return the list of Data objects parsed from the {@link InputStream}. * @throws IOException if there is a syntax error in the input. */ public List<Object> parse(InputStream in, StringBuilder mesg, Map<Object, DataLocation> locationMap) throws IOException { final Parser parser = new Parser(true); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(in); return parser.parse(jsonParser, mesg, locationMap); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } /** * Reads an {@link Reader} and parses its contents into a list of Data objects. * * @param in provides the {@link Reader} * @param mesg provides the {@link StringBuilder} to store validation error messages, * such as duplicate keys in the same {@link DataMap}. * @param locationMap provides where to store the mapping of a Data object * to its location in the in the {@link Reader}. may be * {@code null} if this mapping is not needed by the caller. * This map should usually be an {@link IdentityHashMap}. * @return the list of Data objects parsed from the {@link Reader}. * @throws IOException if there is a syntax error in the input. */ public List<Object> parse(Reader in, StringBuilder mesg, Map<Object, DataLocation> locationMap) throws IOException { final Parser parser = new Parser(true); JsonParser jsonParser = null; try { jsonParser = _jsonFactory.createParser(in); return parser.parse(jsonParser, mesg, locationMap); } catch (IOException e) { throw e; } finally { closeJsonParserQuietly(jsonParser); } } public void objectToJsonGenerator(Object object, JsonGenerator generator) throws IOException { JsonTraverseCallback callback = new JsonTraverseCallback(generator); Data.traverse(object, callback); } protected static class JsonTraverseCallback implements Data.TraverseCallback { protected JsonTraverseCallback(JsonGenerator jsonGenerator) { _jsonGenerator = jsonGenerator; } @Override public Iterable<Map.Entry<String,Object>> orderMap(DataMap map) { return map.entrySet(); } @Override public void nullValue() throws IOException { _jsonGenerator.writeNull(); } @Override public void booleanValue(boolean value) throws IOException { _jsonGenerator.writeBoolean(value); } @Override public void integerValue(int value) throws IOException { _jsonGenerator.writeNumber(value); } @Override public void longValue(long value) throws IOException { _jsonGenerator.writeNumber(value); } @Override public void floatValue(float value) throws IOException { _jsonGenerator.writeNumber(value); } @Override public void doubleValue(double value) throws IOException { _jsonGenerator.writeNumber(value); } @Override public void stringValue(String value) throws IOException { _jsonGenerator.writeString(value); } @Override public void byteStringValue(ByteString value) throws IOException { _jsonGenerator.writeString(value.asAvroString()); } @Override public void illegalValue(Object value) throws DataEncodingException { throw new DataEncodingException("Illegal value encountered: " + value); } @Override public void emptyMap() throws IOException { _jsonGenerator.writeStartObject(); _jsonGenerator.writeEndObject(); } @Override public void startMap(DataMap map) throws IOException { _jsonGenerator.writeStartObject(); } @Override public void key(String key) throws IOException { _jsonGenerator.writeFieldName(key); } @Override public void endMap() throws IOException { _jsonGenerator.writeEndObject(); } @Override public void emptyList() throws IOException { _jsonGenerator.writeStartArray(); _jsonGenerator.writeEndArray(); } @Override public void startList(DataList list) throws IOException { _jsonGenerator.writeStartArray(); } @Override public void index(int index) { } @Override public void endList() throws IOException { _jsonGenerator.writeEndArray(); } private final JsonGenerator _jsonGenerator; } private static class Location implements DataLocation { private Location(JsonLocation location) { _location = location; } public int getColumn() { return _location.getColumnNr(); } public int getLine() { return _location.getLineNr(); } @Override public int compareTo(DataLocation other) { return (int) (_location.getCharOffset() - ((Location) other)._location.getCharOffset()); } @Override public String toString() { return getLine() + "," + getColumn(); } private final JsonLocation _location; } private static class Parser { private StringBuilder _errorBuilder = null; private JsonParser _parser = null; private boolean _debug = false; private Deque<Object> _nameStack = null; private Map<Object, DataLocation> _locationMap = null; Parser() { this(false); } Parser(boolean debug) { _debug = debug; } /** * Returns map of location to object, sorted by location. * * May be used to debug location map. */ private Map<DataLocation, Object> sortedLocationsMap() { if (_locationMap == null) return null; TreeMap<DataLocation, Object> sortedMap = new TreeMap<DataLocation, Object>(); for (Map.Entry<Object, DataLocation> e : _locationMap.entrySet()) { sortedMap.put(e.getValue(), e.getKey()); } return sortedMap; } List<Object> parse(JsonParser parser, StringBuilder mesg, Map<Object, DataLocation> locationMap) throws JsonParseException, IOException { _locationMap = locationMap; DataList list = new DataList(); _errorBuilder = mesg; if (_debug) { _nameStack = new ArrayDeque<Object>(); } _parser = parser; JsonToken token; while ((token = _parser.nextToken()) != null) { parse(list, null, null, token); } _errorBuilder = null; return list; } <T extends DataComplex> T parse(JsonParser parser, Class<T> expectType) throws IOException { _errorBuilder = null; if (_debug) { _nameStack = new ArrayDeque<Object>(); } _parser = parser; final JsonToken token = _parser.nextToken(); final T result; if (expectType == DataMap.class) { if (!JsonToken.START_OBJECT.equals(token)) { throw new DataDecodingException("JSON text for object must start with \"{\"."); } final DataMap map = new DataMap(); parseDataMap(map); if (_errorBuilder != null) { map.addError(_errorBuilder.toString()); } result = expectType.cast(map); } else if (expectType == DataList.class) { if (!JsonToken.START_ARRAY.equals(token)) { throw new DataDecodingException("JSON text for array must start with \"[\"."); } final DataList list = new DataList(); parseDataList(list); if (_errorBuilder != null) { //list.addError(_errorBuilder.toString()); } result = expectType.cast(list); } else { throw new DataDecodingException("Expected type must be either DataMap or DataList."); } return result; } private DataLocation currentDataLocation() { return _locationMap == null ? null : new Location(_parser.getTokenLocation()); } private void saveDataLocation(Object o, DataLocation location) { if (_locationMap != null && o != null) { assert(location != null); _locationMap.put(o, location); } } private Object parse(DataList parentList, DataMap parentMap, String name, JsonToken token) throws IOException { if (token == null) { throw new DataDecodingException("Missing JSON token"); } Object value; DataLocation location = currentDataLocation(); switch (token) { case START_OBJECT: DataMap childMap = new DataMap(); value = childMap; updateParent(parentList, parentMap, name, childMap); parseDataMap(childMap); break; case START_ARRAY: DataList childList = new DataList(); value = childList; updateParent(parentList, parentMap, name, childList); parseDataList(childList); break; default: value = parsePrimitive(token); if (value != null) { updateParent(parentList, parentMap, name, value); } break; } saveDataLocation(value, location); return value; } private void updateParent(DataList parentList, DataMap parentMap, String name, Object value) { if (parentMap != null) { Object replaced = CheckedUtil.putWithoutChecking(parentMap, name, value); if (replaced != null) { if (_errorBuilder == null) { _errorBuilder = new StringBuilder(); } _errorBuilder.append(new Location(_parser.getTokenLocation())).append(": \"").append(name).append("\" defined more than once.\n"); } } else { CheckedUtil.addWithoutChecking(parentList, value); } } private Object parsePrimitive(JsonToken token) throws IOException { Object object; JsonParser.NumberType numberType; switch (token) { case VALUE_STRING: object = _parser.getText(); break; case VALUE_NUMBER_INT: case VALUE_NUMBER_FLOAT: numberType = _parser.getNumberType(); switch (numberType) { case INT: object = _parser.getIntValue(); break; case LONG: object = _parser.getLongValue(); break; case FLOAT: object = _parser.getFloatValue(); break; case DOUBLE: object = _parser.getDoubleValue(); break; case BIG_INTEGER: // repeat to avoid fall through warning error(token, numberType); object = null; break; case BIG_DECIMAL: default: error(token, numberType); object = null; break; } break; case VALUE_TRUE: object = Boolean.TRUE; break; case VALUE_FALSE: object = Boolean.FALSE; break; case VALUE_NULL: object = Data.NULL; break; default: error(token, null); object = null; break; } return object; } private void parseDataMap(DataMap map) throws IOException { while (_parser.nextToken() != JsonToken.END_OBJECT) { String key = _parser.getCurrentName(); if (_debug) { _nameStack.addLast(key); } JsonToken token = _parser.nextToken(); parse(null, map, key, token); if (_debug) { _nameStack.removeLast(); } } } private void parseDataList(DataList list) throws IOException { JsonToken token; int index = 0; while ((token = _parser.nextToken()) != JsonToken.END_ARRAY) { if (_debug) { _nameStack.addLast(index); index++; } parse(list, null, null, token); if (_debug) { _nameStack.removeLast(); } } } private void error(JsonToken token, JsonParser.NumberType type) throws IOException { if (_errorBuilder == null) { _errorBuilder = new StringBuilder(); } _errorBuilder.append(_parser.getTokenLocation()).append(": "); if (_debug) { _errorBuilder.append("name: "); Data.appendNames(_errorBuilder, _nameStack); _errorBuilder.append(", "); } _errorBuilder.append("value: ").append(_parser.getText()).append(", token: ").append(token); if (type != null) { _errorBuilder.append(", number type: ").append(type); } _errorBuilder.append(" not parsed.\n"); } } private static void closeJsonParserQuietly(JsonParser parser) { if (parser != null) { try { parser.close(); } catch (IOException e) { // TODO: use Java 7 try-with-resources statement and Throwable.getSuppressed() } } } protected boolean _allowComments; protected PrettyPrinter _prettyPrinter; protected JsonFactory _jsonFactory; protected int _defaultBufferSize = 4096; protected JsonEncoding _jsonEncoding = JsonEncoding.UTF8; }