8585 * Date date;
8686 * LocalDateTime localDateTime;
8787 * }
88- *
88+ * <p>
8989 * class Address {
9090 * String city;
9191 * String country;
153153 *
154154 * @author Christoph Strobl
155155 * @author Mark Paluch
156+ * @author John Blum
156157 * @since 1.8
157158 */
158159public class Jackson2HashMapper implements HashMapper <Object , String , Object > {
159160
160- private static final boolean SOURCE_VERSION_PRESENT = ClassUtils .isPresent ("javax.lang.model.SourceVersion" , Jackson2HashMapper .class .getClassLoader ());
161-
162- private final HashMapperModule HASH_MAPPER_MODULE = new HashMapperModule ();
161+ private static final boolean SOURCE_VERSION_PRESENT =
162+ ClassUtils .isPresent ("javax.lang.model.SourceVersion" , Jackson2HashMapper .class .getClassLoader ());
163163
164164 private final ObjectMapper typingMapper ;
165165 private final ObjectMapper untypedMapper ;
166166 private final boolean flatten ;
167167
168168 /**
169- * Creates new {@link Jackson2HashMapper} with default {@link ObjectMapper}.
169+ * Creates new {@link Jackson2HashMapper} with a default {@link ObjectMapper}.
170170 *
171- * @param flatten
171+ * @param flatten boolean used to configure whether JSON de/serialized {@link Object} properties
172+ * will be un/flattened using {@literal dot notation}, or whether to retain the hierarchical node structure
173+ * created by Jackson.
172174 */
173175 public Jackson2HashMapper (boolean flatten ) {
174176
175177 this (new ObjectMapper () {
176178
177179 @ Override
178180 protected TypeResolverBuilder <?> _constructDefaultTypeResolverBuilder (DefaultTyping applicability ,
179- PolymorphicTypeValidator ptv ) {
180- return new DefaultTypeResolverBuilder ( applicability , ptv ) {
181- public boolean useForType ( JavaType t ) {
181+ PolymorphicTypeValidator typeValidator ) {
182+
183+ return new DefaultTypeResolverBuilder ( applicability , typeValidator ) {
182184
183- if (t .isPrimitive ()) {
185+ public boolean useForType (JavaType type ) {
186+
187+ if (type .isPrimitive ()) {
184188 return false ;
185189 }
186190
187- if (flatten && t .isTypeOrSubTypeOf (Number .class )) {
191+ if (flatten && type .isTypeOrSubTypeOf (Number .class )) {
188192 return false ;
189193 }
190194
191195 if (EVERYTHING .equals (_appliesFor )) {
192- return !TreeNode .class .isAssignableFrom (t .getRawClass ());
196+ return !TreeNode .class .isAssignableFrom (type .getRawClass ());
193197 }
194198
195- return super .useForType (t );
199+ return super .useForType (type );
196200 }
197201 };
198202 }
199203 }.findAndRegisterModules (), flatten );
200204
201- typingMapper .activateDefaultTyping (typingMapper .getPolymorphicTypeValidator (), DefaultTyping .EVERYTHING ,
202- As .PROPERTY );
203- typingMapper .configure (SerializationFeature .WRITE_NULL_MAP_VALUES , false );
205+ this .typingMapper .activateDefaultTyping (this .typingMapper .getPolymorphicTypeValidator (),
206+ DefaultTyping .EVERYTHING , As .PROPERTY );
207+ this .typingMapper .configure (SerializationFeature .WRITE_NULL_MAP_VALUES , false );
208+
204209 if (flatten ) {
205- typingMapper .disable (MapperFeature .REQUIRE_TYPE_ID_FOR_SUBTYPES );
210+ this . typingMapper .disable (MapperFeature .REQUIRE_TYPE_ID_FOR_SUBTYPES );
206211 }
207212
208213 // Prevent splitting time types into arrays. E
209- typingMapper .configure (SerializationFeature .WRITE_DATES_AS_TIMESTAMPS , false );
210- typingMapper .setSerializationInclusion ( Include . NON_NULL );
211- typingMapper .configure ( DeserializationFeature . FAIL_ON_UNKNOWN_PROPERTIES , false );
212- typingMapper .registerModule (HASH_MAPPER_MODULE );
214+ this . typingMapper .configure (SerializationFeature .WRITE_DATES_AS_TIMESTAMPS , false );
215+ this . typingMapper .configure ( DeserializationFeature . FAIL_ON_UNKNOWN_PROPERTIES , false );
216+ this . typingMapper .setSerializationInclusion ( Include . NON_NULL );
217+ this . typingMapper .registerModule (new HashMapperModule () );
213218 }
214219
215220 /**
216- * Creates new {@link Jackson2HashMapper}.
221+ * Creates new {@link Jackson2HashMapper} initialized with a custom Jackson {@link ObjectMapper} .
217222 *
218- * @param mapper must not be {@literal null}.
219- * @param flatten
223+ * @param mapper Jackson {@link ObjectMapper} used to de/serialize hashed {@link Object objects};
224+ * must not be {@literal null}.
225+ * @param flatten boolean used to configure whether JSON de/serialized {@link Object} properties
226+ * will be un/flattened using {@literal dot notation}, or whether to retain the hierarchical node structure
227+ * created by Jackson.
220228 */
221229 public Jackson2HashMapper (ObjectMapper mapper , boolean flatten ) {
222230
223231 Assert .notNull (mapper , "Mapper must not be null" );
224- this .typingMapper = mapper ;
225- this .flatten = flatten ;
226232
233+ this .flatten = flatten ;
234+ this .typingMapper = mapper ;
227235 this .untypedMapper = new ObjectMapper ();
228- untypedMapper .findAndRegisterModules ();
229236 this .untypedMapper .configure (SerializationFeature .WRITE_NULL_MAP_VALUES , false );
230237 this .untypedMapper .setSerializationInclusion (Include .NON_NULL );
238+ this .untypedMapper .findAndRegisterModules ();
231239 }
232240
233241 @ Override
234242 @ SuppressWarnings ("unchecked" )
235243 public Map <String , Object > toHash (Object source ) {
236244
237- JsonNode tree = typingMapper .valueToTree (source );
238- return flatten ? flattenMap (tree .fields ()) : untypedMapper .convertValue (tree , Map .class );
245+ JsonNode tree = this .typingMapper .valueToTree (source );
246+
247+ return this .flatten ? flattenMap (tree .fields ()) : this .untypedMapper .convertValue (tree , Map .class );
239248 }
240249
241250 @ Override
251+ @ SuppressWarnings ("all" )
242252 public Object fromHash (Map <String , Object > hash ) {
243253
244254 try {
245-
246- if (flatten ) {
255+ if (this .flatten ) {
247256
248257 Map <String , Object > unflattenedHash = doUnflatten (hash );
249- byte [] unflattenedHashedBytes = untypedMapper .writeValueAsBytes (unflattenedHash );
250- Object hashedObject = typingMapper .reader ().forType (Object .class )
258+ byte [] unflattenedHashedBytes = this . untypedMapper .writeValueAsBytes (unflattenedHash );
259+ Object hashedObject = this . typingMapper .reader ().forType (Object .class )
251260 .readValue (unflattenedHashedBytes );
252261
253262 return hashedObject ;
254263 }
255264
256- return typingMapper .treeToValue (untypedMapper .valueToTree (hash ), Object .class );
265+ return this . typingMapper .treeToValue (this . untypedMapper .valueToTree (hash ), Object .class );
257266
258- } catch (IOException e ) {
259- throw new MappingException (e .getMessage (), e );
267+ } catch (IOException cause ) {
268+ throw new MappingException (cause .getMessage (), cause );
260269 }
261270 }
262271
@@ -272,11 +281,8 @@ private Map<String, Object> doUnflatten(Map<String, Object> source) {
272281 String [] keyParts = key .split ("\\ ." );
273282
274283 if (keyParts .length == 1 && isNotIndexed (keyParts [0 ])) {
275- result .put (entry .getKey (), entry .getValue ());
276- continue ;
277- }
278-
279- if (keyParts .length == 1 && isIndexed (keyParts [0 ])) {
284+ result .put (key , entry .getValue ());
285+ } else if (keyParts .length == 1 && isIndexed (keyParts [0 ])) {
280286
281287 String indexedKeyName = keyParts [0 ];
282288 String nonIndexedKeyName = stripIndex (indexedKeyName );
@@ -290,21 +296,25 @@ private Map<String, Object> doUnflatten(Map<String, Object> source) {
290296 result .put (nonIndexedKeyName , createTypedListWithValue (index , entry .getValue ()));
291297 }
292298 } else {
293- treatSeparate .add (key . substring ( 0 , key . indexOf ( '.' )) );
299+ treatSeparate .add (keyParts [ 0 ] );
294300 }
295301 }
296302
297303 for (String partial : treatSeparate ) {
298304
299305 Map <String , Object > newSource = new LinkedHashMap <>();
300306
307+ // Copies all nested, dot properties from the source Map to the new Map beginning from
308+ // the next nested (dot) property
301309 for (Entry <String , Object > entry : source .entrySet ()) {
302- if (entry .getKey ().startsWith (partial )) {
303- newSource .put (entry .getKey ().substring (partial .length () + 1 ), entry .getValue ());
310+ String key = entry .getKey ();
311+ if (key .startsWith (partial )) {
312+ String keyAfterDot = key .substring (partial .length () + 1 );
313+ newSource .put (keyAfterDot , entry .getValue ());
304314 }
305315 }
306316
307- if (partial . endsWith ( "]" )) {
317+ if (isNonNestedIndexed ( partial )) {
308318
309319 String nonIndexPartial = stripIndex (partial );
310320 int index = getIndex (partial );
@@ -330,6 +340,10 @@ private boolean isNotIndexed(@NonNull String value) {
330340 return !isIndexed (value );
331341 }
332342
343+ private boolean isNonNestedIndexed (@ NonNull String value ) {
344+ return value .endsWith ("]" );
345+ }
346+
333347 private int getIndex (@ NonNull String indexedValue ) {
334348 return Integer .parseInt (indexedValue .substring (indexedValue .indexOf ('[' ) + 1 , indexedValue .length () - 1 ));
335349 }
@@ -346,7 +360,7 @@ private int getIndex(@NonNull String indexedValue) {
346360 private Map <String , Object > flattenMap (Iterator <Entry <String , JsonNode >> source ) {
347361
348362 Map <String , Object > resultMap = new HashMap <>();
349- this . doFlatten ("" , source , resultMap );
363+ doFlatten ("" , source , resultMap );
350364 return resultMap ;
351365 }
352366
@@ -378,56 +392,52 @@ private void flattenElement(String propertyPrefix, Object source, Map<String, Ob
378392
379393 while (nodes .hasNext ()) {
380394
381- JsonNode cur = nodes .next ();
382-
383- if (cur .isArray ()) {
384- this .flattenCollection (propertyPrefix , cur .elements (), resultMap );
385- } else {
386- if (nodes .hasNext () && mightBeJavaType (cur )) {
395+ JsonNode currentNode = nodes .next ();
387396
388- JsonNode next = nodes .next ();
397+ if (currentNode .isArray ()) {
398+ flattenCollection (propertyPrefix , currentNode .elements (), resultMap );
399+ } else if (nodes .hasNext () && mightBeJavaType (currentNode )) {
389400
390- if (next .isArray ()) {
391- this .flattenCollection (propertyPrefix , next .elements (), resultMap );
392- }
401+ JsonNode next = nodes .next ();
393402
394- if (cur .asText ().equals ("java.util.Date" )) {
395- resultMap .put (propertyPrefix , next .asText ());
396- break ;
397- }
398- if (next .isNumber ()) {
399- resultMap .put (propertyPrefix , next .numberValue ());
400- break ;
401- }
402- if (next .isTextual ()) {
403+ if (next .isArray ()) {
404+ flattenCollection (propertyPrefix , next .elements (), resultMap );
405+ }
406+ if (currentNode .asText ().equals ("java.util.Date" )) {
407+ resultMap .put (propertyPrefix , next .asText ());
408+ break ;
409+ }
410+ if (next .isNumber ()) {
411+ resultMap .put (propertyPrefix , next .numberValue ());
412+ break ;
413+ }
414+ if (next .isTextual ()) {
415+ resultMap .put (propertyPrefix , next .textValue ());
416+ break ;
417+ }
418+ if (next .isBoolean ()) {
419+ resultMap .put (propertyPrefix , next .booleanValue ());
420+ break ;
421+ }
422+ if (next .isBinary ()) {
403423
404- resultMap . put ( propertyPrefix , next . textValue ());
405- break ;
424+ try {
425+ resultMap . put ( propertyPrefix , next . binaryValue ()) ;
406426 }
407- if (next .isBoolean ()) {
408-
409- resultMap .put (propertyPrefix , next .booleanValue ());
410- break ;
427+ catch (IOException cause ) {
428+ String message = String .format ("Cannot read binary value of '%s'" , propertyPrefix );
429+ throw new IllegalStateException (message , cause );
411430 }
412- if (next .isBinary ()) {
413431
414- try {
415- resultMap .put (propertyPrefix , next .binaryValue ());
416- } catch (IOException cause ) {
417- String message = String .format ("Cannot read binary value of '%s'" , propertyPrefix );
418- throw new IllegalStateException (message , cause );
419- }
420-
421- break ;
422- }
432+ break ;
423433 }
424434 }
425435 }
426-
427436 } else if (element .isContainerNode ()) {
428- this . doFlatten (propertyPrefix , element .fields (), resultMap );
437+ doFlatten (propertyPrefix , element .fields (), resultMap );
429438 } else {
430- resultMap .put (propertyPrefix , new DirectFieldAccessFallbackBeanWrapper (element ).getPropertyValue ("_value" ));
439+ resultMap .put (propertyPrefix , new DirectFieldAccessFallbackBeanWrapper (element )
440+ .getPropertyValue ("_value" ));
431441 }
432442 }
433443
0 commit comments