View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package com.addthis.codec.jackson;
15  
16  import java.io.IOException;
17  
18  import java.util.Iterator;
19  import java.util.regex.Pattern;
20  
21  import com.addthis.codec.annotations.Bytes;
22  import com.addthis.codec.annotations.Time;
23  import com.addthis.codec.codables.SuperCodable;
24  
25  import com.fasterxml.jackson.core.JsonLocation;
26  import com.fasterxml.jackson.core.JsonParser;
27  import com.fasterxml.jackson.core.JsonProcessingException;
28  import com.fasterxml.jackson.core.JsonToken;
29  import com.fasterxml.jackson.databind.DeserializationContext;
30  import com.fasterxml.jackson.databind.JsonDeserializer;
31  import com.fasterxml.jackson.databind.JsonMappingException;
32  import com.fasterxml.jackson.databind.JsonNode;
33  import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
34  import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
35  import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
36  import com.fasterxml.jackson.databind.node.ObjectNode;
37  import com.fasterxml.jackson.databind.util.NameTransformer;
38  
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import io.dropwizard.util.Duration;
43  import io.dropwizard.util.Size;
44  
45  public class CodecBeanDeserializer extends DelegatingDeserializer {
46      private static final Logger log = LoggerFactory.getLogger(CodecBeanDeserializer.class);
47      private static final Pattern NUMBER_UNIT = Pattern.compile("(\\d+)\\s*([^\\s\\d]+)");
48  
49      private final ObjectNode fieldDefaults;
50  
51      protected CodecBeanDeserializer(BeanDeserializerBase src, ObjectNode fieldDefaults) {
52          super(src);
53          this.fieldDefaults = fieldDefaults;
54      }
55  
56      @Override public BeanDeserializerBase getDelegatee() {
57          return (BeanDeserializerBase) _delegatee;
58      }
59  
60      @Override protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
61          return new CodecBeanDeserializer((BeanDeserializerBase) newDelegatee, fieldDefaults);
62      }
63  
64      @Override
65      public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
66          JsonLocation currentLocation = jp.getTokenLocation();
67          JsonToken t = jp.getCurrentToken();
68          try {
69              if (t == JsonToken.START_OBJECT) {
70                  ObjectNode objectNode = jp.readValueAsTree();
71                  handleDefaultsAndRequiredAndNull(ctxt, objectNode);
72                  jp = jp.getCodec().treeAsTokens(objectNode);
73                  jp.nextToken();
74              } else if (t == JsonToken.END_OBJECT) {
75                  // for some reason this is how they chose to handle single field objects
76                  jp.nextToken();
77                  ObjectNode objectNode = ctxt.getNodeFactory().objectNode();
78                  handleDefaultsAndRequiredAndNull(ctxt, objectNode);
79                  jp = jp.getCodec().treeAsTokens(objectNode);
80                  jp.nextToken();
81              }
82              Object value = getDelegatee().deserialize(jp, ctxt);
83              if (value instanceof SuperCodable) {
84                  ((SuperCodable) value).postDecode();
85              }
86              return value;
87          } catch (JsonMappingException ex) {
88              throw Jackson.maybeImproveLocation(currentLocation, ex);
89          }
90      }
91  
92      private void handleDefaultsAndRequiredAndNull(DeserializationContext ctxt, ObjectNode fieldValues)
93              throws JsonMappingException {
94          Iterator<SettableBeanProperty> propertyIterator = getDelegatee().properties();
95          while (propertyIterator.hasNext()) {
96              SettableBeanProperty prop = propertyIterator.next();
97              String propertyName = prop.getName();
98              JsonNode fieldValue = fieldValues.path(propertyName);
99              if (fieldValue.isMissingNode() || fieldValue.isNull()) {
100                 if (fieldDefaults.hasNonNull(propertyName)) {
101                     fieldValue = fieldDefaults.get(propertyName).deepCopy();
102                     fieldValues.set(propertyName, fieldValue);
103                 } else if (prop.isRequired()) {
104                     throw MissingPropertyException.from(ctxt.getParser(), prop.getType().getRawClass(),
105                                                         propertyName, getKnownPropertyNames());
106                 } else if (fieldValue.isNull()
107                            && (prop.getType().isPrimitive() || (prop.getValueDeserializer().getNullValue() == null))) {
108                     // don't overwrite possible hard-coded defaults/ values with nulls unless they are fancy
109                     fieldValues.remove(propertyName);
110                 }
111             }
112             if (fieldValue.isTextual()) {
113                 try {
114                     // sometimes we erroneously get strings that would parse into valid numbers and maybe other edge
115                     // cases (eg. when using system property overrides in typesafe-config). So we'll go ahead and guard
116                     // with this regex to make sure we only get reasonable candidates.
117                     Time time = prop.getAnnotation(Time.class);
118                     if ((time != null) && NUMBER_UNIT.matcher(fieldValue.textValue()).matches()) {
119                         Duration dropWizardDuration = Duration.parse(fieldValue.asText());
120                         long asLong = time.value().convert(dropWizardDuration.getQuantity(), dropWizardDuration.getUnit());
121                         fieldValues.put(propertyName, asLong);
122                     } else if ((prop.getAnnotation(Bytes.class) != null) &&
123                                NUMBER_UNIT.matcher(fieldValue.textValue()).matches()) {
124                         Size dropWizardSize = Size.parse(fieldValue.asText());
125                         long asLong = dropWizardSize.toBytes();
126                         fieldValues.put(propertyName, asLong);
127                     }
128                 } catch (Throwable cause) {
129                     throw JsonMappingException.wrapWithPath(cause, prop.getType().getRawClass(), propertyName);
130                 }
131             }
132         }
133     }
134 
135     // required overrides that don't actually change much
136 
137     @Override
138     public JsonDeserializer<Object> unwrappingDeserializer(NameTransformer unwrapper) {
139         return (JsonDeserializer<Object>) replaceDelegatee(getDelegatee().unwrappingDeserializer(unwrapper));
140     }
141 }