| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.olingo.client.core.serialization; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.AbstractMap.SimpleEntry; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.olingo.client.api.data.ResWrap; |
| import org.apache.olingo.client.api.serialization.ODataDeserializer; |
| import org.apache.olingo.client.api.serialization.ODataDeserializerException; |
| import org.apache.olingo.commons.api.Constants; |
| import org.apache.olingo.commons.api.data.Annotatable; |
| import org.apache.olingo.commons.api.data.Annotation; |
| import org.apache.olingo.commons.api.data.ComplexValue; |
| import org.apache.olingo.commons.api.data.Entity; |
| import org.apache.olingo.commons.api.data.EntityCollection; |
| import org.apache.olingo.commons.api.data.Link; |
| import org.apache.olingo.commons.api.data.Linked; |
| import org.apache.olingo.commons.api.data.Property; |
| import org.apache.olingo.commons.api.data.PropertyType; |
| import org.apache.olingo.commons.api.data.Valuable; |
| import org.apache.olingo.commons.api.data.ValueType; |
| import org.apache.olingo.commons.api.edm.EdmPrimitiveType; |
| import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException; |
| import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind; |
| import org.apache.olingo.commons.api.edm.geo.Geospatial; |
| import org.apache.olingo.commons.api.ex.ODataError; |
| import org.apache.olingo.commons.core.edm.EdmTypeInfo; |
| |
| import com.fasterxml.jackson.core.JsonFactory; |
| import com.fasterxml.jackson.core.JsonParser; |
| import com.fasterxml.jackson.core.ObjectCodec; |
| import com.fasterxml.jackson.databind.JsonNode; |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import com.fasterxml.jackson.databind.node.ArrayNode; |
| import com.fasterxml.jackson.databind.node.ObjectNode; |
| |
| public class JsonDeserializer implements ODataDeserializer { |
| |
| protected final Pattern CUSTOM_ANNOTATION = Pattern.compile("(.+)@(.+)\\.(.+)"); |
| |
| protected final boolean serverMode; |
| |
| private JsonGeoValueDeserializer geoDeserializer; |
| |
| private JsonParser parser; |
| |
| public JsonDeserializer(final boolean serverMode) { |
| this.serverMode = serverMode; |
| } |
| |
| private JsonGeoValueDeserializer getGeoDeserializer() { |
| if (geoDeserializer == null) { |
| geoDeserializer = new JsonGeoValueDeserializer(); |
| } |
| return geoDeserializer; |
| } |
| |
| protected String getJSONAnnotation(final String string) { |
| return StringUtils.prependIfMissing(string, "@"); |
| } |
| |
| protected String getTitle(final Map.Entry<String, JsonNode> entry) { |
| return entry.getKey().substring(0, entry.getKey().indexOf('@')); |
| } |
| |
| protected String setInline(final String name, final String suffix, final JsonNode tree, |
| final ObjectCodec codec, final Link link) throws IOException { |
| |
| final String entityNamePrefix = name.substring(0, name.indexOf(suffix)); |
| |
| Integer count = null; |
| if (tree.hasNonNull(entityNamePrefix+Constants.JSON_COUNT)) { |
| count = tree.get(entityNamePrefix+Constants.JSON_COUNT).asInt(); |
| } |
| |
| if (tree.has(entityNamePrefix)) { |
| final JsonNode inline = tree.path(entityNamePrefix); |
| JsonEntityDeserializer entityDeserializer = new JsonEntityDeserializer(serverMode); |
| |
| if (inline instanceof ObjectNode) { |
| link.setType(Constants.ENTITY_NAVIGATION_LINK_TYPE); |
| link.setInlineEntity(entityDeserializer.doDeserialize(inline.traverse(codec)).getPayload()); |
| |
| } else if (inline instanceof ArrayNode) { |
| link.setType(Constants.ENTITY_SET_NAVIGATION_LINK_TYPE); |
| |
| final EntityCollection entitySet = new EntityCollection(); |
| if (count != null) { |
| entitySet.setCount(count); |
| } |
| for (final Iterator<JsonNode> entries = inline.elements(); entries.hasNext();) { |
| entitySet.getEntities().add(entityDeserializer.doDeserialize(entries.next().traverse(codec)).getPayload()); |
| } |
| |
| link.setInlineEntitySet(entitySet); |
| } |
| } |
| return entityNamePrefix; |
| } |
| |
| protected void links(final Map.Entry<String, JsonNode> field, final Linked linked, final Set<String> toRemove, |
| final JsonNode tree, final ObjectCodec codec) throws IOException { |
| if (serverMode) { |
| serverLinks(field, linked, toRemove, tree, codec); |
| } else { |
| clientLinks(field, linked, toRemove, tree, codec); |
| } |
| } |
| |
| private void clientLinks(final Map.Entry<String, JsonNode> field, final Linked linked, final Set<String> toRemove, |
| final JsonNode tree, final ObjectCodec codec) throws IOException { |
| |
| if (field.getKey().endsWith(Constants.JSON_NAVIGATION_LINK)) { |
| final Link link = new Link(); |
| link.setTitle(getTitle(field)); |
| link.setRel(Constants.NS_NAVIGATION_LINK_REL + getTitle(field)); |
| |
| if (field.getValue().isValueNode()) { |
| link.setHref(field.getValue().textValue()); |
| link.setType(Constants.ENTITY_NAVIGATION_LINK_TYPE); |
| } |
| |
| linked.getNavigationLinks().add(link); |
| |
| toRemove.add(field.getKey()); |
| toRemove.add(setInline(field.getKey(), Constants.JSON_NAVIGATION_LINK, tree, codec, link)); |
| } else if (field.getKey().endsWith(Constants.JSON_ASSOCIATION_LINK)) { |
| final Link link = new Link(); |
| link.setTitle(getTitle(field)); |
| link.setRel(Constants.NS_ASSOCIATION_LINK_REL + getTitle(field)); |
| link.setHref(field.getValue().textValue()); |
| link.setType(Constants.ASSOCIATION_LINK_TYPE); |
| linked.getAssociationLinks().add(link); |
| |
| toRemove.add(field.getKey()); |
| } |
| } |
| |
| private void serverLinks(final Map.Entry<String, JsonNode> field, final Linked linked, final Set<String> toRemove, |
| final JsonNode tree, final ObjectCodec codec) throws IOException { |
| |
| if (field.getKey().endsWith(Constants.JSON_BIND_LINK_SUFFIX) |
| || field.getKey().endsWith(Constants.JSON_NAVIGATION_LINK)) { |
| |
| if (field.getValue().isValueNode()) { |
| final String suffix = field.getKey().replaceAll("^.*@", "@"); |
| |
| final Link link = new Link(); |
| link.setTitle(getTitle(field)); |
| link.setRel(Constants.NS_NAVIGATION_LINK_REL + getTitle(field)); |
| link.setHref(field.getValue().textValue()); |
| link.setType(Constants.ENTITY_NAVIGATION_LINK_TYPE); |
| linked.getNavigationLinks().add(link); |
| |
| toRemove.add(setInline(field.getKey(), suffix, tree, codec, link)); |
| } else if (field.getValue().isArray()) { |
| for (final Iterator<JsonNode> itor = field.getValue().elements(); itor.hasNext();) { |
| final JsonNode node = itor.next(); |
| |
| final Link link = new Link(); |
| link.setTitle(getTitle(field)); |
| link.setRel(Constants.NS_NAVIGATION_LINK_REL + getTitle(field)); |
| link.setHref(node.asText()); |
| link.setType(Constants.ENTITY_SET_NAVIGATION_LINK_TYPE); |
| linked.getNavigationLinks().add(link); |
| toRemove.add(setInline(field.getKey(), Constants.JSON_BIND_LINK_SUFFIX, tree, codec, link)); |
| } |
| } |
| toRemove.add(field.getKey()); |
| } |
| } |
| |
| private Map.Entry<PropertyType, EdmTypeInfo> guessPropertyType(final JsonNode node) { |
| PropertyType type; |
| String typeExpression = null; |
| |
| if (node.isValueNode() || node.isNull()) { |
| type = PropertyType.PRIMITIVE; |
| typeExpression = guessPrimitiveTypeKind(node).getFullQualifiedName().toString(); |
| } else if (node.isArray()) { |
| type = PropertyType.COLLECTION; |
| if (node.has(0) && node.get(0).isValueNode()) { |
| typeExpression = "Collection(" + guessPrimitiveTypeKind(node.get(0)) + ')'; |
| } |
| } else if (node.isObject()) { |
| if (node.has(Constants.ATTR_TYPE)) { |
| type = PropertyType.PRIMITIVE; |
| typeExpression = "Edm.Geography" + node.get(Constants.ATTR_TYPE).asText(); |
| } else { |
| type = PropertyType.COMPLEX; |
| } |
| } else { |
| type = PropertyType.EMPTY; |
| } |
| |
| final EdmTypeInfo typeInfo = typeExpression == null ? null : |
| new EdmTypeInfo.Builder().setTypeExpression(typeExpression).build(); |
| return new SimpleEntry<PropertyType, EdmTypeInfo>(type, typeInfo); |
| } |
| |
| private EdmPrimitiveTypeKind guessPrimitiveTypeKind(final JsonNode node) { |
| return node.isShort() ? EdmPrimitiveTypeKind.Int16 : |
| node.isInt() ? EdmPrimitiveTypeKind.Int32 : |
| node.isLong() ? EdmPrimitiveTypeKind.Int64 : |
| node.isBoolean() ? EdmPrimitiveTypeKind.Boolean : |
| node.isFloat() ? EdmPrimitiveTypeKind.Single : |
| node.isDouble() ? EdmPrimitiveTypeKind.Double : |
| node.isBigDecimal() ? EdmPrimitiveTypeKind.Decimal : |
| EdmPrimitiveTypeKind.String; |
| } |
| |
| protected void populate(final Annotatable annotatable, final List<Property> properties, |
| final ObjectNode tree, final ObjectCodec codec) |
| throws IOException, EdmPrimitiveTypeException { |
| |
| String type = null; |
| Annotation annotation = null; |
| for (final Iterator<Map.Entry<String, JsonNode>> itor = tree.fields(); itor.hasNext();) { |
| final Map.Entry<String, JsonNode> field = itor.next(); |
| final Matcher customAnnotation = CUSTOM_ANNOTATION.matcher(field.getKey()); |
| |
| if (field.getKey().charAt(0) == '@') { |
| final Annotation entityAnnot = new Annotation(); |
| entityAnnot.setTerm(field.getKey().substring(1)); |
| |
| value(entityAnnot, field.getValue(), codec); |
| if (annotatable != null) { |
| annotatable.getAnnotations().add(entityAnnot); |
| } |
| } else if (type == null && field.getKey().endsWith(getJSONAnnotation(Constants.JSON_TYPE))) { |
| type = field.getValue().asText(); |
| } else if (field.getKey().endsWith(getJSONAnnotation(Constants.JSON_COUNT))) { |
| final Property property = new Property(); |
| property.setName(field.getKey()); |
| property.setValue(ValueType.PRIMITIVE, Integer.parseInt(field.getValue().asText())); |
| properties.add(property); |
| } else if (annotation == null && customAnnotation.matches() && !"odata".equals(customAnnotation.group(2))) { |
| annotation = new Annotation(); |
| annotation.setTerm(customAnnotation.group(2) + "." + customAnnotation.group(3)); |
| value(annotation, field.getValue(), codec); |
| } else { |
| final Property property = new Property(); |
| property.setName(field.getKey()); |
| property.setType(type == null |
| ? null |
| : new EdmTypeInfo.Builder().setTypeExpression(type).build().internal()); |
| type = null; |
| |
| value(property, field.getValue(), codec); |
| properties.add(property); |
| |
| if (annotation != null) { |
| property.getAnnotations().add(annotation); |
| annotation = null; |
| } |
| } |
| } |
| } |
| |
| private Object fromPrimitive(final JsonNode node, final EdmTypeInfo typeInfo) throws EdmPrimitiveTypeException { |
| return node.isNull() ? null |
| : typeInfo == null ? node.asText() |
| : typeInfo.getPrimitiveTypeKind().isGeospatial() |
| ? getGeoDeserializer().deserialize(node, typeInfo) |
| : ((EdmPrimitiveType) typeInfo.getType()) |
| .valueOfString(node.asText(), true, null, |
| Constants.DEFAULT_PRECISION, Constants.DEFAULT_SCALE, true, |
| ((EdmPrimitiveType) typeInfo.getType()).getDefaultType()); |
| } |
| |
| private Object fromComplex(final ObjectNode node, final ObjectCodec codec) |
| throws IOException, EdmPrimitiveTypeException { |
| |
| final ComplexValue complexValue = new ComplexValue(); |
| final Set<String> toRemove = new HashSet<String>(); |
| for (final Iterator<Map.Entry<String, JsonNode>> itor = node.fields(); itor.hasNext();) { |
| final Map.Entry<String, JsonNode> field = itor.next(); |
| |
| links(field, complexValue, toRemove, node, codec); |
| } |
| node.remove(toRemove); |
| |
| populate(complexValue, complexValue.getValue(), node, codec); |
| return complexValue; |
| } |
| |
| private void fromCollection(final Valuable valuable, final Iterator<JsonNode> nodeItor, final EdmTypeInfo typeInfo, |
| final ObjectCodec codec) throws IOException, EdmPrimitiveTypeException { |
| |
| final List<Object> values = new ArrayList<Object>(); |
| ValueType valueType = ValueType.COLLECTION_PRIMITIVE; |
| |
| final EdmTypeInfo type = typeInfo == null ? null |
| : new EdmTypeInfo.Builder().setTypeExpression(typeInfo.getFullQualifiedName().toString()).build(); |
| |
| while (nodeItor.hasNext()) { |
| final JsonNode child = nodeItor.next(); |
| |
| if (child.isValueNode()) { |
| if (typeInfo == null || typeInfo.isPrimitiveType()) { |
| final Object value = fromPrimitive(child, type); |
| valueType = value instanceof Geospatial ? ValueType.COLLECTION_GEOSPATIAL : ValueType.COLLECTION_PRIMITIVE; |
| values.add(value); |
| } else { |
| valueType = ValueType.COLLECTION_ENUM; |
| values.add(child.asText()); |
| } |
| } else if (child.isContainerNode()) { |
| if (child.has(Constants.JSON_TYPE)) { |
| ((ObjectNode) child).remove(Constants.JSON_TYPE); |
| } |
| final Object value = fromComplex((ObjectNode) child, codec); |
| valueType = ValueType.COLLECTION_COMPLEX; |
| values.add(value); |
| } |
| } |
| valuable.setValue(valueType, values); |
| } |
| |
| protected void value(final Valuable valuable, final JsonNode node, final ObjectCodec codec) |
| throws IOException, EdmPrimitiveTypeException { |
| |
| EdmTypeInfo typeInfo = StringUtils.isBlank(valuable.getType()) ? null |
| : new EdmTypeInfo.Builder().setTypeExpression(valuable.getType()).build(); |
| |
| final Map.Entry<PropertyType, EdmTypeInfo> guessed = guessPropertyType(node); |
| if (typeInfo == null) { |
| typeInfo = guessed.getValue(); |
| } |
| |
| final PropertyType propType = typeInfo == null ? guessed.getKey() |
| : typeInfo.isCollection() ? PropertyType.COLLECTION |
| : typeInfo.isPrimitiveType() ? PropertyType.PRIMITIVE |
| : node.isValueNode() ? PropertyType.ENUM : PropertyType.COMPLEX; |
| |
| switch (propType) { |
| case COLLECTION: |
| fromCollection(valuable, node.elements(), typeInfo, codec); |
| break; |
| |
| case COMPLEX: |
| if (node.has(Constants.JSON_TYPE)) { |
| valuable.setType(node.get(Constants.JSON_TYPE).asText()); |
| ((ObjectNode) node).remove(Constants.JSON_TYPE); |
| } |
| final Object value = fromComplex((ObjectNode) node, codec); |
| valuable.setValue(ValueType.COMPLEX, value); |
| break; |
| |
| case ENUM: |
| valuable.setValue(ValueType.ENUM, node.asText()); |
| break; |
| |
| case PRIMITIVE: |
| if (valuable.getType() == null && typeInfo != null) { |
| valuable.setType(typeInfo.getFullQualifiedName().toString()); |
| } |
| final Object primitiveValue = fromPrimitive(node, typeInfo); |
| valuable.setValue(primitiveValue instanceof Geospatial ? ValueType.GEOSPATIAL : ValueType.PRIMITIVE, |
| primitiveValue); |
| break; |
| |
| case EMPTY: |
| default: |
| valuable.setValue(ValueType.PRIMITIVE, StringUtils.EMPTY); |
| } |
| } |
| |
| @Override |
| public ResWrap<EntityCollection> toEntitySet(final InputStream input) throws ODataDeserializerException { |
| try { |
| parser = new JsonFactory(new ObjectMapper()).createParser(input); |
| return new JsonEntitySetDeserializer(serverMode).doDeserialize(parser); |
| } catch (final IOException e) { |
| throw new ODataDeserializerException(e); |
| } |
| } |
| |
| @Override |
| public ResWrap<Entity> toEntity(final InputStream input) throws ODataDeserializerException { |
| try { |
| parser = new JsonFactory(new ObjectMapper()).createParser(input); |
| return new JsonEntityDeserializer(serverMode).doDeserialize(parser); |
| } catch (final IOException e) { |
| throw new ODataDeserializerException(e); |
| } |
| } |
| |
| @Override |
| public ResWrap<Property> toProperty(final InputStream input) throws ODataDeserializerException { |
| try { |
| parser = new JsonFactory(new ObjectMapper()).createParser(input); |
| return new JsonPropertyDeserializer(serverMode).doDeserialize(parser); |
| } catch (final IOException e) { |
| throw new ODataDeserializerException(e); |
| } |
| } |
| |
| @Override |
| public ODataError toError(final InputStream input) throws ODataDeserializerException { |
| try { |
| parser = new JsonFactory(new ObjectMapper()).createParser(input); |
| return new JsonODataErrorDeserializer(serverMode).doDeserialize(parser); |
| } catch (final IOException e) { |
| throw new ODataDeserializerException(e); |
| } |
| } |
| } |