/**
* Copyright 2012, 2013 Turn, Inc.
*
* 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.turn.shapeshifter;
import java.util.Iterator;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.Message;
/**
* Implementation of {@link Parser} based on the configuration contained in a
* {@link NamedSchema}.
*
* @author jsilland
*/
class SchemaParser implements Parser {
private final NamedSchema schema;
/**
* Returns a new parser that will use the configured schema.
*
* @param schema the schema to use for parsing
*/
SchemaParser(NamedSchema schema) {
this.schema = schema;
}
/**
* {@inheritDoc}
*/
@Override
public Message parse(JsonNode node, ReadableSchemaRegistry registry) throws ParsingException {
Message.Builder builder = DynamicMessage.newBuilder(schema.getDescriptor());
for (Map.Entry<String, FieldDescriptor> fieldEntry : schema.getFields().entrySet()) {
String fieldName = schema.getPropertyName(fieldEntry.getKey());
FieldDescriptor field = fieldEntry.getValue();
if (node.has(fieldName) && !node.get(fieldName).isNull()) {
JsonNode valueNode = node.get(fieldName);
if (field.isRepeated()) {
if (schema.getMappings().containsKey(field.getName())) {
parseMappedField(registry, builder, fieldName, field, valueNode);
} else {
parseRepeatedField(registry, builder, fieldName, field, valueNode);
}
} else {
Object value = parseValue(valueNode, field, registry);
if (value != null) {
builder.setField(field, value);
}
}
}
}
return builder.build();
}
/**
* Parses a repeated mapped field.
*
* @param registry a registry of schemas, used for parsing enclosed objects
* @param builder the builder in which the parsed field should be set
* @param field the descriptor of the repeated field being parsed
* @param fieldName the JSON name of the field
* @param valueNode the JSON node being parsed
* @throws ParsingException
* @see {@link #mapRepeatedField(String, String)}
*/
private void parseMappedField(ReadableSchemaRegistry registry, Message.Builder builder,
String fieldName, FieldDescriptor field, JsonNode valueNode) throws ParsingException {
if (!valueNode.isObject()) {
throw new IllegalArgumentException(
"Field '" + fieldName +
"' is expected to be an object, but was " +
valueNode.asToken());
}
ObjectNode objectNode = (ObjectNode) valueNode;
Iterator<Map.Entry<String, JsonNode>> subObjectsIterator =
objectNode.fields();
while (subObjectsIterator.hasNext()) {
Map.Entry<String, JsonNode> subObject = subObjectsIterator.next();
Message message = (Message) parseValue(
subObject.getValue(), field, registry);
DynamicMessage.Builder dynamicMessage = DynamicMessage.newBuilder(
field.getMessageType());
dynamicMessage.mergeFrom(message);
dynamicMessage.setField(
schema.getMappings().get(field.getName()), subObject.getKey());
builder.addRepeatedField(field, dynamicMessage.build());
}
}
/**
* Parses a repeated field.
*
* @param registry a registry of schemas, used for parsing enclosed objects
* @param builder the builder in which the parsed field should be set
* @param fieldName the JSON name of the field
* @param field the descriptor of the repeated field being parsed
* @param valueNode the JSON node being parsed
* @throws ParsingException
*/
private void parseRepeatedField(ReadableSchemaRegistry registry, Message.Builder builder,
String fieldName, FieldDescriptor field, JsonNode valueNode) throws ParsingException {
if (!valueNode.isArray()) {
throw new IllegalArgumentException(
"Field '" + fieldName +
"' is expected to be an array, but was " +
valueNode.asToken());
}
ArrayNode array = (ArrayNode) valueNode;
if (array.size() != 0) {
for (JsonNode item : array) {
Object value = parseValue(item, field, registry);
if (value != null) {
builder.addRepeatedField(field, value);
}
}
}
}
/**
* Reads the value of a JSON node and returns the corresponding Java object.
*
* @param jsonNode the node to convert to a Java object
* @param field the protocol buffer field to which the resulting value will be assigned
* @param registry a schema registry in which enclosed message fields are expected to
* be defined
* @return a Java object representing the field's value
* @throws ParsingException
* @throws
* @throws UnmappableValueException in case the JSON value node cannot be converted
* to a Java object that could be assigned to {@code field}
*/
private Object parseValue(JsonNode jsonNode, FieldDescriptor field,
ReadableSchemaRegistry registry) throws ParsingException {
Object value = null;
if (schema.getTransforms().containsKey(field.getName())) {
return schema.getTransforms().get(field.getName()).parse(jsonNode);
}
switch (field.getType()) {
case BOOL:
JsonTokens.checkJsonValueConformance(jsonNode, JsonTokens.VALID_BOOLEAN_TOKENS);
value = Boolean.valueOf(jsonNode.asBoolean());
break;
case BYTES:
break;
case DOUBLE:
value = new Double(jsonNode.asDouble());
break;
case ENUM:
JsonTokens.checkJsonValueConformance(jsonNode, JsonTokens.VALID_ENUM_TOKENS);
String enumValue = schema.getEnumCaseFormat().to(
NamedSchema.PROTO_ENUM_CASE_FORMAT, jsonNode.asText());
value = field.getEnumType().findValueByName(enumValue);
break;
case FLOAT:
JsonTokens.checkJsonValueConformance(jsonNode, JsonTokens.VALID_FLOAT_TOKENS);
value = new Float(jsonNode.asDouble());
break;
case GROUP:
break;
case FIXED32:
case INT32:
case SFIXED32:
case SINT32:
case UINT32:
JsonTokens.checkJsonValueConformance(jsonNode, JsonTokens.VALID_INTEGER_TOKENS);
value = new Integer(jsonNode.asInt());
break;
case FIXED64:
case INT64:
case SFIXED64:
case SINT64:
case UINT64:
JsonTokens.checkJsonValueConformance(jsonNode, JsonTokens.VALID_INTEGER_TOKENS);
value = new Long(jsonNode.asLong());
break;
case MESSAGE:
if (!jsonNode.isObject()) {
throw new IllegalArgumentException(
"Expected to parse object, found value of type " + jsonNode.asToken());
}
Schema subSchema = null;
if (schema.getSubObjectsSchemas().containsKey(field.getName())) {
String schemaName = schema.getSubObjectsSchemas().get(field.getName());
if (registry.contains(schemaName)) {
subSchema = registry.get(schemaName);
} else {
throw new IllegalStateException();
}
} else {
try {
subSchema = registry.get(field.getMessageType());
} catch (SchemaObtentionException soe) {
throw new ParsingException(soe);
}
}
value = subSchema.getParser().parse(jsonNode, registry);
break;
case STRING:
JsonTokens.checkJsonValueConformance(jsonNode, JsonTokens.VALID_STRING_TOKENS);
value = jsonNode.asText();
break;
default:
break;
}
return value;
}
}