|
| 1 | +/* |
| 2 | + * jPOS Project [http://jpos.org] |
| 3 | + * Copyright (C) 2000-2025 jPOS Software SRL |
| 4 | + * |
| 5 | + * This program is free software: you can redistribute it and/or modify |
| 6 | + * it under the terms of the GNU Affero General Public License as |
| 7 | + * published by the Free Software Foundation, either version 3 of the |
| 8 | + * License, or (at your option) any later version. |
| 9 | + * |
| 10 | + * This program is distributed in the hope that it will be useful, |
| 11 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | + * GNU Affero General Public License for more details. |
| 14 | + * |
| 15 | + * You should have received a copy of the GNU Affero General Public License |
| 16 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 17 | + */ |
| 18 | + |
| 19 | +package org.jpos.qrest; |
| 20 | + |
| 21 | +import com.fasterxml.jackson.annotation.JsonInclude; |
| 22 | +import com.fasterxml.jackson.databind.*; |
| 23 | +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; |
| 24 | +import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
| 25 | +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
| 26 | +import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; |
| 27 | +import com.fasterxml.jackson.datatype.jsr310.ser.OffsetDateTimeSerializer; |
| 28 | + |
| 29 | +import java.time.*; |
| 30 | +import java.time.format.DateTimeFormatter; |
| 31 | +import java.time.format.DateTimeFormatterBuilder; |
| 32 | +import java.time.temporal.ChronoField; |
| 33 | + |
| 34 | +/** |
| 35 | + * Utility class providing a shared, preconfigured Jackson {@link ObjectMapper} |
| 36 | + * and robust date/time string parsing for use in jPOS QREST modules. |
| 37 | + * |
| 38 | + * <p> |
| 39 | + * This class centralizes serialization and deserialization behavior, |
| 40 | + * especially around ISO-8601-compliant date/time formats and {@link OffsetDateTime}. |
| 41 | + * </p> |
| 42 | + * |
| 43 | + * <h2>Features</h2> |
| 44 | + * <ul> |
| 45 | + * <li>Provides a globally configured {@code ObjectMapper} with sensible defaults: |
| 46 | + * <ul> |
| 47 | + * <li>Pretty-printed output</li> |
| 48 | + * <li>Support for Java Time module</li> |
| 49 | + * <li>Non-timestamp date formatting</li> |
| 50 | + * <li>Lenient deserialization settings</li> |
| 51 | + * </ul> |
| 52 | + * </li> |
| 53 | + * <li>Parses date-time and time strings with multiple ISO-compatible formats.</li> |
| 54 | + * <li>Supplies reusable Jackson serializers/deserializers for {@link OffsetDateTime}.</li> |
| 55 | + * </ul> |
| 56 | + * |
| 57 | + * <p> |
| 58 | + * Intended for use internally by QREST participants and REST interfaces. |
| 59 | + * </p> |
| 60 | + * |
| 61 | + * @author jPOS |
| 62 | + * @since 1.9.0 |
| 63 | + */ |
| 64 | +public class Converter { |
| 65 | + // Preconfigured Jackson ObjectMapper with custom settings |
| 66 | + private static ObjectMapper mapper = instantiateMapper(); |
| 67 | + |
| 68 | + /** |
| 69 | + * Provides a shared {@link ObjectMapper} instance used throughout the application. |
| 70 | + * |
| 71 | + * @return configured ObjectMapper with time/date support and safe defaults. |
| 72 | + */ |
| 73 | + public static ObjectMapper getMapper() { |
| 74 | + return mapper; |
| 75 | + } |
| 76 | + |
| 77 | + // Flexible formatter for date-time strings, with multiple ISO-8601 variants |
| 78 | + private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder() |
| 79 | + .appendOptional(DateTimeFormatter.ISO_DATE_TIME) |
| 80 | + .appendOptional(DateTimeFormatter.ISO_OFFSET_DATE_TIME) |
| 81 | + .appendOptional(DateTimeFormatter.ISO_INSTANT) |
| 82 | + .appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SX")) |
| 83 | + .appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX")) |
| 84 | + .appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) |
| 85 | + .toFormatter() |
| 86 | + .withZone(ZoneOffset.UTC); |
| 87 | + |
| 88 | + /** |
| 89 | + * Parses a date-time string into an {@link OffsetDateTime}, using flexible ISO-compatible formats. |
| 90 | + * |
| 91 | + * @param str date-time string to parse. |
| 92 | + * @return corresponding OffsetDateTime. |
| 93 | + * @throws DateTimeException if the string cannot be parsed. |
| 94 | + */ |
| 95 | + public static OffsetDateTime parseDateTimeString(String str) { |
| 96 | + return ZonedDateTime.from(DATE_TIME_FORMATTER.parse(str)).toOffsetDateTime(); |
| 97 | + } |
| 98 | + |
| 99 | + // Flexible formatter for time-only strings, with default date values |
| 100 | + private static final DateTimeFormatter TIME_FORMATTER = new DateTimeFormatterBuilder() |
| 101 | + .appendOptional(DateTimeFormatter.ISO_TIME) |
| 102 | + .appendOptional(DateTimeFormatter.ISO_OFFSET_TIME) |
| 103 | + .parseDefaulting(ChronoField.YEAR, 2020) |
| 104 | + .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1) |
| 105 | + .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) |
| 106 | + .toFormatter() |
| 107 | + .withZone(ZoneOffset.UTC); |
| 108 | + |
| 109 | + |
| 110 | + private static StdSerializer<OffsetDateTime> offsetDateTimeSerializer(DateTimeFormatter formatter) { |
| 111 | + return new OffsetDateTimeSerializer(OffsetDateTimeSerializer.INSTANCE, false, formatter) {}; |
| 112 | + } |
| 113 | + |
| 114 | + private static StdDeserializer<OffsetDateTime> offsetDateTimeDeserializer(DateTimeFormatter formatter) { |
| 115 | + return new InstantDeserializer<OffsetDateTime>(InstantDeserializer.OFFSET_DATE_TIME, formatter) {}; |
| 116 | + } |
| 117 | + |
| 118 | + /** |
| 119 | + * Parses a time-only string into an {@link OffsetTime}. |
| 120 | + * |
| 121 | + * @param str the time string to parse. |
| 122 | + * @return corresponding OffsetTime object. |
| 123 | + * @throws DateTimeException if the string cannot be parsed. |
| 124 | + */ |
| 125 | + public static OffsetTime parseTimeString(String str) { |
| 126 | + return ZonedDateTime.from(TIME_FORMATTER.parse(str)).toOffsetDateTime().toOffsetTime(); |
| 127 | + } |
| 128 | + |
| 129 | + /** |
| 130 | + * Instantiates and configures the shared {@link ObjectMapper} instance. |
| 131 | + * |
| 132 | + * <p>Applies the following settings:</p> |
| 133 | + * <ul> |
| 134 | + * <li>Enables BigDecimal for float parsing.</li> |
| 135 | + * <li>Pretty-print output.</li> |
| 136 | + * <li>Ignores unknown properties.</li> |
| 137 | + * <li>Disables timestamp-based date output.</li> |
| 138 | + * <li>Registers the Java Time module.</li> |
| 139 | + * </ul> |
| 140 | + * |
| 141 | + * @return configured ObjectMapper. |
| 142 | + */ |
| 143 | + private static ObjectMapper instantiateMapper () { |
| 144 | + ObjectMapper mapper = new ObjectMapper() |
| 145 | + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) |
| 146 | + .enable(SerializationFeature.INDENT_OUTPUT) |
| 147 | + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) |
| 148 | + .configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false) |
| 149 | + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); |
| 150 | + |
| 151 | + mapper.findAndRegisterModules(); |
| 152 | + mapper.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); |
| 153 | + mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); |
| 154 | + mapper.registerModule(new JavaTimeModule()); |
| 155 | + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); |
| 156 | + return mapper; |
| 157 | + } |
| 158 | +} |
0 commit comments