com.addthis.codec.jackson.CodecTypeDeserializer.java Source code

Java tutorial

Introduction

Here is the source code for com.addthis.codec.jackson.CodecTypeDeserializer.java

Source

/*
 * 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.addthis.codec.jackson;

import javax.annotation.Nullable;

import java.io.IOException;

import java.util.Iterator;

import com.addthis.codec.plugins.PluginMap;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigObject;

import static com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath;

public class CodecTypeDeserializer extends TypeDeserializerBase {
    private final PluginMap pluginMap;
    private final JsonTypeInfo.As inludeAs;
    private final CodecTypeIdResolver idRes;

    protected CodecTypeDeserializer(PluginMap pluginMap, JsonTypeInfo.As inludeAs, JavaType baseType,
            CodecTypeIdResolver idRes, String typePropertyName, boolean typeIdVisible, JavaType defaultImpl) {
        super(baseType, idRes, typePropertyName, typeIdVisible, defaultImpl);
        this.pluginMap = pluginMap;
        this.inludeAs = inludeAs;
        this.idRes = idRes;
    }

    protected CodecTypeDeserializer(CodecTypeDeserializer src, BeanProperty property) {
        super(src, property);
        this.pluginMap = src.pluginMap;
        this.inludeAs = src.inludeAs;
        this.idRes = src.idRes;
    }

    @Override
    public CodecTypeDeserializer forProperty(BeanProperty prop) {
        return (prop == _property) ? this : new CodecTypeDeserializer(this, prop);
    }

    @Override
    public JsonTypeInfo.As getTypeInclusion() {
        return inludeAs;
    }

    // based on methods/ comments from other TypeDeserializers, these methods cannot be trusted
    // to actually reflect the current json type. so reroute all to the switch and check ourselves.

    @Override
    public Object deserializeTypedFromObject(JsonParser jp, DeserializationContext ctxt) throws IOException {
        return deserializeTypedFromAny(jp, ctxt);
    }

    @Override
    public Object deserializeTypedFromArray(JsonParser jp, DeserializationContext ctxt) throws IOException {
        return deserializeTypedFromAny(jp, ctxt);
    }

    @Override
    public Object deserializeTypedFromScalar(JsonParser jp, DeserializationContext ctxt) throws IOException {
        return deserializeTypedFromAny(jp, ctxt);
    }

    @Override
    public Object deserializeTypedFromAny(JsonParser jp, DeserializationContext ctxt) throws IOException {
        // a jackson thing we might as well include
        if (jp.canReadTypeId()) {
            Object typeId = jp.getTypeId();
            if (typeId != null) {
                return _deserializeWithNativeTypeId(jp, ctxt, typeId);
            }
        }
        // can use this to approximate error location if a sub-method throws an exception
        JsonLocation currentLocation = jp.getTokenLocation();
        JsonNode jsonNode;
        // empty objects can appear with END_OBJECT. that has special handling lots of places, but not in readTree
        if (jp.getCurrentToken() == JsonToken.END_OBJECT) {
            jsonNode = ctxt.getNodeFactory().objectNode();
        } else {
            jsonNode = jp.readValueAsTree();
        }
        ObjectCodec objectCodec = jp.getCodec();

        try {
            Object bean = null;
            // _array handler
            if (jsonNode.isArray()) {
                bean = _deserializeTypedFromArray((ArrayNode) jsonNode, objectCodec, ctxt);
                // object handler
            } else if (jsonNode.isObject()) {
                bean = _deserializeTypedFromObject((ObjectNode) jsonNode, objectCodec, ctxt);
            }
            if (bean != null) {
                return bean;
            } else {
                // Jackson 2.6+ throws NPE on null typeId parameter (underlying Map changed from HashMap
                // to ConcurrentHashMap), so use empty string instead of null
                JsonDeserializer<Object> deser = _findDeserializer(ctxt, "");
                JsonParser treeParser = jp.getCodec().treeAsTokens(jsonNode);
                treeParser.nextToken();
                return deser.deserialize(treeParser, ctxt);
            }
        } catch (JsonMappingException ex) {
            throw Jackson.maybeImproveLocation(currentLocation, ex);
        }
    }

    @Nullable
    public Object _deserializeTypedFromObject(ObjectNode objectNode, ObjectCodec objectCodec,
            DeserializationContext ctxt) throws IOException {
        if (objectNode.hasNonNull(_typePropertyName)) {
            return _deserializeObjectFromProperty(objectNode, objectCodec, ctxt);
        }
        String singleKeyName = getSingleKeyIfPresent(objectNode);
        if (singleKeyName != null) {
            Object bean = _deserializeObjectFromSingleKey(objectNode, singleKeyName, objectCodec, ctxt);
            if (bean != null) {
                return bean;
            }
        }
        Object bean = _deserializeObjectFromInlinedType(objectNode, objectCodec, ctxt);
        if (bean != null) {
            return bean;
        }
        if (idRes.isValidTypeId("_default")) {
            ConfigObject aliasDefaults = pluginMap.aliasDefaults("_default");
            JsonDeserializer<Object> deser = _findDeserializer(ctxt, "_default");
            boolean unwrapPrimary = handleDefaultsAndImplicitPrimary(objectNode, aliasDefaults, deser, ctxt);
            try {
                JsonParser treeParser = objectCodec.treeAsTokens(objectNode);
                treeParser.nextToken();
                bean = deser.deserialize(treeParser, ctxt);
            } catch (IOException cause) {
                if (unwrapPrimary) {
                    throw Jackson.maybeUnwrapPath((String) aliasDefaults.get("_primary").unwrapped(), cause);
                } else {
                    throw cause;
                }
            }
        }
        return bean;
    }

    // returns non-null if there is only one key or only one non-meta-data key. ie. (_class, some-key) or (_some-key)
    @Nullable
    private String getSingleKeyIfPresent(ObjectNode objectNode) {
        Iterator<String> fieldNames = objectNode.fieldNames();
        String singleKey = null;
        while (fieldNames.hasNext()) {
            String fieldName = fieldNames.next();
            if ((singleKey == null) || (singleKey.charAt(0) == '_')) {
                singleKey = fieldName;
            } else if (fieldName.charAt(0) != '_') {
                return null; // more than one key found
            }
        }
        return singleKey;
    }

    @Nullable
    private Object _deserializeObjectFromInlinedType(ObjectNode objectNode, ObjectCodec objectCodec,
            DeserializationContext ctxt) throws IOException {
        String matched = null;
        for (String alias : pluginMap.inlinedAliases()) {
            if (objectNode.get(alias) != null) {
                if (matched != null) {
                    String message = String.format(
                            "no type specified, more than one key, and both %s and %s match for inlined types.",
                            matched, alias);
                    JsonMappingException exception = ctxt.instantiationException(_baseType.getRawClass(), message);
                    exception.prependPath(_baseType, matched);
                    throw exception;
                }
                matched = alias;
            }
        }
        if (matched != null) {
            ConfigObject aliasDefaults = pluginMap.aliasDefaults(matched);
            JsonNode configValue = objectNode.get(matched);
            String primaryField = (String) aliasDefaults.get("_primary").unwrapped();
            objectNode.remove(matched);
            Jackson.setAt(objectNode, configValue, primaryField);
            Jackson.merge(objectNode, Jackson.configConverter(aliasDefaults));
            if (_typeIdVisible) {
                objectNode.put(_typePropertyName, matched);
            }
            try {
                JsonDeserializer<Object> deser = _findDeserializer(ctxt, matched);
                JsonParser treeParser = objectCodec.treeAsTokens(objectNode);
                treeParser.nextToken();
                return deser.deserialize(treeParser, ctxt);
            } catch (IOException cause) {
                IOException unwrapped = Jackson.maybeUnwrapPath(primaryField, cause);
                if (unwrapped != cause) {
                    throw wrapWithPath(unwrapped, idRes.typeFromId(ctxt, matched), matched);
                } else {
                    throw unwrapped;
                }
            }
        } else {
            return null;
        }
    }

    @Nullable
    private Object _deserializeObjectFromSingleKey(ObjectNode objectNode, String singleKeyName,
            ObjectCodec objectCodec, DeserializationContext ctxt) throws IOException {
        if (idRes.isValidTypeId(singleKeyName)) {
            ConfigObject aliasDefaults = pluginMap.aliasDefaults(singleKeyName);
            String primaryField;
            if (aliasDefaults.containsKey("_primary")) {
                primaryField = (String) aliasDefaults.get("_primary").unwrapped();
            } else {
                primaryField = null;
            }
            boolean unwrapPrimary = false;
            try {
                JsonNode singleKeyValue = objectNode.get(singleKeyName);
                JsonDeserializer<Object> deser = _findDeserializer(ctxt, singleKeyName);
                if (!singleKeyValue.isObject()) {
                    // if value is not an object, try supporting _primary syntax to derive one
                    if (primaryField != null) {
                        ObjectNode singleKeyObject = (ObjectNode) objectCodec.createObjectNode();
                        Jackson.setAt(singleKeyObject, singleKeyValue, primaryField);
                        Jackson.merge(singleKeyObject, Jackson.configConverter(aliasDefaults));
                        singleKeyValue = singleKeyObject;
                        unwrapPrimary = true;
                    } // else let the downstream serializer try to handle it or complain
                } else {
                    ObjectNode singleKeyObject = (ObjectNode) singleKeyValue;
                    unwrapPrimary = handleDefaultsAndImplicitPrimary(singleKeyObject, aliasDefaults, deser, ctxt);
                }
                if (_typeIdVisible && singleKeyValue.isObject()) {
                    ((ObjectNode) singleKeyValue).put(_typePropertyName, singleKeyName);
                }
                JsonParser treeParser = objectCodec.treeAsTokens(singleKeyValue);
                treeParser.nextToken();
                return deser.deserialize(treeParser, ctxt);
            } catch (IOException cause) {
                if (unwrapPrimary) {
                    cause = Jackson.maybeUnwrapPath(primaryField, cause);
                }
                throw wrapWithPath(cause, idRes.typeFromId(ctxt, singleKeyName), singleKeyName);
            } catch (Throwable cause) {
                throw wrapWithPath(cause, idRes.typeFromId(ctxt, singleKeyName), singleKeyName);
            }
        }
        return null;
    }

    private Object _deserializeObjectFromProperty(ObjectNode objectNode, ObjectCodec objectCodec,
            DeserializationContext ctxt) throws IOException {
        String type = objectNode.get(_typePropertyName).asText();
        if (!_typeIdVisible) {
            objectNode.remove(_typePropertyName);
        }
        JsonDeserializer<Object> deser;
        try {
            deser = _findDeserializer(ctxt, type);
        } catch (Throwable cause) {
            throw wrapWithPath(cause, Class.class, _typePropertyName);
        }
        ConfigObject aliasDefaults = pluginMap.aliasDefaults(type);
        String primaryField;
        if (aliasDefaults.containsKey("_primary")) {
            primaryField = (String) aliasDefaults.get("_primary").unwrapped();
        } else {
            primaryField = null;
        }
        boolean unwrapPrimary = handleDefaultsAndImplicitPrimary(objectNode, aliasDefaults, deser, ctxt);
        try {
            JsonParser treeParser = objectCodec.treeAsTokens(objectNode);
            treeParser.nextToken();
            return deser.deserialize(treeParser, ctxt);
        } catch (IOException cause) {
            if (unwrapPrimary) {
                throw Jackson.maybeUnwrapPath(primaryField, cause);
            } else {
                throw cause;
            }
        }
    }

    @Nullable
    private Object _deserializeTypedFromArray(ArrayNode arrayNode, ObjectCodec objectCodec,
            DeserializationContext ctxt) throws IOException {
        if (idRes.isValidTypeId("_array")) {
            Config aliasDefaults = pluginMap.aliasDefaults("_array").toConfig();
            String arrayField = aliasDefaults.getString("_primary");
            try {
                ObjectNode objectFieldValues = (ObjectNode) objectCodec.createObjectNode();
                Jackson.setAt(objectFieldValues, arrayNode, arrayField);
                ObjectNode aliasFieldDefaults = Jackson.configConverter(aliasDefaults.root());
                Jackson.merge(objectFieldValues, aliasFieldDefaults);
                JsonDeserializer<Object> deser = _findDeserializer(ctxt, "_array");
                JsonParser treeParser = objectCodec.treeAsTokens(objectFieldValues);
                treeParser.nextToken();
                return deser.deserialize(treeParser, ctxt);
            } catch (IOException ex) {
                throw Jackson.maybeUnwrapPath(arrayField, ex);
            }
        } else {
            return null;
        }
    }

    private boolean handleDefaultsAndImplicitPrimary(ObjectNode fieldValues, ConfigObject aliasDefaults,
            JsonDeserializer<?> deserializer, DeserializationContext ctxt) throws JsonMappingException {
        if (!aliasDefaults.isEmpty()) {
            if (deserializer instanceof DelegatingDeserializer) {
                deserializer = ((DelegatingDeserializer) deserializer).getDelegatee();
            }
            if ((deserializer instanceof BeanDeserializerBase) && (aliasDefaults.get("_primary") != null)) {
                BeanDeserializerBase beanDeserializer = (BeanDeserializerBase) deserializer;
                String primaryField = (String) aliasDefaults.get("_primary").unwrapped();
                if (!fieldValues.has(primaryField)) {
                    // user has not explicitly set a value where _primary points, see if _primary is a plugin type
                    SettableBeanProperty primaryProperty = beanDeserializer.findProperty(primaryField);
                    if ((primaryProperty != null) && primaryProperty.hasValueTypeDeserializer()) {
                        TypeDeserializer primaryTypeDeserializer = primaryProperty.getValueTypeDeserializer();
                        if (primaryTypeDeserializer instanceof CodecTypeDeserializer) {
                            CodecTypeIdResolver primaryPropertyTypeIdResolver = ((CodecTypeDeserializer) primaryTypeDeserializer).idRes;
                            String possibleInlinedPrimary = null;
                            Iterator<String> fieldNames = fieldValues.fieldNames();
                            while (fieldNames.hasNext()) {
                                String fieldName = fieldNames.next();
                                if ((fieldName.charAt(0) != '_') && !beanDeserializer.hasProperty(fieldName)) {
                                    if (primaryPropertyTypeIdResolver.isValidTypeId(fieldName)) {
                                        if (possibleInlinedPrimary == null) {
                                            possibleInlinedPrimary = fieldName;
                                        } else {
                                            String message = String.format(
                                                    "%s and %s are both otherwise unknown properties that "
                                                            + "could be types for the _primary property %s whose category is "
                                                            + "%s. This is too ambiguous to resolve.",
                                                    possibleInlinedPrimary, fieldName, primaryField,
                                                    ((CodecTypeDeserializer) primaryTypeDeserializer).pluginMap
                                                            .category());
                                            JsonMappingException ex = ctxt
                                                    .instantiationException(_baseType.getRawClass(), message);
                                            ex.prependPath(beanDeserializer.getValueType(), fieldName);
                                            throw ex;
                                        }
                                    }
                                }
                            }
                            // did we find a good candidate?
                            if (possibleInlinedPrimary != null) {
                                // then wrap the value with its key (its type), and stash it in our primary field
                                JsonNode inlinedPrimaryValue = fieldValues.remove(possibleInlinedPrimary);
                                fieldValues.with(primaryField).set(possibleInlinedPrimary, inlinedPrimaryValue);
                                Jackson.merge(fieldValues, Jackson.configConverter(aliasDefaults));
                                return true;
                            }
                        }
                    }
                }
            }
            // merge alias defaults here since we check for empty etc anyway
            Jackson.merge(fieldValues, Jackson.configConverter(aliasDefaults));
        }
        return false;
    }
}