All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.addthis.codec.jackson.CodecBeanDeserializer Maven / Gradle / Ivy

There is a newer version: 3.8.2
Show newest version
/*
 * 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 java.io.IOException;

import java.util.Iterator;
import java.util.regex.Pattern;

import com.addthis.codec.annotations.Bytes;
import com.addthis.codec.annotations.Time;
import com.addthis.codec.codables.SuperCodable;

import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
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.node.ObjectNode;
import com.fasterxml.jackson.databind.util.NameTransformer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.dropwizard.util.Duration;
import io.dropwizard.util.Size;

public class CodecBeanDeserializer extends DelegatingDeserializer {
    private static final Logger log = LoggerFactory.getLogger(CodecBeanDeserializer.class);
    private static final Pattern NUMBER_UNIT = Pattern.compile("(\\d+)\\s*([^\\s\\d]+)");

    private final ObjectNode fieldDefaults;

    protected CodecBeanDeserializer(BeanDeserializerBase src, ObjectNode fieldDefaults) {
        super(src);
        this.fieldDefaults = fieldDefaults;
    }

    @Override public BeanDeserializerBase getDelegatee() {
        return (BeanDeserializerBase) _delegatee;
    }

    @Override protected JsonDeserializer newDelegatingInstance(JsonDeserializer newDelegatee) {
        return new CodecBeanDeserializer((BeanDeserializerBase) newDelegatee, fieldDefaults);
    }

    @Override
    public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonLocation currentLocation = jp.getTokenLocation();
        JsonToken t = jp.getCurrentToken();
        try {
            if (t == JsonToken.START_OBJECT) {
                ObjectNode objectNode = jp.readValueAsTree();
                handleDefaultsAndRequiredAndNull(ctxt, objectNode);
                jp = jp.getCodec().treeAsTokens(objectNode);
                jp.nextToken();
            } else if (t == JsonToken.END_OBJECT) {
                // for some reason this is how they chose to handle single field objects
                jp.nextToken();
                ObjectNode objectNode = ctxt.getNodeFactory().objectNode();
                handleDefaultsAndRequiredAndNull(ctxt, objectNode);
                jp = jp.getCodec().treeAsTokens(objectNode);
                jp.nextToken();
            }
            Object value = getDelegatee().deserialize(jp, ctxt);
            if (value instanceof SuperCodable) {
                ((SuperCodable) value).postDecode();
            }
            return value;
        } catch (JsonMappingException ex) {
            throw Jackson.maybeImproveLocation(currentLocation, ex);
        }
    }

    private void handleDefaultsAndRequiredAndNull(DeserializationContext ctxt, ObjectNode fieldValues)
            throws JsonMappingException {
        Iterator propertyIterator = getDelegatee().properties();
        while (propertyIterator.hasNext()) {
            SettableBeanProperty prop = propertyIterator.next();
            String propertyName = prop.getName();
            JsonNode fieldValue = fieldValues.path(propertyName);
            if (fieldValue.isMissingNode() || fieldValue.isNull()) {
                if (fieldDefaults.hasNonNull(propertyName)) {
                    fieldValue = fieldDefaults.get(propertyName).deepCopy();
                    fieldValues.set(propertyName, fieldValue);
                } else if (prop.isRequired()) {
                    throw MissingPropertyException.from(ctxt.getParser(), prop.getType().getRawClass(),
                                                        propertyName, getKnownPropertyNames());
                } else if (fieldValue.isNull()
                           && (prop.getType().isPrimitive() || (prop.getValueDeserializer().getNullValue() == null))) {
                    // don't overwrite possible hard-coded defaults/ values with nulls unless they are fancy
                    fieldValues.remove(propertyName);
                }
            }
            if (fieldValue.isTextual()) {
                try {
                    // sometimes we erroneously get strings that would parse into valid numbers and maybe other edge
                    // cases (eg. when using system property overrides in typesafe-config). So we'll go ahead and guard
                    // with this regex to make sure we only get reasonable candidates.
                    Time time = prop.getAnnotation(Time.class);
                    if ((time != null) && NUMBER_UNIT.matcher(fieldValue.textValue()).matches()) {
                        Duration dropWizardDuration = Duration.parse(fieldValue.asText());
                        long asLong = time.value().convert(dropWizardDuration.getQuantity(), dropWizardDuration.getUnit());
                        fieldValues.put(propertyName, asLong);
                    } else if ((prop.getAnnotation(Bytes.class) != null) &&
                               NUMBER_UNIT.matcher(fieldValue.textValue()).matches()) {
                        Size dropWizardSize = Size.parse(fieldValue.asText());
                        long asLong = dropWizardSize.toBytes();
                        fieldValues.put(propertyName, asLong);
                    }
                } catch (Throwable cause) {
                    throw JsonMappingException.wrapWithPath(cause, prop.getType().getRawClass(), propertyName);
                }
            }
        }
    }

    // required overrides that don't actually change much

    @Override
    public JsonDeserializer unwrappingDeserializer(NameTransformer unwrapper) {
        return (JsonDeserializer) replaceDelegatee(getDelegatee().unwrappingDeserializer(unwrapper));
    }
}