/*
* Copyright 2004-2009 the original author or authors.
*
* 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.compass.core.json.jackson.converter;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonToken;
import org.codehaus.jackson.map.BaseMapper;
import org.codehaus.jackson.map.JsonMappingException;
import org.compass.core.converter.ConversionException;
import org.compass.core.converter.json.JsonContentConverter;
import org.compass.core.json.AliasedJsonObject;
import org.compass.core.json.JsonArray;
import org.compass.core.json.JsonObject;
import org.compass.core.json.jackson.JacksonAliasedJsonObject;
import org.compass.core.json.jackson.JacksonJsonArray;
import org.compass.core.json.jackson.JacksonJsonObject;
import org.compass.core.util.StringBuilderWriter;
/**
* A Jackson based json converter.
*
* @author kimchy
*/
public class JacksonContentConverter implements JsonContentConverter {
private static JsonFactory jsonFactory = new JsonFactory();
private static ContentMapper mapper = new ContentMapper(jsonFactory);
public String toJSON(JsonObject jsonObject) throws ConversionException {
StringBuilderWriter sbWriter = StringBuilderWriter.Cached.cached();
try {
JsonGenerator generator = jsonFactory.createJsonGenerator(sbWriter);
generateJsonObject(jsonObject, generator);
} catch (IOException e) {
throw new ConversionException("Failed to convert json to string", e);
}
return sbWriter.getBuilder().toString();
}
public AliasedJsonObject fromJSON(String alias, String json) throws ConversionException {
try {
JacksonJsonObject jsonObject = mapper.readTree(json);
return new JacksonAliasedJsonObject(alias, jsonObject.getNodes());
} catch (Exception e) {
throw new ConversionException("Failed to convert json: " + json + " with alias [" + alias + "]", e);
}
}
/**
* Uses Jackson {@link org.codehaus.jackson.JsonGenerator} in order to generate the json string based on
* a {@link org.compass.core.json.JsonObject}.
*/
private void generateJsonObject(JsonObject jsonObject, JsonGenerator generator) throws IOException {
generator.writeStartObject();
for (Iterator<String> keyIt = jsonObject.keys(); keyIt.hasNext();) {
String key = keyIt.next();
Object value = jsonObject.opt(key);
if (value == null) {
generator.writeNullField(key);
} else if (value instanceof String) {
generator.writeStringField(key, (String) value);
} else if (value instanceof Integer) {
generator.writeNumberField(key, (Integer) value);
} else if (value instanceof Long) {
generator.writeNumberField(key, (Long) value);
} else if (value instanceof Double) {
generator.writeNumberField(key, (Double) value);
} else if (value instanceof Float) {
generator.writeNumberField(key, (Float) value);
} else if (value instanceof BigDecimal) {
generator.writeNumberField(key, (BigDecimal) value);
} else if (value instanceof JsonObject) {
generateJsonObject((JsonObject) value, generator);
} else if (value instanceof JsonArray) {
generateJsonArray((JsonArray) value, generator);
}
}
generator.writeEndObject();
}
private void generateJsonArray(JsonArray jsonArray, JsonGenerator generator) throws IOException {
generator.writeStartArray();
for (int i = 0; i < jsonArray.length(); i++) {
if (jsonArray.isNull(i)) {
generator.writeNull();
}
Object value = jsonArray.opt(i);
if (value == null) {
generator.writeNull();
} else if (value instanceof String) {
generator.writeString((String) value);
} else if (value instanceof Integer) {
generator.writeNumber((Integer) value);
} else if (value instanceof Long) {
generator.writeNumber((Long) value);
} else if (value instanceof Double) {
generator.writeNumber((Double) value);
} else if (value instanceof Float) {
generator.writeNumber((Float) value);
} else if (value instanceof BigDecimal) {
generator.writeNumber((BigDecimal) value);
} else if (value instanceof JsonObject) {
generateJsonObject((JsonObject) value, generator);
} else if (value instanceof JsonArray) {
generateJsonArray((JsonArray) value, generator);
}
}
generator.writeEndArray();
}
/**
* A mapper from Json content (a String for example) to a {@link org.compass.core.json.jackson.JacksonJsonObject}.
*/
public static class ContentMapper extends BaseMapper {
public ContentMapper(JsonFactory jsonFactory) {
super(jsonFactory);
}
public JacksonJsonObject readTree(String jsonContent)
throws IOException, JsonParseException {
return _readMapAndClose(_jsonFactory.createJsonParser(jsonContent));
}
protected JacksonJsonObject _readMapAndClose(JsonParser jp)
throws IOException, JsonParseException {
try {
return readTree(jp);
} finally {
try {
jp.close();
} catch (IOException ioe) {
}
}
}
/**
* Method that will use the current event of the underlying parser
* (and if there's no event yet, tries to advance to an event)
* to construct a node, and advance the parser to point to the
* next event, if any. For structured tokens (objects, arrays),
* will recursively handle and construct contained nodes.
*/
public JacksonJsonObject readTree(JsonParser jp)
throws IOException, JsonParseException, JsonMappingException {
JsonToken curr = jp.getCurrentToken();
if (curr == null) {
curr = jp.nextToken();
// We hit EOF? Nothing more to do, if so:
if (curr == null) {
return null;
}
}
JacksonJsonObject result = (JacksonJsonObject) readAndMap(jp, curr);
/* Need to also advance the reader, if we get this far,
* to allow handling of root level sequence of values
*/
jp.nextToken();
return result;
}
protected Object readAndMap(JsonParser jp, JsonToken currToken)
throws IOException, JsonParseException {
switch (currToken) {
case START_OBJECT: {
Map<String, Object> nodes = new HashMap<String, Object>();
while ((currToken = jp.nextToken()) != JsonToken.END_OBJECT) {
if (currToken != JsonToken.FIELD_NAME) {
_reportProblem(jp, "Unexpected token (" + currToken + "), expected FIELD_NAME");
}
String fieldName = jp.getText();
Object value = readAndMap(jp, jp.nextToken());
if (_cfgDupFields == BaseMapper.DupFields.ERROR) {
Object old = nodes.put(fieldName, value);
if (old != null) {
_reportProblem(jp, "Duplicate value for field '" + fieldName + "', when dup fields mode is " + _cfgDupFields);
}
} else if (_cfgDupFields == BaseMapper.DupFields.USE_LAST) {
// Easy, just add
nodes.put(fieldName, value);
} else { // use first; need to ensure we don't yet have it
if (nodes.get(fieldName) == null) {
nodes.put(fieldName, value);
}
}
}
return new JacksonJsonObject(nodes);
}
case START_ARRAY: {
List<Object> values = new ArrayList<Object>();
while ((currToken = jp.nextToken()) != JsonToken.END_ARRAY) {
Object value = readAndMap(jp, currToken);
values.add(value);
}
return new JacksonJsonArray(values);
}
case VALUE_STRING:
return jp.getText();
case VALUE_NUMBER_INT:
case VALUE_NUMBER_FLOAT:
return jp.getNumberValue();
case VALUE_TRUE:
return Boolean.TRUE;
case VALUE_FALSE:
return Boolean.FALSE;
case VALUE_NULL:
return null;
// These states can not be mapped; input stream is
// off by an event or two
case FIELD_NAME:
case END_OBJECT:
case END_ARRAY:
_reportProblem(jp, "Can not map token " + currToken + ": stream off by a token or two?");
default: // sanity check, should never happen
_throwInternal("Unrecognized event type: " + currToken);
return null; // never gets this far
}
}
}
}