package com.amazonaws.services.dynamodbv2.json.converter.impl; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import com.amazonaws.services.dynamodbv2.json.converter.JacksonConverter; import com.amazonaws.services.dynamodbv2.json.converter.JacksonConverterException; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; /** * Implementation of the {@link JacksonConverter}. */ public class JacksonConverterImpl implements JacksonConverter { /** * Maximum JSON depth. */ private static final int MAX_DEPTH = 50; /** * Constructs a {@link JacksonConverterImpl}. */ public JacksonConverterImpl() { } /** * Asserts the depth is not greater than {@link #MAX_DEPTH}. * * @param depth * Current JSON depth * @throws JacksonConverterException * Depth is greater than {@link #MAX_DEPTH} */ private void assertDepth(final int depth) throws JacksonConverterException { if (depth > MAX_DEPTH) { throw new JacksonConverterException("Max depth reached. The object/array has too much depth."); } } /** * Gets an DynamoDB representation of a JsonNode. * * @param node * The JSON to convert * @param depth * Current JSON depth * @return DynamoDB representation of the JsonNode * @throws JacksonConverterException * Unknown JsonNode type or JSON is too deep */ private AttributeValue getAttributeValue(final JsonNode node, final int depth) throws JacksonConverterException { assertDepth(depth); switch (node.asToken()) { case VALUE_STRING: return new AttributeValue().withS(node.textValue()); case VALUE_NUMBER_INT: case VALUE_NUMBER_FLOAT: return new AttributeValue().withN(node.numberValue().toString()); case VALUE_TRUE: case VALUE_FALSE: return new AttributeValue().withBOOL(node.booleanValue()); case VALUE_NULL: return new AttributeValue().withNULL(true); case START_OBJECT: return new AttributeValue().withM(jsonObjectToMap(node, depth)); case START_ARRAY: return new AttributeValue().withL(jsonArrayToList(node, depth)); default: throw new JacksonConverterException("Unknown node type: " + node); } } /** * Converts a DynamoDB attribute to a JSON representation. * * @param av * DynamoDB attribute * @param depth * Current JSON depth * @return JSON representation of the DynamoDB attribute * @throws JacksonConverterException * Unknown DynamoDB type or JSON is too deep */ private JsonNode getJsonNode(final AttributeValue av, final int depth) throws JacksonConverterException { assertDepth(depth); if (av.getS() != null) { return JsonNodeFactory.instance.textNode(av.getS()); } else if (av.getN() != null) { try { return JsonNodeFactory.instance.numberNode(Integer.parseInt(av.getN())); } catch (final NumberFormatException e) { // Not an integer try { return JsonNodeFactory.instance.numberNode(Float.parseFloat(av.getN())); } catch (final NumberFormatException e2) { // Not a number throw new JacksonConverterException(e.getMessage()); } } } else if (av.getBOOL() != null) { return JsonNodeFactory.instance.booleanNode(av.getBOOL()); } else if (av.getNULL() != null) { return JsonNodeFactory.instance.nullNode(); } else if (av.getL() != null) { return listToJsonArray(av.getL(), depth); } else if (av.getM() != null) { return mapToJsonObject(av.getM(), depth); } else { throw new JacksonConverterException("Unknown type value " + av); } } /** * {@inheritDoc} */ @Override public JsonNode itemListToJsonArray(final List<Map<String, AttributeValue>> items) throws JacksonConverterException { if (items != null) { final ArrayNode array = JsonNodeFactory.instance.arrayNode(); for (final Map<String, AttributeValue> item : items) { array.add(mapToJsonObject(item, 0)); } return array; } throw new JacksonConverterException("Items cannnot be null"); } /** * {@inheritDoc} */ @Override public List<AttributeValue> jsonArrayToList(final JsonNode node) throws JacksonConverterException { return jsonArrayToList(node, 0); } /** * Helper method to convert a JsonArrayNode to a DynamoDB list. * * @param node * Array node to convert * @param depth * Current JSON depth * @return DynamoDB list representation of the array node * @throws JacksonConverterException * JsonNode is not an array or depth is too great */ private List<AttributeValue> jsonArrayToList(final JsonNode node, final int depth) throws JacksonConverterException { assertDepth(depth); if (node != null && node.isArray()) { final List<AttributeValue> result = new ArrayList<AttributeValue>(); final Iterator<JsonNode> children = node.elements(); while (children.hasNext()) { final JsonNode child = children.next(); result.add(getAttributeValue(child, depth)); } return result; } throw new JacksonConverterException("Expected JSON array, but received " + node); } /** * {@inheritDoc} */ @Override public Map<String, AttributeValue> jsonObjectToMap(final JsonNode node) throws JacksonConverterException { return jsonObjectToMap(node, 0); } /** * Transforms a JSON object to a DynamoDB object. * * @param node * JSON object * @param depth * Current JSON depth * @return DynamoDB object representation of JSON * @throws JacksonConverterException * JSON is not an object or depth is too great */ private Map<String, AttributeValue> jsonObjectToMap(final JsonNode node, final int depth) throws JacksonConverterException { assertDepth(depth); if (node != null && node.isObject()) { final Map<String, AttributeValue> result = new HashMap<String, AttributeValue>(); final Iterator<String> keys = node.fieldNames(); while (keys.hasNext()) { final String key = keys.next(); result.put(key, getAttributeValue(node.get(key), depth + 1)); } return result; } throw new JacksonConverterException("Expected JSON Object, but received " + node); } /** * {@inheritDoc} */ @Override public JsonNode listToJsonArray(final List<AttributeValue> item) throws JacksonConverterException { return listToJsonArray(item, 0); } /** * Converts a DynamoDB list to a JSON list. * * @param item * DynamoDB list * @param depth * Current JSON depth * @return JSON array node representation of DynamoDB list * @throws JacksonConverterException * Null DynamoDB list or JSON too deep */ private JsonNode listToJsonArray(final List<AttributeValue> item, final int depth) throws JacksonConverterException { assertDepth(depth); if (item != null) { final ArrayNode node = JsonNodeFactory.instance.arrayNode(); for (final AttributeValue value : item) { node.add(getJsonNode(value, depth + 1)); } return node; } throw new JacksonConverterException("Item cannot be null"); } /** * {@inheritDoc} */ @Override public JsonNode mapToJsonObject(final Map<String, AttributeValue> item) throws JacksonConverterException { return mapToJsonObject(item, 0); } /** * Converts a DynamoDB object to a JSON map. * * @param item * DynamoDB object * @param depth * Current JSON depth * @return JSON map representation of the DynamoDB object * @throws JacksonConverterException * Null DynamoDB object or JSON too deep */ private JsonNode mapToJsonObject(final Map<String, AttributeValue> item, final int depth) throws JacksonConverterException { assertDepth(depth); if (item != null) { final ObjectNode node = JsonNodeFactory.instance.objectNode(); for (final Entry<String, AttributeValue> entry : item.entrySet()) { node.put(entry.getKey(), getJsonNode(entry.getValue(), depth + 1)); } return node; } throw new JacksonConverterException("Item cannot be null"); } }