com.addthis.codec.jackson.CodecBeanDeserializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of codec Show documentation
Show all versions of codec Show documentation
Codec serialization library
/*
* 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