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 javax.annotation.Nonnull;
17  import javax.annotation.Syntax;
18  import javax.validation.ConstraintViolation;
19  import javax.validation.ValidationException;
20  import javax.validation.Validator;
21  
22  import java.io.IOException;
23  
24  import java.util.Set;
25  
26  import com.addthis.codec.config.ConfigTraversingParser;
27  import com.addthis.codec.config.Configs;
28  import com.addthis.codec.plugins.PluginMap;
29  import com.addthis.codec.plugins.PluginRegistry;
30  
31  import com.google.common.annotations.Beta;
32  import com.google.common.base.Preconditions;
33  
34  import com.fasterxml.jackson.core.JsonProcessingException;
35  import com.fasterxml.jackson.databind.JsonNode;
36  import com.fasterxml.jackson.databind.ObjectMapper;
37  import com.typesafe.config.Config;
38  import com.typesafe.config.ConfigException;
39  import com.typesafe.config.ConfigFactory;
40  import com.typesafe.config.ConfigValue;
41  
42  import io.dropwizard.validation.ConstraintViolations;
43  
44  /**
45   * Wrapper over an object mapper, plugin registry, global config, and a validator. Provides
46   * methods for constructing objects that automatically perform validation, and bridge hocon
47   * strings to jackson construction. Normally obtained either from {@link Jackson#defaultCodec()}
48   * or used indirectly from the static helper methods in {@link Configs}.
49   */
50  @Beta
51  public class CodecJackson {
52  
53      private final ObjectMapper objectMapper;
54      private final PluginRegistry pluginRegistry;
55      private final Config globalDefaults;
56      private final Validator validator;
57  
58      public CodecJackson(ObjectMapper objectMapper, PluginRegistry pluginRegistry,
59                          Config globalDefaults, Validator validator) {
60          this.objectMapper = objectMapper;
61          this.pluginRegistry = pluginRegistry;
62          this.globalDefaults = globalDefaults;
63          this.validator = validator;
64      }
65  
66      public CodecJackson withConfig(Config newGlobalDefaults) {
67          if (newGlobalDefaults == this.globalDefaults) {
68              return this;
69          } else {
70              PluginRegistry newPluginRegistry = new PluginRegistry(newGlobalDefaults);
71              ObjectMapper newObjectMapper = Jackson.newObjectMapper(newPluginRegistry);
72              return new CodecJackson(newObjectMapper, newPluginRegistry, newGlobalDefaults, validator);
73          }
74      }
75  
76      public CodecJackson withOverrides(Config overrides) {
77          if (overrides == this.globalDefaults) {
78              return this;
79          } else {
80              PluginRegistry newPluginRegistry = pluginRegistry.withOverrides(overrides);
81              ObjectMapper newObjectMapper = Jackson.newObjectMapper(newPluginRegistry);
82              return new CodecJackson(newObjectMapper, newPluginRegistry, overrides, validator);
83          }
84      }
85  
86      public ObjectMapper getObjectMapper() {
87          return objectMapper;
88      }
89  
90      public PluginRegistry getPluginRegistry() {
91          return pluginRegistry;
92      }
93  
94      public Config getGlobalDefaults() {
95          return globalDefaults;
96      }
97  
98      public Validator getValidator() {
99          return validator;
100     }
101 
102     // doesn't delegate
103 
104     /**
105      * Construct an object of the requested type based on the default values and types (if the requested
106      * is not a concrete class).
107      */
108     public <T> T newDefault(@Nonnull Class<T> type) throws JsonProcessingException, IOException {
109         T value = objectMapper.treeToValue(DefaultCodecJackson.DEFAULT_MAPPER.createObjectNode(), type);
110         return validate(value);
111     }
112 
113     public <T> T decodeObject(@Nonnull Class<T> type, ConfigValue configValue)
114             throws JsonProcessingException, IOException {
115         ConfigTraversingParser configParser = new ConfigTraversingParser(configValue, objectMapper);
116         return validate(configParser.readValueAs(type));
117     }
118 
119     public <T> T decodeObject(@Nonnull Class<T> type, JsonNode jsonNode) throws JsonProcessingException {
120         return validate(objectMapper.treeToValue(jsonNode, type));
121     }
122 
123     // delegates
124 
125     /** Construct an object of the requested plugin category based on the default type and values */
126     public <T> T newDefault(@Nonnull String category) throws JsonProcessingException, IOException {
127         Class<T> type = (Class<T>) pluginRegistry.byCategory().get(category).baseClass();
128         return newDefault(type);
129     }
130 
131     /**
132      * Tries to parse the string as an isolated typesafe-config object, tries to resolve it, and then calls
133      * {@link #decodeObject(String, Config)} with the resultant config and the passed in category. Pretty much just
134      * a convenience function for simple use cases that don't want to care about how ConfigFactory works.
135      */
136     public <T> T decodeObject(@Nonnull String category, @Syntax("HOCON") String configText)
137             throws JsonProcessingException, IOException {
138         PluginMap pluginMap = Preconditions.checkNotNull(pluginRegistry.asMap().get(category),
139                                                          "could not find anything about the category %s", category);
140         Config config = ConfigFactory.parseString(configText).resolve();
141         return (T) decodeObject(pluginMap.baseClass(), config);
142     }
143 
144     /**
145      * Tries to parse the string as an isolated typesafe-config object, tries to resolve it, and then calls
146      * {@link #decodeObject(Class, Config)} with the resultant config and the passed in type. Pretty much just
147      * a convenience function for simple use cases that don't want to care about how ConfigFactory works.
148      */
149     public <T> T decodeObject(@Nonnull Class<T> type, @Syntax("HOCON") String configText)
150             throws JsonProcessingException, IOException {
151         Config config = ConfigFactory.parseString(configText).resolve();
152         return decodeObject(type, config);
153     }
154 
155     /**
156      * Instantiate an object of the requested category based on the provided config. The config should only contain
157      * field and type information for the object to be constructed. Global defaults, plugin configuration, etc, are
158      * provided by this CodecConfig instance's globalConfig and pluginRegistry fields.
159      */
160     public <T> T decodeObject(@Nonnull String category, @Nonnull Config config)
161             throws JsonProcessingException, IOException {
162         PluginMap pluginMap = Preconditions.checkNotNull(pluginRegistry.asMap().get(category),
163                                                          "could not find anything about the category %s", category);
164         return (T) decodeObject(pluginMap.baseClass(), config);
165     }
166 
167     /**
168      * Instantiate an object of the requested type based on the provided config. The config should only contain
169      * field and type information for the object to be constructed. Global defaults, plugin configuration, etc, are
170      * provided by this CodecConfig instance's globalConfig and pluginRegistry fields.
171      */
172     public <T> T decodeObject(@Nonnull Class<T> type, Config config)
173             throws JsonProcessingException, IOException {
174         return decodeObject(type, config.root());
175     }
176 
177     /**
178      * Instantiate an object without a compile time expected type. This expects a config of the
179      * form "{plugin-category: {...}}". ie. there should be exactly one top level key and that
180      * key should be a valid, loaded, plug-in category.
181      */
182     public <T> T decodeObject(@Syntax("HOCON") String configText) throws JsonProcessingException, IOException {
183         Config config = ConfigFactory.parseString(configText).resolve();
184         return decodeObject(config);
185     }
186 
187     /**
188      * Instantiate an object without a compile time expected type. This expects a config of the
189      * form "{plugin-category: {...}}". ie. there should be exactly one top level key and that
190      * key should be a valid, loaded, plug-in category.
191      */
192     public <T> T decodeObject(Config config) throws JsonProcessingException, IOException {
193         if (config.root().size() != 1) {
194             throw new ConfigException.Parse(config.root().origin(),
195                                             "config root must have exactly one key");
196         }
197         String category = config.root().keySet().iterator().next();
198         PluginMap pluginMap = pluginRegistry.asMap().get(category);
199         if (pluginMap == null) {
200             throw new ConfigException.BadValue(config.root().get(category).origin(),
201                                                category,
202                                                "top level key must be a valid category");
203         }
204         ConfigValue configValue = config.root().get(category);
205         return (T) decodeObject(pluginMap.baseClass(), configValue);
206     }
207 
208     public <T> T validate(T value) {
209         Set<ConstraintViolation<T>> violations = validator.validate(value);
210         if (!violations.isEmpty()) {
211             throw new ValidationException(ConstraintViolations.<T>format(violations).toString());
212         }
213         return value;
214     }
215 }