View Javadoc
1   package com.acumenvelocity.ath.common;
2   
3   import java.io.IOException;
4   import java.time.Instant;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Date;
8   import java.util.List;
9   
10  import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
11  import com.fasterxml.jackson.annotation.PropertyAccessor;
12  import com.fasterxml.jackson.core.JsonGenerator;
13  import com.fasterxml.jackson.core.JsonParser;
14  import com.fasterxml.jackson.core.JsonProcessingException;
15  import com.fasterxml.jackson.core.type.TypeReference;
16  import com.fasterxml.jackson.databind.DeserializationContext;
17  import com.fasterxml.jackson.databind.DeserializationFeature;
18  import com.fasterxml.jackson.databind.JsonNode;
19  import com.fasterxml.jackson.databind.MapperFeature;
20  import com.fasterxml.jackson.databind.ObjectMapper;
21  import com.fasterxml.jackson.databind.PropertyNamingStrategies;
22  import com.fasterxml.jackson.databind.SerializationFeature;
23  import com.fasterxml.jackson.databind.SerializerProvider;
24  import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
25  import com.fasterxml.jackson.databind.module.SimpleModule;
26  import com.fasterxml.jackson.databind.node.ArrayNode;
27  import com.fasterxml.jackson.databind.node.ObjectNode;
28  import com.fasterxml.jackson.databind.node.TextNode;
29  import com.fasterxml.jackson.databind.ser.std.StdSerializer;
30  import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
31  
32  /**
33   * Utility class for JSON/Java object conversion using Jackson library.
34   * Provides serialization and deserialization capabilities with custom handlers
35   * for Date and MongoDB ObjectId types. Supports both JSON and YAML formats.
36   * 
37   * <p>
38   * This utility handles special serialization formats commonly used with MongoDB
39   * where dates and object IDs may be represented as extended JSON objects.
40   * </p>
41   * 
42   * @author Acumen Velocity
43   * @version 1.0
44   * @since 1.0
45   * @see ObjectMapper
46   * @see JsonNode
47   */
48  public class JacksonUtil {
49  
50    // Custom modules for handling specific type serialization/deserialization
51    private static SimpleModule dateModule = new SimpleModule();
52    private static SimpleModule objectIdModule = new SimpleModule();
53    private static SimpleModule objectIdModule2 = new SimpleModule();
54  
55    // Static initialization block for configuring custom serializers and deserializers
56    static {
57      configureDateSerialization();
58    }
59  
60    /**
61     * Configures custom serialization and deserialization for Date objects.
62     * Handles both ISO string format and MongoDB extended JSON format for dates.
63     */
64    private static void configureDateSerialization() {
65      // Custom serializer for Date objects to ISO string format
66      dateModule.addSerializer(new StdSerializer<Date>(Date.class) {
67        private static final long serialVersionUID = 1L;
68  
69        /**
70         * Serializes Date object to ISO string format using QUARTZ_DATE_FORMAT.
71         * 
72         * @param date     the Date object to serialize
73         * @param gen      the JsonGenerator to write serialized content
74         * @param provider the SerializerProvider for serialization context
75         * @throws IOException if serialization fails
76         */
77        @Override
78        public void serialize(Date date, JsonGenerator gen, SerializerProvider provider)
79            throws IOException {
80          
81          String dateSt = Const.QUARTZ_DATE_FORMAT.format(date);
82          gen.writeString(dateSt);
83        }
84      });
85  
86      // Custom deserializer for Date objects supporting multiple formats
87      dateModule.addDeserializer(Date.class, new StdDeserializer<Date>(Date.class) {
88        private static final long serialVersionUID = 2L;
89  
90        /**
91         * Deserializes Date from JSON, supporting both ISO string format and
92         * MongoDB extended JSON format ({$date: {/$numberLong: "timestamp"}}).
93         * 
94         * @param jp the JsonParser for reading JSON content
95         * @param dc the DeserializationContext for deserialization context
96         * @return deserialized Date object, or null if parsing fails
97         * @throws IOException             if deserialization fails
98         * @throws JsonProcessingException if JSON processing fails
99         */
100       @Override
101       public Date deserialize(JsonParser jp, DeserializationContext dc)
102           throws IOException, JsonProcessingException {
103         
104         JsonNode tree = jp.readValueAsTree();
105         JsonNode dateVal = tree.get("$date");
106 
107         // Handle ISO string format: {"$date": "2023-01-01T00:00:00Z"}
108         if (dateVal instanceof TextNode) {
109           String dateSt = dateVal == null ? tree.textValue() : dateVal.textValue();
110           Instant instant = Instant.parse(dateSt);
111           
112           return Date.from(instant);
113         }
114 
115         // Handle MongoDB extended format: {"$date": {"$numberLong": "1672531200000"}}
116         if (dateVal instanceof ObjectNode) {
117           JsonNode numberLong = dateVal.get("$numberLong");
118 
119           if (numberLong instanceof TextNode) {
120             String millisSt = numberLong.textValue();
121             long millis = Long.parseLong(millisSt);
122             Instant instant = Instant.ofEpochMilli(millis);
123             
124             return Date.from(instant);
125           }
126         }
127 
128         return null; // Return null if format is not recognized
129       }
130     });
131   }
132 
133   /**
134    * Primary ObjectMapper instance with custom configuration for handling
135    * MongoDB extended JSON formats and custom type serialization.
136    */
137   @SuppressWarnings("deprecation")
138   private static ObjectMapper mapper = new ObjectMapper()
139       .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
140       .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) // Sort map entries for consistent
141                                                               // output
142       .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // Ignore unknown
143                                                                            // properties
144       .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, false) // Maintain property order
145       .setVisibility(PropertyAccessor.FIELD, Visibility.ANY) // Allow field access without
146                                                              // getters/setters
147       .registerModule(dateModule) // Register custom date handling
148       .registerModule(objectIdModule); // Register custom ObjectId handling
149 
150   /**
151    * Alternative ObjectMapper instance with different ObjectId deserialization strategy.
152    * Used when ObjectId values are expected as direct strings rather than extended JSON objects.
153    */
154   @SuppressWarnings("deprecation")
155   private static ObjectMapper mapper2 = new ObjectMapper()
156       .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
157       .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)
158       .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
159       .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, false)
160       .setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
161       .registerModule(dateModule)
162       .registerModule(objectIdModule2); // Use alternative ObjectId deserializer
163 
164   /**
165    * Creates a JsonNode from a JSON string.
166    * 
167    * @param json the JSON string to parse
168    * @return JsonNode representing the parsed JSON, or null if parsing fails
169    */
170   public static JsonNode makeNode(String json) {
171     try {
172       JsonNode node = mapper.readValue(json, JsonNode.class);
173       return node;
174       
175     } catch (IOException e) {
176       Log.error(JacksonUtil.class, e, e.getMessage());
177       return null;
178     }
179   }
180 
181   /**
182    * Converts a Java object to a JsonNode.
183    * 
184    * @param obj the Java object to convert
185    * @return JsonNode representation of the object
186    */
187   public static JsonNode makeNode(Object obj) {
188     JsonNode node = mapper.valueToTree(obj);
189     return node;
190   }
191 
192   /**
193    * Creates a JSON object node with an array field containing the provided objects.
194    * 
195    * @param fieldName the name of the array field
196    * @param arr       the array of objects to include in the array field
197    * @return ObjectNode containing the specified array field
198    */
199   public static JsonNode makeNode(String fieldName, Object[] arr) {
200     ObjectNode objNode = mapper.createObjectNode();
201     ArrayNode arrNode = objNode.putArray(fieldName);
202 
203     // Convert each object to JsonNode and add to array
204     Arrays.stream(arr).forEach(e -> {
205       JsonNode node = mapper.valueToTree(e);
206       arrNode.add(node);
207     });
208     
209     return objNode;
210   }
211 
212   /**
213    * Creates an ArrayNode from an array of objects.
214    * 
215    * @param arr the array of objects to convert
216    * @return ArrayNode containing JsonNode representations of the objects
217    */
218   public static ArrayNode makeArrayNode(Object[] arr) {
219     ArrayNode arrNode = mapper.createArrayNode();
220 
221     Arrays.stream(arr).forEach(e -> {
222       JsonNode node = mapper.valueToTree(e);
223       arrNode.add(node);
224     });
225     
226     return arrNode;
227   }
228 
229   /**
230    * Creates an ArrayNode from a List of objects.
231    * 
232    * @param list the List of objects to convert
233    * @return ArrayNode containing JsonNode representations of the objects, or null if input is null
234    */
235   public static ArrayNode makeArrayNode(List<?> list) {
236     return list == null ? null : makeArrayNode(list.toArray());
237   }
238 
239   /**
240    * Creates an array of JsonNode objects from an array of Java objects.
241    * 
242    * @param arr the array of objects to convert
243    * @return array of JsonNode representations of the objects
244    */
245   public static JsonNode[] makeNodes(Object[] arr) {
246     List<JsonNode> list = new ArrayList<>(arr.length);
247 
248     Arrays.stream(arr).forEach(e -> {
249       JsonNode node = mapper.valueToTree(e);
250       list.add(node);
251     });
252 
253     return list.toArray(new JsonNode[list.size()]);
254   }
255 
256   /**
257    * Creates an array of JsonNode objects from a List of Java objects.
258    * 
259    * @param list the List of objects to convert
260    * @return array of JsonNode representations of the objects, or null if input is null
261    */
262   public static JsonNode[] makeNodes(List<?> list) {
263     return list == null ? null : makeNodes(list.toArray());
264   }
265 
266   /**
267    * Creates a JSON object node with an array field containing objects from a List.
268    * 
269    * @param fieldName the name of the array field
270    * @param list      the List of objects to include in the array field
271    * @return ObjectNode containing the specified array field, or null if input is null
272    */
273   public static JsonNode makeNode(String fieldName, List<?> list) {
274     return list == null ? null : makeNode(fieldName, list.toArray());
275   }
276 
277   /**
278    * Creates a simple JSON object node with a single string field.
279    * 
280    * @param fieldName  the name of the field
281    * @param fieldValue the string value of the field
282    * @return JsonNode representing the simple object
283    */
284   public static JsonNode makeNode(String fieldName, String fieldValue) {
285     return makeNode(makeJson(fieldName, fieldValue));
286   }
287 
288   /**
289    * Creates a simple JSON string with a single field-value pair.
290    * 
291    * @param fieldName  the name of the field
292    * @param fieldValue the string value of the field
293    * @return JSON string representation of the simple object
294    */
295   public static String makeJson(String fieldName, String fieldValue) {
296     return Log.format("{\"{}\": \"{}\"}", fieldName, fieldValue);
297   }
298 
299   /**
300    * Creates a JSON string with an array field from an array of objects.
301    * 
302    * @param fieldName the name of the array field
303    * @param arr       the array of objects to include
304    * @return JSON string representation of the object with array field
305    */
306   public static String makeJson(String fieldName, Object[] arr) {
307     return makeJson(makeNode(fieldName, arr));
308   }
309 
310   /**
311    * Creates a JSON string with an array field from a List of objects.
312    * 
313    * @param fieldName the name of the array field
314    * @param list      the List of objects to include
315    * @return JSON string representation of the object with array field, or null if input is null
316    */
317   public static String makeJson(String fieldName, List<?> list) {
318     return list == null ? null : makeJson(fieldName, list.toArray());
319   }
320 
321   /**
322    * Converts a JsonNode to a JSON string.
323    * 
324    * @param node the JsonNode to convert
325    * @return JSON string representation, or null if conversion fails
326    */
327   public static String makeJson(JsonNode node) {
328     try {
329       return mapper.writeValueAsString(node);
330       
331     } catch (JsonProcessingException e) {
332       Log.error(JacksonUtil.class, e, e.getMessage());
333       return null;
334     }
335   }
336 
337   /**
338    * Converts a YAML string to a pretty-printed JSON string.
339    * 
340    * @param yaml the YAML string to convert
341    * @return pretty-printed JSON string representation, or empty string if conversion fails
342    */
343   public static String makeJson(String yaml) {
344     try {
345       ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
346       Object obj = yamlReader.readValue(yaml, Object.class);
347       ObjectMapper jsonWriter = new ObjectMapper();
348       return jsonWriter.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
349       
350     } catch (IOException e) {
351       Log.error(JacksonUtil.class, e, e.getMessage());
352       return "";
353     }
354   }
355 
356   /**
357    * Converts an object to a pretty-printed JSON string.
358    * 
359    * @param obj the object to convert
360    * @return pretty-printed JSON string representation, or empty string if conversion fails
361    */
362   public static String makePrettyJson(Object obj) {
363     mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
364     
365     try {
366       return mapper.writeValueAsString(obj);
367       
368     } catch (JsonProcessingException e) {
369       Log.error(JacksonUtil.class, e, e.getMessage());
370       return "";
371     }
372   }
373 
374   /**
375    * Converts an object to JSON string with configurable pretty-printing.
376    * 
377    * @param obj         the object to convert
378    * @param prettyPrint true to enable pretty-printing, false for compact format
379    * @return JSON string representation, or empty string if conversion fails
380    */
381   public static String toJson(Object obj, boolean prettyPrint) {
382     if (prettyPrint) {
383       mapper.enable(SerializationFeature.INDENT_OUTPUT);
384       
385     } else {
386       mapper.disable(SerializationFeature.INDENT_OUTPUT);
387     }
388     
389     try {
390       return mapper.writeValueAsString(obj);
391       
392     } catch (JsonProcessingException e) {
393       Log.error(JacksonUtil.class, e, e.getMessage());
394       return "";
395     }
396   }
397 
398   /**
399    * Deserializes JSON string to an object of the specified class using the primary ObjectMapper.
400    * 
401    * @param <T>      the type of the object to deserialize
402    * @param json     the JSON string to deserialize
403    * @param objClass the class of the object to deserialize
404    * @return deserialized object, or null if deserialization fails
405    */
406   public static <T> T fromJson(String json, Class<? extends T> objClass) {
407     try {
408       return mapper.readValue(json, objClass);
409       
410     } catch (IOException e) {
411       Log.error(JacksonUtil.class, e, e.getMessage());
412       return null;
413     }
414   }
415 
416   public static <T> T fromJson(String json, TypeReference<T> type) {
417     try {
418       return mapper.readValue(json, type);
419 
420     } catch (IOException e) {
421       Log.error(JacksonUtil.class, e, e.getMessage());
422       return null;
423     }
424   }
425 
426   /**
427    * Deserializes JSON string to an object of the specified class using the alternative
428    * ObjectMapper.
429    * This version uses the ObjectId deserializer that expects direct string values.
430    * 
431    * @param <T>      the type of the object to deserialize
432    * @param json     the JSON string to deserialize
433    * @param objClass the class of the object to deserialize
434    * @return deserialized object, or null if deserialization fails
435    */
436   public static <T> T fromJson2(String json, Class<? extends T> objClass) {
437     try {
438       return mapper2.readValue(json, objClass);
439       
440     } catch (IOException e) {
441       Log.error(JacksonUtil.class, e, e.getMessage());
442       return null;
443     }
444   }
445 
446   public static <T> T fromJsonNode(JsonNode jsonNode, Class<? extends T> objClass) {
447     try {
448       return mapper.treeToValue(jsonNode, objClass);
449 
450     } catch (IOException e) {
451       Log.error(JacksonUtil.class, e, e.getMessage());
452       return null;
453     }
454   }
455 
456   public static Object fromJsonNode(JsonNode jsonNode) {
457     return fromJsonNode(jsonNode, Object.class);
458   }
459 }