Java tutorial
/* * 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<SettableBeanProperty> 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<Object> unwrappingDeserializer(NameTransformer unwrapper) { return (JsonDeserializer<Object>) replaceDelegatee(getDelegatee().unwrappingDeserializer(unwrapper)); } }