diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ebc26ed986..3e63715106 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -35,6 +35,11 @@ exports tools.jackson.databind.deser.std; exports tools.jackson.databind.exc; exports tools.jackson.databind.ext; + exports tools.jackson.databind.ext.javatime; + exports tools.jackson.databind.ext.javatime.deser; + exports tools.jackson.databind.ext.javatime.deser.key; + exports tools.jackson.databind.ext.javatime.ser; + exports tools.jackson.databind.ext.javatime.ser.key; exports tools.jackson.databind.ext.jdk8; // Needed by Ion module for SqlDate deserializer: exports tools.jackson.databind.ext.sql; diff --git a/src/main/java/tools/jackson/databind/JacksonModule.java b/src/main/java/tools/jackson/databind/JacksonModule.java index b279aa2900..1ef63632d7 100644 --- a/src/main/java/tools/jackson/databind/JacksonModule.java +++ b/src/main/java/tools/jackson/databind/JacksonModule.java @@ -5,8 +5,7 @@ import java.util.function.UnaryOperator; import tools.jackson.core.*; -import tools.jackson.databind.cfg.MapperBuilder; -import tools.jackson.databind.cfg.MutableConfigOverride; +import tools.jackson.databind.cfg.*; import tools.jackson.databind.deser.*; import tools.jackson.databind.jsontype.NamedType; import tools.jackson.databind.ser.Serializers; @@ -146,6 +145,9 @@ public static interface SetupContext public boolean isEnabled(TokenStreamFactory.Feature f); public boolean isEnabled(StreamReadFeature f); public boolean isEnabled(StreamWriteFeature f); + public boolean isEnabled(DatatypeFeature f); + + public DatatypeFeatures datatypeFeatures(); /* /****************************************************************** diff --git a/src/main/java/tools/jackson/databind/SerializationFeature.java b/src/main/java/tools/jackson/databind/SerializationFeature.java index 15f1e86891..e070b5e45f 100644 --- a/src/main/java/tools/jackson/databind/SerializationFeature.java +++ b/src/main/java/tools/jackson/databind/SerializationFeature.java @@ -210,6 +210,21 @@ public enum SerializationFeature implements ConfigFeature */ WRITE_DATE_KEYS_AS_TIMESTAMPS(false), + /** + * Feature that controls whether numeric timestamp values are + * to be written using nanosecond timestamps (enabled) or not (disabled); + * if and only if datatype supports such resolution. + * Only newer datatypes (such as Java8 Date/Time) support such resolution -- + * older types (pre-Java8 java.util.Date etc) and Joda do not -- + * and this setting has no effect on such types. + *

+ * If disabled, standard millisecond timestamps are assumed. + * This is the counterpart to {@link DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}. + *

+ * Feature is enabled by default, to support most accurate time values possible. + */ + WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS(true), + /** * Feature that determines whether date/date-time values should be serialized * so that they include timezone id, in cases where type itself contains @@ -360,21 +375,6 @@ public enum SerializationFeature implements ConfigFeature */ WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED(false), - /** - * Feature that controls whether numeric timestamp values are - * to be written using nanosecond timestamps (enabled) or not (disabled); - * if and only if datatype supports such resolution. - * Only newer datatypes (such as Java8 Date/Time) support such resolution -- - * older types (pre-Java8 java.util.Date etc) and Joda do not -- - * and this setting has no effect on such types. - *

- * If disabled, standard millisecond timestamps are assumed. - * This is the counterpart to {@link DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}. - *

- * Feature is enabled by default, to support most accurate time values possible. - */ - WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS(true), - /** * Feature that determines whether {@link java.util.Map} entries are first * sorted by key before serialization or not: if enabled, additional sorting diff --git a/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java b/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java index c8621160ea..f3c506c074 100644 --- a/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java +++ b/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java @@ -14,17 +14,21 @@ public class DatatypeFeatures protected final static int FEATURE_INDEX_ENUM = 0; protected final static int FEATURE_INDEX_JSON_NODE = 1; + protected final static int FEATURE_INDEX_DATETIME = 2; - private final int _enabledFor1, _enabledFor2; + private final int _enabledFor1, _enabledFor2, _enabledFor3; - private final int _explicitFor1, _explicitFor2; + private final int _explicitFor1, _explicitFor2, _explicitFor3; protected DatatypeFeatures(int enabledFor1, int explicitFor1, - int enabledFor2, int explicitFor2) { + int enabledFor2, int explicitFor2, + int enabledFor3, int explicitFor3) { _enabledFor1 = enabledFor1; _explicitFor1 = explicitFor1; _enabledFor2 = enabledFor2; _explicitFor2 = explicitFor2; + _enabledFor3 = enabledFor3; + _explicitFor3 = explicitFor3; } public static DatatypeFeatures defaultFeatures() { @@ -32,13 +36,16 @@ public static DatatypeFeatures defaultFeatures() { } private DatatypeFeatures _with(int enabledFor1, int explicitFor1, - int enabledFor2, int explicitFor2) { + int enabledFor2, int explicitFor2, + int enabledFor3, int explicitFor3) { if ((_enabledFor1 == enabledFor1) && (_explicitFor1 == explicitFor1) - && (_enabledFor2 == enabledFor2) && (_explicitFor2 == explicitFor2)) { + && (_enabledFor2 == enabledFor2) && (_explicitFor2 == explicitFor2) + && (_enabledFor3 == enabledFor3) && (_explicitFor3 == explicitFor3)) { return this; } return new DatatypeFeatures(enabledFor1, explicitFor1, - enabledFor2, explicitFor2); + enabledFor2, explicitFor2, + enabledFor3, explicitFor3); } /* @@ -61,10 +68,16 @@ public DatatypeFeatures with(DatatypeFeature f) { switch (f.featureIndex()) { case 0: return _with(_enabledFor1 | mask, _explicitFor1 | mask, - _enabledFor2, _explicitFor2); + _enabledFor2, _explicitFor2, + _enabledFor3, _explicitFor3); case 1: return _with(_enabledFor1, _explicitFor1, - _enabledFor2 | mask, _explicitFor2 | mask); + _enabledFor2 | mask, _explicitFor2 | mask, + _enabledFor3, _explicitFor3); + case 2: + return _with(_enabledFor1, _explicitFor1, + _enabledFor2, _explicitFor2, + _enabledFor3 | mask, _explicitFor3 | mask); default: VersionUtil.throwInternal(); return this; @@ -88,10 +101,16 @@ public DatatypeFeatures withFeatures(DatatypeFeature... features) { switch (features[0].featureIndex()) { case 0: return _with(_enabledFor1 | mask, _explicitFor1 | mask, - _enabledFor2, _explicitFor2); + _enabledFor2, _explicitFor2, + _enabledFor3, _explicitFor3); case 1: return _with(_enabledFor1, _explicitFor1, - _enabledFor2 | mask, _explicitFor2 | mask); + _enabledFor2 | mask, _explicitFor2 | mask, + _enabledFor3, _explicitFor3); + case 2: + return _with(_enabledFor1, _explicitFor1, + _enabledFor2, _explicitFor2, + _enabledFor3 | mask, _explicitFor3 | mask); default: VersionUtil.throwInternal(); return this; @@ -112,10 +131,16 @@ public DatatypeFeatures without(DatatypeFeature f) { switch (f.featureIndex()) { case 0: return _with(_enabledFor1 & ~mask, _explicitFor1 | mask, - _enabledFor2, _explicitFor2); + _enabledFor2, _explicitFor2, + _enabledFor3, _explicitFor3); case 1: return _with(_enabledFor1, _explicitFor1, - _enabledFor2 & ~mask, _explicitFor2 | mask); + _enabledFor2 & ~mask, _explicitFor2 | mask, + _enabledFor3, _explicitFor3); + case 2: + return _with(_enabledFor1, _explicitFor1, + _enabledFor2, _explicitFor2, + _enabledFor3 & ~mask, _explicitFor3 | mask); default: VersionUtil.throwInternal(); return this; @@ -139,10 +164,16 @@ public DatatypeFeatures withoutFeatures(DatatypeFeature... features) { switch (features[0].featureIndex()) { case 0: return _with(_enabledFor1 & ~mask, _explicitFor1 | mask, - _enabledFor2, _explicitFor2); + _enabledFor2, _explicitFor2, + _enabledFor3, _explicitFor3); case 1: return _with(_enabledFor1, _explicitFor1, - _enabledFor2 & ~mask, _explicitFor2 | mask); + _enabledFor2 & ~mask, _explicitFor2 | mask, + _enabledFor3, _explicitFor3); + case 2: + return _with(_enabledFor1, _explicitFor1, + _enabledFor2, _explicitFor2, + _enabledFor3 & ~mask, _explicitFor3 | mask); default: VersionUtil.throwInternal(); return this; @@ -179,6 +210,8 @@ public boolean isEnabled(DatatypeFeature f) { return f.enabledIn(_enabledFor1); case 1: return f.enabledIn(_enabledFor2); + case 2: + return f.enabledIn(_enabledFor3); default: VersionUtil.throwInternal(); return false; @@ -200,6 +233,8 @@ public boolean isExplicitlySet(DatatypeFeature f) { return f.enabledIn(_explicitFor1); case 1: return f.enabledIn(_explicitFor2); + case 2: + return f.enabledIn(_explicitFor3); default: VersionUtil.throwInternal(); return false; @@ -215,8 +250,6 @@ public boolean isExplicitlySet(DatatypeFeature f) { * @param f Feature to check * * @return Whether given feature has been explicitly enabled - * - * @since 2.15 */ public boolean isExplicitlyEnabled(DatatypeFeature f) { switch (f.featureIndex()) { @@ -224,6 +257,8 @@ public boolean isExplicitlyEnabled(DatatypeFeature f) { return f.enabledIn(_explicitFor1 & _enabledFor1); case 1: return f.enabledIn(_explicitFor2 & _enabledFor2); + case 2: + return f.enabledIn(_explicitFor3 & _enabledFor3); default: VersionUtil.throwInternal(); return false; @@ -239,8 +274,6 @@ public boolean isExplicitlyEnabled(DatatypeFeature f) { * @param f Feature to check * * @return Whether given feature has been explicitly disabled - * - * @since 2.15 */ public boolean isExplicitlyDisabled(DatatypeFeature f) { switch (f.featureIndex()) { @@ -248,6 +281,8 @@ public boolean isExplicitlyDisabled(DatatypeFeature f) { return f.enabledIn(_explicitFor1 & ~_enabledFor1); case 1: return f.enabledIn(_explicitFor2 & ~_enabledFor2); + case 2: + return f.enabledIn(_explicitFor3 & ~_enabledFor3); default: VersionUtil.throwInternal(); return false; @@ -276,6 +311,11 @@ public Boolean getExplicitState(DatatypeFeature f) { return f.enabledIn(_enabledFor2); } return null; + case 2: + if (f.enabledIn(_explicitFor3)) { + return f.enabledIn(_enabledFor3); + } + return null; default: VersionUtil.throwInternal(); return null; @@ -298,7 +338,8 @@ private static class DefaultHolder static { DEFAULT_FEATURES = new DatatypeFeatures( collectDefaults(EnumFeature.values()), 0, - collectDefaults(JsonNodeFeature.values()), 0 + collectDefaults(JsonNodeFeature.values()), 0, + collectDefaults(DateTimeFeature.values()), 0 ); } diff --git a/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java b/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java new file mode 100644 index 0000000000..e8193db7e2 --- /dev/null +++ b/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java @@ -0,0 +1,84 @@ +package tools.jackson.databind.cfg; + +/** + * Configurable on/off features to configure Date/Time handling. + * Mostly used to configure + * Java 8 Time ({@code java.time}) type handling (see + * {@link tools.jackson.databind.ext.javatime.JavaTimeInitializer}) + * but also to "legacy" ({@link java.util.Date}, {@link java.util.Calendar}) + * and Joda Date/Time. + */ +public enum DateTimeFeature implements DatatypeFeature +{ + /** + * Feature that determines whether {@link java.time.ZoneId} is normalized + * (via call to {@code java.time.ZoneId#normalized()}) when deserializing + * types like {@link java.time.ZonedDateTime}. + *

+ * Default setting is enabled, for backwards-compatibility with + * Jackson 2.15. + */ + NORMALIZE_DESERIALIZED_ZONE_ID(true), + + /** + * Feature that determines whether the {@link java.util.TimeZone} of the + * {@link tools.jackson.databind.DeserializationContext} is used + * when leniently deserializing {@link java.time.LocalDate} or + * {@link java.time.LocalDateTime} from the UTC/ISO instant format. + *

+ * Default setting is disabled, for backwards-compatibility with + * Jackson 2.18. + * + * @since 2.19 + */ + USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING(false), + + /** + * Feature that controls whether stringified numbers (Strings that without + * quotes would be legal JSON Numbers) may be interpreted as + * timestamps (enabled) or not (disabled), in case where there is an + * explicitly defined pattern ({@code DateTimeFormatter}, usually by + * using {@code @JsonFormat} annotation) for value. + *

+ * Note that when the default pattern is used (no custom pattern defined), + * stringified numbers are always accepted as timestamps regardless of + * this feature. + */ + ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false), + + /** + * Feature that determines whether {@link java.time.Month} is serialized + * and deserialized as using a zero-based index (FALSE) or a one-based index (TRUE). + * For example, "1" would be serialized/deserialized as Month.JANUARY if TRUE and Month.FEBRUARY if FALSE. + *

+ * Default setting is false, meaning that Month is serialized/deserialized as a zero-based index. + */ + ONE_BASED_MONTHS(false) + ; + + private final static int FEATURE_INDEX = DatatypeFeatures.FEATURE_INDEX_DATETIME; + + /** + * Whether feature is enabled or disabled by default. + */ + private final boolean _enabledByDefault; + + private final int _mask; + + private DateTimeFeature(boolean enabledByDefault) { + _enabledByDefault = enabledByDefault; + _mask = (1 << ordinal()); + } + + @Override + public boolean enabledByDefault() { return _enabledByDefault; } + @Override + public boolean enabledIn(int flags) { return (flags & _mask) != 0; } + @Override + public int getMask() { return _mask; } + + @Override + public int featureIndex() { + return FEATURE_INDEX; + } +} diff --git a/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java b/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java index b61652d35f..063d0b2c48 100644 --- a/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java +++ b/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java @@ -16,6 +16,7 @@ import tools.jackson.core.util.Snapshottable; import tools.jackson.databind.*; import tools.jackson.databind.deser.*; +import tools.jackson.databind.ext.javatime.JavaTimeInitializer; import tools.jackson.databind.introspect.*; import tools.jackson.databind.jsontype.*; import tools.jackson.databind.jsontype.impl.DefaultTypeResolverBuilder; @@ -419,8 +420,9 @@ public MapperBuilderState saveStateApplyModules() { if (_savedState == null) { _savedState = _saveState(); + ModuleContextBase ctxt = _constructModuleContext(); + JavaTimeInitializer.getInstance().setupModule(ctxt); if (_modules != null) { - ModuleContextBase ctxt = _constructModuleContext(); _modules.values().forEach(m -> m.setupModule(ctxt)); // and since context may buffer some changes, ensure those are flushed: ctxt.applyChanges(this); @@ -485,7 +487,6 @@ public boolean isEnabled(SerializationFeature f) { public boolean isEnabled(DatatypeFeature f) { return _datatypeFeatures.isEnabled(f); } - public boolean isEnabled(StreamReadFeature f) { return f.enabledIn(_streamReadFeatures); } diff --git a/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java b/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java index 7476564126..efce0cbef1 100644 --- a/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java +++ b/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java @@ -4,6 +4,7 @@ import java.util.function.UnaryOperator; import tools.jackson.core.*; + import tools.jackson.databind.*; import tools.jackson.databind.JacksonModule.SetupContext; import tools.jackson.databind.deser.*; @@ -125,6 +126,16 @@ public boolean isEnabled(StreamWriteFeature f) { return _builder.isEnabled(f); } + @Override + public boolean isEnabled(DatatypeFeature f) { + return _builder.isEnabled(f); + } + + @Override + public DatatypeFeatures datatypeFeatures() { + return _builder.datatypeFeatures(); + } + /* /********************************************************************** /* Mutators for adding deserializers, related diff --git a/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java b/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java new file mode 100644 index 0000000000..5c84100aba --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java @@ -0,0 +1,224 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime; + +import java.time.*; + +import tools.jackson.databind.*; +import tools.jackson.databind.cfg.DatatypeFeatures; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.deser.ValueInstantiator; +import tools.jackson.databind.deser.ValueInstantiators; +import tools.jackson.databind.deser.std.StdValueInstantiator; +import tools.jackson.databind.ext.javatime.deser.*; +import tools.jackson.databind.ext.javatime.deser.key.*; +import tools.jackson.databind.ext.javatime.ser.*; +import tools.jackson.databind.ext.javatime.ser.key.ZonedDateTimeKeySerializer; +import tools.jackson.databind.introspect.AnnotatedClass; +import tools.jackson.databind.introspect.AnnotatedClassResolver; +import tools.jackson.databind.introspect.AnnotatedMethod; +import tools.jackson.databind.module.SimpleDeserializers; +import tools.jackson.databind.module.SimpleKeyDeserializers; +import tools.jackson.databind.module.SimpleSerializers; +import tools.jackson.databind.ser.std.ToStringSerializer; + +/** + * Class that registers capability of serializing {@code java.time} objects with the Jackson core. + *

+ * In Jackson 3, the module is embedded in databind and handlers are automatically + * registered: approach is similar to one used by full {@link JacksonModule}s. + *

+ * Most {@code java.time} types are serialized as numbers (integers or decimals as appropriate) if the + * {@link tools.jackson.databind.SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} feature is enabled + * (or, for {@link Duration}, {@link tools.jackson.databind.SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS}), + * and otherwise are serialized in standard + * ISO-8601 string representation. + * ISO-8601 specifies formats for representing offset dates and times, zoned dates and times, + * local dates and times, periods, durations, zones, and more. All {@code java.time} types + * have built-in translation to and from ISO-8601 formats. + *

+ * Granularity of timestamps is controlled through the companion features + * {@link tools.jackson.databind.SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} and + * {@link tools.jackson.databind.DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}. For serialization, timestamps are + * written as fractional numbers (decimals), where the number is seconds and the decimal is fractional seconds, if + * {@code WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is enabled (it is by default), with resolution as fine as nanoseconds depending on the + * underlying JDK implementation. If {@code WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is disabled, timestamps are written as a whole number of + * milliseconds. At deserialization time, decimal numbers are always read as fractional second timestamps with up-to-nanosecond resolution, + * since the meaning of the decimal is unambiguous. The more ambiguous integer types are read as fractional seconds without a decimal point + * if {@code READ_DATE_TIMESTAMPS_AS_NANOSECONDS} is enabled (it is by default), and otherwise they are read as milliseconds. + *

+ * Some exceptions to this standard serialization/deserialization rule: + *

+ */ +public final class JavaTimeInitializer + implements java.io.Serializable +{ + private static final long serialVersionUID = 1L; + private static final JavaTimeInitializer INSTANCE = new JavaTimeInitializer(); + + public static JavaTimeInitializer getInstance() { + return INSTANCE; + } + + private JavaTimeInitializer() { } + + public void setupModule(JacksonModule.SetupContext context) { + final DatatypeFeatures datatypeFeatures = context.datatypeFeatures(); + context.addDeserializers(new SimpleDeserializers() + // // Instant variants: + .addDeserializer(Instant.class, + InstantDeserializer.INSTANT.withFeatures(datatypeFeatures)) + .addDeserializer(OffsetDateTime.class, + InstantDeserializer.OFFSET_DATE_TIME.withFeatures(datatypeFeatures)) + .addDeserializer(ZonedDateTime.class, + InstantDeserializer.ZONED_DATE_TIME.withFeatures(datatypeFeatures)) + + // // Other deserializers + .addDeserializer(Duration.class, DurationDeserializer.INSTANCE) + .addDeserializer(LocalDateTime.class, + LocalDateTimeDeserializer.INSTANCE.withFeatures(datatypeFeatures)) + .addDeserializer(LocalDate.class, + LocalDateDeserializer.INSTANCE.withFeatures(datatypeFeatures)) + .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE) + .addDeserializer(MonthDay.class, MonthDayDeserializer.INSTANCE) + .addDeserializer(OffsetTime.class, OffsetTimeDeserializer.INSTANCE) + .addDeserializer(Period.class, JSR310StringParsableDeserializer.PERIOD) + .addDeserializer(Year.class, YearDeserializer.INSTANCE) + .addDeserializer(YearMonth.class, YearMonthDeserializer.INSTANCE) + .addDeserializer(ZoneId.class, JSR310StringParsableDeserializer.ZONE_ID) + .addDeserializer(ZoneOffset.class, JSR310StringParsableDeserializer.ZONE_OFFSET) + ); + + // then serializers: + context.addSerializers(new SimpleSerializers() + .addSerializer(Duration.class, DurationSerializer.INSTANCE) + .addSerializer(Instant.class, InstantSerializer.INSTANCE) + .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) + .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE) + .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE) + .addSerializer(MonthDay.class, MonthDaySerializer.INSTANCE) + .addSerializer(OffsetDateTime.class, OffsetDateTimeSerializer.INSTANCE) + .addSerializer(OffsetTime.class, OffsetTimeSerializer.INSTANCE) + .addSerializer(Period.class, new ToStringSerializer(Period.class)) + .addSerializer(Year.class, YearSerializer.INSTANCE) + .addSerializer(YearMonth.class, YearMonthSerializer.INSTANCE) + + .addSerializer(ZonedDateTime.class, ZonedDateTimeSerializer.INSTANCE) + + // Need to override Type Id handling + // (actual concrete type is `ZoneRegion`, but that's not visible) + .addSerializer(ZoneId.class, new ZoneIdSerializer()) + + .addSerializer(ZoneOffset.class, new ToStringSerializer(ZoneOffset.class)) + ); + + // key serializers + context.addKeySerializers(new SimpleSerializers() + .addSerializer(ZonedDateTime.class, ZonedDateTimeKeySerializer.INSTANCE) + ); + + // key deserializers + context.addKeyDeserializers(new SimpleKeyDeserializers() + .addDeserializer(Duration.class, DurationKeyDeserializer.INSTANCE) + .addDeserializer(Instant.class, InstantKeyDeserializer.INSTANCE) + .addDeserializer(LocalDateTime.class, LocalDateTimeKeyDeserializer.INSTANCE) + .addDeserializer(LocalDate.class, LocalDateKeyDeserializer.INSTANCE) + .addDeserializer(LocalTime.class, LocalTimeKeyDeserializer.INSTANCE) + .addDeserializer(MonthDay.class, MonthDayKeyDeserializer.INSTANCE) + .addDeserializer(OffsetDateTime.class, OffsetDateTimeKeyDeserializer.INSTANCE) + .addDeserializer(OffsetTime.class, OffsetTimeKeyDeserializer.INSTANCE) + .addDeserializer(Period.class, PeriodKeyDeserializer.INSTANCE) + .addDeserializer(Year.class, YearKeyDeserializer.INSTANCE) + .addDeserializer(YearMonth.class, YearMonthKeyDeserializer.INSTANCE) + .addDeserializer(ZonedDateTime.class, ZonedDateTimeKeyDeserializer.INSTANCE) + .addDeserializer(ZoneId.class, ZoneIdKeyDeserializer.INSTANCE) + .addDeserializer(ZoneOffset.class, ZoneOffsetKeyDeserializer.INSTANCE) + ); + + // [modules-java8#274]: 1-based Month (de)serializer need to be applied via modifiers: + final boolean oneBasedMonthEnabled = context.isEnabled(DateTimeFeature.ONE_BASED_MONTHS); + context.addDeserializerModifier(new JavaTimeDeserializerModifier(oneBasedMonthEnabled)); + context.addSerializerModifier(new JavaTimeSerializerModifier(oneBasedMonthEnabled)); + + context.addValueInstantiators(new ValueInstantiators.Base() { + @Override + public ValueInstantiator modifyValueInstantiator(DeserializationConfig config, + BeanDescription beanDesc, ValueInstantiator defaultInstantiator) + { + JavaType type = beanDesc.getType(); + Class raw = type.getRawClass(); + + // 15-May-2015, tatu: In theory not safe, but in practice we do need to do "fuzzy" matching + // because we will (for now) be getting a subtype, but in future may want to downgrade + // to the common base type. Even more, serializer may purposefully force use of base type. + // So... in practice it really should always work, in the end. :) + if (ZoneId.class.isAssignableFrom(raw)) { + // let's assume we should be getting "empty" StdValueInstantiator here: + if (defaultInstantiator instanceof StdValueInstantiator) { + StdValueInstantiator inst = (StdValueInstantiator) defaultInstantiator; + // one further complication: we need ZoneId info, not sub-class + AnnotatedClass ac; + if (raw == ZoneId.class) { + ac = beanDesc.getClassInfo(); + } else { + // we don't need Annotations, so constructing directly is fine here + // even if it's not generally recommended + ac = AnnotatedClassResolver.resolve(config, + config.constructType(ZoneId.class), config); + } + if (!inst.canCreateFromString()) { + AnnotatedMethod factory = _findFactory(ac, "of", String.class); + if (factory != null) { + inst.configureFromStringCreator(factory); + } + // otherwise... should we indicate an error? + } + // return ZoneIdInstantiator.construct(config, beanDesc, defaultInstantiator); + } + } + return defaultInstantiator; + } + }); + } + + private AnnotatedMethod _findFactory(AnnotatedClass cls, String name, Class... argTypes) + { + final int argCount = argTypes.length; + for (AnnotatedMethod method : cls.getFactoryMethods()) { + if (!name.equals(method.getName()) + || (method.getParameterCount() != argCount)) { + continue; + } + for (int i = 0; i < argCount; ++i) { + Class argType = method.getParameter(i).getRawType(); + if (!argType.isAssignableFrom(argTypes[i])) { + continue; + } + } + return method; + } + return null; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/DurationDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/DurationDeserializer.java new file mode 100644 index 0000000000..ec6e87975e --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/DurationDeserializer.java @@ -0,0 +1,214 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.math.BigDecimal; +import java.time.DateTimeException; +import java.time.Duration; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.JsonTokenId; +import tools.jackson.core.StreamReadCapability; +import tools.jackson.core.io.NumberInput; +import tools.jackson.databind.*; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; +import tools.jackson.databind.ext.javatime.util.DurationUnitConverter; + +/** + * Deserializer for Java 8 temporal {@link Duration}s. + */ +public class DurationDeserializer extends JSR310DeserializerBase +{ + public static final DurationDeserializer INSTANCE = new DurationDeserializer(); + + /** + * When defined (not {@code null}) integer values will be converted into duration + * unit configured for the converter. + * Using this converter will typically override the value specified in + * {@link DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS} as it is + * considered that the unit set in {@link JsonFormat#pattern()} has precedence + * since it is more specific. + *

+ * See [jackson-modules-java8#184] for more info. + */ + protected final DurationUnitConverter _durationUnitConverter; + + /** + * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + * + * @since 2.16 + */ + protected final Boolean _readTimestampsAsNanosOverride; + + public DurationDeserializer() { + super(Duration.class); + _durationUnitConverter = null; + _readTimestampsAsNanosOverride = null; + } + + /** + * @since 2.11 + */ + protected DurationDeserializer(DurationDeserializer base, Boolean leniency) { + super(base, leniency); + _durationUnitConverter = base._durationUnitConverter; + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + } + + /** + * @since 2.12 + */ + protected DurationDeserializer(DurationDeserializer base, DurationUnitConverter converter) { + super(base, base._isLenient); + _durationUnitConverter = converter; + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + } + + /** + * @since 2.16 + */ + protected DurationDeserializer(DurationDeserializer base, + Boolean leniency, + DurationUnitConverter converter, + Boolean readTimestampsAsNanosOverride) { + super(base, leniency); + _durationUnitConverter = converter; + _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride; + } + + @Override + protected DurationDeserializer withLeniency(Boolean leniency) { + return new DurationDeserializer(this, leniency); + } + + protected DurationDeserializer withConverter(DurationUnitConverter converter) { + return new DurationDeserializer(this, converter); + } + + @Override + public ValueDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) + { + JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType()); + DurationDeserializer deser = this; + boolean leniency = _isLenient; + DurationUnitConverter unitConverter = _durationUnitConverter; + Boolean timestampsAsNanosOverride = _readTimestampsAsNanosOverride; + if (format != null) { + if (format.hasLenient()) { + leniency = format.getLenient(); + } + if (format.hasPattern()) { + final String pattern = format.getPattern(); + unitConverter = DurationUnitConverter.from(pattern); + if (unitConverter == null) { + ctxt.reportBadDefinition(getValueType(ctxt), + String.format( + "Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]", + pattern, DurationUnitConverter.descForAllowed())); + } + } + timestampsAsNanosOverride = + format.getFeature(JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + } + if (leniency != _isLenient + || !Objects.equals(unitConverter, _durationUnitConverter) + || !Objects.equals(timestampsAsNanosOverride, _readTimestampsAsNanosOverride)) { + return new DurationDeserializer( + this, leniency, unitConverter, timestampsAsNanosOverride); + } + return deser; + } + + @Override + public Duration deserialize(JsonParser parser, DeserializationContext context) + throws JacksonException + { + switch (parser.currentTokenId()) + { + case JsonTokenId.ID_NUMBER_FLOAT: + BigDecimal value = parser.getDecimalValue(); + // [modules-java8#337] since 2.19, Duration does not need negative adjustment + return DecimalUtils.extractSecondsAndNanos(value, Duration::ofSeconds, false); + case JsonTokenId.ID_NUMBER_INT: + return _fromTimestamp(context, parser.getLongValue()); + case JsonTokenId.ID_STRING: + return _fromString(parser, context, parser.getString()); + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + case JsonTokenId.ID_EMBEDDED_OBJECT: + // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded + // values quite easily + return (Duration) parser.getEmbeddedObject(); + + case JsonTokenId.ID_START_ARRAY: + return _deserializeFromArray(parser, context); + } + return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING, + JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT); + } + + protected Duration _fromString(JsonParser parser, DeserializationContext ctxt, + String value0) + throws JacksonException + { + String value = value0.trim(); + if (value.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(parser, ctxt, value); + } + // 30-Sep-2020: Should allow use of "Timestamp as String" for + // some textual formats + if (ctxt.isEnabled(StreamReadCapability.UNTYPED_SCALARS) + && _isValidTimestampString(value)) { + return _fromTimestamp(ctxt, NumberInput.parseLong(value)); + } + + try { + return Duration.parse(value); + } catch (DateTimeException e) { + // null format -> "default formatter" + return _handleDateTimeFormatException(ctxt, e, null, value); + } + } + + protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) + { + if (_durationUnitConverter != null) { + return _durationUnitConverter.convert(ts); + } + // 20-Oct-2020, tatu: This makes absolutely no sense but... somehow + // became the default handling. + if (shouldReadTimestampsAsNanoseconds(ctxt)) { + return Duration.ofSeconds(ts); + } + return Duration.ofMillis(ts); + } + + protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) { + return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride : + context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/InstantDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/InstantDeserializer.java new file mode 100644 index 0000000000..e06cdfdb4e --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/InstantDeserializer.java @@ -0,0 +1,561 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.math.BigDecimal; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.*; +import tools.jackson.core.io.NumberInput; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.cfg.DatatypeFeatures; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; + +/** + * Deserializer for Java 8 temporal {@link Instant}s, {@link OffsetDateTime}, + * and {@link ZonedDateTime}s. + * + * @author Nick Williams + */ +public class InstantDeserializer + extends JSR310DateTimeDeserializerBase +{ + private final static boolean DEFAULT_NORMALIZE_ZONE_ID = DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID.enabledByDefault(); + private final static boolean DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS + = DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS.enabledByDefault(); + + /** + * Constants used to check if ISO 8601 time string is colon-less. See [jackson-modules-java8#131] + */ + protected static final Pattern ISO8601_COLONLESS_OFFSET_REGEX = Pattern.compile("[+-][0-9]{4}(?=\\[|$)"); + + // @since 2.18.2 + private static OffsetDateTime decimalToOffsetDateTime(FromDecimalArguments args) { + // [jackson-modules-java8#308] Since 2.18.2 : Fix can't deserialize OffsetDateTime.MIN: Invalid value for EpochDay + if (args.integer == OffsetDateTime.MIN.toEpochSecond() && args.fraction == OffsetDateTime.MIN.getNano()) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(OffsetDateTime.MIN.toEpochSecond(), OffsetDateTime.MIN.getNano()), OffsetDateTime.MIN.getOffset()); + } + // [jackson-modules-java8#308] Since 2.18.2 : For OffsetDateTime.MAX case + if (args.integer == OffsetDateTime.MAX.toEpochSecond() && args.fraction == OffsetDateTime.MAX.getNano()) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(OffsetDateTime.MAX.toEpochSecond(), OffsetDateTime.MAX.getNano()), OffsetDateTime.MAX.getOffset()); + } + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(args.integer, args.fraction), args.zoneId); + } + + public static final InstantDeserializer INSTANT = new InstantDeserializer<>( + Instant.class, DateTimeFormatter.ISO_INSTANT, + Instant::from, + a -> Instant.ofEpochMilli(a.value), + a -> Instant.ofEpochSecond(a.integer, a.fraction), + null, + true, // yes, replace zero offset with Z + DEFAULT_NORMALIZE_ZONE_ID, + DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS + ); + + public static final InstantDeserializer OFFSET_DATE_TIME = new InstantDeserializer<>( + OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME, + OffsetDateTime::from, + a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId), + InstantDeserializer::decimalToOffsetDateTime, + (d, z) -> (d.isEqual(OffsetDateTime.MIN) || d.isEqual(OffsetDateTime.MAX) ? d : d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()))), + true, // yes, replace zero offset with Z + DEFAULT_NORMALIZE_ZONE_ID, + DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS + ); + + public static final InstantDeserializer ZONED_DATE_TIME = new InstantDeserializer<>( + ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME, + ZonedDateTime::from, + a -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId), + a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), + ZonedDateTime::withZoneSameInstant, + false, // keep zero offset and Z separate since zones explicitly supported + DEFAULT_NORMALIZE_ZONE_ID, + DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS + ); + + protected final Function fromMilliseconds; + + protected final Function fromNanoseconds; + + protected final Function parsedToValue; + + protected final BiFunction adjust; + + /** + * In case of vanilla `Instant` we seem to need to translate "+0000 | +00:00 | +00" + * timezone designator into plain "Z" for some reason; see + * [jackson-modules-java8#18] for more info + * + * @since 2.9.0 + */ + protected final boolean replaceZeroOffsetAsZ; + + /** + * Flag for JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE + */ + protected final Boolean _adjustToContextTZOverride; + + /** + * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + * + * @since 2.16 + */ + protected final Boolean _readTimestampsAsNanosOverride; + + /** + * Flag set from + * {@link DateTimeFeature#NORMALIZE_DESERIALIZED_ZONE_ID} to + * determine whether {@link ZoneId} is to be normalized during deserialization. + * + * @since 2.16 + */ + protected final boolean _normalizeZoneId; + + /** + * Flag set from + * {@link DateTimeFeature#ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS} + * to determine whether stringified numbers are interpreted as timestamps + * (enabled) nor not (disabled) in addition to a custom pattern ({code DateTimeFormatter}). + *

+ * NOTE: stringified timestamps are always allowed with default patterns; + * this flag only affects handling of custom patterns. + * + * @since 2.16 + */ + protected final boolean _alwaysAllowStringifiedDateTimestamps; + + /** + * @since 2.16 + */ + protected InstantDeserializer(Class supportedType, + DateTimeFormatter formatter, + Function parsedToValue, + Function fromMilliseconds, + Function fromNanoseconds, + BiFunction adjust, + boolean replaceZeroOffsetAsZ, + boolean normalizeZoneId, + boolean readNumericStringsAsTimestamp + ) + { + super(supportedType, formatter); + this.parsedToValue = parsedToValue; + this.fromMilliseconds = fromMilliseconds; + this.fromNanoseconds = fromNanoseconds; + this.adjust = adjust == null ? ((d, z) -> d) : adjust; + this.replaceZeroOffsetAsZ = replaceZeroOffsetAsZ; + this._adjustToContextTZOverride = null; + this._readTimestampsAsNanosOverride = null; + _normalizeZoneId = normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = readNumericStringsAsTimestamp; + } + + @SuppressWarnings("unchecked") + protected InstantDeserializer(InstantDeserializer base, DateTimeFormatter f) + { + super((Class) base.handledType(), f); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT); + _adjustToContextTZOverride = base._adjustToContextTZOverride; + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; + } + + @SuppressWarnings("unchecked") + protected InstantDeserializer(InstantDeserializer base, Boolean adjustToContextTimezoneOverride) + { + super((Class) base.handledType(), base._formatter); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ; + _adjustToContextTZOverride = adjustToContextTimezoneOverride; + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; + } + + @SuppressWarnings("unchecked") + protected InstantDeserializer(InstantDeserializer base, DateTimeFormatter f, Boolean leniency) + { + super((Class) base.handledType(), f, leniency); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT); + _adjustToContextTZOverride = base._adjustToContextTZOverride; + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; + } + + /** + * @since 2.16 + */ + protected InstantDeserializer(InstantDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape, + Boolean adjustToContextTimezoneOverride, + Boolean readTimestampsAsNanosOverride) + { + super(base, leniency, formatter, shape); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ; + _adjustToContextTZOverride = adjustToContextTimezoneOverride; + _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride; + _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; + } + + @SuppressWarnings("unchecked") + protected InstantDeserializer(InstantDeserializer base, + DatatypeFeatures features) + { + super((Class) base.handledType(), base._formatter); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ; + _adjustToContextTZOverride = base._adjustToContextTZOverride; + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + + _normalizeZoneId = features.isEnabled(DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID); + _alwaysAllowStringifiedDateTimestamps = features.isEnabled(DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS); + } + + @Override + protected InstantDeserializer withDateFormat(DateTimeFormatter dtf) { + if (dtf == _formatter) { + return this; + } + return new InstantDeserializer<>(this, dtf); + } + + @Override + protected InstantDeserializer withLeniency(Boolean leniency) { + return new InstantDeserializer<>(this, _formatter, leniency); + } + + public InstantDeserializer withFeatures(DatatypeFeatures features) { + if ((_normalizeZoneId == features.isEnabled(DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID)) + && (_alwaysAllowStringifiedDateTimestamps == features.isEnabled(DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS)) + ) { + return this; + } + return new InstantDeserializer<>(this, features); + } + + @SuppressWarnings("unchecked") + @Override // @since 2.12.1 + protected JSR310DateTimeDeserializerBase _withFormatOverrides(DeserializationContext ctxt, + BeanProperty property, JsonFormat.Value formatOverrides) + { + InstantDeserializer deser = (InstantDeserializer) super._withFormatOverrides(ctxt, + property, formatOverrides); + Boolean adjustToContextTZOverride = formatOverrides.getFeature( + JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature( + JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + if (!Objects.equals(adjustToContextTZOverride, deser._adjustToContextTZOverride) + || !Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) { + return new InstantDeserializer<>(deser, deser._isLenient, deser._formatter, + deser._shape, adjustToContextTZOverride, readTimestampsAsNanosOverride); + } + return deser; + } + + @SuppressWarnings("unchecked") + @Override + public T deserialize(JsonParser parser, DeserializationContext context) + throws JacksonException + { + //NOTE: Timestamps contain no timezone info, and are always in configured TZ. Only + //string values have to be adjusted to the configured TZ. + switch (parser.currentTokenId()) + { + case JsonTokenId.ID_NUMBER_FLOAT: + return _fromDecimal(context, parser.getDecimalValue()); + case JsonTokenId.ID_NUMBER_INT: + return _fromLong(context, parser.getLongValue()); + case JsonTokenId.ID_STRING: + return _fromString(parser, context, parser.getString()); + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + case JsonTokenId.ID_EMBEDDED_OBJECT: + // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded + // values quite easily + return (T) parser.getEmbeddedObject(); + + case JsonTokenId.ID_START_ARRAY: + return _deserializeFromArray(parser, context); + } + return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING, + JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT); + } + + protected boolean shouldAdjustToContextTimezone(DeserializationContext context) { + return (_adjustToContextTZOverride != null) ? _adjustToContextTZOverride : + context.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + } + + protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) { + return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride : + context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + } + + // Helper method to find Strings of form "all digits" and "digits-comma-digits" + protected int _countPeriods(String str) + { + int commas = 0; + int i = 0; + int ch = str.charAt(i); + if (ch == '-' || ch == '+') { + ++i; + } + for (int end = str.length(); i < end; ++i) { + ch = str.charAt(i); + if (ch < '0' || ch > '9') { + if (ch == '.') { + ++commas; + } else { + return -1; + } + } + } + return commas; + } + + protected T _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + // only check for other parsing modes if we are using default formatter or explicitly asked to + if (_alwaysAllowStringifiedDateTimestamps || + _formatter == DateTimeFormatter.ISO_INSTANT || + _formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME || + _formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME + ) { + // 22-Jan-2016, [datatype-jsr310#16]: Allow quoted numbers too + int dots = _countPeriods(string); + if (dots >= 0) { // negative if not simple number + try { + if (dots == 0) { + return _fromLong(ctxt, NumberInput.parseLong(string)); + } + if (dots == 1) { + return _fromDecimal(ctxt, NumberInput.parseBigDecimal(string, false)); + } + } catch (NumberFormatException e) { + // fall through to default handling, to get error there + } + } + + string = replaceZeroOffsetAsZIfNecessary(string); + } + + // For some reason DateTimeFormatter.ISO_INSTANT only supports UTC ISO 8601 strings, so it have to be excluded + if (_formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME || + _formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME) { + + // 21-March-2021, Oeystein: Work-around to support basic iso 8601 format (colon-less). + // As per JSR-310; Only extended 8601 formats (with colon) are supported for + // ZonedDateTime.parse() and OffsetDateTime.parse(). + // https://github.com/FasterXML/jackson-modules-java8/issues/131 + string = addInColonToOffsetIfMissing(string); + } + + T value; + try { + TemporalAccessor acc = _formatter.parse(string); + value = parsedToValue.apply(acc); + if (shouldAdjustToContextTimezone(ctxt)) { + return adjust.apply(value, getZone(ctxt)); + } + } catch (DateTimeException e) { + value = _handleDateTimeFormatException(ctxt, e, _formatter, string); + } + return value; + } + + protected T _fromLong(DeserializationContext context, long timestamp) + { + if(shouldReadTimestampsAsNanoseconds(context)){ + return fromNanoseconds.apply(new FromDecimalArguments( + timestamp, 0, this.getZone(context) + )); + } + return fromMilliseconds.apply(new FromIntegerArguments( + timestamp, this.getZone(context))); + } + + protected T _fromDecimal(DeserializationContext context, BigDecimal value) + { + FromDecimalArguments args = + DecimalUtils.extractSecondsAndNanos(value, (s, ns) -> new FromDecimalArguments(s, ns, getZone(context)), + // [modules-java8#337] since 2.19, only Instant needs negative adjustment + true); + return fromNanoseconds.apply(args); + } + + private ZoneId getZone(DeserializationContext context) + { + // Instants are always in UTC, so don't waste compute cycles + // Normalizing the zone to prevent discrepancies. + // See https://github.com/FasterXML/jackson-modules-java8/pull/267 for details + if (_valueClass == Instant.class) { + return null; + } + ZoneId zoneId = context.getTimeZone().toZoneId(); + return _normalizeZoneId ? zoneId.normalized() : zoneId; + } + + private String replaceZeroOffsetAsZIfNecessary(String text) + { + if (replaceZeroOffsetAsZ) { + return replaceZeroOffsetAsZ(text); + } + + return text; + } + + private static String replaceZeroOffsetAsZ(String text) + { + int plusIndex = text.lastIndexOf('+'); + if (plusIndex < 0) { + return text; + } + int maybeOffsetIndex = plusIndex + 1; + int remaining = text.length() - maybeOffsetIndex; + switch (remaining) { + case 2: + return text.regionMatches(maybeOffsetIndex, "00", 0, remaining) + ? text.substring(0, plusIndex) + 'Z' + : text; + case 4: + return text.regionMatches(maybeOffsetIndex, "0000", 0, remaining) + ? text.substring(0, plusIndex) + 'Z' + : text; + case 5: + return text.regionMatches(maybeOffsetIndex, "00:00", 0, remaining) + ? text.substring(0, plusIndex) + 'Z' + : text; + } + return text; + } + + // @since 2.13 + private static String addInColonToOffsetIfMissing(String text) + { + int timeIndex = text.indexOf('T'); + if (timeIndex < 0 || timeIndex > text.length() - 1) { + return text; + } + + int offsetIndex = text.indexOf('+', timeIndex + 1); + if (offsetIndex < 0) { + offsetIndex = text.indexOf('-', timeIndex + 1); + } + + if (offsetIndex < 0 || offsetIndex > text.length() - 5) { + return text; + } + + int colonIndex = text.indexOf(':', offsetIndex); + if (colonIndex == offsetIndex + 3) { + return text; + } + + if (Character.isDigit(text.charAt(offsetIndex + 1)) + && Character.isDigit(text.charAt(offsetIndex + 2)) + && Character.isDigit(text.charAt(offsetIndex + 3)) + && Character.isDigit(text.charAt(offsetIndex + 4))) { + String match = text.substring(offsetIndex, offsetIndex + 5); + return text.substring(0, offsetIndex) + + match.substring(0, 3) + ':' + match.substring(3) + + text.substring(offsetIndex + match.length()); + } + + // fallback to slow regex path, should be fully handled by the above + final Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher(text); + if (matcher.find()) { + String match = matcher.group(0); + return matcher.replaceFirst(match.substring(0, 3) + ':' + match.substring(3)); + } + return text; + } + + public static class FromIntegerArguments // since 2.8.3 + { + public final long value; + public final ZoneId zoneId; + + FromIntegerArguments(long value, ZoneId zoneId) + { + this.value = value; + this.zoneId = zoneId; + } + } + + public static class FromDecimalArguments // since 2.8.3 + { + public final long integer; + public final int fraction; + public final ZoneId zoneId; + + FromDecimalArguments(long integer, int fraction, ZoneId zoneId) + { + this.integer = integer; + this.fraction = fraction; + this.zoneId = zoneId; + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DateTimeDeserializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DateTimeDeserializerBase.java new file mode 100644 index 0000000000..2a61db5d65 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DateTimeDeserializerBase.java @@ -0,0 +1,175 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.ResolverStyle; +import java.util.Locale; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; + +import tools.jackson.databind.*; + +public abstract class JSR310DateTimeDeserializerBase + extends JSR310DeserializerBase +{ + protected final DateTimeFormatter _formatter; + + /** + * Setting that indicates the {@Link JsonFormat.Shape} specified for this deserializer + * as a {@link com.fasterxml.jackson.annotation.JsonFormat.Shape} annotation on + * property or class, or due to per-type "config override", or from global settings: + * If Shape is NUMBER_INT, the input value is considered to be epoch days. If not a + * NUMBER_INT, and the deserializer was not specified with the leniency setting of true, + * then an exception will be thrown. + */ + protected final Shape _shape; + + protected JSR310DateTimeDeserializerBase(Class supportedType, DateTimeFormatter f) { + super(supportedType); + _formatter = f; + _shape = null; + } + + public JSR310DateTimeDeserializerBase(Class supportedType, DateTimeFormatter f, Boolean leniency) { + super(supportedType, leniency); + _formatter = f; + _shape = null; + } + + protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base, + DateTimeFormatter f) { + super(base); + _formatter = f; + _shape = base._shape; + } + + protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base, + Boolean leniency) { + super(base, leniency); + _formatter = base._formatter; + _shape = base._shape; + } + + protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base, + Shape shape) { + super(base); + _formatter = base._formatter; + _shape = shape; + } + + /** + * @since 2.16 + */ + protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape) { + super(base, leniency); + _formatter = formatter; + _shape = shape; + } + + protected abstract JSR310DateTimeDeserializerBase withDateFormat(DateTimeFormatter dtf); + + @Override + protected abstract JSR310DateTimeDeserializerBase withLeniency(Boolean leniency); + + /** + * The default implementation returns this, because shape is more likely applicable in case of the serialization, + * usage during deserialization could cover only very specific cases. + */ + protected JSR310DateTimeDeserializerBase withShape(Shape shape) { + return this; + } + + @Override + public ValueDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) + { + JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType()); + return (format == null) ? this : _withFormatOverrides(ctxt, property, format); + } + + /** + * @param ctxt Active deserialization context + * @param property (optional) Property on which this deserializer is used, or {@code null} + * for root value + * @param formatOverrides Format overrides to use (non-null) + * + * @return Either this deserializer as is, or newly constructed variant if created + * for different configuration + * + * @since 2.12.1 + */ + protected JSR310DateTimeDeserializerBase _withFormatOverrides(DeserializationContext ctxt, + BeanProperty property, JsonFormat.Value formatOverrides) + { + JSR310DateTimeDeserializerBase deser = this; + + // 17-Aug-2019, tatu: For 2.10 let's start considering leniency/strictness too + if (formatOverrides.hasLenient()) { + Boolean leniency = formatOverrides.getLenient(); + if (leniency != null) { + deser = deser.withLeniency(leniency); + } + } + if (formatOverrides.hasPattern()) { + final String pattern = formatOverrides.getPattern(); + final Locale locale = formatOverrides.hasLocale() ? formatOverrides.getLocale() : ctxt.getLocale(); + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); + if (acceptCaseInsensitiveValues(ctxt, formatOverrides)) { + builder.parseCaseInsensitive(); + } + builder.appendPattern(pattern); + DateTimeFormatter df; + if (locale == null) { + df = builder.toFormatter(); + } else { + df = builder.toFormatter(locale); + } + + // [#148]: allow strict parsing + if (!deser.isLenient()) { + df = df.withResolverStyle(ResolverStyle.STRICT); + } + + // [#69]: For instant serializers/deserializers we need to configure the formatter with + //a time zone picked up from JsonFormat annotation, otherwise serialization might not work + if (formatOverrides.hasTimeZone()) { + df = df.withZone(formatOverrides.getTimeZone().toZoneId()); + } + deser = deser.withDateFormat(df); + } + // [#58]: For LocalDate deserializers we need to configure the formatter with + //a shape picked up from JsonFormat annotation, to decide if the value is EpochSeconds + JsonFormat.Shape shape = formatOverrides.getShape(); + if (shape != null && shape != _shape) { + deser = deser.withShape(shape); + } + // any use for TimeZone? + + return deser; + } + + private boolean acceptCaseInsensitiveValues(DeserializationContext ctxt, JsonFormat.Value format) + { + Boolean enabled = format.getFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES); + if (enabled == null) { + enabled = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES); + } + return enabled; + } + + protected void _throwNoNumericTimestampNeedTimeZone(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + ctxt.reportInputMismatch(handledType(), +"raw timestamp (%d) not allowed for `%s`: need additional information such as an offset or time-zone (see class Javadocs)", +p.getNumberValue(), handledType().getName()); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DeserializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DeserializerBase.java new file mode 100644 index 0000000000..23ca16f384 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DeserializerBase.java @@ -0,0 +1,240 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.io.NumberInput; + +import tools.jackson.databind.*; +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.deser.std.StdScalarDeserializer; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.type.LogicalType; +import tools.jackson.databind.util.ClassUtil; + +/** + * Base class that indicates that all JSR310 datatypes are deserialized from scalar JSON types. + * + * @author Nick Williams + */ +abstract class JSR310DeserializerBase extends StdScalarDeserializer +{ + /** + * Flag that indicates what leniency setting is enabled for this deserializer (either + * due {@link com.fasterxml.jackson.annotation.JsonFormat.Shape} annotation on property or class, or due to per-type + * "config override", or from global settings): leniency/strictness has effect + * on accepting some non-default input value representations (such as integer values + * for dates). + *

+ * Note that global default setting is for leniency to be enabled, for Jackson 2.x, + * and has to be explicitly change to force strict handling: this is to keep backwards + * compatibility with earlier versions. + */ + protected final boolean _isLenient; + + protected JSR310DeserializerBase(Class supportedType) { + super(supportedType); + _isLenient = true; + } + + protected JSR310DeserializerBase(Class supportedType, + Boolean leniency) { + super(supportedType); + _isLenient = !Boolean.FALSE.equals(leniency); + } + + protected JSR310DeserializerBase(JSR310DeserializerBase base) { + super(base); + _isLenient = base._isLenient; + } + + protected JSR310DeserializerBase(JSR310DeserializerBase base, Boolean leniency) { + super(base); + _isLenient = !Boolean.FALSE.equals(leniency); + } + + protected abstract JSR310DeserializerBase withLeniency(Boolean leniency); + + /** + * @return {@code true} if lenient handling is enabled; {code false} if not (strict mode) + */ + protected boolean isLenient() { + return _isLenient; + } + + /** + * Replacement for {@code isLenient()} for specific case of deserialization + * from empty or blank String. + * + * @since 2.12 + */ + @SuppressWarnings("unchecked") + protected T _fromEmptyString(JsonParser p, DeserializationContext ctxt, + String str) + throws JacksonException + { + final CoercionAction act = _checkFromStringCoercion(ctxt, str); + switch (act) { // note: Fail handled above + case AsEmpty: + return (T) getEmptyValue(ctxt); + case TryConvert: + case AsNull: + default: + } + // 22-Oct-2020, tatu: Although we should probably just accept this, + // for backwards compatibility let's for now allow override by + // "Strict" checks + if (!_isLenient) { + return _failForNotLenient(p, ctxt, JsonToken.VALUE_STRING); + } + + return null; + } + + // Presumably all types here are Date/Time oriented ones? + @Override + public LogicalType logicalType() { return LogicalType.DateTime; } + + @Override + public Object deserializeWithType(JsonParser parser, DeserializationContext context, + TypeDeserializer typeDeserializer) + throws JacksonException + { + return typeDeserializer.deserializeTypedFromAny(parser, context); + } + + // @since 2.12 + protected boolean _isValidTimestampString(String str) { + // 30-Sep-2020, tatu: Need to support "numbers as Strings" for data formats + // that only have String values for scalars (CSV, Properties, XML) + // NOTE: we do allow negative values, but has to fit in 64-bits: + return _isIntNumber(str) && NumberInput.inLongRange(str, (str.charAt(0) == '-')); + } + + protected BOGUS _reportWrongToken(DeserializationContext context, + JsonToken exp, String unit) + throws JacksonException + { + context.reportWrongTokenException((ValueDeserializer)this, exp, + "Expected %s for '%s' of %s value", + exp.name(), unit, ClassUtil.getClassDescription(handledType())); + return null; + } + + protected BOGUS _reportWrongToken(JsonParser parser, DeserializationContext context, + JsonToken... expTypes) + throws JacksonException + { + // 20-Apr-2016, tatu: No multiple-expected-types handler yet, construct message here + return context.reportInputMismatch(handledType(), + "Unexpected token (%s), expected one of %s for %s value", + parser.currentToken(), + Arrays.asList(expTypes).toString(), + ClassUtil.getClassDescription(handledType())); + } + + @SuppressWarnings("unchecked") + protected R _handleDateTimeException(DeserializationContext context, + DateTimeException e0, String value) + throws JacksonException + { + try { + return (R) context.handleWeirdStringValue(handledType(), value, + "Failed to deserialize %s: (%s) %s", + ClassUtil.getClassDescription(handledType()), e0.getClass().getName(), e0.getMessage()); + } catch (JacksonException e) { + e.initCause(e0); + throw e; + } + } + + // @since 3.0 + @SuppressWarnings("unchecked") + protected R _handleDateTimeFormatException(DeserializationContext context, + DateTimeException e0, DateTimeFormatter format, String value) + throws JacksonException + { + final String formatterDesc = (format == null) ? "[default format]" : format.toString(); + try { + return (R) context.handleWeirdStringValue(handledType(), value, + "Failed to deserialize %s (with format '%s'): (%s) %s", + ClassUtil.getClassDescription(handledType()), + formatterDesc, e0.getClass().getName(), e0.getMessage()); + } catch (JacksonException e) { + e.initCause(e0); + throw e; + } + } + + @SuppressWarnings("unchecked") + protected R _handleUnexpectedToken(DeserializationContext ctxt, + JsonParser parser, String message, Object... args) + { + return (R) ctxt.handleUnexpectedToken(getValueType(ctxt), parser.currentToken(), + parser, message, args); + } + + protected R _handleUnexpectedToken(DeserializationContext context, + JsonParser parser, JsonToken... expTypes) + { + return _handleUnexpectedToken(context, parser, + "Unexpected token (%s), expected one of %s for %s value", + parser.currentToken(), + Arrays.asList(expTypes), + ClassUtil.getClassDescription(handledType())); + } + + @SuppressWarnings("unchecked") + protected T _failForNotLenient(JsonParser p, DeserializationContext ctxt, + JsonToken expToken) + throws JacksonException + { + return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), expToken, p, + "Cannot deserialize instance of %s out of %s token: not allowed because 'strict' mode set for property or type (enable 'lenient' handling to allow)", + ClassUtil.nameOf(handledType()), p.currentToken()); + } + + /* + public Object handleUnexpectedToken(Class instClass, JsonToken t, + JsonParser p, String msg, Object... msgArgs) + */ + + /** + * Helper method used to peel off spurious wrappings of DateTimeException + * + * @param e DateTimeException to peel + * + * @return DateTimeException that does not have another DateTimeException as its cause. + */ + protected DateTimeException _peelDTE(DateTimeException e) { + while (true) { + Throwable t = e.getCause(); + if (t != null && t instanceof DateTimeException) { + e = (DateTimeException) t; + continue; + } + break; + } + return e; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310StringParsableDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310StringParsableDeserializer.java new file mode 100644 index 0000000000..2cb31ed3ea --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310StringParsableDeserializer.java @@ -0,0 +1,186 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; + +import tools.jackson.core.JsonToken; + +import tools.jackson.core.util.VersionUtil; + +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.cfg.CoercionInputShape; +import tools.jackson.databind.jsontype.TypeDeserializer; + +/** + * Deserializer for all Java 8 temporal {@link java.time} types that cannot be represented + * with numbers and that have parse functions that can take {@link String}s, + * and where format is not configurable. + * + * @author Nick Williams + * @author Tatu Saloranta + */ +public class JSR310StringParsableDeserializer + extends JSR310DeserializerBase +{ + protected final static int TYPE_PERIOD = 1; + protected final static int TYPE_ZONE_ID = 2; + protected final static int TYPE_ZONE_OFFSET = 3; + + public static final ValueDeserializer PERIOD = + createDeserializer(Period.class, TYPE_PERIOD); + + public static final ValueDeserializer ZONE_ID = + createDeserializer(ZoneId.class, TYPE_ZONE_ID); + + public static final ValueDeserializer ZONE_OFFSET = + createDeserializer(ZoneOffset.class, TYPE_ZONE_OFFSET); + + protected final int _typeSelector; + + @SuppressWarnings("unchecked") + protected JSR310StringParsableDeserializer(Class supportedType, int typeSelector) + { + super((Class)supportedType); + _typeSelector = typeSelector; + } + + protected JSR310StringParsableDeserializer(JSR310StringParsableDeserializer base, Boolean leniency) { + super(base, leniency); + _typeSelector = base._typeSelector; + } + + @SuppressWarnings("unchecked") + protected static ValueDeserializer createDeserializer(Class type, int typeId) { + return (ValueDeserializer) new JSR310StringParsableDeserializer(type, typeId); + } + + @Override + protected JSR310StringParsableDeserializer withLeniency(Boolean leniency) { + if (_isLenient == !Boolean.FALSE.equals(leniency)) { + return this; + } + // TODO: or should this be casting as above in createDeserializer? But then in createContext, we need to + // call the withLeniency method in this class. (See if we can follow InstantDeser convention here?) + return new JSR310StringParsableDeserializer(this, leniency); + } + + @Override + public ValueDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) + { + JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType()); + JSR310StringParsableDeserializer deser = this; + if (format != null) { + if (format.hasLenient()) { + Boolean leniency = format.getLenient(); + if (leniency != null) { + deser = this.withLeniency(leniency); + } + } + } + return deser; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + if (p.hasToken(JsonToken.VALUE_STRING)) { + return _fromString(p, ctxt, p.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (p.isExpectedStartObjectToken()) { + return _fromString(p, ctxt, + ctxt.extractScalarFromObject(p, this, handledType())); + } + if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded + // values quite easily + return p.getEmbeddedObject(); + } + if (p.isExpectedStartArrayToken()) { + return _deserializeFromArray(p, ctxt); + } + + throw ctxt.wrongTokenException(p, handledType(), JsonToken.VALUE_STRING, null); + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext context, + TypeDeserializer deserializer) + throws JacksonException + { + // This is a nasty kludge right here, working around issues like + // [datatype-jsr310#24]. But should work better than not having the work-around. + JsonToken t = p.currentToken(); + if ((t != null) && t.isScalarValue()) { + return deserialize(p, context); + } + return deserializer.deserializeTypedFromAny(p, context); + } + + protected Object _fromString(JsonParser p, DeserializationContext ctxt, + String string) + throws JacksonException + { + string = string.trim(); + if (string.length() == 0) { + CoercionAction act = ctxt.findCoercionAction(logicalType(), _valueClass, + CoercionInputShape.EmptyString); + if (act == CoercionAction.Fail) { + ctxt.reportInputMismatch(this, +"Cannot coerce empty String (\"\") to %s (but could if enabling coercion using `CoercionConfig`)", +_coercedTypeDesc()); + } + // 21-Jun-2020, tatu: As of 2.12, leniency considered legacy setting, + // but still supported. + if (!isLenient()) { + return _failForNotLenient(p, ctxt, JsonToken.VALUE_STRING); + } + if (act == CoercionAction.AsEmpty) { + return getEmptyValue(ctxt); + } + // None of the types has specific null value + return null; + } + try { + switch (_typeSelector) { + case TYPE_PERIOD: + return Period.parse(string); + case TYPE_ZONE_ID: + return ZoneId.of(string); + case TYPE_ZONE_OFFSET: + return ZoneOffset.of(string); + } + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, null, string); + } + VersionUtil.throwInternal(); + return null; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java new file mode 100644 index 0000000000..7976888128 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java @@ -0,0 +1,28 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Month; + +import tools.jackson.databind.*; +import tools.jackson.databind.deser.ValueDeserializerModifier; + +/** + * @since 2.17 + */ +public class JavaTimeDeserializerModifier extends ValueDeserializerModifier { + private static final long serialVersionUID = 1L; + + private final boolean _oneBaseMonths; + + public JavaTimeDeserializerModifier(boolean oneBaseMonths) { + _oneBaseMonths = oneBaseMonths; + } + + @Override + public ValueDeserializer modifyEnumDeserializer(DeserializationConfig config, JavaType type, + BeanDescription beanDesc, ValueDeserializer defaultDeserializer) { + if (_oneBaseMonths && type.hasRawClass(Month.class)) { + return new OneBasedMonthDeserializer(defaultDeserializer); + } + return defaultDeserializer; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserializer.java new file mode 100644 index 0000000000..06a08d466b --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserializer.java @@ -0,0 +1,214 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.*; + +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.cfg.CoercionInputShape; +import tools.jackson.databind.cfg.DatatypeFeatures; +import tools.jackson.databind.cfg.DateTimeFeature; + +/** + * Deserializer for Java 8 temporal {@link LocalDate}s. + * + * @author Nick Williams + */ +public class LocalDateDeserializer extends JSR310DateTimeDeserializerBase +{ + private final static boolean DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING + = DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING.enabledByDefault(); + + private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + + public static final LocalDateDeserializer INSTANCE = new LocalDateDeserializer(); + + /** + * Flag set from + * {@link DateTimeFeature#USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING} + * to determine whether the {@link java.util.TimeZone} of the + * {@link tools.jackson.databind.DeserializationContext} is used + * when leniently deserializing from the UTC/ISO instant format. + */ + protected final boolean _useTimeZoneForLenientDateParsing; + + protected LocalDateDeserializer() { + this(DEFAULT_FORMATTER); + } + + public LocalDateDeserializer(DateTimeFormatter dtf) { + super(LocalDate.class, dtf); + _useTimeZoneForLenientDateParsing = DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING; + } + + public LocalDateDeserializer(LocalDateDeserializer base, DateTimeFormatter dtf) { + super(base, dtf); + _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing; + } + + protected LocalDateDeserializer(LocalDateDeserializer base, Boolean leniency) { + super(base, leniency); + _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing; + } + + protected LocalDateDeserializer(LocalDateDeserializer base, JsonFormat.Shape shape) { + super(base, shape); + _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing; + } + + /** + * Since 2.19 + */ + protected LocalDateDeserializer(LocalDateDeserializer base, DatatypeFeatures features) { + super(LocalDate.class, base._formatter); + _useTimeZoneForLenientDateParsing = features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING); + } + + @Override + protected LocalDateDeserializer withDateFormat(DateTimeFormatter dtf) { + return new LocalDateDeserializer(this, dtf); + } + + @Override + protected LocalDateDeserializer withLeniency(Boolean leniency) { + return new LocalDateDeserializer(this, leniency); + } + + @Override + protected LocalDateDeserializer withShape(JsonFormat.Shape shape) { return new LocalDateDeserializer(this, shape); } + + /** + * Since 2.19 + */ + public LocalDateDeserializer withFeatures(DatatypeFeatures features) { + if (_useTimeZoneForLenientDateParsing == + features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)) { + return this; + } + return new LocalDateDeserializer(this, features); + } + + @Override + public LocalDate deserialize(JsonParser parser, DeserializationContext context) + throws JacksonException + { + if (parser.hasToken(JsonToken.VALUE_STRING)) { + return _fromString(parser, context, parser.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (parser.isExpectedStartObjectToken()) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (parser.isExpectedStartArrayToken()) { + JsonToken t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + if (context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + && (t == JsonToken.VALUE_STRING || t==JsonToken.VALUE_EMBEDDED_OBJECT)) { + final LocalDate parsed = deserialize(parser, context); + if (parser.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(parser, context); + } + return parsed; + } + if (t == JsonToken.VALUE_NUMBER_INT) { + int year = parser.getIntValue(); + int month = parser.nextIntValue(-1); + int day = parser.nextIntValue(-1); + + if (parser.nextToken() != JsonToken.END_ARRAY) { + throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY, + "Expected array to end"); + } + return LocalDate.of(year, month, day); + } + context.reportInputMismatch(handledType(), + "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT", + t); + } + if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + return (LocalDate) parser.getEmbeddedObject(); + } + // 06-Jan-2018, tatu: Is this actually safe? Do users expect such coercion? + if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + CoercionAction act = context.findCoercionAction(logicalType(), _valueClass, + CoercionInputShape.Integer); + _checkCoercionFail(context, act, handledType(), parser.getLongValue(), + "Integer value (" + parser.getLongValue() + ")"); + + // issue 58 - also check for NUMBER_INT, which needs to be specified when serializing. + if (_shape == JsonFormat.Shape.NUMBER_INT || isLenient()) { + return LocalDate.ofEpochDay(parser.getLongValue()); + } + return _failForNotLenient(parser, context, JsonToken.VALUE_STRING); + } + return _handleUnexpectedToken(context, parser, "Expected array or string."); + } + + protected LocalDate _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + // as per [datatype-jsr310#37], only check for optional (and, incorrect...) time marker 'T' + // if we are using default formatter + final DateTimeFormatter format = _formatter; + try { + if (format == DEFAULT_FORMATTER) { + // JavaScript by default includes time in JSON serialized Dates (UTC/ISO instant format). + if (string.length() > 10 && string.charAt(10) == 'T') { + if (isLenient()) { + if (string.endsWith("Z")) { + if (_useTimeZoneForLenientDateParsing) { + return Instant.parse(string).atZone(ctxt.getTimeZone().toZoneId()).toLocalDate(); + } + return LocalDate.parse(string.substring(0, string.length() - 1), + DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + return LocalDate.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + JavaType t = getValueType(ctxt); + return (LocalDate) ctxt.handleWeirdStringValue(t.getRawClass(), + string, +"Should not contain time component when 'strict' mode set for property or type (enable 'lenient' handling to allow)" + ); + } + } + return LocalDate.parse(string, format); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, format, string); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserializer.java new file mode 100644 index 0000000000..ead83305d6 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserializer.java @@ -0,0 +1,254 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.*; +import tools.jackson.databind.*; +import tools.jackson.databind.cfg.DatatypeFeatures; +import tools.jackson.databind.cfg.DateTimeFeature; + +/** + * Deserializer for Java 8 temporal {@link LocalDateTime}s. + * + * @author Nick Williams + */ +public class LocalDateTimeDeserializer + extends JSR310DateTimeDeserializerBase +{ + private final static boolean DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING + = DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING.enabledByDefault(); + + private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); + + /** + * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + * + * @since 2.16 + */ + protected final Boolean _readTimestampsAsNanosOverride; + + /** + * Flag set from + * {@link DateTimeFeature#USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING} + * to determine whether the {@link java.util.TimeZone} of the + * {@link tools.jackson.databind.DeserializationContext} is used + * when leniently deserializing from the UTC/ISO instant format. + * + * @since 2.19 + */ + protected final boolean _useTimeZoneForLenientDateParsing; + + protected LocalDateTimeDeserializer() { // was private before 2.12 + this(DEFAULT_FORMATTER); + } + + public LocalDateTimeDeserializer(DateTimeFormatter formatter) { + super(LocalDateTime.class, formatter); + _readTimestampsAsNanosOverride = null; + _useTimeZoneForLenientDateParsing = DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING; + } + + /** + * Since 2.10 + */ + protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, Boolean leniency) { + super(base, leniency); + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing; + } + + /** + * Since 2.16 + */ + protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape, + Boolean readTimestampsAsNanosOverride) { + super(base, leniency, formatter, shape); + _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride; + _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing; + } + + /** + * Since 2.19 + */ + protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, DatatypeFeatures features) { + super(LocalDateTime.class, base._formatter); + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + _useTimeZoneForLenientDateParsing = features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING); + } + + @Override + protected LocalDateTimeDeserializer withDateFormat(DateTimeFormatter dtf) { + return new LocalDateTimeDeserializer(this, _isLenient, dtf, _shape, _readTimestampsAsNanosOverride); + } + + @Override + protected LocalDateTimeDeserializer withLeniency(Boolean leniency) { + return new LocalDateTimeDeserializer(this, leniency); + } + + @Override + protected JSR310DateTimeDeserializerBase _withFormatOverrides(DeserializationContext ctxt, + BeanProperty property, JsonFormat.Value formatOverrides) + { + LocalDateTimeDeserializer deser = (LocalDateTimeDeserializer) + super._withFormatOverrides(ctxt, property, formatOverrides); + Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature( + JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) { + return new LocalDateTimeDeserializer(deser, deser._isLenient, deser._formatter, + deser._shape, readTimestampsAsNanosOverride); + } + return deser; + } + + /** + * Since 2.19 + */ + public LocalDateTimeDeserializer withFeatures(DatatypeFeatures features) { + if (_useTimeZoneForLenientDateParsing == + features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)) { + return this; + } + return new LocalDateTimeDeserializer(this, features); + } + + @Override + public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws JacksonException + { + if (parser.hasTokenId(JsonTokenId.ID_STRING)) { + return _fromString(parser, context, parser.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (parser.isExpectedStartObjectToken()) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (parser.isExpectedStartArrayToken()) { + JsonToken t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT) + && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + final LocalDateTime parsed = deserialize(parser, context); + if (parser.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(parser, context); + } + return parsed; + } + if (t == JsonToken.VALUE_NUMBER_INT) { + LocalDateTime result; + + int year = parser.getIntValue(); + int month = parser.nextIntValue(-1); + int day = parser.nextIntValue(-1); + int hour = parser.nextIntValue(-1); + int minute = parser.nextIntValue(-1); + + t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + result = LocalDateTime.of(year, month, day, hour, minute); + } else { + int second = parser.getIntValue(); + t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + result = LocalDateTime.of(year, month, day, hour, minute, second); + } else { + int partialSecond = parser.getIntValue(); + if (partialSecond < 1_000 && !shouldReadTimestampsAsNanoseconds(context)) + partialSecond *= 1_000_000; // value is milliseconds, convert it to nanoseconds + if (parser.nextToken() != JsonToken.END_ARRAY) { + throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY, + "Expected array to end"); + } + result = LocalDateTime.of(year, month, day, hour, minute, second, partialSecond); + } + } + return result; + } + context.reportInputMismatch(handledType(), + "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT", + t); + } + if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + return (LocalDateTime) parser.getEmbeddedObject(); + } + if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + _throwNoNumericTimestampNeedTimeZone(parser, context); + } + return _handleUnexpectedToken(context, parser, "Expected array or string."); + } + + protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) { + return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride : + context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + } + + protected LocalDateTime _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + final DateTimeFormatter format = _formatter; + try { + // 21-Oct-2020, tatu: Changed as per [modules-base#94] for 2.12, + // had bad timezone handle change from [modules-base#56] + if (_formatter == DEFAULT_FORMATTER) { + // ... only allow iff lenient mode enabled since + // JavaScript by default includes time and zone in JSON serialized Dates (UTC/ISO instant format). + if (string.length() > 10 && string.charAt(10) == 'T') { + if (string.endsWith("Z")) { + if (isLenient()) { + if (_useTimeZoneForLenientDateParsing) { + return Instant.parse(string).atZone(ctxt.getTimeZone().toZoneId()).toLocalDateTime(); + } + return LocalDateTime.parse(string.substring(0, string.length()-1), + _formatter); + } + JavaType t = getValueType(ctxt); + return (LocalDateTime) ctxt.handleWeirdStringValue(t.getRawClass(), + string, +"Should not contain offset when 'strict' mode set for property or type (enable 'lenient' handling to allow)" + ); + } + } + } + return LocalDateTime.parse(string, _formatter); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, format, string); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserializer.java new file mode 100644 index 0000000000..131a1ff569 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserializer.java @@ -0,0 +1,196 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; + +/** + * Deserializer for Java 8 temporal {@link LocalTime}s. + * + * @author Nick Williams + */ +public class LocalTimeDeserializer extends JSR310DateTimeDeserializerBase +{ + private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_TIME; + + public static final LocalTimeDeserializer INSTANCE = new LocalTimeDeserializer(); + + /** + * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + * + * @since 2.16 + */ + protected final Boolean _readTimestampsAsNanosOverride; + + protected LocalTimeDeserializer() { // was private before 2.12 + this(DEFAULT_FORMATTER); + } + + public LocalTimeDeserializer(DateTimeFormatter formatter) { + super(LocalTime.class, formatter); + _readTimestampsAsNanosOverride = null; + } + + protected LocalTimeDeserializer(LocalTimeDeserializer base, Boolean leniency) { + super(base, leniency); + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + } + + /** + * Since 2.16 + */ + protected LocalTimeDeserializer(LocalTimeDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape, + Boolean readTimestampsAsNanosOverride) { + super(base, leniency, formatter, shape); + _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride; + } + + @Override + protected LocalTimeDeserializer withDateFormat(DateTimeFormatter dtf) { + return new LocalTimeDeserializer(this, _isLenient, dtf, _shape, _readTimestampsAsNanosOverride); + } + + @Override + protected LocalTimeDeserializer withLeniency(Boolean leniency) { + return new LocalTimeDeserializer(this, leniency); + } + + @Override + protected JSR310DateTimeDeserializerBase _withFormatOverrides(DeserializationContext ctxt, + BeanProperty property, JsonFormat.Value formatOverrides) + { + LocalTimeDeserializer deser = (LocalTimeDeserializer) + super._withFormatOverrides(ctxt, property, formatOverrides); + Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature( + JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) { + return new LocalTimeDeserializer(deser, deser._isLenient, deser._formatter, + deser._shape, readTimestampsAsNanosOverride); + } + return deser; + } + + @Override + public LocalTime deserialize(JsonParser parser, DeserializationContext context) throws JacksonException + { + if (parser.hasToken(JsonToken.VALUE_STRING)) { + return _fromString(parser, context, parser.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (parser.isExpectedStartObjectToken()) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (parser.isExpectedStartArrayToken()) { + JsonToken t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + if (context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + && (t == JsonToken.VALUE_STRING || t==JsonToken.VALUE_EMBEDDED_OBJECT)) { + final LocalTime parsed = deserialize(parser, context); + if (parser.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(parser, context); + } + return parsed; + } + if (t == JsonToken.VALUE_NUMBER_INT) { + int hour = parser.getIntValue(); + + parser.nextToken(); + int minute = parser.getIntValue(); + LocalTime result; + + t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + result = LocalTime.of(hour, minute); + } else { + int second = parser.getIntValue(); + t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + result = LocalTime.of(hour, minute, second); + } else { + int partialSecond = parser.getIntValue(); + if(partialSecond < 1_000 && !shouldReadTimestampsAsNanoseconds(context)) + partialSecond *= 1_000_000; // value is milliseconds, convert it to nanoseconds + t = parser.nextToken(); + if (t != JsonToken.END_ARRAY) { + throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY, + "Expected array to end"); + } + result = LocalTime.of(hour, minute, second, partialSecond); + } + } + return result; + } + context.reportInputMismatch(handledType(), + "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT", + t); + } + if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + return (LocalTime) parser.getEmbeddedObject(); + } + if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + _throwNoNumericTimestampNeedTimeZone(parser, context); + } + return _handleUnexpectedToken(context, parser, "Expected array or string."); + } + + protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) { + return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride : + context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + } + + protected LocalTime _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + final DateTimeFormatter format = _formatter; + try { + if (format == DEFAULT_FORMATTER) { + if (string.contains("T")) { + return LocalTime.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + } + return LocalTime.parse(string, format); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, format, string); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserializer.java new file mode 100644 index 0000000000..9f7b3df94b --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserializer.java @@ -0,0 +1,129 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.MonthDay; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; + +/** + * Deserializer for Java 8 temporal {@link MonthDay}s. + */ +public class MonthDayDeserializer extends JSR310DateTimeDeserializerBase +{ + public static final MonthDayDeserializer INSTANCE = new MonthDayDeserializer(); + + /** + * NOTE: only {@code public} so that use via annotations (see [modules-java8#202]) + * is possible + */ + public MonthDayDeserializer() { + this(null); + } + + public MonthDayDeserializer(DateTimeFormatter formatter) { + super(MonthDay.class, formatter); + } + + /** + * Since 2.12 + */ + protected MonthDayDeserializer(MonthDayDeserializer base, Boolean leniency) { + super(base, leniency); + } + + /** + * Since 2.16 + */ + protected MonthDayDeserializer(MonthDayDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape) { + super(base, leniency, formatter, shape); + } + + @Override + protected MonthDayDeserializer withLeniency(Boolean leniency) { + return new MonthDayDeserializer(this, leniency); + } + + @Override + protected MonthDayDeserializer withDateFormat(DateTimeFormatter dtf) { + return new MonthDayDeserializer(this, _isLenient, dtf, _shape); + } + + @Override + public MonthDay deserialize(JsonParser parser, DeserializationContext context) + throws JacksonException + { + if (parser.hasToken(JsonToken.VALUE_STRING)) { + return _fromString(parser, context, parser.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (parser.isExpectedStartObjectToken()) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (parser.isExpectedStartArrayToken()) { + JsonToken t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT) + && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + final MonthDay parsed = deserialize(parser, context); + if (parser.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(parser, context); + } + return parsed; + } + if (t != JsonToken.VALUE_NUMBER_INT) { + _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "month"); + } + int month = parser.getIntValue(); + int day = parser.nextIntValue(-1); + if (day == -1) { + if (!parser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "day"); + } + day = parser.getIntValue(); + } + if (parser.nextToken() != JsonToken.END_ARRAY) { + throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY, + "Expected array to end"); + } + return MonthDay.of(month, day); + } + if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + return (MonthDay) parser.getEmbeddedObject(); + } + return _handleUnexpectedToken(context, parser, + JsonToken.VALUE_STRING, JsonToken.START_ARRAY); + } + + protected MonthDay _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + try { + if (_formatter == null) { + return MonthDay.parse(string); + } + return MonthDay.parse(string, _formatter); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, _formatter, string); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserializer.java new file mode 100644 index 0000000000..436f633b51 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserializer.java @@ -0,0 +1,195 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.*; +import tools.jackson.databind.*; + +/** + * Deserializer for Java 8 temporal {@link OffsetTime}s. + * + * @author Nick Williams + */ +public class OffsetTimeDeserializer extends JSR310DateTimeDeserializerBase +{ + public static final OffsetTimeDeserializer INSTANCE = new OffsetTimeDeserializer(); + + /** + * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + * + * @since 2.16 + */ + protected final Boolean _readTimestampsAsNanosOverride; + + protected OffsetTimeDeserializer() { // was private before 2.12 + this(DateTimeFormatter.ISO_OFFSET_TIME); + } + + protected OffsetTimeDeserializer(DateTimeFormatter dtf) { + super(OffsetTime.class, dtf); + _readTimestampsAsNanosOverride = null; + } + + /** + * Since 2.11 + */ + protected OffsetTimeDeserializer(OffsetTimeDeserializer base, Boolean leniency) { + super(base, leniency); + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + } + + /** + * Since 2.16 + */ + protected OffsetTimeDeserializer(OffsetTimeDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape, + Boolean readTimestampsAsNanosOverride) { + super(base, leniency, formatter, shape); + _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride; + } + + @Override + protected OffsetTimeDeserializer withDateFormat(DateTimeFormatter dtf) { + return new OffsetTimeDeserializer(this, _isLenient, dtf, _shape, _readTimestampsAsNanosOverride); + } + + @Override + protected OffsetTimeDeserializer withLeniency(Boolean leniency) { + return new OffsetTimeDeserializer(this, leniency); + } + + @Override + protected JSR310DateTimeDeserializerBase _withFormatOverrides(DeserializationContext ctxt, + BeanProperty property, JsonFormat.Value formatOverrides) + { + OffsetTimeDeserializer deser = (OffsetTimeDeserializer) + super._withFormatOverrides(ctxt, property, formatOverrides); + Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature( + JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) { + return new OffsetTimeDeserializer(deser, deser._isLenient, deser._formatter, + deser._shape, readTimestampsAsNanosOverride); + } + return deser; + } + + @Override + public OffsetTime deserialize(JsonParser parser, DeserializationContext context) + throws JacksonException + { + if (parser.hasToken(JsonToken.VALUE_STRING)) { + return _fromString(parser, context, parser.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (parser.isExpectedStartObjectToken()) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (!parser.isExpectedStartArrayToken()) { + if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + return (OffsetTime) parser.getEmbeddedObject(); + } + if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + _throwNoNumericTimestampNeedTimeZone(parser, context); + } + throw context.wrongTokenException(parser, handledType(), JsonToken.START_ARRAY, + "Expected array or string."); + } + JsonToken t = parser.nextToken(); + if (t != JsonToken.VALUE_NUMBER_INT) { + if (t == JsonToken.END_ARRAY) { + return null; + } + if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT) + && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + final OffsetTime parsed = deserialize(parser, context); + if (parser.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(parser, context); + } + return parsed; + } + context.reportInputMismatch(handledType(), + "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT", + t); + } + int hour = parser.getIntValue(); + int minute = parser.nextIntValue(-1); + if (minute == -1) { + t = parser.currentToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + if (t != JsonToken.VALUE_NUMBER_INT) { + _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "minutes"); + } + minute = parser.getIntValue(); + } + int partialSecond = 0; + int second = 0; + if (parser.nextToken() == JsonToken.VALUE_NUMBER_INT) { + second = parser.getIntValue(); + if (parser.nextToken() == JsonToken.VALUE_NUMBER_INT) { + partialSecond = parser.getIntValue(); + if (partialSecond < 1_000 && !shouldReadTimestampsAsNanoseconds(context)) { + partialSecond *= 1_000_000; // value is milliseconds, convert it to nanoseconds + } + parser.nextToken(); + } + } + if (parser.currentToken() == JsonToken.VALUE_STRING) { + OffsetTime result = OffsetTime.of(hour, minute, second, partialSecond, ZoneOffset.of(parser.getString())); + if (parser.nextToken() != JsonToken.END_ARRAY) { + _reportWrongToken(context, JsonToken.END_ARRAY, "timezone"); + } + return result; + } + throw context.wrongTokenException(parser, handledType(), JsonToken.VALUE_STRING, + "Expected string for TimeZone after numeric values"); + } + + protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) { + return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride : + context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + } + + protected OffsetTime _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + try { + return OffsetTime.parse(string, _formatter); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, _formatter, string); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java new file mode 100644 index 0000000000..6509c72423 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java @@ -0,0 +1,71 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Month; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; + +import tools.jackson.databind.*; +import tools.jackson.databind.deser.std.DelegatingDeserializer; +import tools.jackson.databind.exc.InvalidFormatException; + +public class OneBasedMonthDeserializer extends DelegatingDeserializer { + public OneBasedMonthDeserializer(ValueDeserializer defaultDeserializer) { + super(defaultDeserializer); + } + + @Override + public Object deserialize(JsonParser parser, DeserializationContext context) { + JsonToken token = parser.currentToken(); + switch (token) { + case VALUE_NUMBER_INT: + return _decodeMonth(parser.getIntValue(), parser); + case VALUE_STRING: + String monthSpec = parser.getString(); + int oneBasedMonthNumber = _decodeNumber(monthSpec); + if (oneBasedMonthNumber >= 0) { + return _decodeMonth(oneBasedMonthNumber, parser); + } + default: + // Otherwise fall through to default handling + break; + } + return getDelegatee().deserialize(parser, context); + } + + /** + * @return Numeric value of input text that represents a 1-digit or 2-digit number. + * Negative value in other cases (empty string, not a number, 3 or more digits). + */ + private int _decodeNumber(String text) { + int numValue; + switch (text.length()) { + case 1: + char c = text.charAt(0); + boolean cValid = ('0' <= c && c <= '9'); + numValue = cValid ? (c - '0') : -1; + break; + case 2: + char c1 = text.charAt(0); + char c2 = text.charAt(1); + boolean c12valid = ('0' <= c1 && c1 <= '9' && '0' <= c2 && c2 <= '9'); + numValue = c12valid ? (10 * (c1 - '0') + (c2 - '0')) : -1; + break; + default: + numValue = -1; + } + return numValue; + } + + private Month _decodeMonth(int oneBasedMonthNumber, JsonParser parser) throws InvalidFormatException { + if (Month.JANUARY.getValue() <= oneBasedMonthNumber && oneBasedMonthNumber <= Month.DECEMBER.getValue()) { + return Month.of(oneBasedMonthNumber); + } + throw new InvalidFormatException(parser, "Month number " + oneBasedMonthNumber + " not allowed for 1-based Month.", oneBasedMonthNumber, Integer.class); + } + + @Override + protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { + return new OneBasedMonthDeserializer(newDelegatee); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/YearDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearDeserializer.java new file mode 100644 index 0000000000..041a64866e --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearDeserializer.java @@ -0,0 +1,132 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.Year; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.StreamReadCapability; +import tools.jackson.core.io.NumberInput; +import tools.jackson.databind.DeserializationContext; + +/** + * Deserializer for Java 8 temporal {@link Year}s. + * + * @author Nick Williams + */ +public class YearDeserializer extends JSR310DateTimeDeserializerBase +{ + public static final YearDeserializer INSTANCE = new YearDeserializer(); + + /** + * NOTE: only {@code public} so that use via annotations (see [modules-java8#202]) + * is possible + */ + public YearDeserializer() { // public since 2.12 + this(null); + } + + public YearDeserializer(DateTimeFormatter formatter) { + super(Year.class, formatter); + } + + /** + * Since 2.12 + */ + protected YearDeserializer(YearDeserializer base, Boolean leniency) { + super(base, leniency); + } + + /** + * Since 2.16 + */ + public YearDeserializer(YearDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape) { + super(base, leniency, formatter, shape); + } + + @Override + protected YearDeserializer withDateFormat(DateTimeFormatter dtf) { + return new YearDeserializer(this, _isLenient, dtf, _shape); + } + + @Override + protected YearDeserializer withLeniency(Boolean leniency) { + return new YearDeserializer(this, leniency); + } + + @Override + public Year deserialize(JsonParser parser, DeserializationContext context) throws JacksonException + { + JsonToken t = parser.currentToken(); + if (t == JsonToken.VALUE_STRING) { + return _fromString(parser, context, parser.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (t == JsonToken.START_OBJECT) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (t == JsonToken.VALUE_NUMBER_INT) { + return _fromNumber(context, parser.getIntValue()); + } + if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { + return (Year) parser.getEmbeddedObject(); + } + if (parser.hasToken(JsonToken.START_ARRAY)){ + return _deserializeFromArray(parser, context); + } + return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING, JsonToken.VALUE_NUMBER_INT); + } + + protected Year _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + // 30-Sep-2020: Should allow use of "Timestamp as String" for XML/CSV + if (ctxt.isEnabled(StreamReadCapability.UNTYPED_SCALARS) + && _isValidTimestampString(string)) { + return _fromNumber(ctxt, NumberInput.parseInt(string)); + } + try { + if (_formatter == null) { + return Year.parse(string); + } + return Year.parse(string, _formatter); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, _formatter, string); + } + } + + protected Year _fromNumber(DeserializationContext ctxt, int value) { + return Year.of(value); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserializer.java new file mode 100644 index 0000000000..d5ae40118c --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserializer.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.DateTimeException; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.core.JsonToken; + +/** + * Deserializer for Java 8 temporal {@link YearMonth}s. + * + * @author Nick Williams + * @since 2.2.0 + */ +public class YearMonthDeserializer extends JSR310DateTimeDeserializerBase +{ + public static final YearMonthDeserializer INSTANCE = new YearMonthDeserializer(); + + /** + * NOTE: only {@code public} so that use via annotations (see [modules-java8#202]) + * is possible + */ + public YearMonthDeserializer() // public since 2.12 + { + this(DateTimeFormatter.ofPattern("u-MM")); + } + + public YearMonthDeserializer(DateTimeFormatter formatter) + { + super(YearMonth.class, formatter); + } + + /** + * Since 2.11 + */ + protected YearMonthDeserializer(YearMonthDeserializer base, Boolean leniency) { + super(base, leniency); + } + + /** + * Since 2.16 + */ + public YearMonthDeserializer(YearMonthDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape) { + super(base, leniency, formatter, shape); + } + + @Override + protected YearMonthDeserializer withDateFormat(DateTimeFormatter dtf) { + return new YearMonthDeserializer(this, _isLenient, dtf, _shape); + } + + @Override + protected YearMonthDeserializer withLeniency(Boolean leniency) { + return new YearMonthDeserializer(this, leniency); + } + + @Override + public YearMonth deserialize(JsonParser parser, DeserializationContext context) throws JacksonException + { + if (parser.hasToken(JsonToken.VALUE_STRING)) { + return _fromString(parser, context, parser.getString()); + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (parser.isExpectedStartObjectToken()) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (parser.isExpectedStartArrayToken()) { + JsonToken t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT) + && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + final YearMonth parsed = deserialize(parser, context); + if (parser.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(parser, context); + } + return parsed; + } + if (t != JsonToken.VALUE_NUMBER_INT) { + _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "years"); + } + int year = parser.getIntValue(); + int month = parser.nextIntValue(-1); + if (month == -1) { + if (!parser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "months"); + } + month = parser.getIntValue(); + } + if (parser.nextToken() != JsonToken.END_ARRAY) { + throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY, + "Expected array to end"); + } + return YearMonth.of(year, month); + } + if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + return (YearMonth) parser.getEmbeddedObject(); + } + return _handleUnexpectedToken(context, parser, + JsonToken.VALUE_STRING, JsonToken.START_ARRAY); + } + + protected YearMonth _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + try { + return YearMonth.parse(string, _formatter); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, _formatter, string); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/DurationKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/DurationKeyDeserializer.java new file mode 100644 index 0000000000..75434b3b95 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/DurationKeyDeserializer.java @@ -0,0 +1,27 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.Duration; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class DurationKeyDeserializer extends Jsr310KeyDeserializer { + + public static final DurationKeyDeserializer INSTANCE = new DurationKeyDeserializer(); + + private DurationKeyDeserializer() { + // singleton + } + + @Override + protected Duration deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return Duration.parse(key); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, Duration.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/InstantKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/InstantKeyDeserializer.java new file mode 100644 index 0000000000..995772514e --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/InstantKeyDeserializer.java @@ -0,0 +1,28 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class InstantKeyDeserializer extends Jsr310KeyDeserializer { + + public static final InstantKeyDeserializer INSTANCE = new InstantKeyDeserializer(); + + private InstantKeyDeserializer() { + // singleton + } + + @Override + protected Instant deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return DateTimeFormatter.ISO_INSTANT.parse(key, Instant::from); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, Instant.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/Jsr310KeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/Jsr310KeyDeserializer.java new file mode 100644 index 0000000000..33144b1c82 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/Jsr310KeyDeserializer.java @@ -0,0 +1,41 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.util.ClassUtil; + +abstract class Jsr310KeyDeserializer extends KeyDeserializer +{ + @Override + public final Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + // 17-Aug-2019, tatu: Jackson 2.x had special handling for "null" key marker, which + // is why we have this unnecessary dispatching, for now + return deserialize(key, ctxt); + } + + protected abstract Object deserialize(String key, DeserializationContext ctxt) + throws JacksonException; + + @SuppressWarnings("unchecked") + protected T _handleDateTimeException(DeserializationContext ctxt, + Class type, DateTimeException e0, String value) + throws JacksonException + { + try { + return (T) ctxt.handleWeirdKey(type, value, + "Failed to deserialize %s: (%s) %s", + ClassUtil.nameOf(type), + e0.getClass().getName(), + e0.getMessage()); + + } catch (JacksonException e) { + e.initCause(e0); + throw e; + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateKeyDeserializer.java new file mode 100644 index 0000000000..944ac72f8f --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateKeyDeserializer.java @@ -0,0 +1,28 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class LocalDateKeyDeserializer extends Jsr310KeyDeserializer { + + public static final LocalDateKeyDeserializer INSTANCE = new LocalDateKeyDeserializer(); + + private LocalDateKeyDeserializer() { + // singleton + } + + @Override + protected LocalDate deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return LocalDate.parse(key, DateTimeFormatter.ISO_LOCAL_DATE); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, LocalDate.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateTimeKeyDeserializer.java new file mode 100644 index 0000000000..0e4dacaad6 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateTimeKeyDeserializer.java @@ -0,0 +1,29 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class LocalDateTimeKeyDeserializer extends Jsr310KeyDeserializer { + + public static final LocalDateTimeKeyDeserializer INSTANCE = new LocalDateTimeKeyDeserializer(); + + private LocalDateTimeKeyDeserializer() { + // singleton + } + + @Override + protected LocalDateTime deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return LocalDateTime.parse(key, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, LocalDateTime.class, e, key); + } + } + +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalTimeKeyDeserializer.java new file mode 100644 index 0000000000..7e327e01bc --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalTimeKeyDeserializer.java @@ -0,0 +1,29 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class LocalTimeKeyDeserializer extends Jsr310KeyDeserializer { + + public static final LocalTimeKeyDeserializer INSTANCE = new LocalTimeKeyDeserializer(); + + private LocalTimeKeyDeserializer() { + // singleton + } + + @Override + protected LocalTime deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return LocalTime.parse(key, DateTimeFormatter.ISO_LOCAL_TIME); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, LocalTime.class, e, key); + } + } + +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/MonthDayKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/MonthDayKeyDeserializer.java new file mode 100644 index 0000000000..c8ff90150f --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/MonthDayKeyDeserializer.java @@ -0,0 +1,40 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; + +import java.time.DateTimeException; +import java.time.MonthDay; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class MonthDayKeyDeserializer extends Jsr310KeyDeserializer { + + public static final MonthDayKeyDeserializer INSTANCE = new MonthDayKeyDeserializer(); + + // formatter copied from MonthDay + private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder() + .appendLiteral("--") + .appendValue(MONTH_OF_YEAR, 2) + .appendLiteral('-') + .appendValue(DAY_OF_MONTH, 2) + .toFormatter(); + + private MonthDayKeyDeserializer() { + // singleton + } + + @Override + protected MonthDay deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return MonthDay.parse(key, PARSER); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, MonthDay.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetDateTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetDateTimeKeyDeserializer.java new file mode 100644 index 0000000000..398c33979c --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetDateTimeKeyDeserializer.java @@ -0,0 +1,28 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class OffsetDateTimeKeyDeserializer extends Jsr310KeyDeserializer { + + public static final OffsetDateTimeKeyDeserializer INSTANCE = new OffsetDateTimeKeyDeserializer(); + + private OffsetDateTimeKeyDeserializer() { + // singleton + } + + @Override + protected OffsetDateTime deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return OffsetDateTime.parse(key, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, OffsetDateTime.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetTimeKeyDeserializer.java new file mode 100644 index 0000000000..7f82cb916a --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetTimeKeyDeserializer.java @@ -0,0 +1,29 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.OffsetTime; +import java.time.format.DateTimeFormatter; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class OffsetTimeKeyDeserializer extends Jsr310KeyDeserializer { + + public static final OffsetTimeKeyDeserializer INSTANCE = new OffsetTimeKeyDeserializer(); + + private OffsetTimeKeyDeserializer() { + // singleton + } + + @Override + protected OffsetTime deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return OffsetTime.parse(key, DateTimeFormatter.ISO_OFFSET_TIME); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, OffsetTime.class, e, key); + } + } + +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/PeriodKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/PeriodKeyDeserializer.java new file mode 100644 index 0000000000..19c87fcc5d --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/PeriodKeyDeserializer.java @@ -0,0 +1,27 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.Period; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class PeriodKeyDeserializer extends Jsr310KeyDeserializer { + + public static final PeriodKeyDeserializer INSTANCE = new PeriodKeyDeserializer(); + + private PeriodKeyDeserializer() { + // singletin + } + + @Override + protected Period deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return Period.parse(key); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, Period.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearKeyDeserializer.java new file mode 100644 index 0000000000..1aea9c5693 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearKeyDeserializer.java @@ -0,0 +1,29 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.Year; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class YearKeyDeserializer extends Jsr310KeyDeserializer { + + public static final YearKeyDeserializer INSTANCE = new YearKeyDeserializer(); + + private YearKeyDeserializer() { + // singleton + } + + @Override + protected Year deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return Year.of(Integer.parseInt(key)); + } catch (NumberFormatException nfe) { + return _handleDateTimeException(ctxt, Year.class, new DateTimeException("Number format exception", nfe), key); + } catch (DateTimeException dte) { + return _handleDateTimeException(ctxt, Year.class, dte, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearMonthKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearMonthKeyDeserializer.java new file mode 100644 index 0000000000..bde36b2393 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearMonthKeyDeserializer.java @@ -0,0 +1,37 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.YEAR; + +import java.time.DateTimeException; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.SignStyle; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class YearMonthKeyDeserializer extends Jsr310KeyDeserializer { + public static final YearMonthKeyDeserializer INSTANCE = new YearMonthKeyDeserializer(); + + // parser copied from YearMonth + private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() + .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(MONTH_OF_YEAR, 2) + .toFormatter(); + + private YearMonthKeyDeserializer() { } // singleton + + @Override + protected YearMonth deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return YearMonth.parse(key, FORMATTER); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, YearMonth.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneIdKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneIdKeyDeserializer.java new file mode 100644 index 0000000000..9519672794 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneIdKeyDeserializer.java @@ -0,0 +1,27 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.ZoneId; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class ZoneIdKeyDeserializer extends Jsr310KeyDeserializer { + + public static final ZoneIdKeyDeserializer INSTANCE = new ZoneIdKeyDeserializer(); + + private ZoneIdKeyDeserializer() { + // singleton + } + + @Override + protected Object deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return ZoneId.of(key); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, ZoneId.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneOffsetKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneOffsetKeyDeserializer.java new file mode 100644 index 0000000000..a8812bee1a --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneOffsetKeyDeserializer.java @@ -0,0 +1,27 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.ZoneOffset; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class ZoneOffsetKeyDeserializer extends Jsr310KeyDeserializer { + + public static final ZoneOffsetKeyDeserializer INSTANCE = new ZoneOffsetKeyDeserializer(); + + private ZoneOffsetKeyDeserializer() { + // singleton + } + + @Override + protected ZoneOffset deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + return ZoneOffset.of(key); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, ZoneOffset.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializer.java new file mode 100644 index 0000000000..af56212b02 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializer.java @@ -0,0 +1,28 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.DateTimeException; +import java.time.ZonedDateTime; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; + +public class ZonedDateTimeKeyDeserializer extends Jsr310KeyDeserializer { + + public static final ZonedDateTimeKeyDeserializer INSTANCE = new ZonedDateTimeKeyDeserializer(); + + protected ZonedDateTimeKeyDeserializer() { + // singleton + } + + @Override + protected ZonedDateTime deserialize(String key, DeserializationContext ctxt) + throws JacksonException + { + try { + // Not supplying a formatter allows the use of all supported formats + return ZonedDateTime.parse(key); + } catch (DateTimeException e) { + return _handleDateTimeException(ctxt, ZonedDateTime.class, e, key); + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/package-info.java b/src/main/java/tools/jackson/databind/ext/javatime/package-info.java new file mode 100644 index 0000000000..c83bff19ea --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/package-info.java @@ -0,0 +1,7 @@ +/** +Contains support for Java (8) Time (JSR-310) types: always available (as of Jackson 3.0) +but included similar to {@code JacksonModule}s for better configurability. + +*/ + +package tools.jackson.databind.ext.javatime; diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/DurationSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/DurationSerializer.java new file mode 100644 index 0000000000..00fb9db912 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/DurationSerializer.java @@ -0,0 +1,185 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; + +import tools.jackson.databind.*; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; +import tools.jackson.databind.ext.javatime.util.DurationUnitConverter; +import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import tools.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat; + +/** + * Serializer for Java 8 temporal {@link Duration}s. + *

+ * NOTE: since 2.10, {@link SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS} + * determines global default used for determining if serialization should use + * numeric (timestamps) or textual representation. Before this, + * {@link SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} was used. + * + * @author Nick Williams + */ +public class DurationSerializer extends JSR310FormattedSerializerBase +{ + public static final DurationSerializer INSTANCE = new DurationSerializer(); + + /** + * When defined (not {@code null}) duration values will be converted into integers + * with the unit configured for the converter. + * Only available when {@link SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS} is enabled + * and {@link SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is not enabled + * since the duration converters do not support fractions + * @since 2.12 + */ + private DurationUnitConverter _durationUnitConverter; + + protected DurationSerializer() { // was private before 2.12 + super(Duration.class); + } + + protected DurationSerializer(DurationSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp) { + super(base, dtf, useTimestamp, null, null); + } + + protected DurationSerializer(DurationSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp, Boolean useNanoseconds) { + super(base, dtf, useTimestamp, useNanoseconds, null); + } + + protected DurationSerializer(DurationSerializer base, DurationUnitConverter converter) { + super(base, base._formatter, base._useTimestamp, base._useNanoseconds, base._shape); + _durationUnitConverter = converter; + } + + @Override + protected DurationSerializer withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new DurationSerializer(this, dtf, useTimestamp); + } + + protected DurationSerializer withConverter(DurationUnitConverter converter) { + return new DurationSerializer(this, converter); + } + + // @since 2.10 + @Override + protected SerializationFeature getTimestampsFeature() { + return SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS; + } + + @Override + public ValueSerializer createContextual(SerializationContext ctxt, BeanProperty property) + { + DurationSerializer ser = (DurationSerializer) super.createContextual(ctxt, property); + JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType()); + if (format != null && format.hasPattern()) { + final String pattern = format.getPattern(); + DurationUnitConverter p = DurationUnitConverter.from(pattern); + if (p == null) { + ctxt.reportBadDefinition(handledType(), + String.format( + "Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]", + pattern, DurationUnitConverter.descForAllowed())); + } + ser = ser.withConverter(p); + } + return ser; + } + + @Override + public void serialize(Duration duration, JsonGenerator generator, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + // 03-Aug-2022, tatu: As per [modules-java8#224] need to consider + // Pattern first, and only then nano-seconds/millis difference + if (_durationUnitConverter != null) { + generator.writeNumber(_durationUnitConverter.convert(duration)); + } else if (useNanoseconds(ctxt)) { + generator.writeNumber(_toNanos(duration)); + } else { + generator.writeNumber(duration.toMillis()); + } + } else { + generator.writeString(duration.toString()); + } + } + + // 20-Oct-2020, tatu: [modules-java8#165] Need to take care of + // negative values too, and without work-around values + // returned are wonky wrt conversions + private BigDecimal _toNanos(Duration duration) { + BigDecimal bd; + if (duration.isNegative()) { + duration = duration.abs(); + bd = DecimalUtils.toBigDecimal(duration.getSeconds(), + duration.getNano()) + .negate(); + } else { + bd = DecimalUtils.toBigDecimal(duration.getSeconds(), + duration.getNano()); + } + return bd; + } + + @Override + protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint); + if (v2 != null) { + v2.numberType(JsonParser.NumberType.LONG); + SerializationContext ctxt = visitor.getContext(); + if ((ctxt != null) && useNanoseconds(ctxt)) { + // big number, no more specific qualifier to use... + } else { // otherwise good old Unix timestamp, in milliseconds + v2.format(JsonValueFormat.UTC_MILLISEC); + } + } + } + + @Override + protected JsonToken serializationShape(SerializationContext ctxt) { + if (useTimestamp(ctxt)) { + if (useNanoseconds(ctxt)) { + return JsonToken.VALUE_NUMBER_FLOAT; + } + return JsonToken.VALUE_NUMBER_INT; + } + return JsonToken.VALUE_STRING; + } + + @Override + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) { + return new DurationSerializer(this, _formatter, _useTimestamp, writeNanoseconds); + } + + @Override + protected DateTimeFormatter _useDateTimeFormatter(SerializationContext ctxt, JsonFormat.Value format) { + return null; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java new file mode 100644 index 0000000000..5b8b401b08 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Serializer for Java 8 temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s. + * + * @author Nick Williams + */ +public class InstantSerializer extends InstantSerializerBase +{ + public static final InstantSerializer INSTANCE = new InstantSerializer(); + + protected InstantSerializer() { + super(Instant.class, Instant::toEpochMilli, Instant::getEpochSecond, Instant::getNano, + // null -> use 'value.toString()', default format + null); + } + + /* + protected InstantSerializer(InstantSerializer base, + Boolean useTimestamp, DateTimeFormatter formatter) { + this(base, formatter, useTimestamp, base._useNanoseconds, base); + } + */ + + protected InstantSerializer(InstantSerializer base, DateTimeFormatter formatter, + Boolean useTimestamp, Boolean useNanoseconds, + JsonFormat.Shape shape) { + super(base, formatter, useTimestamp, useNanoseconds, shape); + } + + @Override + protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter formatter, + Boolean useTimestamp, + JsonFormat.Shape shape) { + return new InstantSerializer(this, formatter, useTimestamp, this._useNanoseconds , shape); + } + + @Override + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) { + return new InstantSerializer(this, _formatter, _useTimestamp, writeNanoseconds, + this._shape); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializerBase.java new file mode 100644 index 0000000000..57f9e381cc --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializerBase.java @@ -0,0 +1,147 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.JsonParser.NumberType; + +import tools.jackson.databind.*; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; +import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import tools.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import tools.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor; +import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat; + +/** + * Base class for serializers used for {@link java.time.Instant} and + * other {@link Temporal} subtypes. + */ +public abstract class InstantSerializerBase + extends JSR310FormattedSerializerBase +{ + private final DateTimeFormatter defaultFormat; + + private final ToLongFunction getEpochMillis; + + private final ToLongFunction getEpochSeconds; + + private final ToIntFunction getNanoseconds; + + protected InstantSerializerBase(Class supportedType, ToLongFunction getEpochMillis, + ToLongFunction getEpochSeconds, ToIntFunction getNanoseconds, + DateTimeFormatter defaultFormat) + { + // Bit complicated, just because we actually want to "hide" default formatter, + // so that it won't accidentally force use of textual presentation + super(supportedType, null); + this.defaultFormat = defaultFormat; + this.getEpochMillis = getEpochMillis; + this.getEpochSeconds = getEpochSeconds; + this.getNanoseconds = getNanoseconds; + } + + protected InstantSerializerBase(InstantSerializerBase base, + DateTimeFormatter dtf, + Boolean useTimestamp, Boolean useNanoseconds, + JsonFormat.Shape shape) + { + super(base, dtf, useTimestamp, useNanoseconds, shape); + defaultFormat = base.defaultFormat; + getEpochMillis = base.getEpochMillis; + getEpochSeconds = base.getEpochSeconds; + getNanoseconds = base.getNanoseconds; + } + + @Override + protected abstract JSR310FormattedSerializerBase withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, + JsonFormat.Shape shape); + + @Override + public void serialize(T value, JsonGenerator generator, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + if (useNanoseconds(ctxt)) { + generator.writeNumber(DecimalUtils.toBigDecimal( + getEpochSeconds.applyAsLong(value), getNanoseconds.applyAsInt(value) + )); + return; + } + generator.writeNumber(getEpochMillis.applyAsLong(value)); + return; + } + + generator.writeString(formatValue(value, ctxt)); + } + + // Overridden to ensure that our timestamp handling is as expected + @Override + protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + if (useNanoseconds(visitor.getContext())) { + JsonNumberFormatVisitor v2 = visitor.expectNumberFormat(typeHint); + if (v2 != null) { + v2.numberType(NumberType.BIG_DECIMAL); + } + } else { + JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint); + if (v2 != null) { + v2.numberType(NumberType.LONG); + v2.format(JsonValueFormat.UTC_MILLISEC); + } + } + } + + @Override + protected JsonToken serializationShape(SerializationContext ctxt) { + if (useTimestamp(ctxt)) { + if (useNanoseconds(ctxt)) { + return JsonToken.VALUE_NUMBER_FLOAT; + } + return JsonToken.VALUE_NUMBER_INT; + } + return JsonToken.VALUE_STRING; + } + + protected String formatValue(T value, SerializationContext ctxt) + { + DateTimeFormatter formatter = (_formatter == null) ? defaultFormat :_formatter; + if (formatter != null) { + if (formatter.getZone() == null) { // timezone set if annotated on property + // If the user specified to use the context TimeZone explicitly, and the formatter provided doesn't contain a TZ + // Then we use the TZ specified in the objectMapper + if (ctxt.getConfig().hasExplicitTimeZone() + && ctxt.isEnabled(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE)) { + formatter = formatter.withZone(ctxt.getTimeZone().toZoneId()); + } + } + return formatter.format(value); + } + + return value.toString(); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310FormattedSerializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310FormattedSerializerBase.java new file mode 100644 index 0000000000..32c2a6311a --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310FormattedSerializerBase.java @@ -0,0 +1,258 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; + +import tools.jackson.databind.*; +import tools.jackson.databind.jsonFormatVisitors.*; + +/** + * Base class that provides an array schema instead of scalar schema if + * {@link SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} is enabled. + * + * @author Nick Williams + */ +abstract class JSR310FormattedSerializerBase + extends JSR310SerializerBase +{ + /** + * Flag that indicates that serialization must be done as the + * Java timestamp, regardless of other settings. + */ + protected final Boolean _useTimestamp; + + /** + * Flag that indicates that numeric timestamp values must be written using + * nanosecond timestamps if the datatype supports such resolution, + * regardless of other settings. + */ + protected final Boolean _useNanoseconds; + + /** + * Specific format to use, if not default format: non-null value + * also indicates that serialization is to be done as JSON String, + * not numeric timestamp, unless {@code #_useTimestamp} is true. + */ + protected final DateTimeFormatter _formatter; + + protected final JsonFormat.Shape _shape; + + /** + * Lazily constructed {@code JavaType} representing type + * {@code List}. + * + * @since 2.10 + */ + protected transient volatile JavaType _integerListType; + + protected JSR310FormattedSerializerBase(Class supportedType) { + this(supportedType, null); + } + + protected JSR310FormattedSerializerBase(Class supportedType, + DateTimeFormatter formatter) { + super(supportedType); + _useTimestamp = null; + _useNanoseconds = null; + _shape = null; + _formatter = formatter; + } + + /* + + protected JSR310FormattedSerializerBase(JSR310FormattedSerializerBase base, + DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) + { + this(base, dtf, useTimestamp, null, shape); + } + */ + + protected JSR310FormattedSerializerBase(JSR310FormattedSerializerBase base, + DateTimeFormatter dtf, + Boolean useTimestamp, Boolean useNanoseconds, + JsonFormat.Shape shape) + { + super(base.handledType()); + _useTimestamp = useTimestamp; + _useNanoseconds = useNanoseconds; + _formatter = dtf; + _shape = shape; + } + + protected abstract JSR310FormattedSerializerBase withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape); + + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, + Boolean writeNanoseconds) { + return this; + } + + @Override + public ValueSerializer createContextual(SerializationContext ctxt, + BeanProperty property) + { + JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType()); + if (format != null) { + Boolean useTimestamp = null; + + // Simple case first: serialize as numeric timestamp? + JsonFormat.Shape shape = format.getShape(); + if (shape == JsonFormat.Shape.ARRAY || shape.isNumeric() ) { + useTimestamp = Boolean.TRUE; + } else { + useTimestamp = (shape == JsonFormat.Shape.STRING) ? Boolean.FALSE : null; + } + DateTimeFormatter dtf = _formatter; + + // If not, do we have a pattern? + if (format.hasPattern()) { + dtf = _useDateTimeFormatter(ctxt, format); + } + JSR310FormattedSerializerBase ser = this; + if ((shape != _shape) || (useTimestamp != _useTimestamp) || (dtf != _formatter)) { + ser = ser.withFormat(dtf, useTimestamp, shape); + } + Boolean writeZoneId = format.getFeature(JsonFormat.Feature.WRITE_DATES_WITH_ZONE_ID); + Boolean writeNanoseconds = format.getFeature(JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); + if ((writeZoneId != null) || (writeNanoseconds != null)) { + ser = ser.withFeatures(writeZoneId, writeNanoseconds); + } + return ser; + } + return this; + } + + /** + * @deprecated Since 2.15 + */ + @Deprecated + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + if (useTimestamp(visitor.getContext())) { + _acceptTimestampVisitor(visitor, typeHint); + } else { + JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint); + if (v2 != null) { + v2.format(JsonValueFormat.DATE_TIME); + } + } + } + + protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + // By default, most sub-types use JSON Array, so do this: + // 28-May-2019, tatu: serialized as a List, presumably + JsonArrayFormatVisitor v2 = visitor.expectArrayFormat(_integerListType(visitor.getContext())); + if (v2 != null) { + v2.itemsFormat(JsonFormatTypes.INTEGER); + } + } + + protected JavaType _integerListType(SerializationContext ctxt) { + JavaType t = _integerListType; + if (t == null) { + t = ctxt.getTypeFactory() + .constructCollectionType(List.class, Integer.class); + _integerListType = t; + } + return t; + } + + /** + * Overridable method that determines {@link SerializationFeature} that is used as + * the global default in determining if date/time value serialized should use numeric + * format ("timestamp") or not. + *

+ * Note that this feature is just the baseline setting and may be overridden on per-type + * or per-property basis. + * + * @since 2.10 + */ + protected SerializationFeature getTimestampsFeature() { + return SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; + } + + protected boolean useTimestamp(SerializationContext ctxt) { + if (_useTimestamp != null) { + return _useTimestamp.booleanValue(); + } + if (_shape != null) { + if (_shape == Shape.STRING) { + return false; + } + if (_shape == Shape.NUMBER_INT) { + return true; + } + } + // assume that explicit formatter definition implies use of textual format + return (_formatter == null) && useTimestampFromGlobalDefaults(ctxt); + } + + protected boolean useTimestampFromGlobalDefaults(SerializationContext ctxt) { + return (ctxt != null) + && ctxt.isEnabled(getTimestampsFeature()); + } + + protected boolean _useTimestampExplicitOnly(SerializationContext ctxt) { + if (_useTimestamp != null) { + return _useTimestamp.booleanValue(); + } + return false; + } + + protected boolean useNanoseconds(SerializationContext ctxt) { + if (_useNanoseconds != null) { + return _useNanoseconds.booleanValue(); + } + if (_shape != null) { + if (_shape == Shape.NUMBER_INT) { + return false; + } + if (_shape == Shape.NUMBER_FLOAT) { + return true; + } + } + return (ctxt != null) + && ctxt.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); + } + + // modules-java8#189: to be overridden by other formatters using this as base class + protected DateTimeFormatter _useDateTimeFormatter(SerializationContext ctxt, JsonFormat.Value format) { + DateTimeFormatter dtf; + final String pattern = format.getPattern(); + final Locale locale = format.hasLocale() ? format.getLocale() : ctxt.getLocale(); + if (locale == null) { + dtf = DateTimeFormatter.ofPattern(pattern); + } else { + dtf = DateTimeFormatter.ofPattern(pattern, locale); + } + //Issue #69: For instant serializers/deserializers we need to configure the formatter with + //a time zone picked up from JsonFormat annotation, otherwise serialization might not work + if (format.hasTimeZone()) { + dtf = dtf.withZone(format.getTimeZone().toZoneId()); + } + return dtf; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310SerializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310SerializerBase.java new file mode 100644 index 0000000000..0c208768b7 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310SerializerBase.java @@ -0,0 +1,40 @@ +package tools.jackson.databind.ext.javatime.ser; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; + +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.ser.std.StdSerializer; + +/** + * Base class that indicates that all JSR310 datatypes are serialized as scalar JSON types. + * + * @author Nick Williams + */ +abstract class JSR310SerializerBase extends StdSerializer +{ + protected JSR310SerializerBase(Class supportedType) { + super(supportedType); + } + + @Override + public void serializeWithType(T value, JsonGenerator g, SerializationContext ctxt, + TypeSerializer typeSer) + throws JacksonException + { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, serializationShape(ctxt))); + serialize(value, g, ctxt); + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + /** + * Overridable helper method used from {@link #serializeWithType}, to indicate + * shape of value during serialization; needed to know how type id is to be + * serialized. + */ + protected abstract JsonToken serializationShape(SerializationContext ctxt); +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/JavaTimeSerializerModifier.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/JavaTimeSerializerModifier.java new file mode 100644 index 0000000000..33c0b12ffe --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/JavaTimeSerializerModifier.java @@ -0,0 +1,28 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Month; + +import tools.jackson.databind.*; +import tools.jackson.databind.ser.ValueSerializerModifier; + +/** + * @since 2.17 + */ +public class JavaTimeSerializerModifier extends ValueSerializerModifier { + private static final long serialVersionUID = 1L; + + private final boolean _oneBaseMonths; + + public JavaTimeSerializerModifier(boolean oneBaseMonths) { + _oneBaseMonths = oneBaseMonths; + } + + @Override + public ValueSerializer modifyEnumSerializer(SerializationConfig config, JavaType valueType, + BeanDescription beanDesc, ValueSerializer serializer) { + if (_oneBaseMonths && valueType.hasRawClass(Month.class)) { + return new OneBasedMonthSerializer(serializer); + } + return serializer; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerializer.java new file mode 100644 index 0000000000..ba1d4b2879 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerializer.java @@ -0,0 +1,132 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; +import tools.jackson.databind.*; +import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; +import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import tools.jackson.databind.jsontype.TypeSerializer; + +/** + * Serializer for Java 8 temporal {@link LocalDate}s. + * + * @author Nick Williams + */ +public class LocalDateSerializer extends JSR310FormattedSerializerBase +{ + public static final LocalDateSerializer INSTANCE = new LocalDateSerializer(); + + protected LocalDateSerializer() { + super(LocalDate.class); + } + + protected LocalDateSerializer(LocalDateSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + super(base, dtf, useTimestamp, null, shape); + } + + public LocalDateSerializer(DateTimeFormatter formatter) { + super(LocalDate.class, formatter); + } + + @Override + protected LocalDateSerializer withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new LocalDateSerializer(this, dtf, useTimestamp, shape); + } + + @Override + public void serialize(LocalDate date, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + if (_shape == JsonFormat.Shape.NUMBER_INT) { + g.writeNumber(date.toEpochDay()); + } else { + g.writeStartArray(); + _serializeAsArrayContents(date, g, ctxt); + g.writeEndArray(); + } + } else { + g.writeString((_formatter == null) ? date.toString() : date.format(_formatter)); + } + } + + @Override + public void serializeWithType(LocalDate value, JsonGenerator g, + SerializationContext ctxt, TypeSerializer typeSer) + throws JacksonException + { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, serializationShape(ctxt))); + // need to write out to avoid double-writing array markers + JsonToken shape = (typeIdDef == null) ? null : typeIdDef.valueShape; + if (shape == JsonToken.START_ARRAY) { + _serializeAsArrayContents(value, g, ctxt); + } else if (shape == JsonToken.VALUE_NUMBER_INT) { + g.writeNumber(value.toEpochDay()); + } else { + g.writeString((_formatter == null) ? value.toString() : value.format(_formatter)); + } + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + protected void _serializeAsArrayContents(LocalDate value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + g.writeNumber(value.getYear()); + g.writeNumber(value.getMonthValue()); + g.writeNumber(value.getDayOfMonth()); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + SerializationContext ctxt = visitor.getContext(); + boolean useTimestamp = (ctxt != null) && useTimestamp(ctxt); + if (useTimestamp) { + _acceptTimestampVisitor(visitor, typeHint); + } else { + JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint); + if (v2 != null) { + v2.format(JsonValueFormat.DATE); + } + } + } + + @Override // since 2.9 + protected JsonToken serializationShape(SerializationContext ctxt) { + if (useTimestamp(ctxt)) { + if (_shape == JsonFormat.Shape.NUMBER_INT) { + return JsonToken.VALUE_NUMBER_INT; + } + return JsonToken.START_ARRAY; + } + return JsonToken.VALUE_STRING; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java new file mode 100644 index 0000000000..540202a2ec --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java @@ -0,0 +1,133 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeSerializer; + +/** + * Serializer for Java 8 temporal {@link LocalDateTime}s. + * + * @author Nick Williams + */ +public class LocalDateTimeSerializer extends JSR310FormattedSerializerBase +{ + public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); + + protected LocalDateTimeSerializer() { + this(null); + } + + public LocalDateTimeSerializer(DateTimeFormatter f) { + super(LocalDateTime.class, f); + } + + protected LocalDateTimeSerializer(LocalDateTimeSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp, Boolean useNanoseconds) { + super(base, dtf, useTimestamp, useNanoseconds, null); + } + + @Override + protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter f, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new LocalDateTimeSerializer(this, f, useTimestamp, _useNanoseconds); + } + + protected DateTimeFormatter _defaultFormatter() { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME; + } + + @Override + public void serialize(LocalDateTime value, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + g.writeStartArray(); + _serializeAsArrayContents(value, g, ctxt); + g.writeEndArray(); + } else { + DateTimeFormatter dtf = _formatter; + if (dtf == null) { + dtf = _defaultFormatter(); + } + g.writeString(value.format(dtf)); + } + } + + @Override + public void serializeWithType(LocalDateTime value, JsonGenerator g, SerializationContext ctxt, + TypeSerializer typeSer) + throws JacksonException + { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, serializationShape(ctxt))); + // need to write out to avoid double-writing array markers + if ((typeIdDef != null) + && typeIdDef.valueShape == JsonToken.START_ARRAY) { + _serializeAsArrayContents(value, g, ctxt); + } else { + DateTimeFormatter dtf = _formatter; + if (dtf == null) { + dtf = _defaultFormatter(); + } + g.writeString(value.format(dtf)); + } + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + private final void _serializeAsArrayContents(LocalDateTime value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + g.writeNumber(value.getYear()); + g.writeNumber(value.getMonthValue()); + g.writeNumber(value.getDayOfMonth()); + g.writeNumber(value.getHour()); + g.writeNumber(value.getMinute()); + final int secs = value.getSecond(); + final int nanos = value.getNano(); + if ((secs > 0) || (nanos > 0)) { + g.writeNumber(secs); + if (nanos > 0) { + if (useNanoseconds(ctxt)) { + g.writeNumber(nanos); + } else { + g.writeNumber(value.get(ChronoField.MILLI_OF_SECOND)); + } + } + } + } + + @Override + protected JsonToken serializationShape(SerializationContext ctxt) { + return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING; + } + + @Override + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) { + return new LocalDateTimeSerializer(this, _formatter, _useTimestamp, writeNanoseconds); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerializer.java new file mode 100644 index 0000000000..8ae9a92428 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerializer.java @@ -0,0 +1,153 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; + +import tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; +import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import tools.jackson.databind.jsontype.TypeSerializer; +/** + * Serializer for Java 8 temporal {@link LocalTime}s. + * + * @author Nick Williams + * @since 2.2 + */ +public class LocalTimeSerializer extends JSR310FormattedSerializerBase +{ + public static final LocalTimeSerializer INSTANCE = new LocalTimeSerializer(); + + protected LocalTimeSerializer() { + this(null); + } + + public LocalTimeSerializer(DateTimeFormatter formatter) { + super(LocalTime.class, formatter); + } + + protected LocalTimeSerializer(LocalTimeSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp, Boolean useNanoseconds) { + super(base, dtf, useTimestamp, useNanoseconds, null); + } + + @Override + protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new LocalTimeSerializer(this, dtf, useTimestamp, _useNanoseconds); + } + + // since 2.7: TODO in 3.x; change to use per-type defaulting + protected DateTimeFormatter _defaultFormatter() { + return DateTimeFormatter.ISO_LOCAL_TIME; + } + + @Override + public void serialize(LocalTime value, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + g.writeStartArray(); + _serializeAsArrayContents(value, g, ctxt); + g.writeEndArray(); + } else { + DateTimeFormatter dtf = _formatter; + if (dtf == null) { + dtf = _defaultFormatter(); + } + g.writeString(value.format(dtf)); + } + } + + @Override + public void serializeWithType(LocalTime value, JsonGenerator g, + SerializationContext ctxt, TypeSerializer typeSer) + throws JacksonException + { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, serializationShape(ctxt))); + // need to write out to avoid double-writing array markers + if ((typeIdDef != null) + && typeIdDef.valueShape == JsonToken.START_ARRAY) { + _serializeAsArrayContents(value, g, ctxt); + } else { + DateTimeFormatter dtf = _formatter; + if (dtf == null) { + dtf = _defaultFormatter(); + } + g.writeString(value.format(dtf)); + } + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + private final void _serializeAsArrayContents(LocalTime value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + g.writeNumber(value.getHour()); + g.writeNumber(value.getMinute()); + int secs = value.getSecond(); + int nanos = value.getNano(); + if ((secs > 0) || (nanos > 0)) + { + g.writeNumber(secs); + if (nanos > 0) { + if (useNanoseconds(ctxt)) { + g.writeNumber(nanos); + } else { + g.writeNumber(value.get(ChronoField.MILLI_OF_SECOND)); + } + } + } + } + + @Override + protected JsonToken serializationShape(SerializationContext ctxt) { + return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING; + } + + @Override + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, Boolean useNanoseconds) { + return new LocalTimeSerializer(this, _formatter, + _useTimestamp, useNanoseconds); + } + + // as per [modules-java8#105] + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + if (useTimestamp(visitor.getContext())) { + _acceptTimestampVisitor(visitor, typeHint); + } else { + JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint); + if (v2 != null) { + v2.format(JsonValueFormat.TIME); + } + } + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerializer.java new file mode 100644 index 0000000000..e2124940ff --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerializer.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.MonthDay; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; + +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeSerializer; + +/** + * Serializer for Java 8 temporal {@link MonthDay}s. + *

+ * NOTE: unlike many other date/time type serializers, this serializer will only + * use Array notation if explicitly instructed to do so with JsonFormat + * (either directly or through per-type defaults) and NOT with global defaults. + * + * @since 2.7.1 + */ +public class MonthDaySerializer extends JSR310FormattedSerializerBase +{ + public static final MonthDaySerializer INSTANCE = new MonthDaySerializer(); + + protected MonthDaySerializer() { // was private before 2.12 + this(null); + } + + public MonthDaySerializer(DateTimeFormatter formatter) { + super(MonthDay.class, formatter); + } + + private MonthDaySerializer(MonthDaySerializer base, DateTimeFormatter dtf, Boolean useTimestamp) { + super(base, dtf, useTimestamp, null, null); + } + + @Override + protected MonthDaySerializer withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new MonthDaySerializer(this, dtf, useTimestamp); + } + + @Override + public void serialize(MonthDay value, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + if (_useTimestampExplicitOnly(ctxt)) { + g.writeStartArray(); + _serializeAsArrayContents(value, g, ctxt); + g.writeEndArray(); + } else { + g.writeString((_formatter == null) ? value.toString() : value.format(_formatter)); + } + } + + @Override + public void serializeWithType(MonthDay value, JsonGenerator g, + SerializationContext ctxt, TypeSerializer typeSer) + throws JacksonException + { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, serializationShape(ctxt))); + // need to write out to avoid double-writing array markers + if ((typeIdDef != null) + && typeIdDef.valueShape == JsonToken.START_ARRAY) { + _serializeAsArrayContents(value, g, ctxt); + } else { + g.writeString((_formatter == null) ? value.toString() : value.format(_formatter)); + } + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + protected void _serializeAsArrayContents(MonthDay value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + g.writeNumber(value.getMonthValue()); + g.writeNumber(value.getDayOfMonth()); + } + + @Override + protected JsonToken serializationShape(SerializationContext ctxt) { + return _useTimestampExplicitOnly(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java new file mode 100644 index 0000000000..968e8f9ec7 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java @@ -0,0 +1,43 @@ +package tools.jackson.databind.ext.javatime.ser; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +public class OffsetDateTimeSerializer extends InstantSerializerBase +{ + public static final OffsetDateTimeSerializer INSTANCE = new OffsetDateTimeSerializer(); + + protected OffsetDateTimeSerializer() { + super(OffsetDateTime.class, dt -> dt.toInstant().toEpochMilli(), + OffsetDateTime::toEpochSecond, OffsetDateTime::getNano, + DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + protected OffsetDateTimeSerializer(OffsetDateTimeSerializer base, + Boolean useTimestamp, DateTimeFormatter formatter, + JsonFormat.Shape shape) { + this(base, formatter, useTimestamp, base._useNanoseconds, shape); + } + + protected OffsetDateTimeSerializer(OffsetDateTimeSerializer base, + DateTimeFormatter formatter, + Boolean useTimestamp, Boolean useNanoseconds, + JsonFormat.Shape shape) { + super(base, formatter, useTimestamp, useNanoseconds, shape); + } + + @Override + protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter formatter, + Boolean useTimestamp, + JsonFormat.Shape shape) + { + return new OffsetDateTimeSerializer(this, useTimestamp, formatter, shape); + } + + @Override + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) { + return new OffsetDateTimeSerializer(this, _formatter, + _useTimestamp, writeNanoseconds, _shape); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerializer.java new file mode 100644 index 0000000000..46dadbbdce --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerializer.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.OffsetTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeSerializer; + +/** + * Serializer for Java 8 temporal {@link OffsetTime}s. + * + * @author Nick Williams + */ +public class OffsetTimeSerializer extends JSR310FormattedSerializerBase +{ + public static final OffsetTimeSerializer INSTANCE = new OffsetTimeSerializer(); + + protected OffsetTimeSerializer() { + super(OffsetTime.class); + } + + protected OffsetTimeSerializer(OffsetTimeSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp) { + this(base, dtf, useTimestamp, null); + } + + protected OffsetTimeSerializer(OffsetTimeSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp, Boolean useNanoseconds) { + super(base, dtf, useTimestamp, useNanoseconds, null); + } + + @Override + protected OffsetTimeSerializer withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new OffsetTimeSerializer(this, dtf, useTimestamp); + } + + @Override + public void serialize(OffsetTime time, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + g.writeStartArray(); + _serializeAsArrayContents(time, g, ctxt); + g.writeEndArray(); + } else { + String str = (_formatter == null) ? time.toString() : time.format(_formatter); + g.writeString(str); + } + } + + @Override + public void serializeWithType(OffsetTime value, JsonGenerator g, SerializationContext ctxt, + TypeSerializer typeSer) + throws JacksonException + { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, serializationShape(ctxt))); + // need to write out to avoid double-writing array markers + if ((typeIdDef != null) + && typeIdDef.valueShape == JsonToken.START_ARRAY) { + _serializeAsArrayContents(value, g, ctxt); + } else { + String str = (_formatter == null) ? value.toString() : value.format(_formatter); + g.writeString(str); + } + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + private final void _serializeAsArrayContents(OffsetTime value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + g.writeNumber(value.getHour()); + g.writeNumber(value.getMinute()); + final int secs = value.getSecond(); + final int nanos = value.getNano(); + if ((secs > 0) || (nanos > 0)) { + g.writeNumber(secs); + if (nanos > 0) { + if(useNanoseconds(ctxt)) { + g.writeNumber(nanos); + } else { + g.writeNumber(value.get(ChronoField.MILLI_OF_SECOND)); + } + } + } + g.writeString(value.getOffset().toString()); + } + + @Override + protected JsonToken serializationShape(SerializationContext ctxt) { + return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING; + } + + @Override + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) { + return new OffsetTimeSerializer(this, _formatter, _useTimestamp, writeNanoseconds); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerializer.java new file mode 100644 index 0000000000..f2680cf18e --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerializer.java @@ -0,0 +1,33 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Month; + +import tools.jackson.core.JsonGenerator; + +import tools.jackson.databind.*; + +/** + * @since 2.17 + */ +public class OneBasedMonthSerializer extends ValueSerializer { + private final ValueSerializer _defaultSerializer; + + @SuppressWarnings("unchecked") + public OneBasedMonthSerializer(ValueSerializer defaultSerializer) + { + _defaultSerializer = (ValueSerializer) defaultSerializer; + } + + @Override + public void serialize(Month value, JsonGenerator gen, SerializationContext ctxt) + { + // 15-Jan-2024, tatu: [modules-java8#274] This is not really sufficient + // (see `jackson-databind` `EnumSerializer` for full logic), but has to + // do for now. May need to add `@JsonFormat.shape` handling in future. + if (ctxt.isEnabled(SerializationFeature.WRITE_ENUMS_USING_INDEX)) { + gen.writeNumber(value.ordinal() + 1); + return; + } + _defaultSerializer.serialize(value, gen, ctxt); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializer.java new file mode 100644 index 0000000000..32659a1275 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializer.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; + +import tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; +import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import tools.jackson.databind.jsontype.TypeSerializer; + +/** + * Serializer for Java 8 temporal {@link YearMonth}s. + * + * @author Nick Williams + * @since 2.2 + */ +public class YearMonthSerializer extends JSR310FormattedSerializerBase +{ + public static final YearMonthSerializer INSTANCE = new YearMonthSerializer(); + + protected YearMonthSerializer() { // was private before 2.12 + this(null); + } + + public YearMonthSerializer(DateTimeFormatter formatter) { + super(YearMonth.class, formatter); + } + + private YearMonthSerializer(YearMonthSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp) { + super(base, dtf, useTimestamp, null, null); + } + + @Override + protected YearMonthSerializer withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new YearMonthSerializer(this, dtf, useTimestamp); + } + + @Override + public void serialize(YearMonth value, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + g.writeStartArray(); + _serializeAsArrayContents(value, g, ctxt); + g.writeEndArray(); + return; + } + g.writeString((_formatter == null) ? value.toString() : value.format(_formatter)); + } + + @Override + public void serializeWithType(YearMonth value, JsonGenerator g, + SerializationContext ctxt, TypeSerializer typeSer) + throws JacksonException + { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, serializationShape(ctxt))); + // need to write out to avoid double-writing array markers + if ((typeIdDef != null) + && typeIdDef.valueShape == JsonToken.START_ARRAY) { + _serializeAsArrayContents(value, g, ctxt); + } else { + g.writeString((_formatter == null) ? value.toString() : value.format(_formatter)); + } + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + protected void _serializeAsArrayContents(YearMonth value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + g.writeNumber(value.getYear()); + g.writeNumber(value.getMonthValue()); + } + + @Override + protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + SerializationContext ctxt = visitor.getContext(); + boolean useTimestamp = (ctxt != null) && useTimestamp(ctxt); + if (useTimestamp) { + super._acceptTimestampVisitor(visitor, typeHint); + } else { + JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint); + if (v2 != null) { + v2.format(JsonValueFormat.DATE_TIME); + } + } + } + + @Override // since 2.9 + protected JsonToken serializationShape(SerializationContext ctxt) { + return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/YearSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearSerializer.java new file mode 100644 index 0000000000..302591df3a --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearSerializer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Year; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; + +import tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import tools.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; + +/** + * Serializer for Java 8 temporal {@link Year}s. + * + * @author Nick Williams + */ +public class YearSerializer extends JSR310FormattedSerializerBase +{ + public static final YearSerializer INSTANCE = new YearSerializer(); + + protected YearSerializer() { + this(null); + } + + public YearSerializer(DateTimeFormatter formatter) { + super(Year.class, formatter); + } + + protected YearSerializer(YearSerializer base, DateTimeFormatter dtf, + Boolean useTimestamp) { + super(base, dtf, useTimestamp, null, null); + } + + @Override + protected YearSerializer withFormat(DateTimeFormatter dtf, + Boolean useTimestamp, JsonFormat.Shape shape) { + return new YearSerializer(this, dtf, useTimestamp); + } + + // Need to ensure Year still defaults to numeric ("timestamp") regardless + // of general global setting (since that defaults to textual in Jackson 3.x) + @Override + protected boolean useTimestampFromGlobalDefaults(SerializationContext ctxt) { + return true; + } + + @Override + public void serialize(Year year, JsonGenerator generator, SerializationContext ctxt) + throws JacksonException + { + if (useTimestamp(ctxt)) { + generator.writeNumber(year.getValue()); + } else { + String str = (_formatter == null) ? year.toString() : year.format(_formatter); + generator.writeString(str); + } + } + + // Override because we have String/Int, NOT String/Array + @Override + protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + { + JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint); + if (v2 != null) { + v2.numberType(JsonParser.NumberType.LONG); + } + } + + @Override // since 2.9 + protected JsonToken serializationShape(SerializationContext ctxt) { + return useTimestamp(ctxt) ? JsonToken.VALUE_NUMBER_INT : JsonToken.VALUE_STRING; + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerializer.java new file mode 100644 index 0000000000..af75de87ca --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerializer.java @@ -0,0 +1,33 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.ZoneId; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.ser.std.ToStringSerializerBase; + +public class ZoneIdSerializer extends ToStringSerializerBase +{ + public ZoneIdSerializer() { super(ZoneId.class); } + + @Override + public void serializeWithType(Object value, JsonGenerator g, + SerializationContext ctxt, TypeSerializer typeSer) + throws JacksonException + { + // Better ensure we don't use specific sub-classes: + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt, + typeSer.typeId(value, ZoneId.class, JsonToken.VALUE_STRING)); + serialize(value, g, ctxt); + typeSer.writeTypeSuffix(g, ctxt, typeIdDef); + } + + @Override + public String valueToString(Object value) { + return value.toString(); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java new file mode 100644 index 0000000000..8240cac337 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java @@ -0,0 +1,107 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.SerializationFeature; + +public class ZonedDateTimeSerializer extends InstantSerializerBase { + public static final ZonedDateTimeSerializer INSTANCE = new ZonedDateTimeSerializer(); + + /** + * Flag for JsonFormat.Feature.WRITE_DATES_WITH_ZONE_ID + */ + protected final Boolean _writeZoneId; + + protected ZonedDateTimeSerializer() { + // ISO_ZONED_DATE_TIME is an extended version of ISO compliant format + // ISO_OFFSET_DATE_TIME with additional information :Zone Id + // (This is not part of the ISO-8601 standard) + this(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + public ZonedDateTimeSerializer(DateTimeFormatter formatter) { + super(ZonedDateTime.class, dt -> dt.toInstant().toEpochMilli(), + ZonedDateTime::toEpochSecond, ZonedDateTime::getNano, + formatter); + _writeZoneId = null; + } + + protected ZonedDateTimeSerializer(ZonedDateTimeSerializer base, + DateTimeFormatter formatter, + Boolean useTimestamp, Boolean useNanoseconds, + Boolean writeZoneId, + JsonFormat.Shape shape) + { + super(base, formatter, useTimestamp, useNanoseconds, shape); + _writeZoneId = writeZoneId; + } + + @Override + protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter formatter, + Boolean useTimestamp, + JsonFormat.Shape shape) + { + return new ZonedDateTimeSerializer(this, formatter, + useTimestamp, _useNanoseconds, _writeZoneId, + shape); + } + + @Override + protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, + Boolean useNanoseconds) + { + return new ZonedDateTimeSerializer(this, _formatter, + _useTimestamp, useNanoseconds, writeZoneId, _shape); + } + + @Override + public void serialize(ZonedDateTime value, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + if (!useTimestamp(ctxt)) { + // [modules-java8#333]: `@JsonFormat` with pattern should override + // `SerializationFeature.WRITE_DATES_WITH_ZONE_ID` + if ((_formatter != null) && (_shape == JsonFormat.Shape.STRING)) { + ; // use default handling + } else if (shouldWriteWithZoneId(ctxt)) { + // write with zone + g.writeString(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(value)); + return; + } + } + super.serialize(value, g, ctxt); + } + + @Override + protected String formatValue(ZonedDateTime value, SerializationContext ctxt) { + String formatted = super.formatValue(value, ctxt); + // [modules-java8#333]: `@JsonFormat` with pattern should override + // `SerializationFeature.WRITE_DATES_WITH_ZONE_ID` + if (_formatter != null && _shape == JsonFormat.Shape.STRING) { + // Why not `if (shouldWriteWithZoneId(provider))` ? + if (Boolean.TRUE.equals(_writeZoneId)) { + formatted += "[" + value.getZone().getId() + "]"; + } + } + return formatted; + } + public boolean shouldWriteWithZoneId(SerializationContext ctxt) { + return (_writeZoneId != null) ? _writeZoneId : + ctxt.isEnabled(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); + } + + @Override + protected JsonToken serializationShape(SerializationContext ctxt) { + if (!useTimestamp(ctxt) && shouldWriteWithZoneId(ctxt)) { + return JsonToken.VALUE_STRING; + } + return super.serializationShape(ctxt); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/key/ZonedDateTimeKeySerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/key/ZonedDateTimeKeySerializer.java new file mode 100644 index 0000000000..3c0ef465a3 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/key/ZonedDateTimeKeySerializer.java @@ -0,0 +1,49 @@ +package tools.jackson.databind.ext.javatime.ser.key; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; + +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.SerializationFeature; + +public class ZonedDateTimeKeySerializer extends ValueSerializer { + + public static final ZonedDateTimeKeySerializer INSTANCE = new ZonedDateTimeKeySerializer(); + + private ZonedDateTimeKeySerializer() { + // singleton + } + + @Override + public void serialize(ZonedDateTime value, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + /* [modules-java8#127]: Serialization of timezone data is disabled by default, but can be + * turned on by enabling `SerializationFeature.WRITE_DATES_WITH_ZONE_ID` + */ + if (ctxt.isEnabled(SerializationFeature.WRITE_DATES_WITH_ZONE_ID)) { + g.writeName(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(value)); + } else if (useTimestamps(ctxt)) { + if (useNanos(ctxt)) { + g.writeName(DecimalUtils.toBigDecimal(value.toEpochSecond(), value.getNano()).toString()); + } else { + g.writeName(String.valueOf(value.toInstant().toEpochMilli())); + } + } else { + g.writeName(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value)); + } + } + + private static boolean useNanos(SerializationContext ctxt) { + return ctxt.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); + } + + private static boolean useTimestamps(SerializationContext ctxt) { + return ctxt.isEnabled(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/util/DecimalUtils.java b/src/main/java/tools/jackson/databind/ext/javatime/util/DecimalUtils.java new file mode 100644 index 0000000000..67634e9476 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/util/DecimalUtils.java @@ -0,0 +1,136 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.util; + +import tools.jackson.core.io.NumberInput; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.function.BiFunction; + +/** + * Utilities to aid in the translation of decimal types to/from multiple parts. + * + * @author Nick Williams + */ +public final class DecimalUtils +{ + private DecimalUtils() { } + + public static String toDecimal(long seconds, int nanoseconds) + { + StringBuilder sb = new StringBuilder(20) + .append(seconds) + .append('.'); + // 14-Mar-2016, tatu: Although we do not yet (with 2.7) trim trailing zeroes, + // for general case, + if (nanoseconds == 0L) { + // !!! TODO: 14-Mar-2016, tatu: as per [datatype-jsr310], should trim + // trailing zeroes + if (seconds == 0L) { + return "0.0"; + } + +// sb.append('0'); + sb.append("000000000"); + } else { + StringBuilder nanoSB = new StringBuilder(9); + nanoSB.append(nanoseconds); + // May need to both prepend leading nanos (if value less than 0.1) + final int nanosLen = nanoSB.length(); + int prepZeroes = 9 - nanosLen; + while (prepZeroes > 0) { + --prepZeroes; + sb.append('0'); + } + + // !!! TODO: 14-Mar-2016, tatu: as per [datatype-jsr310], should trim + // trailing zeroes + /* + // AND possibly trim trailing ones + int i = nanosLen; + while ((i > 1) && nanoSB.charAt(i-1) == '0') { + --i; + } + if (i < nanosLen) { + nanoSB.setLength(i); + } + */ + sb.append(nanoSB); + } + return sb.toString(); + } + + /** + * Factory method for constructing {@link BigDecimal} out of second, nano-second + * components. + */ + public static BigDecimal toBigDecimal(long seconds, int nanoseconds) + { + if (nanoseconds == 0L) { + // 14-Mar-2015, tatu: Let's retain one zero to avoid interpretation + // as integral number + if (seconds == 0L) { // except for "0.0" where it can not be done without scientific notation + return BigDecimal.ZERO.setScale(1); + } + return BigDecimal.valueOf(seconds).setScale(9); + } + return NumberInput.parseBigDecimal(toDecimal(seconds, nanoseconds), false); + } + + /** + * Extracts the seconds and nanoseconds component of {@code seconds} as {@code long} and {@code int} + * values, passing them to the given converter. The implementation avoids latency issues present + * on some JRE releases. + * + * @since 2.19 + */ + public static T extractSecondsAndNanos(BigDecimal seconds, + BiFunction convert, boolean negativeAdjustment) { + // Complexity is here to workaround unbounded latency in some BigDecimal operations. + // https://github.com/FasterXML/jackson-databind/issues/2141 + long secondsOnly; + int nanosOnly; + + BigDecimal nanoseconds = seconds.scaleByPowerOfTen(9); + if (nanoseconds.precision() - nanoseconds.scale() <= 0) { + // There are no non-zero digits to the left of the decimal point. + // This protects against very negative exponents. + secondsOnly = nanosOnly = 0; + } + else if (seconds.scale() < -63) { + // There would be no low-order bits once we chop to a long. + // This protects against very positive exponents. + secondsOnly = nanosOnly = 0; + } + else { + // Now we know that seconds has reasonable scale, we can safely chop it apart. + secondsOnly = seconds.longValue(); + nanosOnly = nanoseconds.subtract(BigDecimal.valueOf(secondsOnly).scaleByPowerOfTen(9)).intValue(); + + if (secondsOnly < 0 && secondsOnly > Instant.MIN.getEpochSecond()) { + // [modules-java8#337] since 2.19, not always we need to adjust nanos + if (negativeAdjustment) { + // Issue #69 and Issue #120: avoid sending a negative adjustment to the Instant constructor, we want this as the actual nanos + nanosOnly = Math.abs(nanosOnly); + } + } + } + + return convert.apply(secondsOnly, nanosOnly); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverter.java b/src/main/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverter.java new file mode 100644 index 0000000000..6c85a47b90 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverter.java @@ -0,0 +1,82 @@ +package tools.jackson.databind.ext.javatime.util; + +import static tools.jackson.databind.ext.javatime.util.DurationUnitConverter.DurationSerialization.deserializer; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Handles the conversion of the duration based on the API of {@link Duration} for a restricted set of {@link ChronoUnit}. + * Only the units considered as accurate are supported in this converter since are the only ones capable of handling + * deserialization in a precise manner (see {@link ChronoUnit#isDurationEstimated}). + * + * @since 2.12 + */ +public class DurationUnitConverter { + + protected static class DurationSerialization { + final Function serializer; + final Function deserializer; + + DurationSerialization( + Function serializer, + Function deserializer) { + this.serializer = serializer; + this.deserializer = deserializer; + } + + static Function deserializer(TemporalUnit unit) { + return v -> Duration.of(v, unit); + } + } + + private final static Map UNITS; + + static { + Map units = new LinkedHashMap<>(); + units.put(ChronoUnit.NANOS.name(), new DurationSerialization(Duration::toNanos, deserializer(ChronoUnit.NANOS))); + units.put(ChronoUnit.MICROS.name(), new DurationSerialization(d -> d.toNanos() / 1000, deserializer(ChronoUnit.MICROS))); + units.put(ChronoUnit.MILLIS.name(), new DurationSerialization(Duration::toMillis, deserializer(ChronoUnit.MILLIS))); + units.put(ChronoUnit.SECONDS.name(), new DurationSerialization(Duration::getSeconds, deserializer(ChronoUnit.SECONDS))); + units.put(ChronoUnit.MINUTES.name(), new DurationSerialization(Duration::toMinutes, deserializer(ChronoUnit.MINUTES))); + units.put(ChronoUnit.HOURS.name(), new DurationSerialization(Duration::toHours, deserializer(ChronoUnit.HOURS))); + units.put(ChronoUnit.HALF_DAYS.name(), new DurationSerialization(d -> d.toHours() / 12, deserializer(ChronoUnit.HALF_DAYS))); + units.put(ChronoUnit.DAYS.name(), new DurationSerialization(Duration::toDays, deserializer(ChronoUnit.DAYS))); + UNITS = units; + } + + + final DurationSerialization serialization; + + DurationUnitConverter(DurationSerialization serialization) { + this.serialization = serialization; + } + + public Duration convert(long value) { + return serialization.deserializer.apply(value); + } + + public long convert(Duration duration) { + return serialization.serializer.apply(duration); + } + + /** + * @return Description of all allowed valued as a sequence of + * double-quoted values separated by comma + */ + public static String descForAllowed() { + return "\"" + UNITS.keySet().stream() + .collect(Collectors.joining("\", \"")) + + "\""; + } + + public static DurationUnitConverter from(String unit) { + DurationSerialization def = UNITS.get(unit); + return (def == null) ? null : new DurationUnitConverter(def); + } +} diff --git a/src/main/java/tools/jackson/databind/ext/package-info.java b/src/main/java/tools/jackson/databind/ext/package-info.java index aa8e25e823..bcafe9cbc5 100644 --- a/src/main/java/tools/jackson/databind/ext/package-info.java +++ b/src/main/java/tools/jackson/databind/ext/package-info.java @@ -3,21 +3,21 @@ may or may not be present in runtime environment, but that are commonly enough used so that explicit support can be added.

-Currently supported extensions include: +Currently included extensions are:

    -
  • Support for Java 1.5 core XML datatypes: the reason these are -considered "external" is that some platforms that claim to be 1.5 conformant +
  • Java core XML datatypes: the reason these are +considered "external" is that some platforms that claim to be conformant are only partially so (Google Android, GAE) and do not included these - types. + types; and with Java 9 and above also due to JPMS reasons.
  • -
  • Joda time. This package has superior date/time handling functionality, -and is thus supported. However, to minimize forced dependencies this -support is added as extension so that Joda is not needed by Jackson -itself: but if it is present, its core types are supported to some -degree +
  • Selected {@code java.sql} types. +
  • +
  • Selected {@code java.beans} annotations: {@code @Transient}, {@code ConstructorProperties}. +
  • +
  • Java (8) Time (JSR-310) type support: as of Jackson 3.0 included in databind + but added similar to {@code JacksonModule}s for improved configurability.
- */ package tools.jackson.databind.ext; diff --git a/src/main/java/tools/jackson/databind/json/JsonMapper.java b/src/main/java/tools/jackson/databind/json/JsonMapper.java index 7bdb122bf1..5583e87722 100644 --- a/src/main/java/tools/jackson/databind/json/JsonMapper.java +++ b/src/main/java/tools/jackson/databind/json/JsonMapper.java @@ -4,6 +4,7 @@ import tools.jackson.core.json.JsonFactory; import tools.jackson.core.json.JsonReadFeature; import tools.jackson.core.json.JsonWriteFeature; + import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.cfg.MapperBuilderState; diff --git a/src/main/java/tools/jackson/databind/util/BeanUtil.java b/src/main/java/tools/jackson/databind/util/BeanUtil.java index 523287ed3c..54118cff38 100644 --- a/src/main/java/tools/jackson/databind/util/BeanUtil.java +++ b/src/main/java/tools/jackson/databind/util/BeanUtil.java @@ -117,19 +117,7 @@ public static String checkUnsupportedType(MapperConfig config, JavaType type) final String className = type.getRawClass().getName(); String typeName, moduleName; - if (isJava8TimeClass(className)) { - // [modules-java8#207]: do NOT check/block helper types in sub-packages, - // but only main-level types (to avoid issues with module) - if (className.indexOf('.', 10) >= 0) { - return null; - } - // [databind#4718]: Also don't worry about Exception type(s) - if (type.isTypeOrSubTypeOf(Throwable.class)) { - return null; - } - typeName = "Java 8 date/time"; - moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"; - } else if (isJodaTimeClass(className)) { + if (isJodaTimeClass(className)) { typeName = "Joda date/time"; moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-joda"; } else { @@ -138,15 +126,7 @@ public static String checkUnsupportedType(MapperConfig config, JavaType type) return String.format("%s type %s not supported by default: add Module \"%s\" to enable handling", typeName, ClassUtil.getTypeDescription(type), moduleName); } - - public static boolean isJava8TimeClass(Class rawType) { - return isJava8TimeClass(rawType.getName()); - } - - private static boolean isJava8TimeClass(String className) { - return className.startsWith("java.time."); - } - + public static boolean isJodaTimeClass(Class rawType) { return isJodaTimeClass(rawType.getName()); } diff --git a/src/test/java/module-info.java b/src/test/java/module-info.java index 1126144cbb..03045f6b14 100644 --- a/src/test/java/module-info.java +++ b/src/test/java/module-info.java @@ -35,6 +35,14 @@ opens tools.jackson.databind.deser.jdk; opens tools.jackson.databind.deser.std; opens tools.jackson.databind.exc; + opens tools.jackson.databind.ext.javatime; + opens tools.jackson.databind.ext.javatime.deser; + opens tools.jackson.databind.ext.javatime.deser.key; + opens tools.jackson.databind.ext.javatime.key; + opens tools.jackson.databind.ext.javatime.misc; + opens tools.jackson.databind.ext.javatime.ser; + opens tools.jackson.databind.ext.javatime.tofix; + opens tools.jackson.databind.ext.javatime.util; opens tools.jackson.databind.introspect; opens tools.jackson.databind.json; opens tools.jackson.databind.jsonFormatVisitors; diff --git a/src/test/java/tools/jackson/databind/ext/javatime/DateTimeTestBase.java b/src/test/java/tools/jackson/databind/ext/javatime/DateTimeTestBase.java new file mode 100644 index 0000000000..7611d526d2 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/DateTimeTestBase.java @@ -0,0 +1,76 @@ +package tools.jackson.databind.ext.javatime; + +import java.time.ZoneId; +import java.util.*; + +import tools.jackson.core.json.JsonWriteFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.json.JsonMapper; + +public class DateTimeTestBase +{ + protected static final ZoneId UTC = ZoneId.of("UTC"); + + protected static final ZoneId Z_CHICAGO = ZoneId.of("America/Chicago"); + protected static final ZoneId Z_BUDAPEST = ZoneId.of("Europe/Budapest"); + + // 14-Mar-2016, tatu: Serialization of trailing zeroes may change [datatype-jsr310#67] + // Note, tho, that "0.0" itself is special case; need to avoid scientific notation: + final protected static String NO_NANOSECS_SER = "0.0"; + final protected static String NO_NANOSECS_SUFFIX = ".000000000"; + + protected static ObjectMapper newMapper() { + return newMapperBuilder().build(); + } + + protected static MapperBuilder newMapperBuilder() { + return JsonMapper.builder() + .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES); + } + + protected static MapperBuilder newMapperBuilder(TimeZone tz) { + return JsonMapper.builder() + .defaultTimeZone(tz) + .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES); + } + + protected static ObjectMapper newMapper(TimeZone tz) { + return newMapperBuilder(tz).build(); + } + + protected static JsonMapper.Builder mapperBuilder() { + return JsonMapper.builder() + .defaultLocale(Locale.ENGLISH) + .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES); + } + + protected String q(String value) { + return "\"" + value + "\""; + } + + protected String a2q(String json) { + return json.replace("'", "\""); + } + + protected void verifyException(Throwable e, String... matches) + { + String msg = e.getMessage(); + String lmsg = (msg == null) ? "" : msg.toLowerCase(); + for (String match : matches) { + String lmatch = match.toLowerCase(); + if (lmsg.indexOf(lmatch) >= 0) { + return; + } + } + throw new Error("Expected an exception with one of substrings ("+Arrays.asList(matches)+"): got one with message \""+msg+"\""); + } + + protected static Map asMap(T key, String value) { + return Collections.singletonMap(key, value); + } + + protected static String mapAsString(String key, String value) { + return String.format("{\"%s\":\"%s\"}", key, value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/MockObjectConfiguration.java b/src/test/java/tools/jackson/databind/ext/javatime/MockObjectConfiguration.java new file mode 100644 index 0000000000..6484240ca3 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/MockObjectConfiguration.java @@ -0,0 +1,8 @@ +package tools.jackson.databind.ext.javatime; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY, property = "@class") +public interface MockObjectConfiguration +{ +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/TestDecimalUtils.java b/src/test/java/tools/jackson/databind/ext/javatime/TestDecimalUtils.java new file mode 100644 index 0000000000..d369823a4a --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/TestDecimalUtils.java @@ -0,0 +1,153 @@ +package tools.jackson.databind.ext.javatime; + +import java.math.BigDecimal; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import tools.jackson.databind.ext.javatime.util.DecimalUtils; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestDecimalUtils extends DateTimeTestBase +{ + @Test + public void testToDecimal01() + { + String decimal = DecimalUtils.toDecimal(0, 0); + assertEquals(NO_NANOSECS_SER, decimal, "The returned decimal is not correct."); + + decimal = DecimalUtils.toDecimal(15, 72); + assertEquals("15.000000072", decimal, "The returned decimal is not correct."); + + decimal = DecimalUtils.toDecimal(19827342231L, 192837465); + assertEquals("19827342231.192837465", decimal, "The returned decimal is not correct."); + + decimal = DecimalUtils.toDecimal(19827342231L, 0); + assertEquals("19827342231"+NO_NANOSECS_SUFFIX, decimal, + "The returned decimal is not correct."); + + decimal = DecimalUtils.toDecimal(19827342231L, 999888000); + assertEquals("19827342231.999888000", decimal, + "The returned decimal is not correct."); + + decimal = DecimalUtils.toDecimal(-22704862, 599000000); + assertEquals("-22704862.599000000", decimal, + "The returned decimal is not correct."); + } + + private void checkExtractNanos(long expectedSeconds, int expectedNanos, BigDecimal decimal) + { + long seconds = decimal.longValue(); + assertEquals(expectedSeconds, seconds, "The second part is not correct."); + } + + @Test + public void testExtractNanosecondDecimal01() + { + BigDecimal value = new BigDecimal("0"); + checkExtractNanos(0L, 0, value); + } + + @Test + public void testExtractNanosecondDecimal02() + { + BigDecimal value = new BigDecimal("15.000000072"); + checkExtractNanos(15L, 72, value); + } + + @Test + public void testExtractNanosecondDecimal03() + { + BigDecimal value = new BigDecimal("15.72"); + checkExtractNanos(15L, 720000000, value); + } + + @Test + public void testExtractNanosecondDecimal04() + { + BigDecimal value = new BigDecimal("19827342231.192837465"); + checkExtractNanos(19827342231L, 192837465, value); + } + + @Test + public void testExtractNanosecondDecimal05() + { + BigDecimal value = new BigDecimal("19827342231"); + checkExtractNanos(19827342231L, 0, value); + } + + @Test + public void testExtractNanosecondDecimal06() + { + BigDecimal value = new BigDecimal("19827342231.999999999"); + checkExtractNanos(19827342231L, 999999999, value); + } + + private void checkExtractSecondsAndNanos(long expectedSeconds, int expectedNanos, BigDecimal decimal) + { + DecimalUtils.extractSecondsAndNanos(decimal, (Long s, Integer ns) -> { + assertEquals(expectedSeconds, s.longValue(), "The second part is not correct."); + assertEquals(expectedNanos, ns.intValue(), "The nanosecond part is not correct."); + return null; + }, true); + } + + @Test + public void testExtractSecondsAndNanos01() + { + BigDecimal value = new BigDecimal("0"); + checkExtractSecondsAndNanos(0L, 0, value); + } + + @Test + public void testExtractSecondsAndNanos02() + { + BigDecimal value = new BigDecimal("15.000000072"); + checkExtractSecondsAndNanos(15L, 72, value); + } + + @Test + public void testExtractSecondsAndNanos03() + { + BigDecimal value = new BigDecimal("15.72"); + checkExtractSecondsAndNanos(15L, 720000000, value); + } + + @Test + public void testExtractSecondsAndNanos04() + { + BigDecimal value = new BigDecimal("19827342231.192837465"); + checkExtractSecondsAndNanos(19827342231L, 192837465, value); + } + + @Test + public void testExtractSecondsAndNanos05() + { + BigDecimal value = new BigDecimal("19827342231"); + checkExtractSecondsAndNanos(19827342231L, 0, value); + } + + @Test + public void testExtractSecondsAndNanos06() + { + BigDecimal value = new BigDecimal("19827342231.999999999"); + checkExtractSecondsAndNanos(19827342231L, 999999999, value); + } + + @Test + public void testExtractSecondsAndNanosFromNegativeBigDecimal() + { + BigDecimal value = new BigDecimal("-22704862.599000000"); + checkExtractSecondsAndNanos(-22704862L, 599000000, value); + } + + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testExtractSecondsAndNanos07() + { + BigDecimal value = new BigDecimal("1e10000000"); + checkExtractSecondsAndNanos(0L, 0, value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java b/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java new file mode 100644 index 0000000000..ff0fd25638 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestFeatures +{ + @Test + public void testWriteDateTimestampsAsNanosecondsSettingEnabledByDefault() + { + assertTrue(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS.enabledByDefault(), + "Write date timestamps as nanoseconds setting should be enabled by default."); + } + + @Test + public void testReadDateTimestampsAsNanosecondsSettingEnabledByDefault() + { + assertTrue(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS.enabledByDefault(), + "Read date timestamps as nanoseconds setting should be enabled by default."); + } + + @Test + public void testAdjustDatesToContextTimeZoneSettingEnabledByDefault() + { + assertTrue(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE.enabledByDefault(), + "Adjust dates to context time zone setting should be enabled by default."); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/DefaultTypingTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/DefaultTypingTest.java new file mode 100644 index 0000000000..df6fb8c1c2 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/DefaultTypingTest.java @@ -0,0 +1,51 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.ZoneId; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.DatabindContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.jsontype.PolymorphicTypeValidator; + +import static org.junit.jupiter.api.Assertions.*; + +public class DefaultTypingTest extends DateTimeTestBase +{ + static class NoCheckSubTypeValidator + extends PolymorphicTypeValidator.Base + { + private static final long serialVersionUID = 1L; + + @Override + public Validity validateBaseType(DatabindContext ctxt, JavaType baseType) { + return Validity.ALLOWED; + } + } + + private final ObjectMapper TYPING_MAPPER = newMapperBuilder() + .activateDefaultTyping(new NoCheckSubTypeValidator()) + .build(); + + // for [datatype-jsr310#24] + @Test + public void testZoneIdAsIs() throws Exception + { + ZoneId exp = ZoneId.of("America/Chicago"); + String json = TYPING_MAPPER.writeValueAsString(exp); + ZoneId act = TYPING_MAPPER.readValue(json, ZoneId.class); + assertEquals(exp, act); + } + + // This one WILL add type info, since `ZoneId` is abstract type: + @Test + public void testZoneWithForcedBaseType() throws Exception + { + ZoneId exp = ZoneId.of("America/Chicago"); + String json = TYPING_MAPPER.writerFor(ZoneId.class).writeValueAsString(exp); + ZoneId act = TYPING_MAPPER.readValue(json, ZoneId.class); + assertEquals(exp, act); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeser337Test.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeser337Test.java new file mode 100644 index 0000000000..b11a5fffd8 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeser337Test.java @@ -0,0 +1,48 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.*; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DurationDeser337Test extends DateTimeTestBase +{ + @Test + public void testWithDurationsAsTimestamps() throws Exception + { + final ObjectMapper MAPPER_DURATION_TIMESTAMPS = mapperBuilder() + .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .build(); + + Duration duration = Duration.parse("PT-43.636S"); + + String ser = MAPPER_DURATION_TIMESTAMPS.writeValueAsString(duration); + + assertEquals("-43.636000000", ser); + + Duration deser = MAPPER_DURATION_TIMESTAMPS.readValue(ser, Duration.class); + + assertEquals(duration, deser); + assertEquals(deser.toString(), "PT-43.636S"); + } + + @Test + public void testWithoutDurationsAsTimestamps() throws Exception + { + ObjectMapper mapper = mapperBuilder() + .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .build(); + + Duration duration = Duration.parse("PT-43.636S"); + + String ser = mapper.writeValueAsString(duration); + assertEquals(q("PT-43.636S"), ser); + + Duration deser = mapper.readValue(ser, Duration.class); + assertEquals(duration, deser); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeserTest.java new file mode 100644 index 0000000000..d6a76f7e62 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeserTest.java @@ -0,0 +1,585 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAmount; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.exc.InvalidDefinitionException; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class DurationDeserTest extends DateTimeTestBase +{ + private final ObjectReader READER = newMapper().readerFor(Duration.class); + + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + final static class Wrapper { + public Duration value; + + public Wrapper() { } + public Wrapper(Duration v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosDisabled { + @JsonFormat( + without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public Duration value; + + public WrapperWithReadTimestampsAsNanosDisabled() { } + public WrapperWithReadTimestampsAsNanosDisabled(Duration v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosEnabled { + @JsonFormat( + with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public Duration value; + + public WrapperWithReadTimestampsAsNanosEnabled() { } + public WrapperWithReadTimestampsAsNanosEnabled(Duration v) { value = v; } + } + + + @Test + public void testDeserializationAsFloat01() throws Exception + { + Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("60.0"); + assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsFloat02() throws Exception + { + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("60.0"); + assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsFloat03() throws Exception + { + Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("13498.000008374"); + assertEquals(Duration.ofSeconds(13498L, 8374), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsFloat04() throws Exception + { + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("13498.000008374"); + assertEquals(Duration.ofSeconds(13498L, 8374), value, "The value is not correct."); + } + + /** + * Test the upper-bound of Duration. + */ + @Test + public void testDeserializationAsFloatEdgeCase01() throws Exception + { + String input = Long.MAX_VALUE + ".999999999"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(Long.MAX_VALUE, value.getSeconds()); + assertEquals(999999999, value.getNano()); + } + + /** + * Test the lower-bound of Duration. + */ + @Test + public void testDeserializationAsFloatEdgeCase02() throws Exception + { + String input = Long.MIN_VALUE + ".0"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(Long.MIN_VALUE, value.getSeconds()); + assertEquals(0, value.getNano()); + } + + @Test + public void testDeserializationAsFloatEdgeCase03() throws Exception + { + // Duration can't go this low + assertThrows(ArithmeticException.class, () -> { + READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.MIN_VALUE + ".1"); + }); + } + + /* + * DurationDeserializer currently uses BigDecimal.longValue() which has surprising behavior + * for numbers outside the range of Long. Numbers less than 1e64 will result in the lower 64 bits. + * Numbers at or above 1e64 will always result in zero. + */ + + @Test + public void testDeserializationAsFloatEdgeCase04() throws Exception + { + // Just beyond the upper-bound of Duration. + String input = new BigInteger(Long.toString(Long.MAX_VALUE)).add(BigInteger.ONE) + ".0"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(Long.MIN_VALUE, value.getSeconds()); // We've turned a positive number into negative duration! + } + + @Test + public void testDeserializationAsFloatEdgeCase05() throws Exception + { + // Just beyond the lower-bound of Duration. + String input = new BigInteger(Long.toString(Long.MIN_VALUE)).subtract(BigInteger.ONE) + ".0"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(Long.MAX_VALUE, value.getSeconds()); // We've turned a negative number into positive duration! + } + + @Test + public void testDeserializationAsFloatEdgeCase06() throws Exception + { + // Into the positive zone where everything becomes zero. + String input = "1e64"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(0, value.getSeconds()); + } + + @Test + public void testDeserializationAsFloatEdgeCase07() throws Exception + { + // Into the negative zone where everything becomes zero. + String input = "-1e64"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(0, value.getSeconds()); + } + + /** + * Numbers with very large exponents can take a long time, but still result in zero. + * https://github.com/FasterXML/jackson-databind/issues/2141 + */ + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase08() throws Exception + { + String input = "1e10000000"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(0, value.getSeconds()); + } + + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase09() throws Exception + { + String input = "-1e10000000"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(0, value.getSeconds()); + } + + /** + * Same for large negative exponents. + */ + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase10() throws Exception + { + String input = "1e-10000000"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(0, value.getSeconds()); + } + + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase11() throws Exception + { + String input = "-1e-10000000"; + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(input); + assertEquals(0, value.getSeconds()); + } + + @Test + public void testDeserializationAsInt01() throws Exception + { + Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("60"); + assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsInt02() throws Exception + { + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("60000"); + assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsInt03() throws Exception + { + Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("13498"); + assertEquals(Duration.ofSeconds(13498L, 0), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsInt04() throws Exception + { + Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("13498000"); + assertEquals(Duration.ofSeconds(13498L, 0), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsInt05() throws Exception + { + ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class); + WrapperWithReadTimestampsAsNanosEnabled expected = + new WrapperWithReadTimestampsAsNanosEnabled(Duration.ofSeconds(13498L, 0)); + WrapperWithReadTimestampsAsNanosEnabled actual = + reader.readValue(wrapperPayload(13498)); + assertEquals(expected.value, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsInt06() throws Exception + { + ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + WrapperWithReadTimestampsAsNanosDisabled expected = + new WrapperWithReadTimestampsAsNanosDisabled(Duration.ofSeconds(13498L, 0)); + WrapperWithReadTimestampsAsNanosDisabled actual = + reader.readValue(wrapperPayload(13498000)); + assertEquals(expected.value, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsString01() throws Exception + { + Duration exp = Duration.ofSeconds(60L, 0); + Duration value = READER.readValue('"' + exp.toString() + '"'); + assertEquals(exp, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsString02() throws Exception + { + Duration exp = Duration.ofSeconds(13498L, 8374); + Duration value = READER.readValue('"' + exp.toString() + '"'); + assertEquals(exp, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsString03() throws Exception + { + assertNull(READER.readValue("\" \""), "The value should be null."); + } + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + Duration duration = Duration.ofSeconds(13498L, 8374); + + String prefix = "[\"" + Duration.class.getName() + "\","; + + ObjectMapper mapper = newMapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .build(); + TemporalAmount value = mapper.readerFor(TemporalAmount.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(prefix + "13498.000008374]"); + + assertTrue(value instanceof Duration, "The value should be a Duration."); + assertEquals(duration, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02() throws Exception + { + String prefix = "[\"" + Duration.class.getName() + "\","; + ObjectMapper mapper = newMapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .build(); + TemporalAmount value = mapper.readerFor(TemporalAmount.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(prefix + "13498]"); + assertTrue(value instanceof Duration, "The value should be a Duration."); + assertEquals(Duration.ofSeconds(13498L), value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03() throws Exception + { + String prefix = "[\"" + Duration.class.getName() + "\","; + ObjectMapper mapper = newMapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .build(); + TemporalAmount value = mapper + .readerFor(TemporalAmount.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(prefix + "13498837]"); + assertTrue(value instanceof Duration, "The value should be a Duration."); + assertEquals(Duration.ofSeconds(13498L, 837000000), value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo04() throws Exception + { + Duration duration = Duration.ofSeconds(13498L, 8374); + String prefix = "[\"" + Duration.class.getName() + "\","; + ObjectMapper mapper = newMapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .build(); + TemporalAmount value = mapper.readerFor(TemporalAmount.class) + .readValue(prefix + '"' + duration.toString() + "\"]"); + assertTrue(value instanceof Duration, "The value should be a Duration."); + assertEquals(duration, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsArrayDisabled() throws Exception { + Duration exp = Duration.ofSeconds(13498L, 8374); + try { + READER.readValue("[\"" + exp.toString() + "\"]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.Duration` from Array value"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() throws Throwable + { + try { + READER.readValue("[]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.Duration` from Array value"); + } + try { + READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.Duration` from Array value"); + } + } + + @Test + public void testDeserializationAsArrayEnabled() throws Exception { + Duration exp = Duration.ofSeconds(13498L, 8374); + Duration value = newMapper().readerFor(Duration.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[\"" + exp.toString() + "\"]"); + assertEquals(exp, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + Duration value = newMapper().readerFor(Duration.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, + DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "duration"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String dateValAsNullStr = null; + String dateValAsEmptyStr = ""; + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + Duration actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr)); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + Duration actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "duration"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(Duration.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + final String dateValAsNullStr = null; + + // even with strict, null value should be deserialized without throwing an exception + String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String dateValAsEmptyStr = ""; + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr)); + assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr)); + } + + /* + /********************************************************** + /* Tests for custom patterns (modules-java8#184) + /********************************************************** + */ + + @Test + public void shouldDeserializeInNanos_whenNanosUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("NANOS"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + assertEquals(Duration.ofNanos(25), wrapper.value); + } + + @Test + public void shouldDeserializeInMicros_whenMicrosUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("MICROS"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + + assertEquals(Duration.of(25, ChronoUnit.MICROS), wrapper.value); + } + + @Test + public void shouldDeserializeInMillis_whenMillisUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("MILLIS"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + + assertEquals(Duration.ofMillis(25), wrapper.value); + } + + @Test + public void shouldDeserializeInSeconds_whenSecondsUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("SECONDS"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + + assertEquals(Duration.ofSeconds(25), wrapper.value); + } + + @Test + public void shouldDeserializeInMinutes_whenMinutesUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("MINUTES"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + + assertEquals(Duration.ofMinutes(25), wrapper.value); + } + + @Test + public void shouldDeserializeInHours_whenHoursUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("HOURS"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + + assertEquals(Duration.ofHours(25), wrapper.value); + } + + @Test + public void shouldDeserializeInHalfDays_whenHalfDaysUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("HALF_DAYS"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + + assertEquals(Duration.of(25, ChronoUnit.HALF_DAYS), wrapper.value); + } + + @Test + public void shouldDeserializeInDays_whenDaysUnitAsPattern_andValueIsInteger() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("DAYS"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25)); + + assertEquals(Duration.ofDays(25), wrapper.value); + } + + @Test + public void shouldIgnoreUnitPattern_whenValueIsFloat() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("MINUTES"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue(wrapperPayload(25.5)); + + assertEquals(Duration.parse("PT25.5S"), wrapper.value); + } + + @Test + public void shouldIgnoreUnitPattern_whenValueIsString() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("MINUTES"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + Wrapper wrapper = reader.readValue("{\"value\":\"PT25S\"}"); + + assertEquals(Duration.parse("PT25S"), wrapper.value); + } + + @Test + public void shouldFailForInvalidPattern() throws Exception { + ObjectMapper mapper = _mapperForPatternOverride("Nanos"); + ObjectReader reader = mapper.readerFor(Wrapper.class); + + try { + /*Wrapper wrapper =*/ reader.readValue(wrapperPayload(25)); + fail("Should not allow invalid 'pattern'"); + } catch (InvalidDefinitionException e) { + verifyException(e, "Bad 'pattern' definition (\"Nanos\")"); + verifyException(e, "expected one of ["); + } + } + + private String wrapperPayload(Number number) { + return "{\"value\":" + number + "}"; + } + + private ObjectMapper _mapperForPatternOverride(String patternStr) { + return mapperBuilder() + .withConfigOverride(Duration.class, + o -> o.setFormat(JsonFormat.Value.forPattern(patternStr))) + .build(); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeser291Test.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeser291Test.java new file mode 100644 index 0000000000..d9e3b49da9 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeser291Test.java @@ -0,0 +1,51 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Instant; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.*; + +// [modules-java8#291] InstantDeserializer fails to parse negative numeric timestamp strings for +// pre-1970 values. +public class InstantDeser291Test + extends DateTimeTestBase +{ + private final JsonMapper MAPPER = JsonMapper.builder() + .defaultLocale(Locale.ENGLISH) + .enable(DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS) + .build(); + private final ObjectReader READER = MAPPER.readerFor(Instant.class); + + private static final Instant INSTANT_3_SEC_AFTER_EPOC = Instant.ofEpochSecond(3); + private static final Instant INSTANT_3_SEC_BEFORE_EPOC = Instant.ofEpochSecond(-3); + + private static final String STR_3_SEC = "\"3.000000000\""; + private static final String STR_POSITIVE_3 = "\"+3.000000000\""; + private static final String STR_NEGATIVE_3 = "\"-3.000000000\""; + + /** + * Baseline that always succeeds, even before resolution of issue 291 + * @throws Exception + */ + @Test + public void testNormalNumericalString() throws Exception { + assertEquals(INSTANT_3_SEC_AFTER_EPOC, READER.readValue(STR_3_SEC)); + } + + @Test + public void testNegativeNumericalString() throws Exception { + assertEquals(INSTANT_3_SEC_BEFORE_EPOC, READER.readValue(STR_NEGATIVE_3)); + } + + @Test + public void testAllowedPlusSignNumericalString() throws Exception { + assertEquals(INSTANT_3_SEC_AFTER_EPOC, READER.readValue(STR_POSITIVE_3)); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeserTest.java new file mode 100644 index 0000000000..fe77ce6633 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeserTest.java @@ -0,0 +1,654 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.SerializationFeature; + +import static org.junit.jupiter.api.Assertions.*; +import static tools.jackson.databind.ext.javatime.deser.InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX; + +public class InstantDeserTest extends DateTimeTestBase +{ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT; + private static final String CUSTOM_PATTERN = "yyyy-MM-dd HH:mm:ss"; + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + static class Wrapper { + @JsonFormat( + // 22-Jun-2015, tatu: I'll be damned if I understand why pattern does not + // work here... but it doesn't. Someone with better date-fu has to come + // and fix this; until then I will only verify that we can force textual + // representation here + //pattern="YYYY-mm-dd", + shape=JsonFormat.Shape.STRING + ) + public Instant value; + + public Wrapper() { } + public Wrapper(Instant v) { value = v; } + } + + static class WrapperWithCustomPattern { + @JsonFormat( + pattern = CUSTOM_PATTERN, + shape=JsonFormat.Shape.STRING, + timezone = "UTC" + ) + public Instant valueInUTC; + + public WrapperWithCustomPattern() { } + public WrapperWithCustomPattern(Instant v) { + valueInUTC = v; + } + } + + static class WrapperWithReadTimestampsAsNanosDisabled { + @JsonFormat( + without=JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public Instant value; + + public WrapperWithReadTimestampsAsNanosDisabled() { } + public WrapperWithReadTimestampsAsNanosDisabled(Instant v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosEnabled { + @JsonFormat( + with=JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public Instant value; + + public WrapperWithReadTimestampsAsNanosEnabled() { } + public WrapperWithReadTimestampsAsNanosEnabled(Instant v) { value = v; } + } + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(Instant.class); + + /* + /********************************************************************** + /* Basic deserialization from floating point value (seconds with fractions) + /********************************************************************** + */ + + @Test + public void testDeserializationAsFloat01() throws Exception { + assertEquals(Instant.ofEpochSecond(0L), + READER.readValue("0.000000000")); + } + + @Test + public void testDeserializationAsFloat02() throws Exception { + assertEquals(Instant.ofEpochSecond(123456789L, 183917322), + READER.readValue("123456789.183917322")); + } + + @Test + public void testDeserializationAsFloat03() throws Exception + { + Instant date = Instant.now(); + Instant value = READER.readValue( + DecimalUtils.toDecimal(date.getEpochSecond(), date.getNano())); + assertEquals(date, value); + } + + /** + * Test the upper-bound of Instant. + */ + @Test + public void testDeserializationAsFloatEdgeCase01() throws Exception + { + String input = Instant.MAX.getEpochSecond() + ".999999999"; + Instant value = READER.readValue(input); + assertEquals(value, Instant.MAX); + assertEquals(Instant.MAX.getEpochSecond(), value.getEpochSecond()); + assertEquals(999999999, value.getNano()); + } + + /** + * Test the lower-bound of Instant. + */ + @Test + public void testDeserializationAsFloatEdgeCase02() throws Exception + { + String input = Instant.MIN.getEpochSecond() + ".0"; + Instant value = READER.readValue(input); + assertEquals(value, Instant.MIN); + assertEquals(Instant.MIN.getEpochSecond(), value.getEpochSecond()); + assertEquals(0, value.getNano()); + } + + @Test + public void testDeserializationAsFloatEdgeCase03() throws Exception + { + // Instant can't go this low + String input = Instant.MIN.getEpochSecond() + ".1"; + assertThrows(DateTimeException.class, () -> READER.readValue(input)); + } + + /* + * InstantDeserializer currently uses BigDecimal.longValue() which has surprising behavior + * for numbers outside the range of Long. Numbers less than 1e64 will result in the lower 64 bits. + * Numbers at or above 1e64 will always result in zero. + */ + @Test + public void testDeserializationAsFloatEdgeCase04() throws Exception + { + // 1ns beyond the upper-bound of Instant. + String input = (Instant.MAX.getEpochSecond() + 1) + ".0"; + assertThrows(DateTimeException.class, () -> READER.readValue(input)); + } + + @Test + public void testDeserializationAsFloatEdgeCase05() throws Exception + { + // 1ns beyond the lower-bound of Instant. + String input = (Instant.MIN.getEpochSecond() - 1) + ".0"; + assertThrows(DateTimeException.class, () -> READER.readValue(input)); + } + + @Test + public void testDeserializationAsFloatEdgeCase06() throws Exception + { + // Into the positive zone where everything becomes zero. + Instant value = READER.readValue("1e64"); + assertEquals(0, value.getEpochSecond()); + } + + @Test + public void testDeserializationAsFloatEdgeCase07() throws Exception + { + // Into the negative zone where everything becomes zero. + Instant value = READER.readValue("-1e64"); + assertEquals(0, value.getEpochSecond()); + } + + /** + * Numbers with very large exponents can take a long time, but still result in zero. + * https://github.com/FasterXML/jackson-databind/issues/2141 + */ + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase08() throws Exception + { + Instant value = READER.readValue("1e10000000"); + assertEquals(0, value.getEpochSecond()); + } + + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase09() throws Exception + { + Instant value = READER.readValue("-1e10000000"); + assertEquals(0, value.getEpochSecond()); + } + + /** + * Same for large negative exponents. + */ + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase10() throws Exception + { + Instant value = READER.readValue("1e-10000000"); + assertEquals(0, value.getEpochSecond()); + } + + @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) + @Test + public void testDeserializationAsFloatEdgeCase11() throws Exception + { + Instant value = READER.readValue("-1e-10000000"); + assertEquals(0, value.getEpochSecond()); + } + + /* + /********************************************************************** + /* Basic deserialization from Integer (long) value, as nanos + /********************************************************************** + */ + + @Test + public void testDeserializationAsInt01Nanoseconds() throws Exception + { + Instant date = Instant.ofEpochSecond(0L); + Instant value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsInt02Nanoseconds() throws Exception + { + final long ts = 123456789L; + Instant date = Instant.ofEpochSecond(ts); + Instant value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(String.valueOf(ts)); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsInt03Nanoseconds() throws Exception + { + Instant date = Instant.now(); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + + Instant value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.getEpochSecond())); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsInt04Nanoseconds() throws Exception + { + ObjectReader reader = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosEnabled.class); + Instant date = Instant.now(); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + WrapperWithReadTimestampsAsNanosEnabled expected = + new WrapperWithReadTimestampsAsNanosEnabled(date); + WrapperWithReadTimestampsAsNanosEnabled actual = reader.readValue( + a2q("{'value':" + date.getEpochSecond() + "}")); + assertEquals(expected.value, actual.value); + } + + /* + /********************************************************************** + /* Basic deserialization from Integer (long) value, as milliseconds + /********************************************************************** + */ + + @Test + public void testDeserializationAsInt01Milliseconds() throws Exception + { + Instant date = Instant.ofEpochSecond(0L); + Instant value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsInt02Milliseconds() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 422000000); + Instant value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789422"); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsInt03Milliseconds() throws Exception + { + Instant date = Instant.now(); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + + Instant value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toEpochMilli())); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsInt04Milliseconds() throws Exception + { + ObjectReader reader = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + Instant date = Instant.now(); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + WrapperWithReadTimestampsAsNanosDisabled expected = + new WrapperWithReadTimestampsAsNanosDisabled(date); + WrapperWithReadTimestampsAsNanosDisabled actual = reader.readValue( + a2q("{'value':" + date.toEpochMilli() + "}")); + assertEquals(expected.value, actual.value); + } + + /* + /********************************************************************** + /* Basic deserialization from String (ISO-8601 timestamps) + /********************************************************************** + */ + + @Test + public void testDeserializationAsString01() throws Exception + { + Instant date = Instant.ofEpochSecond(0L); + Instant value = READER.readValue('"' + FORMATTER.format(date) + '"'); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsString02() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 183917322); + Instant value = READER.readValue('"' + FORMATTER.format(date) + '"'); + assertEquals(date, value); + } + + @Test + public void testDeserializationAsString03() throws Exception + { + Instant date = Instant.now(); + + Instant value = READER.readValue('"' + FORMATTER.format(date) + '"'); + assertEquals(date, value); + } + + /* + /********************************************************************** + /* Polymorphic deserialization, numeric timestamps + /********************************************************************** + */ + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 183917322); + ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + Instant.class.getName() + "\",123456789.183917322]", Temporal.class + ); + assertTrue(value instanceof Instant, "The value should be an Instant."); + assertEquals(date, value); + } + + @Test + public void testDeserializationWithTypeInfo02() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 0); + ObjectMapper m = newMapperBuilder() + .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + Instant.class.getName() + "\",123456789]", Temporal.class + ); + assertTrue(value instanceof Instant, "The value should be an Instant."); + assertEquals(date, value); + } + + @Test + public void testDeserializationWithTypeInfo03() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 422000000); + ObjectMapper m = newMapperBuilder() + .disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + Instant.class.getName() + "\",123456789422]", Temporal.class + ); + + assertTrue(value instanceof Instant, "The value should be an Instant."); + assertEquals(date, value); + } + + @Test + public void testDeserializationWithTypeInfo04() throws Exception + { + Instant date = Instant.now(); + ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + Instant.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", Temporal.class + ); + assertTrue(value instanceof Instant, "The value should be an Instant."); + assertEquals(date, value); + } + + /* + /********************************************************************** + /* Deserialization with custom pattern overrides (for String values) + /********************************************************************** + */ + + @Test + public void testCustomPatternWithAnnotations01() throws Exception + { + final Wrapper input = new Wrapper(Instant.ofEpochMilli(0)); + String json = MAPPER.writeValueAsString(input); + assertEquals(a2q("{'value':'1970-01-01T00:00:00Z'}"), json); + + Wrapper result = MAPPER.readValue(json, Wrapper.class); + assertEquals(input.value, result.value); + } + + // [datatype-jsr310#69] + @Test + public void testCustomPatternWithAnnotations02() throws Exception + { + //Test date is pushed one year after start of the epoch just to avoid possible issues with UTC-X TZs which could + //push the instant before tha start of the epoch + final Instant instant = ZonedDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneOffset.UTC).plusYears(1).toInstant(); + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(CUSTOM_PATTERN); + final String valueInUTC = formatter.withZone(ZoneOffset.UTC).format(instant); + + final WrapperWithCustomPattern input = new WrapperWithCustomPattern(instant); + String json = MAPPER.writeValueAsString(input); + + assertTrue(json.contains(a2q("'valueInUTC':'" + valueInUTC + "'")), + "Instant in UTC timezone was not serialized as expected."); + + WrapperWithCustomPattern result = MAPPER.readValue(json, WrapperWithCustomPattern.class); + assertEquals(input.valueInUTC, result.valueInUTC, + "Instant in UTC timezone was not deserialized as expected."); + } + + /* + /********************************************************************** + /* Deserialization, timezone overrides + /********************************************************************** + */ + + // [jackson-modules-java8#18] + @Test + public void testDeserializationFromStringWithZeroZoneOffset01() throws Exception { + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+00:00"); + Instant result = READER.readValue(json); + assertEquals(date, result); + } + + @Test + public void testDeserializationFromStringWithZeroZoneOffset02() throws Exception { + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+0000"); + Instant result = READER.readValue(json); + assertEquals(date, result); + } + + @Test + public void testDeserializationFromStringWithZeroZoneOffset03() throws Exception { + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+00"); + Instant result = READER.readValue(json); + assertEquals(date, result); + } + + @Test + public void testDeserializationFromStringWithZeroZoneOffset04() throws Exception { + assumeInstantCanParseOffsets(); + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+00:30"); + Instant result = READER.readValue(json); + assertNotEquals(date, result); + } + + @Test + public void testDeserializationFromStringWithZeroZoneOffset05() throws Exception { + assumeInstantCanParseOffsets(); + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+01:30"); + Instant result = READER.readValue(json); + assertNotEquals(date, result); + } + + @Test + public void testDeserializationFromStringWithZeroZoneOffset06() throws Exception { + assumeInstantCanParseOffsets(); + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "-00:00"); + Instant result = READER.readValue(json); + assertEquals(date, result); + } + + private String formatWithZeroZoneOffset(Instant date, String offset){ + return '"' + FORMATTER.format(date).replaceFirst("Z$", offset) + '"'; + } + + private static void assumeInstantCanParseOffsets() { + // 27-Jan-2025, tatu: We are on JDK 17 so true always + // DateTimeFormatter.ISO_INSTANT didn't handle offsets until JDK 12+. + ; + } + + /* + /********************************************************************** + /* Deserialization, misc other + /********************************************************************** + */ + + // [datatype-jsr310#16] + @Test + public void testDeserializationFromStringAsNumber() throws Exception + { + // First, baseline test with floating-point numbers + Instant inst = Instant.now(); + String json = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(inst); + Instant result = READER.readValue(json); + assertNotNull(result); + assertEquals(result, inst); + + // but then quoted as JSON String + result = READER.readValue(String.format("\"%s\"", json)); + assertNotNull(result); + assertEquals(result, inst); + } + + // [datatype-jsr310#79] + @Test + public void testRoundTripOfInstantAndJavaUtilDate() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false) + .build(); + + Instant givenInstant = LocalDate.of(2016, 1, 1).atStartOfDay().atZone(ZoneOffset.UTC).toInstant(); + String json = mapper.writeValueAsString(java.util.Date.from(givenInstant)); + Instant actual = mapper.readValue(json, Instant.class); + + assertEquals(givenInstant, actual); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "duration"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String dateValAsNullStr = null; + String dateValAsEmptyStr = ""; + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + Duration actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr)); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + Duration actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "instant"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(Instant.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr)); + } + + /* + /************************************************************************ + /* Tests for InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX + /************************************************************************ + */ + @Test + public void testISO8601ColonlessRegexFindsOffset() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100"); + + assertTrue(matcher.find(), "Matcher finds +0100 as an colonless offset"); + assertEquals(matcher.group(), "+0100", "Matcher groups +0100 as an colonless offset"); + } + + @Test + public void testISO8601ColonlessRegexFindsOffsetWithTZ() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100[Europe/Paris]"); + + assertTrue(matcher.find(), "Matcher finds +0100 as an colonless offset"); + assertEquals(matcher.group(), "+0100", "Matcher groups +0100 as an colonless offset"); + } + + @Test + public void testISO8601ColonlessRegexDoesNotAffectNegativeYears() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+01:00[Europe/Paris]"); + + assertFalse(matcher.find(), "Matcher does not find -2000 (years) as an offset without colon"); + } + + @Test + public void testISO8601ColonlessRegexDoesNotAffectNegativeYearsWithColonless() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+0100[Europe/Paris]"); + + assertTrue(matcher.find(), "Matcher finds +0100 as an colonless offset"); + assertEquals(matcher.group(), "+0100", "Matcher groups +0100 as an colonless offset"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserTest.java new file mode 100644 index 0000000000..dd7f902374 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserTest.java @@ -0,0 +1,600 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.*; +import java.time.temporal.Temporal; +import java.util.Map; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import com.fasterxml.jackson.annotation.OptBoolean; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; + +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.cfg.CoercionInputShape; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.*; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalDateDeserTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(LocalDate.class); + private final ObjectReader READER_USING_TIME_ZONE = JsonMapper.builder() + .enable(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING) + .build() + .readerFor(LocalDate.class); + + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + final static class Wrapper { + @JsonFormat(pattern="yyyy_MM_dd'T'HH:mmZ", + shape=JsonFormat.Shape.STRING) + public LocalDate value; + + public Wrapper() { } + public Wrapper(LocalDate v) { value = v; } + } + + final static class ShapeWrapper { + @JsonFormat(shape=JsonFormat.Shape.NUMBER_INT) + public LocalDate date; + + public ShapeWrapper() { } + public ShapeWrapper(LocalDate v) { date = v; } + } + + static class StrictWrapperWithFormat { + @JsonFormat(pattern="yyyy-MM-dd", + lenient = OptBoolean.FALSE) + public LocalDate value; + + public StrictWrapperWithFormat() { } + public StrictWrapperWithFormat(LocalDate v) { value = v; } + } + + final static class StrictWrapperWithYearOfEra { + @JsonFormat(pattern="yyyy-MM-dd G", + lenient = OptBoolean.FALSE) + public LocalDate value; + + public StrictWrapperWithYearOfEra() { } + public StrictWrapperWithYearOfEra(LocalDate v) { value = v; } + } + + final static class StrictWrapperWithYearWithoutEra { + @JsonFormat(pattern="uuuu-MM-dd", + lenient = OptBoolean.FALSE) + public LocalDate value; + + public StrictWrapperWithYearWithoutEra() { } + public StrictWrapperWithYearWithoutEra(LocalDate v) { value = v; } + } + + /* + /********************************************************** + /* Deserialization from Int array representation + /********************************************************** + */ + + @Test + public void testDeserializationAsTimestamp01() + { + assertEquals(LocalDate.of(1986, Month.JANUARY, 17), + READER.readValue("[1986,1,17]")); + } + + @Test + public void testDeserializationAsTimestamp02() + { + assertEquals(LocalDate.of(2013, Month.AUGUST, 21), + READER.readValue("[2013,8,21]")); + } + + /* + /********************************************************** + /* Deserialization from String representation + /********************************************************** + */ + + @Test + public void testDeserializationAsString01() + { + assertEquals(LocalDate.of(2000, Month.JANUARY, 1), READER.readValue(q("2000-01-01"))); + + LocalDate date = LocalDate.of(1986, Month.JANUARY, 17); + assertEquals(date, READER.readValue('"' + date.toString() + '"')); + + date = LocalDate.of(2013, Month.AUGUST, 21); + assertEquals(date, READER.readValue('"' + date.toString() + '"')); + } + + @Test + public void testDeserializationAsString02() + { + LocalDateTime date = LocalDateTime.now(); + assertEquals(date.toLocalDate(), READER.readValue('"' + date.toString() + '"')); + } + + @Test + public void testLenientDeserializationAsString01() throws Exception + { + Instant instant = Instant.now(); + LocalDate value = READER.readValue(q(instant.toString())); + assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value); + } + + @Test + public void testLenientDeserializationAsString02() throws Exception + { + ObjectReader reader = READER.with(TimeZone.getTimeZone(Z_BUDAPEST)); + Instant instant = Instant.now(); + LocalDate value = reader.readValue(q(instant.toString())); + assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value); + } + + @Test + public void testLenientDeserializationAsString03() throws Exception + { + Instant instant = Instant.now(); + LocalDate value = READER_USING_TIME_ZONE.readValue(q(instant.toString())); + assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value); + } + + @ParameterizedTest + @CsvSource({ + "Europe/Budapest, 2024-07-21T21:59:59Z, 2024-07-21", + "Europe/Budapest, 2024-07-21T22:00:00Z, 2024-07-22", + "America/Chicago, 2024-07-22T04:59:59Z, 2024-07-21", + "America/Chicago, 2024-07-22T05:00:00Z, 2024-07-22" + }) + public void testLenientDeserializationAsString04(TimeZone zone, String string, LocalDate expected) throws Exception + { + ObjectReader reader = READER_USING_TIME_ZONE.with(zone); + LocalDate value = reader.readValue(q(string)); + assertEquals(expected, value); + } + + @Test + public void testBadDeserializationAsString01() + { + try { + READER.readValue(q("notalocaldate")); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type"); + verifyException(e, "from String \""); + } + } + + @Test + public void testBadDeserializationAsString02() + { + try { + READER.readValue(q("2015-06-19TShouldNotParse")); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type"); + verifyException(e, "from String \""); + } + } + + @Test + public void testDeserializationWithTypeInfo01() + { + ObjectMapper mapper = mapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + LocalDate date = LocalDate.of(2005, Month.NOVEMBER, 5); + Temporal value = mapper.readValue( + "[\"" + LocalDate.class.getName() + "\",\"" + date.toString() + "\"]", Temporal.class + ); + assertEquals(date, value); + } + + /* + /********************************************************** + /* Deserialization from alternate representation: int (number + /* of days since Epoch) + /********************************************************** + */ + + // By default, lenient handling on so we can do this: + @Test + public void testLenientDeserializeFromInt() + { + assertEquals(LocalDate.of(1970, Month.JANUARY, 3), READER.readValue("2")); + + assertEquals(LocalDate.of(1970, Month.FEBRUARY, 10), READER.readValue("40")); + } + + // But with alternate setting, not so + @Test + public void testStricDeserializeFromInt() + { + ObjectMapper mapper = mapperBuilder() + .withConfigOverride(LocalDate.class, + c -> c.setFormat(JsonFormat.Value.forLeniency(false)) + ) + .build(); + try { + mapper.readValue("2", LocalDate.class); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize instance of"); + verifyException(e, "not allowed because 'strict' mode set for property or type"); + } + + // 17-Aug-2019, tatu: Should possibly test other mechanism too, but for now let's + // be content with just one... + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() + { + + String key = "date"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String dateValAsNullStr = null; + String dateValAsEmptyStr = ""; + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + LocalDate actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr)); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + LocalDate actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(actualDateFromNullStr, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + // ( expected = MismatchedInputException.class) + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "date"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(LocalDate.class, + c -> c.setFormat(JsonFormat.Value.forLeniency(false)) + ) + .build(); + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + final String dateValAsNullStr = null; + + // even with strict, null value should be deserialized without throwing an exception + String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String dateValAsEmptyStr = ""; + // TODO: nothing stops us from writing an empty string, maybe there should be a check there too? + String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", dateValAsEmptyStr)); + // with strict, deserializing an empty string is not permitted + assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr)); + } + + /* + /********************************************************** + /* Tests for alternate array handling + /********************************************************** + */ + + @Test + public void testDeserializationAsArrayDisabled() + { + try { + READER.readValue("[\"2000-01-01\"]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (VALUE_STRING) within Array"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() + { + // works even without the feature enabled + assertNull(READER.readValue("[]")); + } + + @Test + public void testDeserializationAsArrayEnabled() + { + LocalDate actual = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[\"2000-01-01\"]"); + assertEquals(LocalDate.of(2000, 1, 1), actual); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() + { + LocalDate value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, + DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + /* + /********************************************************** + /* Custom format + /********************************************************** + */ + + // for [datatype-jsr310#37] + @Test + public void testCustomFormat() + { + Wrapper w = MAPPER.readValue("{\"value\":\"2015_07_28T13:53+0300\"}", Wrapper.class); + LocalDate date = w.value; + assertEquals(28, date.getDayOfMonth()); + } + + /* + /********************************************************** + /* Strict Custom format + /********************************************************** + */ + + // for [modules-java8#148] + @Test + public void testStrictWithCustomFormat() + { + try { + /*StrictWrapperWithFormat w =*/ MAPPER.readValue( + "{\"value\":\"2019-11-31\"}", + StrictWrapperWithFormat.class); + fail("Should not pass"); + } catch (InvalidFormatException e) { + verifyException(e, "Cannot deserialize value of type `java.time.LocalDate` from String"); + verifyException(e, "\"2019-11-31\""); + } + } + + @Test + public void testStrictCustomFormatForInvalidFormat() throws Exception + { + try { + /*StrictWrapperWithFormat w = */ MAPPER.readValue( + "{\"value\":\"2019-11-30\"}", + StrictWrapperWithFormat.class); + fail("Should not pass"); + } catch (InvalidFormatException e) { + // 25-Mar-2021, tatu: Really bad exception message we got... but + // it is what it is + verifyException(e, "Cannot deserialize value of type `java.time.LocalDate` from String"); + verifyException(e, "\"2019-11-30\""); + } + } + + @Test + public void testStrictCustomFormatForInvalidFormatWithEra() throws Exception + { + assertThrows(InvalidFormatException.class, () -> { + /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30\"}", StrictWrapperWithYearOfEra.class); + }); + } + + @Test + public void testStrictCustomFormatForInvalidDateWithEra() throws Exception + { + assertThrows(InvalidFormatException.class, () -> { + /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 AD\"}", StrictWrapperWithYearOfEra.class); + }); + } + + @Test + public void testStrictCustomFormatForValidDateWithEra() throws Exception + { + StrictWrapperWithYearOfEra w = MAPPER.readValue("{\"value\":\"2019-11-30 AD\"}", StrictWrapperWithYearOfEra.class); + + assertEquals(w.value, LocalDate.of(2019, 11, 30)); + } + + @Test + public void testStrictCustomFormatForInvalidFormatWithoutEra() throws Exception + { + assertThrows(InvalidFormatException.class, () -> { + /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 AD\"}", StrictWrapperWithYearWithoutEra.class); + }); + } + + @Test + public void testStrictCustomFormatForInvalidDateWithoutEra() throws Exception + { + assertThrows(InvalidFormatException.class, () -> { + /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31\"}", StrictWrapperWithYearWithoutEra.class); + }); + } + + @Test + public void testStrictCustomFormatForValidDateWithoutEra() throws Exception + { + StrictWrapperWithYearWithoutEra w = MAPPER.readValue("{\"value\":\"2019-11-30\"}", StrictWrapperWithYearWithoutEra.class); + + assertEquals(w.value, LocalDate.of(2019, 11, 30)); + } + + /* + /********************************************************************** + /* Case-insensitive tests + /********************************************************************** + */ + + @Test + public void testDeserializationCaseInsensitiveEnabledOnValue() + { + ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(LocalDate.class, o -> o.setFormat(JsonFormat.Value + .forPattern("dd-MMM-yyyy") + .withFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES)) + ) + .build(); + ObjectReader reader = mapper.readerFor(LocalDate.class); + String[] jsons = new String[] { q("01-Jan-2000"), q("01-JAN-2000"), + q("01-jan-2000")}; + for (String json : jsons) { + expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), json); + } + } + + @Test + public void testDeserializationCaseInsensitiveEnabled() + { + final ObjectMapper mapper = mapperBuilder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES) + .withConfigOverride(LocalDate.class, o -> o.setFormat( + JsonFormat.Value.forPattern("dd-MMM-yyyy"))) + .build(); + ObjectReader reader = mapper.readerFor(LocalDate.class); + String[] jsons = new String[] { q("01-Jan-2000"), q("01-JAN-2000"), + q("01-jan-2000")}; + for(String json : jsons) { + expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), json); + } + } + + @Test + public void testDeserializationCaseInsensitiveDisabled() + { + final ObjectMapper mapper = mapperBuilder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false) + .withConfigOverride(LocalDate.class, o -> o.setFormat( + JsonFormat.Value.forPattern("dd-MMM-yyyy"))) + .build(); + ObjectReader reader = mapper.readerFor(LocalDate.class); + expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), q("01-Jan-2000")); + } + + @Test + public void testDeserializationCaseInsensitiveDisabled_InvalidDate() + { + final ObjectMapper mapper = mapperBuilder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false) + .withConfigOverride(LocalDate.class, o -> JsonFormat.Value.forPattern("dd-MMM-yyyy")) + .build(); + ObjectReader reader = mapper.readerFor(LocalDate.class); + String[] jsons = new String[] { q("01-JAN-2000"), q("01-jan-2000")}; + for(String json : jsons) { + try { + reader.readValue(a2q(json)); + fail("expected DateTimeParseException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.LocalDate` from String "); + } + } + } + + /* + /********************************************************************** + /* Tests for issue 58 - NUMBER_INT should be specified when deserializing + /* LocalDate as EpochDays + /********************************************************************** + */ + + @Test + public void testLenientDeserializeFromNumberInt() { + ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(LocalDate.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.NUMBER_INT))) + .build(); + + assertEquals(LocalDate.of(1970, Month.MAY, 4), + mapper.readValue("123", LocalDate.class)); + } + + @Test + public void testStrictDeserializeFromNumberInt() + { + ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(LocalDate.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + + ShapeWrapper w = mapper.readValue("{\"date\":123}", ShapeWrapper.class); + LocalDate localDate = w.date; + + assertEquals(LocalDate.of(1970, Month.MAY, 4), localDate); + } + + @Test + public void testStrictDeserializeFromString() + { + ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(LocalDate.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + try { + mapper.readValue("{\"value\":123}", Wrapper.class); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize instance of `java.time.LocalDate`"); + } + } + + /********************************************************************** + * + * coercion config test + * + /********************************************************************** + */ + + @Test + public void testDeserializeFromIntegerWithCoercionActionFail() { + ObjectMapper mapper = newMapperBuilder() + .withCoercionConfig(LocalDate.class, cfg -> + cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + ).build(); + MismatchedInputException exception = assertThrows(MismatchedInputException.class, + () -> mapper.readValue("123", LocalDate.class)); + + assertTrue(exception.getMessage().contains("Cannot coerce Integer value (123) to `java.time.LocalDate`")); + } + + @Test + public void testDeserializeFromEmptyStringWithCoercionActionFail() { + ObjectMapper mapper = newMapperBuilder() + .withCoercionConfig(LocalDate.class, cfg -> + cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail) + ).build(); + + MismatchedInputException exception = assertThrows(MismatchedInputException.class, + () -> mapper.readValue(a2q("{'value':''}"), Wrapper.class)); + + assertTrue(exception.getMessage().contains("Cannot coerce empty String (\"\") to `java.time.LocalDate`")); + } + + /* + /********************************************************************** + /* Helper methods + /********************************************************************** + */ + + private void expectSuccess(ObjectReader reader, Object exp, String json) { + final LocalDate value = reader.readValue(a2q(json)); + assertNotNull(value); + assertEquals(exp, value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserTest.java new file mode 100644 index 0000000000..ff1bd6659d --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserTest.java @@ -0,0 +1,737 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.temporal.Temporal; +import java.util.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; +import com.fasterxml.jackson.annotation.OptBoolean; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.*; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.deser.DeserializationProblemHandler; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalDateTimeDeserTest + extends DateTimeTestBase +{ + private final static ObjectMapper MAPPER = newMapper(); + private final static ObjectReader READER = MAPPER.readerFor(LocalDateTime.class); + + private final static ObjectMapper STRICT_MAPPER = mapperBuilder() + .withConfigOverride(LocalDateTime.class, + c -> c.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + + private final ObjectReader READER_USING_TIME_ZONE = JsonMapper.builder() + .enable(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING) + .build() + .readerFor(LocalDateTime.class); + + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + final static class StrictWrapper { + @JsonFormat(pattern="yyyy-MM-dd HH:mm", + lenient = OptBoolean.FALSE) + public LocalDateTime value; + + public StrictWrapper() { } + public StrictWrapper(LocalDateTime v) { value = v; } + } + + final static class StrictWrapperWithYearOfEra { + @JsonFormat(pattern="yyyy-MM-dd HH:mm G", + lenient = OptBoolean.FALSE) + public LocalDateTime value; + + public StrictWrapperWithYearOfEra() { } + public StrictWrapperWithYearOfEra(LocalDateTime v) { value = v; } + } + + final static class StrictWrapperWithYearWithoutEra { + @JsonFormat(pattern="uuuu-MM-dd HH:mm", + lenient = OptBoolean.FALSE) + public LocalDateTime value; + + public StrictWrapperWithYearWithoutEra() { } + public StrictWrapperWithYearWithoutEra(LocalDateTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosDisabled { + @JsonFormat( + without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public LocalDateTime value; + + public WrapperWithReadTimestampsAsNanosDisabled() { } + public WrapperWithReadTimestampsAsNanosDisabled(LocalDateTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosEnabled { + @JsonFormat( + with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public LocalDateTime value; + + public WrapperWithReadTimestampsAsNanosEnabled() { } + public WrapperWithReadTimestampsAsNanosEnabled(LocalDateTime v) { value = v; } + } + + /* + /********************************************************** + /* Tests for deserializing from int array + /********************************************************** + */ + + @Test + public void testDeserializationAsTimestamp01() + { + LocalDateTime value = READER.readValue("[1986,1,17,15,43]"); + LocalDateTime time = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp02() + { + LocalDateTime value = READER.readValue("[2013,8,21,9,22,57]"); + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp03Nanoseconds() + { + ObjectReader r = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + LocalDateTime value = r.readValue("[2013,8,21,9,22,0,57]"); + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp03Milliseconds() + { + ObjectReader r = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + LocalDateTime value = r.readValue("[2013,8,21,9,22,0,57]"); + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57000000); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Nanoseconds() + { + ObjectReader r = MAPPER.readerFor(LocalDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + LocalDateTime value = r.readValue("[2005,11,5,22,31,5,829837]"); + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Milliseconds01() + { + ObjectReader r = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + LocalDateTime value = r.readValue("[2005,11,5,22,31,5,829837]"); + + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Milliseconds02() + { + ObjectReader r = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + LocalDateTime value = r.readValue("[2005,11,5,22,31,5,829]"); + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829000000); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Nanoseconds() throws Exception + { + ObjectReader r = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosEnabled.class); + WrapperWithReadTimestampsAsNanosEnabled actual = + r.readValue(a2q("{'value':[2013,8,21,9,22,0,57]}")); + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57); + assertEquals(time, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Milliseconds01() throws Exception + { + ObjectReader r = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + WrapperWithReadTimestampsAsNanosDisabled actual = + r.readValue(a2q("{'value':[2013,8,21,9,22,0,57]}")); + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57000000); + assertEquals(time, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Milliseconds02() throws Exception + { + ObjectReader r = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + WrapperWithReadTimestampsAsNanosDisabled actual = + r.readValue(a2q("{'value':[2013,8,21,9,22,0,4257]}")); + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 4257); + assertEquals(time, actual.value, "The value is not correct."); + } + + /* + /********************************************************** + /* Tests for deserializing from textual representation + /********************************************************** + */ + + @Test + public void testDeserializationAsString01() + { + LocalDateTime exp = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43); + LocalDateTime value = READER.readValue(q(exp.toString())); + assertEquals(exp, value, "The value is not correct."); + + assertEquals(LocalDateTime.of(2000, Month.JANUARY, 1, 12, 0), + READER.readValue(q("2000-01-01T12:00")), + "The value is not correct."); + } + + @Test + public void testDeserializationAsString02() + { + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57); + LocalDateTime value = MAPPER.readValue(q(time.toString()), LocalDateTime.class); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsString03() + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + LocalDateTime value = MAPPER.readValue(q(time.toString()), LocalDateTime.class); + assertEquals(time, value, "The value is not correct."); + } + + /* + /********************************************************** + /* Tests for deserializing from textual representation, + /* fail cases, leniency checking + /********************************************************** + */ + + // [modules-java#94]: "Z" offset MAY be allowed, requires leniency + @Test + public void testAllowZuluIfLenient() + { + final LocalDateTime EXP = LocalDateTime.of(2020, Month.OCTOBER, 22, 4, 16, 20, 504000000); + final String input = q("2020-10-22T04:16:20.504Z"); + final ObjectReader r = MAPPER.readerFor(LocalDateTime.class); + + // First, defaults: + assertEquals(EXP, r.readValue(input), "The value is not correct."); + + // but ensure that global timezone setting doesn't matter + LocalDateTime value = r.with(TimeZone.getTimeZone(Z_CHICAGO)) + .readValue(input); + assertEquals(EXP, value, "The value is not correct."); + + value = r.with(TimeZone.getTimeZone(Z_BUDAPEST)) + .readValue(input); + assertEquals(EXP, value, "The value is not correct."); + } + + @ParameterizedTest + @CsvSource({ + "UTC, 2020-10-22T04:16:20.504Z, 2020-10-22T04:16:20.504", + "Europe/Budapest, 2020-10-22T04:16:20.504Z, 2020-10-22T06:16:20.504", + "Europe/Budapest, 2020-10-25T00:16:20.504Z, 2020-10-25T02:16:20.504", + "Europe/Budapest, 2020-10-25T01:16:20.504Z, 2020-10-25T02:16:20.504", + "America/Chicago, 2020-10-22T04:16:20.504Z, 2020-10-21T23:16:20.504", + "America/Chicago, 2020-11-01T06:16:20.504Z, 2020-11-01T01:16:20.504", + "America/Chicago, 2020-11-01T07:16:20.504Z, 2020-11-01T01:16:20.504" + }) + public void testUseTimeZoneForZuluIfEnabled(TimeZone zone, String string, LocalDateTime expected) throws Exception + { + ObjectReader reader = READER_USING_TIME_ZONE.with(zone); + LocalDateTime value = reader.readValue(q(string)); + assertEquals(expected, value); + } + + // [modules-java#94]: "Z" offset not allowed if strict mode + @Test + public void testFailOnZuluIfStrict() + { + try { + STRICT_MAPPER.readValue(q("2020-10-22T00:16:20.504Z"), LocalDateTime.class); + fail("Should not pass"); + } catch (InvalidFormatException e) { + verifyException(e, "Cannot deserialize value of type "); + verifyException(e, "Should not contain offset when 'strict' mode"); + } + } + + @Test + public void testBadDeserializationAsString01() + { + try { + READER.readValue(q("notalocaldatetime")); + fail("expected fail"); + } catch (InvalidFormatException e) { + verifyException(e, "Cannot deserialize value of type"); + verifyException(e, "from String \""); + } + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() + { + String key = "datetime"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String dateValAsNullStr = null; + String dateValAsEmptyStr = ""; + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + LocalDateTime actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr)); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + LocalDateTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(actualDateFromNullStr, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "datetime"; + final ObjectReader objectReader = STRICT_MAPPER.readerFor(MAP_TYPE_REF); + final String dateValAsNullStr = null; + + // even with strict, null value should be deserialized without throwing an exception + String valueFromNullStr = STRICT_MAPPER.writeValueAsString(asMap(key, dateValAsNullStr)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String dateValAsEmptyStr = ""; + // TODO: nothing stops us from writing an empty string, maybe there should be a check there too? + String valueFromEmptyStr = STRICT_MAPPER.writeValueAsString(asMap("date", dateValAsEmptyStr)); + // with strict, deserializing an empty string is not permitted + try { + objectReader.readValue(valueFromEmptyStr); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize instance of `java.time.LocalDateTime` out of "); + } + } + + /* + /********************************************************** + /* Tests for alternate array handling + /********************************************************** + */ + + @Test + public void testDeserializationAsArrayDisabled() throws Throwable + { + try { + READER.readValue("[\"2000-01-01T12:00\"]"); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (VALUE_STRING) within Array"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() throws Throwable + { + // works even without the feature enabled + assertNull(READER.readValue("[]")); + } + + @Test + public void testDeserializationAsArrayEnabled() throws Throwable + { + LocalDateTime value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[\"2000-01-01T12:00\"]"); + assertEquals(LocalDateTime.of(2000, 1, 1, 12, 0, 0, 0), + value, "The value is not correct."); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + LocalDateTime value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + /* + /********************************************************** + /* Tests for polymorphic handling + /********************************************************** + */ + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + final ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,829837]]"); + assertTrue(value instanceof LocalDateTime, "The value should be a LocalDateTime."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 422000000); + + final ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,422]]"); + assertTrue(value instanceof LocalDateTime, "The value should be a LocalDateTime."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + final ObjectMapper m = newMapperBuilder(). + addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + LocalDateTime.class.getName() + "\",\"" + time.toString() + "\"]", Temporal.class + ); + assertTrue(value instanceof LocalDateTime, "The value should be a LocalDateTime."); + assertEquals(time, value, "The value is not correct."); + } + + /* + /********************************************************** + /* Tests for `DeserialiazationProblemHandler` usage + /********************************************************** + */ + + @Test + public void testDateTimeExceptionIsHandled() throws Throwable + { + LocalDateTime now = LocalDateTime.now(); + DeserializationProblemHandler handler = new DeserializationProblemHandler() { + @Override + public Object handleWeirdStringValue(DeserializationContext ctxt, Class targetType, + String valueToConvert, String failureMsg) { + if (LocalDateTime.class == targetType) { + if ("now".equals(valueToConvert)) { + return now; + } + } + return NOT_HANDLED; + } + }; + ObjectMapper handledMapper = mapperBuilder().addHandler(handler).build(); + assertEquals(now, handledMapper.readValue(q("now"), LocalDateTime.class)); + } + + @Test + public void testUnexpectedTokenIsHandled() throws Throwable + { + LocalDateTime now = LocalDateTime.now(); + DeserializationProblemHandler handler = new DeserializationProblemHandler() { + @Override + public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType, + JsonToken t, JsonParser p, String failureMsg) { + if (targetType.hasRawClass(LocalDateTime.class)) { + if (t.isBoolean()) { + return now; + } + } + return NOT_HANDLED; + } + }; + ObjectMapper handledMapper = mapperBuilder().addHandler(handler).build(); + assertEquals(now, handledMapper.readValue("true", LocalDateTime.class)); + } + + /* + /********************************************************** + /* Tests for specific reported issues + /********************************************************** + */ + + // [datatype-jrs310#54] + @Test + public void testDeserializeToDate() throws Exception + { + ObjectMapper m = newMapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + String localDateTimeJson = m.writeValueAsString(LocalDateTime.of(1999,10,12,13,45,5)); + assertEquals("\"1999-10-12T13:45:05\"", localDateTimeJson); + Date date = m.readValue(localDateTimeJson,Date.class); + assertNotNull(date); + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(date.getTime()); + assertEquals(1999, cal.get(Calendar.YEAR)); + assertEquals(12, cal.get(Calendar.DAY_OF_MONTH)); + assertEquals(13, cal.get(Calendar.HOUR_OF_DAY)); + assertEquals(45, cal.get(Calendar.MINUTE)); + assertEquals(5, cal.get(Calendar.SECOND)); + } + + // [modules-java8#47]: should indicate why timestamp won't work + @Test + public void testDeserilizeFromSimpleTimestamp() throws Exception + { + ObjectReader r = MAPPER.readerFor(LocalDateTime.class); + LocalDateTime value; + try { + value = r.readValue("1235"); + fail("Should not succeed, instead got: "+value); + } catch (MismatchedInputException e) { + verifyException(e, "raw timestamp (1235) not allowed for `java.time.LocalDateTime`"); + } + } + + /* + /********************************************************************** + /* Case-insensitive tests + /********************************************************************** + */ + + // [modules-java8#80]: handle case-insensitive date/time + @Test + public void testDeserializationCaseInsensitiveEnabledOnValue() throws Throwable + { + final ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(LocalDateTime.class, o -> o.setFormat(JsonFormat.Value + .forPattern("dd-MMM-yyyy HH:mm") + .withFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES))) + .build(); + ObjectReader reader = mapper.readerFor(LocalDateTime.class); + String[] jsons = new String[] {"'01-Jan-2000 13:14'","'01-JAN-2000 13:14'", "'01-jan-2000 13:14'"}; + for(String json : jsons) { + expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 14), json); + } + } + + @Test + public void testDeserializationCaseInsensitiveEnabled() throws Throwable + { + final ObjectMapper mapper = newMapperBuilder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, true) + .withConfigOverride(LocalDateTime.class, o -> o.setFormat( + JsonFormat.Value.forPattern("dd-MMM-yyyy HH:mm"))) + .build(); + ObjectReader reader = mapper.readerFor(LocalDateTime.class); + String[] jsons = new String[] {"'01-Jan-2000 13:45'","'01-JAN-2000 13:45'", "'01-jan-2000 13:45'"}; + for(String json : jsons) { + expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 45), json); + } + } + + @Test + public void testDeserializationCaseInsensitiveDisabled() throws Throwable + { + final ObjectMapper mapper = newMapperBuilder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false) + .withConfigOverride(LocalDateTime.class, o -> o.setFormat( + JsonFormat.Value.forPattern("dd-MMM-yyyy HH:mm"))) + .build(); + ObjectReader reader = mapper.readerFor(LocalDateTime.class); + expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 45), + q("01-Jan-2000 13:45")); + } + + @Test + public void testDeserializationCaseInsensitiveDisabled_InvalidDate() throws Throwable + { + final ObjectMapper mapper = newMapperBuilder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false) + .withConfigOverride(LocalDateTime.class, o -> o.setFormat( + JsonFormat.Value.forPattern("dd-MMM-yyyy"))) + .build(); + ObjectReader reader = mapper.readerFor(LocalDateTime.class); + String[] jsons = new String[] {"'01-JAN-2000'", "'01-jan-2000'"}; + for(String json : jsons) { + try { + reader.readValue(a2q(json)); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Failed to deserialize `java.time.LocalDateTime` (with format"); + } + } + } + + /* + /********************************************************************** + /* Strict JsonFormat tests + /********************************************************************** + */ + + // [modules-java8#148]: handle strict deserializaiton for date/time + @Test + public void testStrictCustomFormatForInvalidFormat() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapper w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 15:45\"}", StrictWrapper.class)); + } + + @Test + public void testStrictCustomFormatForInvalidFormatWithEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 15:45\"}", StrictWrapperWithYearOfEra.class)); + } + + @Test + public void testStrictCustomFormatForInvalidDateWithEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 15:45 AD\"}", StrictWrapperWithYearOfEra.class)); + } + + @Test + public void testStrictCustomFormatForInvalidTimeWithEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 25:45 AD\"}", StrictWrapperWithYearOfEra.class)); + } + + @Test + public void testStrictCustomFormatForInvalidDateAndTimeWithEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 25:45 AD\"}", StrictWrapperWithYearOfEra.class)); + + } + + @Test + public void testStrictCustomFormatValidDateAndTimeWithEra() throws Exception + { + StrictWrapperWithYearOfEra w = MAPPER.readValue("{\"value\":\"2019-11-30 20:45 AD\"}", StrictWrapperWithYearOfEra.class); + + assertEquals(w.value, LocalDateTime.of(2019, 11, 30, 20, 45)); + } + + @Test + public void testStrictCustomFormatForInvalidFormatWithoutEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 15:45 AD\"}", StrictWrapperWithYearWithoutEra.class)); + } + + @Test + public void testStrictCustomFormatForInvalidTimeWithoutEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 25:45\"}", StrictWrapperWithYearWithoutEra.class)); + } + + @Test + public void testStrictCustomFormatForInvalidDateWithoutEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 15:45\"}", StrictWrapperWithYearWithoutEra.class)); + } + + @Test + public void testStrictCustomFormatForInvalidDateAndTimeWithoutEra() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 25:45\"}", StrictWrapperWithYearWithoutEra.class)); + } + + @Test + public void testStrictCustomFormatForValidDateAndTimeWithoutEra() throws Exception + { + StrictWrapperWithYearWithoutEra w = MAPPER.readValue("{\"value\":\"2019-11-30 20:45\"}", + StrictWrapperWithYearWithoutEra.class); + + assertEquals(w.value, LocalDateTime.of(2019, 11, 30, 20, 45)); + } + + // [datatype-jsr310#124] Issue serializing and deserializing LocalDateTime.MAX and LocalDateTime.MIN + @Test + public void testDeserializationOfLocalDateTimeMax() throws Exception + { + ObjectMapper enabledMapper = mapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build(); + _testLocalDateTimeRoundTrip(enabledMapper, LocalDateTime.MAX); + _testLocalDateTimeRoundTrip(enabledMapper, LocalDateTime.MIN); + + ObjectMapper disabledMapper = mapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build(); + _testLocalDateTimeRoundTrip(disabledMapper, LocalDateTime.MAX); + _testLocalDateTimeRoundTrip(disabledMapper, LocalDateTime.MIN); + } + + private void _testLocalDateTimeRoundTrip(ObjectMapper mapper, LocalDateTime localDateTime) + throws Exception + { + String ser = mapper.writeValueAsString(localDateTime); + LocalDateTime result = mapper.readValue(ser, LocalDateTime.class); + assertEquals(localDateTime, result); + } + + private void expectSuccess(ObjectReader reader, Object exp, String json) throws IOException { + final LocalDateTime value = reader.readValue(a2q(json)); + assertNotNull(value, "The value should not be null."); + assertEquals(exp, value, "The value is not correct."); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserTest.java new file mode 100644 index 0000000000..da8c801086 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserTest.java @@ -0,0 +1,343 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.LocalTime; +import java.time.temporal.Temporal; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; +import com.fasterxml.jackson.annotation.OptBoolean; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalTimeDeserTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(LocalTime.class); + + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + final static class StrictWrapper { + @JsonFormat(pattern="HH:mm", lenient = OptBoolean.FALSE) + public LocalTime value; + + public StrictWrapper() { } + public StrictWrapper(LocalTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosDisabled { + @JsonFormat( + without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public LocalTime value; + + public WrapperWithReadTimestampsAsNanosDisabled() { } + public WrapperWithReadTimestampsAsNanosDisabled(LocalTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosEnabled { + @JsonFormat( + with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public LocalTime value; + + public WrapperWithReadTimestampsAsNanosEnabled() { } + public WrapperWithReadTimestampsAsNanosEnabled(LocalTime v) { value = v; } + } + + @Test + public void testDeserializationAsTimestamp01() throws Exception + { + LocalTime time = LocalTime.of(15, 43); + LocalTime value = READER.readValue("[15,43]"); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp02() throws Exception + { + LocalTime time = LocalTime.of(9, 22, 57); + LocalTime value = READER.readValue("[9,22,57]"); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp03Nanoseconds() throws Exception + { + LocalTime value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[9,22,0,57]"); + assertEquals(LocalTime.of(9, 22, 0, 57), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp03Milliseconds() throws Exception + { + LocalTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[9,22,0,57]"); + assertEquals(LocalTime.of(9, 22, 0, 57000000), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Nanoseconds() throws Exception + { + LocalTime value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[22,31,5,829837]"); + assertEquals(LocalTime.of(22, 31, 5, 829837), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Milliseconds01() throws Exception + { + LocalTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[22,31,5,829837]"); + assertEquals(LocalTime.of(22, 31, 5, 829837), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Milliseconds02() throws Exception + { + LocalTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[22,31,5,829]"); + assertEquals(LocalTime.of(22, 31, 5, 829000000), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Nanoseconds() throws Exception + { + ObjectReader wrapperReader = + newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class); + WrapperWithReadTimestampsAsNanosEnabled actual = wrapperReader + .readValue(a2q("{'value':[9,22,0,57]}")); + assertEquals(LocalTime.of(9, 22, 0, 57), actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Milliseconds01() throws Exception + { + ObjectReader wrapperReader = + newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + WrapperWithReadTimestampsAsNanosDisabled actual = wrapperReader + .readValue(a2q("{'value':[9,22,0,57]}")); + assertEquals(LocalTime.of(9, 22, 0, 57000000), actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Milliseconds02() throws Exception + { + ObjectReader wrapperReader = + newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + WrapperWithReadTimestampsAsNanosDisabled actual = wrapperReader + .readValue(a2q("{'value':[9,22,0,4257]}")); + assertEquals(LocalTime.of(9, 22, 0, 4257), actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationFromString() throws Exception + { + LocalTime time = LocalTime.of(15, 43); + LocalTime value = READER.readValue('"' + time.toString() + '"'); + assertEquals(time, value, "The value is not correct."); + + expectSuccess(LocalTime.of(12, 0), "'12:00'"); + + time = LocalTime.of(9, 22, 57); + value = READER.readValue('"' + time.toString() + '"'); + assertEquals(time, value, "The value is not correct."); + + time = LocalTime.of(22, 31, 5, 829837); + value = READER.readValue('"' + time.toString() + '"'); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testBadDeserializationFromString() throws Throwable + { + try { + READER.readValue(q("notalocaltime")); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.LocalTime` from String"); + } + } + + @Test + public void testDeserializationAsArrayDisabled() throws Throwable + { + try { + READER.readValue(a2q("['12:00']")); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (VALUE_STRING) within Array"); + } + + // 25-Jul-2017, tatu: Why does it work? Is it supposed to? + // works even without the feature enabled + assertNull(READER.readValue("[]")); + } + + @Test + public void testDeserializationAsArrayEnabled() throws Throwable + { + LocalTime value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue(a2q("['12:00']")); + expect(LocalTime.of(12, 0), value); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + LocalTime value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, + DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue(a2q("[]")); + assertNull(value); + } + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 829837); + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readerFor(Temporal.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[\"" + LocalTime.class.getName() + "\",[22,31,5,829837]]"); + + assertNotNull(value, "The value should not be null."); + assertTrue(value instanceof LocalTime, "The value should be a LocalTime."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 422000000); + + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readerFor(Temporal.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[\"" + LocalTime.class.getName() + "\",[22,31,5,422]]"); + assertTrue(value instanceof LocalTime, "The value should be a LocalTime."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 829837); + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readValue( + "[\"" + LocalTime.class.getName() + "\",\"" + time.toString() + "\"]", Temporal.class + ); + assertTrue(value instanceof LocalTime, "The value should be a LocalTime."); + assertEquals(time, value, "The value is not correct."); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "localTime"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String dateValAsEmptyStr = ""; + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + LocalTime actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr)); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + LocalTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "localTime"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(LocalTime.class, + c -> c.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", "")); + assertThrows(MismatchedInputException.class, + () -> objectReader.readValue(valueFromEmptyStr)); + } + + /* + /********************************************************************** + /* Strict JsonFormat tests + /********************************************************************** + */ + + // [modules-java8#148]: handle strict deserializaiton for date/time + + @Test + public void testStrictCustomFormatInvalidTime() throws Exception + { + assertThrows(InvalidFormatException.class, + () -> /*StrictWrapper w =*/ MAPPER.readValue("{\"value\":\"25:45\"}", StrictWrapper.class)); + } + + private void expectSuccess(Object exp, String aposJson) throws Exception { + final LocalTime value = READER.readValue(a2q(aposJson)); + expect(exp, value); + } + + private static void expect(Object exp, Object value) { + assertEquals(exp, value, "The value is not correct."); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserTest.java new file mode 100644 index 0000000000..b70a03c448 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserTest.java @@ -0,0 +1,211 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Month; +import java.time.MonthDay; +import java.time.temporal.TemporalAccessor; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class MonthDayDeserTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(MonthDay.class); + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + static class Wrapper { + @JsonFormat(pattern="MM/dd") + public MonthDay value; + + public Wrapper(MonthDay v) { value = v; } + public Wrapper() { } + } + + static class WrapperAsArray { + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public MonthDay value; + + public WrapperAsArray(MonthDay v) { value = v; } + public WrapperAsArray() { } + } + + + @Test + public void testDeserializationAsString01() throws Exception + { + expectSuccess(MonthDay.of(Month.JANUARY, 1), "'--01-01'"); + } + + @Test + public void testBadDeserializationAsString01() throws Throwable + { + try { + READER.readValue(q("notamonthday")); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.MonthDay` from String"); + } + } + + @Test + public void testDeserializationAsArrayDisabled() throws Throwable + { + try { + read("['--01-01']"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + // expecting array-of-ints + verifyException(e, "Unexpected token"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() throws Throwable + { + // since 2.10, empty array taken as `null` + + MonthDay value = READER.readValue("[]"); + assertNull(value); + + value = newMapper() + .readerFor(MonthDay.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[]"); + assertNull(value); + } + + @Test + public void testDeserializationAsArrayEnabled() throws Throwable + { + MonthDay value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue(a2q("['--01-01']")); + expect(MonthDay.of(Month.JANUARY, 1), value); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + MonthDay value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + @Test + public void testDeserialization01() throws Exception + { + assertEquals(MonthDay.of(Month.JANUARY, 17), MAPPER.readValue("\"--01-17\"", MonthDay.class), + "The value is not correct."); + } + + @Test + public void testDeserialization02() throws Exception + { + assertEquals(MonthDay.of(Month.AUGUST, 21), + MAPPER.readValue("\"--08-21\"", MonthDay.class), + "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + final ObjectMapper mapper = mapperBuilder() + .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class) + .build(); + MonthDay monthDay = MonthDay.of(Month.NOVEMBER, 5); + TemporalAccessor value = mapper.readValue("[\"" + MonthDay.class.getName() + "\",\"--11-05\"]", TemporalAccessor.class); + assertEquals(monthDay, value, "The value is not correct."); + } + + @Test + public void testFormatAnnotation() throws Exception + { + final Wrapper input = new Wrapper(MonthDay.of(12, 28)); + String json = MAPPER.writeValueAsString(input); + assertEquals("{\"value\":\"12/28\"}", json); + + Wrapper output = MAPPER.readValue(json, Wrapper.class); + assertEquals(input.value, output.value); + } + + @Test + public void testFormatAnnotationArray() throws Exception + { + final WrapperAsArray input = new WrapperAsArray(MonthDay.of(12, 28)); + String json = MAPPER.writeValueAsString(input); + assertEquals("{\"value\":[12,28]}", json); + + // 13-May-2019, tatu: [modules-java#107] not fully implemented so can't yet test + WrapperAsArray output = MAPPER.readValue(json, WrapperAsArray.class); + assertEquals(input.value, output.value); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + // minor changes in 2.12 + @Test + public void testDeserializeFromEmptyString() throws Exception + { + final String key = "monthDay"; + + // First: by default, lenient, so empty String fine + final ObjectReader objectReader = MAPPER.readerFor(MAP_TYPE_REF); + + String doc = MAPPER.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(doc); + assertNull(actualMapFromNullStr.get(key)); + + doc = MAPPER.writeValueAsString(asMap(key, "")); + assertNotNull(objectReader.readValue(doc)); + + // But can make strict: + final ObjectMapper strictMapper = mapperBuilder() + .withConfigOverride(MonthDay.class, o -> o.setFormat( + JsonFormat.Value.forLeniency(false))) + .build(); + doc = strictMapper.writeValueAsString(asMap("date", "")); + try { + strictMapper.readerFor(MAP_TYPE_REF) + .readValue(doc); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "not allowed because 'strict' mode set for"); + } + } + + private void expectSuccess(Object exp, String aposJson) throws Exception { + final MonthDay value = read(aposJson); + notNull(value); + expect(exp, value); + } + + private MonthDay read(final String aposJson) throws Exception { + return READER.readValue(a2q(aposJson)); + } + + private static void notNull(Object value) { + assertNotNull(value, "The value should not be null."); + } + + private static void expect(Object exp, Object value) { + assertEquals(exp, value, "The value is not correct."); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetDateTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetDateTimeDeserTest.java new file mode 100644 index 0000000000..71362dd509 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetDateTimeDeserTest.java @@ -0,0 +1,824 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.Arrays; +import java.util.Map; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; + +import static org.junit.jupiter.api.Assertions.*; + +public class OffsetDateTimeDeserTest + extends DateTimeTestBase +{ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + private static final ZoneId Z1 = ZoneId.of("America/Chicago"); + + private static final ZoneId Z2 = ZoneId.of("America/Anchorage"); + + private static final ZoneId Z3 = ZoneId.of("America/Los_Angeles"); + + final static class Wrapper { + @JsonFormat( + pattern="yyyy_MM_dd'T'HH:mm:ssZ", + shape=JsonFormat.Shape.STRING) + public OffsetDateTime value; + + public Wrapper() { } + public Wrapper(OffsetDateTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosDisabled { + @JsonFormat( + without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public OffsetDateTime value; + + public WrapperWithReadTimestampsAsNanosDisabled() { } + public WrapperWithReadTimestampsAsNanosDisabled(OffsetDateTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosEnabled { + @JsonFormat( + with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public OffsetDateTime value; + + public WrapperWithReadTimestampsAsNanosEnabled() { } + public WrapperWithReadTimestampsAsNanosEnabled(OffsetDateTime v) { value = v; } + } + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectMapper MAPPER_DEFAULT_TZ = newMapper(TimeZone.getDefault()); + + @Test + public void testDeserializationAsFloat01WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER.readValue("0.000000000", OffsetDateTime.class); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat01WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readValue("0.000000000", OffsetDateTime.class); + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat02WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + OffsetDateTime value = MAPPER.readValue("123456789.183917322", OffsetDateTime.class); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat02WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(TimeZone.getDefault()) + .readValue("123456789.183917322"); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat03WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + + OffsetDateTime value = MAPPER + .readerFor(OffsetDateTime.class) + .readValue(DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano())); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat03WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .readValue(DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano())); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01NanosecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01NanosecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .with(TimeZone.getDefault()) + .readValue("0"); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01MillisecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01MillisecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02NanosecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789"); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02NanosecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789"); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02MillisecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789422"); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02MillisecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789422"); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03NanosecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toEpochSecond())); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03NanosecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toEpochSecond())); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03MillisecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + date = date.minus(date.getNano() - (date.get(ChronoField.MILLI_OF_SECOND) * 1_000_000L), ChronoUnit.NANOS); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toInstant().toEpochMilli())); + + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03MillisecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + date = date.minus(date.getNano() - (date.get(ChronoField.MILLI_OF_SECOND) * 1_000_000L), ChronoUnit.NANOS); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toInstant().toEpochMilli())); + + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt04NanosecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z1); + ObjectMapper m = newMapper(); + WrapperWithReadTimestampsAsNanosEnabled actual = m.readValue( + a2q("{'value':123456789}"), + WrapperWithReadTimestampsAsNanosEnabled.class); + + assertNotNull(actual, "The actual should not be null."); + assertNotNull(actual.value, "The actual value should not be null."); + assertIsEqual(date, actual.value); + assertEquals(ZoneOffset.UTC, actual.value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt04NanosecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z1); + ObjectMapper m = mapperBuilder() + .defaultTimeZone(TimeZone.getDefault()) + .build(); + WrapperWithReadTimestampsAsNanosEnabled actual = m.readValue( + a2q("{'value':123456789}"), + WrapperWithReadTimestampsAsNanosEnabled.class); + + assertNotNull(actual, "The actual should not be null."); + assertNotNull(actual.value, "The actual value should not be null."); + assertIsEqual(date, actual.value); + assertEquals(getDefaultOffset(date), actual.value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt04MillisecondsWithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z1); + ObjectMapper m = newMapper(); + WrapperWithReadTimestampsAsNanosDisabled actual = m.readValue( + a2q("{'value':123456789422}"), + WrapperWithReadTimestampsAsNanosDisabled.class); + + assertNotNull(actual, "The actual should not be null."); + assertNotNull(actual.value, "The actual value should not be null."); + assertIsEqual(date, actual.value); + assertEquals(ZoneOffset.UTC, actual.value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt04MillisecondsWithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z1); + ObjectMapper m = mapperBuilder() + .defaultTimeZone(TimeZone.getDefault()) + .build(); + WrapperWithReadTimestampsAsNanosDisabled actual = m.readValue( + a2q("{'value':123456789422}"), + WrapperWithReadTimestampsAsNanosDisabled.class); + + assertNotNull(actual, "The actual should not be null."); + assertNotNull(actual.value, "The actual value should not be null."); + assertIsEqual(date, actual.value); + assertEquals(getDefaultOffset(date), actual.value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithTimeZoneTurnedOff() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(getOffset(value, Z1), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithTimeZoneColonless() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ObjectReader r = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + + String sDate = offsetWithoutColon(FORMATTER.format(date)); + + OffsetDateTime value = r.readValue('"' + sDate + '"'); + + assertIsEqual(date, value); + assertEquals(getOffset(value, Z1), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString02WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString02WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString02WithTimeZoneTurnedOff() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + + assertIsEqual(date, value); + assertEquals(getOffset(value, Z2), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString02WithTimeZoneColonless() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ObjectReader r = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + + String sDate = offsetWithoutColon(FORMATTER.format(date)); + + OffsetDateTime value = r.readValue('"' + sDate + '"'); + + assertIsEqual(date, value); + assertEquals(getOffset(value, Z2), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString03WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString03WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString03WithTimeZoneTurnedOff() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(getOffset(value, Z3), value.getOffset(), "The time zone is not correct."); + } + + + @Test + public void testDeserializationAsString03WithTimeZoneColonless() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + ObjectReader r = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + + String sDate = offsetWithoutColon(FORMATTER.format(date)); + + OffsetDateTime value = r.readValue('"' + sDate + '"'); + + assertIsEqual(date, value); + assertEquals(getOffset(value, Z3), value.getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo01WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + OffsetDateTime.class.getName() + "\",123456789.183917322]", Temporal.class + ); + + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo01WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ObjectMapper m = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + OffsetDateTime.class.getName() + "\",123456789.183917322]", Temporal.class + ); + + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + OffsetDateTime.class.getName() + "\",123456789]"); + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + ObjectMapper m = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + OffsetDateTime.class.getName() + "\",123456789]"); + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + OffsetDateTime.class.getName() + "\",123456789422]"); + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + ObjectMapper m = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + OffsetDateTime.class.getName() + "\",123456789422]"); + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo04WithoutTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue( + "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]"); + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo04WithTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + ObjectMapper m = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue( + "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]"); + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + assertIsEqual(date, (OffsetDateTime) value); + assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo04WithTimeZoneTurnedOff() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + + ObjectMapper m = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readerFor(Temporal.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue( + "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]"); + + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + OffsetDateTime cast = (OffsetDateTime) value; + assertIsEqual(date, cast); + assertEquals(getOffset(cast, Z3), cast.getOffset(), "The time zone is not correct."); + } + + @Test + public void testCustomPatternWithAnnotations() throws Exception + { + OffsetDateTime inputValue = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), UTC); + final Wrapper input = new Wrapper(inputValue); + String json = MAPPER.writeValueAsString(input); + assertEquals(a2q("{'value':'1970_01_01T00:00:00+0000'}"), json); + + Wrapper result = MAPPER.readValue(json, Wrapper.class); + assertEquals(input.value, result.value); + } + + // [datatype-jsr310#79] + @Test + public void testRoundTripOfOffsetDateTimeAndJavaUtilDate() throws Exception + { + Instant givenInstant = LocalDate.of(2016, 1, 1).atStartOfDay().atZone(ZoneOffset.UTC).toInstant(); + + String json = MAPPER.writer() + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, + SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(java.util.Date.from(givenInstant)); + OffsetDateTime actual = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(json); + assertEquals(givenInstant.atOffset(ZoneOffset.UTC), actual); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "OffsetDateTime"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + OffsetDateTime actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + OffsetDateTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "OffsetDateTime"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(OffsetDateTime.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr)); + } + + // [module-java8#166] + @Test + public void testDeserializationNoAdjustIfMIN() throws Exception + { + OffsetDateTime date = OffsetDateTime.MIN; + ObjectMapper m = mapperBuilder() + .enable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .defaultTimeZone(TimeZone.getTimeZone(Z1)) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", Temporal.class + ); + + assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime."); + OffsetDateTime actualValue = (OffsetDateTime) value; + assertIsEqual(date, actualValue); + assertEquals(date.getOffset(),actualValue.getOffset()); + } + + @Test + public void testDeserializationNoAdjustIfMAX() throws Exception + { + OffsetDateTime date = OffsetDateTime.MAX; + ObjectMapper m = mapperBuilder() + .enable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .defaultTimeZone(TimeZone.getTimeZone(Z1)) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = m.readValue( + "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", Temporal.class + ); + + assertInstanceOf(OffsetDateTime.class, value, "The value should be an OffsetDateTime."); + OffsetDateTime actualValue = (OffsetDateTime) value; + assertIsEqual(date, actualValue); + assertEquals(date.getOffset(),actualValue.getOffset()); + } + + // [jackson-modules-java8#308] Can't deserialize OffsetDateTime.MIN: Invalid value for EpochDay + @Test + public void testOffsetDateTimeMinOrMax() throws Exception + { + _testOffsetDateTimeMinOrMax(OffsetDateTime.MIN); + _testOffsetDateTimeMinOrMax(OffsetDateTime.MAX); + } + + @Test + public void OffsetDateTime_with_offset_can_be_deserialized() throws Exception { + ObjectReader r = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + + String base = "2015-07-24T12:23:34.184"; + for (String offset : Arrays.asList("+00", "-00")) { + String time = base + offset; + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + '"')); + } + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + "00" + '"')); + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + ":00" + '"')); + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30"), r.readValue('"' + time + "30" + '"')); + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30"), r.readValue('"' + time + ":30" + '"')); + } + + for (String prefix : Arrays.asList("-", "+")) { + for (String hours : Arrays.asList("00", "01", "02", "03", "11", "12")) { + String time = base + prefix + hours; + OffsetDateTime expectedHour = OffsetDateTime.parse(time + ":00"); + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertIsEqual(expectedHour, r.readValue('"' + time + '"')); + } + assertIsEqual(expectedHour, r.readValue('"' + time + "00" + '"')); + assertIsEqual(expectedHour, r.readValue('"' + time + ":00" + '"')); + assertIsEqual(OffsetDateTime.parse(time + ":30"), r.readValue('"' + time + "30" + '"')); + assertIsEqual(OffsetDateTime.parse(time + ":30"), r.readValue('"' + time + ":30" + '"')); + } + } + } + + private void _testOffsetDateTimeMinOrMax(OffsetDateTime offsetDateTime) + throws Exception + { + String ser = MAPPER.writeValueAsString(offsetDateTime); + OffsetDateTime result = MAPPER.readValue(ser, OffsetDateTime.class); + assertIsEqual(offsetDateTime, result); + } + + private static void assertIsEqual(OffsetDateTime expected, OffsetDateTime actual) + { + assertTrue(expected.isEqual(actual), + "The value is not correct. Expected timezone-adjusted <" + expected + ">, actual <" + actual + ">."); + } + + private static ZoneOffset getDefaultOffset(OffsetDateTime date) + { + return ZoneId.systemDefault().getRules().getOffset(date.toLocalDateTime()); + } + + private static ZoneOffset getOffset(OffsetDateTime date, ZoneId zone) + { + return zone.getRules().getOffset(date.toLocalDateTime()); + } + + private static String offsetWithoutColon(String string){ + return new StringBuilder(string).deleteCharAt(string.lastIndexOf(":")).toString(); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserTest.java new file mode 100644 index 0000000000..ea8cd44f23 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserTest.java @@ -0,0 +1,349 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.temporal.Temporal; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class OffsetTimeDeserTest extends DateTimeTestBase +{ + + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + // for [datatype-jsr310#45] + static class Pojo45s { + public String name; + public List objects; + } + + static class Pojo45 { + public java.time.LocalDate partDate; + public java.time.OffsetTime starttime; + public java.time.OffsetTime endtime; + public String comments; + } + + static class WrapperWithReadTimestampsAsNanosDisabled { + @JsonFormat( + without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public OffsetTime value; + + public WrapperWithReadTimestampsAsNanosDisabled() { } + public WrapperWithReadTimestampsAsNanosDisabled(OffsetTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosEnabled { + @JsonFormat( + with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public OffsetTime value; + + public WrapperWithReadTimestampsAsNanosEnabled() { } + public WrapperWithReadTimestampsAsNanosEnabled(OffsetTime v) { value = v; } + } + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(OffsetTime.class); + + @Test + public void testDeserializationAsTimestamp01() throws Exception + { + OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300")); + OffsetTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[15,43,\"+0300\"]"); + + assertNotNull(value, "The value should not be null."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp02() throws Exception + { + OffsetTime time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630")); + OffsetTime value = READER.readValue("[9,22,57,\"-06:30\"]"); + + assertNotNull(value, "The value should not be null."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp03Nanoseconds() throws Exception + { + OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630")); + OffsetTime value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[9,22,0,57,\"-06:30\"]"); + + assertNotNull(value, "The value should not be null."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp03Milliseconds() throws Exception { + OffsetTime time = OffsetTime.of(9, 22, 0, 57000000, ZoneOffset.of("-0630")); + OffsetTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[9,22,0,57,\"-06:30\"]"); + + assertNotNull(value, "The value should not be null."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Nanoseconds() throws Exception { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + OffsetTime value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[22,31,5,829837,\"+11:00\"]"); + + assertNotNull(value, "The value should not be null."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Milliseconds01() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + OffsetTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[22,31,5,829837,\"+11:00\"]"); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp04Milliseconds02() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829000000, ZoneOffset.of("+1100")); + OffsetTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[22,31,5,829,\"+11:00\"]"); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Nanoseconds() throws Exception + { + ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class); + OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630")); + WrapperWithReadTimestampsAsNanosEnabled actual = reader + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(a2q("{'value':[9,22,0,57,'-06:30']}")); + + assertNotNull(actual, "The value should not be null."); + assertEquals(time, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Milliseconds01() throws Exception + { + ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + OffsetTime time = OffsetTime.of(9, 22, 0, 57000000, ZoneOffset.of("-0630")); + WrapperWithReadTimestampsAsNanosDisabled actual = reader + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(a2q("{'value':[9,22,0,57,'-06:30']}")); + + assertNotNull(actual, "The value should not be null."); + assertEquals(time, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsTimestamp05Milliseconds02() throws Exception + { + ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + OffsetTime time = OffsetTime.of(9, 22, 0, 4257, ZoneOffset.of("-0630")); + WrapperWithReadTimestampsAsNanosDisabled actual = reader + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(a2q("{'value':[9,22,0,4257,'-06:30']}")); + + assertNotNull(actual, "The value should not be null."); + assertEquals(time, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationFromString01() throws Exception + { + OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300")); + OffsetTime value = READER.readValue('"' + time.toString() + '"'); + assertEquals(time, value, "The value is not correct."); + + time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630")); + value = READER.readValue('"' + time.toString() + '"'); + assertEquals(time, value, "The value is not correct."); + + time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + value = READER.readValue('"' + time.toString() + '"'); + assertEquals(time, value, "The value is not correct."); + + assertEquals(OffsetTime.of(12, 0, 0, 0, ZoneOffset.UTC), + READER.readValue(a2q("'12:00Z'"))); + } + + @Test + public void testBadDeserializationFromString01() throws Throwable + { + try { + READER.readValue(q("notanoffsettime")); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.OffsetTime` from String"); + } + } + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + + final ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readerFor(Temporal.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + OffsetTime.class.getName() + "\",[22,31,5,829837,\"+11:00\"]]"); + assertInstanceOf(OffsetTime.class, value, "The value should be a OffsetTime."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 422000000, ZoneOffset.of("+1100")); + + final ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readerFor(Temporal.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[\"" + OffsetTime.class.getName() + "\",[22,31,5,422,\"+11:00\"]]"); + + assertNotNull(value, "The value should not be null."); + assertInstanceOf(OffsetTime.class, value, "The value should be a OffsetTime."); + assertEquals(time, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + + final ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readValue( + "[\"" + OffsetTime.class.getName() + "\",\"" + time.toString() + "\"]", Temporal.class + ); + assertTrue(value instanceof OffsetTime, "The value should be a OffsetTime."); + assertEquals(time, value, "The value is not correct."); + } + + // for [datatype-jsr310#45] + @Test + public void testDeserOfArrayOf() throws Exception + { + final String JSON = a2q + ("{'name':'test','objects':[{'partDate':[2015,10,13],'starttime':[15,7,'+0'],'endtime':[2,14,'+0'],'comments':'in the comments'}]}"); + Pojo45s result = READER.forType(Pojo45s.class).readValue(JSON); + assertNotNull(result); + assertNotNull(result.objects); + assertEquals(1, result.objects.size()); + } + + @Test + public void testDeserializationAsArrayDisabled() throws Throwable + { + try { + READER.readValue(a2q("['12:00Z']")); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + // not the greatest error message... + verifyException(e, "Unexpected token (VALUE_STRING) within Array, expected"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() throws Throwable + { + // works even without the feature enabled + assertNull(READER.readValue(a2q("[]"))); + } + + @Test + public void testDeserializationAsArrayEnabled() throws Throwable + { + OffsetTime value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue(a2q("['12:00Z']")); + assertEquals(OffsetTime.of(12, 0, 0, 0, ZoneOffset.UTC), value); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + OffsetTime value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, + DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "OffsetTime"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + OffsetTime actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + OffsetTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "OffsetTime"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(OffsetTime.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr)); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java new file mode 100644 index 0000000000..c9dd95bb2b --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java @@ -0,0 +1,207 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Month; +import java.time.temporal.TemporalAccessor; + +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.cfg.CoercionInputShape; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.*; + +public class OneBasedMonthDeserTest extends DateTimeTestBase +{ + static class Wrapper { + public Month value; + + public Wrapper(Month v) { value = v; } + public Wrapper() { } + } + + @ParameterizedTest + @EnumSource(Month.class) + public void testDeserializationAsString01_oneBased(Month expectedMonth) throws Exception + { + int monthNum = expectedMonth.getValue(); + assertEquals(expectedMonth, readerForOneBased().readValue("\"" + monthNum + '"')); + } + + @ParameterizedTest + @EnumSource(Month.class) + public void testDeserializationAsString01_zeroBased(Month expectedMonth) throws Exception + { + int monthNum = expectedMonth.ordinal(); + assertEquals(expectedMonth, readerForZeroBased().readValue("\"" + monthNum + '"')); + } + + + @ParameterizedTest + @EnumSource(Month.class) + public void testDeserializationAsString02_oneBased(Month month) throws Exception + { + assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"')); + } + + @ParameterizedTest + @EnumSource(Month.class) + public void testDeserializationAsString02_zeroBased(Month month) throws Exception + { + assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"')); + } + + @ParameterizedTest + @CsvSource({ + "notamonth , 'Cannot deserialize value of type `java.time.Month` from String \"notamonth\": not one of the values accepted for Enum class:'", + "JANUAR , 'Cannot deserialize value of type `java.time.Month` from String \"JANUAR\": not one of the values accepted for Enum class:'", + "march , 'Cannot deserialize value of type `java.time.Month` from String \"march\": not one of the values accepted for Enum class:'", + "0 , 'Month number 0 not allowed for 1-based Month.'", + "13 , 'Month number 13 not allowed for 1-based Month.'", + }) + public void testBadDeserializationAsString01_oneBased(String monthSpec, String expectedMessage) { + String value = "\"" + monthSpec + '"'; + assertError( + () -> readerForOneBased().readValue(value), + InvalidFormatException.class, + expectedMessage + ); + } + + static void assertError(Executable codeToRun, Class expectedException, String expectedMessage) { + try { + codeToRun.execute(); + fail(String.format("Expecting %s, but nothing was thrown!", expectedException.getName())); + } catch (Throwable actualException) { + if (!expectedException.isInstance(actualException)) { + fail(String.format("Expecting exception of type %s, but %s was thrown instead", expectedException.getName(), actualException.getClass().getName())); + } + if (actualException.getMessage() == null || !actualException.getMessage().contains(expectedMessage)) { + fail(String.format("Expecting exception with message containing: '%s', but the actual error message was:'%s'", expectedMessage, actualException.getMessage())); + } + } + } + + + @Test + public void testDeserialization01_zeroBased() throws Exception + { + assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("1")); + } + + @Test + public void testDeserialization01_oneBased() throws Exception + { + assertEquals(Month.JANUARY, readerForOneBased().readValue("1")); + } + + @Test + public void testDeserialization02_zeroBased() throws Exception + { + assertEquals(Month.SEPTEMBER, readerForZeroBased().readValue("\"8\"")); + } + + @Test + public void testDeserialization02_oneBased() throws Exception + { + assertEquals(Month.AUGUST, readerForOneBased().readValue("\"8\"")); + } + + @Test + public void testDeserializationWithTypeInfo01_oneBased() throws Exception + { + ObjectMapper MAPPER = JsonMapper.builder() + .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class) + .enable(DateTimeFeature.ONE_BASED_MONTHS) + .build(); + + TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",11]", TemporalAccessor.class); + assertEquals(Month.NOVEMBER, value); + } + + @Test + public void testDeserializationWithTypeInfo01_zeroBased() throws Exception + { + ObjectMapper MAPPER = JsonMapper.builder() + .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class) + .disable(DateTimeFeature.ONE_BASED_MONTHS) + .build(); + + TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class); + assertEquals(Month.DECEMBER, value); + } + + @Test + public void testFormatAnnotation_zeroBased() throws Exception + { + Wrapper output = readerForZeroBased() + .forType(Wrapper.class) + .readValue("{\"value\":\"11\"}"); + assertEquals(new Wrapper(Month.DECEMBER).value, output.value); + } + + @Test + public void testFormatAnnotation_oneBased() throws Exception + { + Wrapper output = readerForOneBased() + .forType(Wrapper.class) + .readValue("{\"value\":\"11\"}"); + assertEquals(new Wrapper(Month.NOVEMBER).value, output.value); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testDeserializeFromEmptyString() throws Exception + { + final ObjectMapper mapper = newMapper(); + + // Nulls are handled in general way, not by deserializer so they are ok + Month m = mapper.readerFor(Month.class).readValue(" null "); + assertNull(m); + + // But coercion from empty String not enabled for Enums by default: + try { + mapper.readerFor(Month.class).readValue("\"\""); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce empty String"); + } + // But can allow coercion of empty String to, say, null + ObjectMapper emptyStringMapper = mapperBuilder() + .withCoercionConfig(Month.class, + h -> h.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull)) + .build(); + m = emptyStringMapper.readerFor(Month.class).readValue("\"\""); + assertNull(m); + } + + private ObjectReader readerForZeroBased() { + return JsonMapper.builder() + .disable(DateTimeFeature.ONE_BASED_MONTHS) + .build() + .readerFor(Month.class); + } + + private ObjectReader readerForOneBased() { + return JsonMapper.builder() + .enable(DateTimeFeature.ONE_BASED_MONTHS) + .build() + .readerFor(Month.class); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/PeriodDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/PeriodDeserTest.java new file mode 100644 index 0000000000..c915148553 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/PeriodDeserTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Period; +import java.time.temporal.TemporalAmount; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.cfg.CoercionInputShape; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.type.LogicalType; + +import static org.junit.jupiter.api.Assertions.*; + +public class PeriodDeserTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + @Test + public void testDeserialization01() throws Exception + { + Period period = Period.of(1, 6, 15); + Period value = MAPPER.readValue('"' + period.toString() + '"', Period.class); + assertEquals(period, value, "The value is not correct."); + } + + @Test + public void testDeserialization02() throws Exception + { + Period period = Period.of(0, 0, 21); + Period value = MAPPER.readValue('"' + period.toString() + '"', Period.class); + assertEquals(period, value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + Period period = Period.of(5, 1, 12); + + final ObjectMapper mapper = mapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .build(); + TemporalAmount value = mapper.readValue( + "[\"" + Period.class.getName() + "\",\"" + period.toString() + "\"]", TemporalAmount.class + ); + + assertNotNull(value, "The value should not be null."); + assertInstanceOf(Period.class, value, "The value should be a Period."); + assertEquals(period, value, "The value is not correct."); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "period"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + Period actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + Period actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "period"; + final ObjectMapper mapper = mapperBuilder() + .withCoercionConfig(LogicalType.DateTime, + cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail)) + .build(); + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", "")); + try { + objectReader.readValue(valueFromEmptyStr); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce empty String"); + } + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/YearDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearDeserTest.java new file mode 100644 index 0000000000..13a3272860 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearDeserTest.java @@ -0,0 +1,252 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.Year; +import java.time.temporal.Temporal; +import java.util.Map; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class YearDeserTest extends DateTimeTestBase +{ + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + static class FormattedYear { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "'Y'yyyy") + public Year value; + + protected FormattedYear() {} + public FormattedYear(Year year) { + value = year; + } + } + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(Year.class); + + @Test + public void testDeserializationAsString01() + { + assertEquals(Year.of(2000), READER.readValue(q("2000")), + "The value is not correct."); + } + + @Test + public void testBadDeserializationAsString01() + { + try { + READER.readValue(q("notayear")); + fail("expected DateTimeParseException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.Year` from String \"notayear\""); + } + } + + @Test + public void testDeserializationAsArrayDisabled() + { + try { + read("['2000']"); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.Year` from Array"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() + { + try { + read("[]"); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.Year` from Array"); + } + try { + READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[]"); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.Year` from Array"); + } + } + + @Test + public void testDeserializationAsArrayEnabled() throws Throwable + { + Year value= newMapper() + .readerFor(Year.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue(a2q("['2000']")); + assertEquals(Year.of(2000), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + Year value = newMapper() + .readerFor(Year.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + @Test + public void testDefaultDeserialization() throws Exception + { + Year value = READER.readValue("1986"); + assertEquals(Year.of(1986), value, "The value is not correct."); + value = READER.readValue("2013"); + assertEquals(Year.of(2013), value, "The value is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readValue("[\"" + Year.class.getName() + "\",2005]", Temporal.class); + assertTrue(value instanceof Year, "The value should be a Year."); + assertEquals(Year.of(2005), value, "The value is not correct."); + } + + @Test + public void testWithCustomFormat() throws Exception + { + FormattedYear input = new FormattedYear(Year.of(2018)); + String json = MAPPER.writeValueAsString(input); + assertEquals("{\"value\":\"Y2018\"}", json); + FormattedYear result = MAPPER.readValue(json, FormattedYear.class); + assertEquals(input.value, result.value); + } + + @Test + public void testWithFormatViaConfigOverride() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(Year.class, + vc -> vc.setFormat((JsonFormat.Value.forPattern("'X'yyyy")))) + .build(); + Year input = Year.of(2018); + String json = mapper.writeValueAsString(input); + assertEquals("\"X2018\"", json); + Year result = mapper.readValue(json, Year.class); + assertEquals(input, result); + } + + /* + /********************************************************** + /* Tests for specific issues + /********************************************************** + */ + + // [module-java8#78] + final static class ObjectTest { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "'Y'yyyy") + public Year value; + + protected ObjectTest() { } + public ObjectTest(Year y) { + value = y; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + ObjectTest other = (ObjectTest) o; + return Objects.equals(this.value, other.value); + } + + // stupid Javac 8 barfs on override missing?! + @Override + public int hashCode() { return 42; } + } + + // [module-java8#78] + @Test + public void testWithCustomFormat78() throws Exception + { + ObjectTest input = new ObjectTest(Year.of(2018)); + String json = MAPPER.writeValueAsString(input); + assertEquals("{\"value\":\"Y2018\"}", json); + ObjectTest result = MAPPER.readValue(json, ObjectTest.class); + assertEquals(input, result); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + // minor changes in 2.12 + @Test + public void testDeserializeFromEmptyString() throws Exception + { + final String key = "Year"; + final ObjectReader objectReader = MAPPER.readerFor(MAP_TYPE_REF); + + // First: by default, lenient, so empty String fine + String doc = MAPPER.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(doc); + assertNull(actualMapFromNullStr.get(key)); + + doc = MAPPER.writeValueAsString(asMap("date", "")); + Map actualMapFromEmptyStr = objectReader.readValue(doc); + assertNotNull(actualMapFromEmptyStr); + + // But can make strict: + final ObjectMapper strictMapper = mapperBuilder() + .withConfigOverride(Year.class, o -> o.setFormat( + JsonFormat.Value.forLeniency(false))) + .build(); + doc = strictMapper.writeValueAsString(asMap("date", "")); + try { + actualMapFromEmptyStr = strictMapper.readerFor(MAP_TYPE_REF) + .readValue(doc); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "not allowed because 'strict' mode set for"); + } + } + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + private Year read(final String json) { + return READER.readValue(a2q(json)); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserTest.java new file mode 100644 index 0000000000..19023dd701 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserTest.java @@ -0,0 +1,145 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.io.IOException; +import java.time.Month; +import java.time.YearMonth; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class YearMonthDeserTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(YearMonth.class); + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + @Test + public void testDeserializationAsString01() throws Exception + { + final YearMonth value = read("'2000-01'"); + assertEquals(YearMonth.of(2000, Month.JANUARY), value, + "The value is not correct"); + } + + @Test + public void testBadDeserializationAsString01() throws Exception + { + try { + read(q("notayearmonth")); + fail("expected DateTimeParseException"); + } catch (InvalidFormatException e) { + verifyException(e, "could not be parsed"); + } + } + + @Test + public void testDeserializationAsArrayDisabled() throws Exception + { + try { + read("['2000-01']"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, +"Unexpected token (`JsonToken.VALUE_STRING`), expected `JsonToken.VALUE_NUMBER_INT`"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() throws Exception + { + // works even without the feature enabled + assertNull(read("[]")); + } + + @Test + public void testDeserializationAsArrayEnabled() throws Exception + { + YearMonth value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue(a2q("['2000-01']")); + assertEquals(YearMonth.of(2000, Month.JANUARY), value, + "The value is not correct"); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Exception + { + YearMonth value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, + DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue( "[]"); + assertNull(value); + } + + // [modules-java8#249] + @Test + public void testYearAbove10k() throws Exception + { + YearMonth input = YearMonth.of(10000, 1); + String json = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(input); + YearMonth result = READER.readValue(json); + assertEquals(input, result); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "yearMonth"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String dateValAsEmptyStr = ""; + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + YearMonth actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr)); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + YearMonth actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertNull(actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "YearMonth"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(YearMonth.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", "")); + assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr)); + } + + private YearMonth read(final String json) throws IOException { + return READER.readValue(a2q(json)); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneIdDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneIdDeserTest.java new file mode 100644 index 0000000000..3578189584 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneIdDeserTest.java @@ -0,0 +1,140 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.ZoneId; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.cfg.CoercionInputShape; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.type.LogicalType; + +import static org.junit.jupiter.api.Assertions.*; + +public class ZoneIdDeserTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + private final ObjectMapper MOCK_OBJECT_MIXIN_MAPPER = mapperBuilder() + .addMixIn(ZoneId.class, MockObjectConfiguration.class) + .build(); + + @Test + public void testSimpleZoneIdDeser() + { + assertEquals(ZoneId.of("America/Chicago"), + MAPPER.readValue("\"America/Chicago\"", ZoneId.class)); + assertEquals(ZoneId.of("America/Anchorage"), + MAPPER.readValue("\"America/Anchorage\"", ZoneId.class)); + } + + @Test + public void testPolymorphicZoneIdDeser() + { + ObjectMapper mapper = JsonMapper.builder() + .addMixIn(ZoneId.class, MockObjectConfiguration.class) + .build(); + ZoneId value = mapper.readValue("[\"" + ZoneId.class.getName() + "\",\"America/Denver\"]", ZoneId.class); + assertEquals(ZoneId.of("America/Denver"), value); + } + + @Test + public void testDeserialization01() + { + assertEquals(ZoneId.of("America/Chicago"), + MAPPER.readValue("\"America/Chicago\"", ZoneId.class)); + } + + @Test + public void testDeserialization02() + { + assertEquals(ZoneId.of("America/Anchorage"), + MAPPER.readValue("\"America/Anchorage\"", ZoneId.class)); + } + + @Test + public void testDeserializationWithTypeInfo02() + { + ZoneId value = MOCK_OBJECT_MIXIN_MAPPER.readValue("[\"" + ZoneId.class.getName() + "\",\"America/Denver\"]", ZoneId.class); + assertEquals(ZoneId.of("America/Denver"), value); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() + { + + String key = "zoneId"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + ZoneId actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + ZoneId actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, + "empty string failed to deserialize to null with lenient setting"); + } + + public void testStrictDeserializeFromEmptyString() + { + + final String key = "zoneId"; + final ObjectMapper mapper = mapperBuilder() + .withCoercionConfig(LogicalType.DateTime, + cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail)) + .build(); + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + try { + objectReader.readValue(valueFromEmptyStr); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce empty String"); + verifyException(e, ZoneId.class.getName()); + } + } + + // [module-java8#68] + @Test + public void testZoneIdDeserFromEmpty() + { + // by default, should be fine + assertNull(MAPPER.readValue(q(" "), ZoneId.class)); + // but fail if coercion illegal + final ObjectMapper mapper = mapperBuilder() + .withCoercionConfig(LogicalType.DateTime, + cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail)) + .build(); + try { + mapper.readValue(q(" "), ZoneId.class); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce empty String"); + verifyException(e, ZoneId.class.getName()); + } + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneOffsetDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneOffsetDeserTest.java new file mode 100644 index 0000000000..7fdd1421c9 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneOffsetDeserTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.deser; + +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.cfg.CoercionAction; +import tools.jackson.databind.cfg.CoercionInputShape; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.type.LogicalType; + +import static org.junit.jupiter.api.Assertions.*; + +public class ZoneOffsetDeserTest extends DateTimeTestBase +{ + private final static ObjectMapper MAPPER = newMapper(); + private final static ObjectReader READER = MAPPER.readerFor(ZoneOffset.class); + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + @Test + public void testSimpleZoneOffsetDeser() throws Exception + { + assertEquals(ZoneOffset.of("Z"), READER.readValue("\"Z\""), + "The value is not correct."); + assertEquals(ZoneOffset.of("+0300"), READER.readValue(q("+0300")), + "The value is not correct."); + assertEquals(ZoneOffset.of("-0630"), READER.readValue("\"-06:30\""), + "The value is not correct."); + } + + @Test + public void testPolymorphicZoneOffsetDeser() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .addMixIn(ZoneId.class, MockObjectConfiguration.class) + .build(); + ZoneId value = mapper.readValue("[\"" + ZoneOffset.class.getName() + "\",\"+0415\"]", ZoneId.class); + assertTrue(value instanceof ZoneOffset); + assertEquals(ZoneOffset.of("+0415"), value); + } + + @Test + public void testDeserializationWithTypeInfo03() throws Exception + { + ObjectMapper mapper = mapperBuilder() + .addMixIn(ZoneId.class, MockObjectConfiguration.class) + .build(); + ZoneId value = mapper.readValue("[\"" + ZoneOffset.class.getName() + "\",\"+0415\"]", ZoneId.class); + assertTrue(value instanceof ZoneOffset, "The value should be a ZoneOffset."); + assertEquals(ZoneOffset.of("+0415"), value, "The value is not correct."); + } + + @Test + public void testBadDeserializationAsString01() throws Throwable + { + try { + READER.readValue("\"notazonedoffset\""); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Invalid ID for ZoneOffset"); + } + } + + @Test + public void testDeserializationAsArrayDisabled() throws Throwable + { + try { + READER.readValue("[\"+0300\"]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.ZoneOffset` from Array value"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() throws Throwable + { + try { + READER.readValue("[]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.ZoneOffset` from Array value"); + } + try { + READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.ZoneOffset` from Array value"); + } + } + + @Test + public void testDeserializationAsArrayEnabled() throws Throwable + { + ZoneOffset value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[\"+0300\"]"); + assertEquals(ZoneOffset.of("+0300"), value, "The value is not correct."); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + ZoneOffset value = READER + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "zoneOffset"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + ZoneId actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + ZoneId actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "zoneOffset"; + final ObjectMapper mapper = mapperBuilder() + .withCoercionConfig(LogicalType.DateTime, + cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail)) + .build(); + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + try { + objectReader.readValue(valueFromEmptyStr); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce empty String"); + verifyException(e, ZoneOffset.class.getName()); + } + } + + // [module-java8#68] + @Test + public void testZoneOffsetDeserFromEmpty() throws Exception + { + // by default, should be fine + assertNull(MAPPER.readValue(q(" "), ZoneOffset.class)); + // but fail if coercion illegal + final ObjectMapper mapper = mapperBuilder() + .withCoercionConfig(LogicalType.DateTime, + cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail)) + .build(); + try { + mapper.readerFor(ZoneOffset.class) + .readValue(q(" ")); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce empty String"); + verifyException(e, ZoneOffset.class.getName()); + } + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/ZonedDateTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZonedDateTimeDeserTest.java new file mode 100644 index 0000000000..a38124ddc0 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZonedDateTimeDeserTest.java @@ -0,0 +1,310 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Map; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Feature; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.*; + +public class ZonedDateTimeDeserTest extends DateTimeTestBase +{ + private final ObjectReader READER = newMapper().readerFor(ZonedDateTime.class); + + private final ObjectReader READER_NON_NORMALIZED_ZONEID = JsonMapper.builder() + .disable(DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID) + .build() + .readerFor(ZonedDateTime.class); + + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; + + static class WrapperWithFeatures { + @JsonFormat(without = JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + public ZonedDateTime value; + } + + static class WrapperWithReadTimestampsAsNanosDisabled { + @JsonFormat( + without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public ZonedDateTime value; + + public WrapperWithReadTimestampsAsNanosDisabled() { } + public WrapperWithReadTimestampsAsNanosDisabled(ZonedDateTime v) { value = v; } + } + + static class WrapperWithReadTimestampsAsNanosEnabled { + @JsonFormat( + with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + public ZonedDateTime value; + + public WrapperWithReadTimestampsAsNanosEnabled() { } + public WrapperWithReadTimestampsAsNanosEnabled(ZonedDateTime v) { value = v; } + } + + @Test + public void testDeserFromString() throws Exception + { + assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), + READER.readValue(q("2000-01-01T12:00Z")), + "The value is not correct."); + } + + // [modules-java#281] + @Test + public void testDeserFromStringNoZoneIdNormalization() throws Exception + { + // 11-Nov-2023, tatu: Not sure this is great test but... does show diff + // behavior with and without `JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID` + assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, TimeZone.getTimeZone("UTC").toZoneId()), + READER_NON_NORMALIZED_ZONEID.readValue(q("2000-01-01T12:00Z")), + "The value is not correct."); + } + + @Test + public void testDeserializationAsInt01() throws Exception + { + ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class); + ZonedDateTime date = ZonedDateTime.of( + LocalDateTime.ofEpochSecond(1, 1000000, ZoneOffset.UTC), + ZoneOffset.UTC); + WrapperWithReadTimestampsAsNanosDisabled actual = + reader.readValue(a2q("{'value':1001}")); + assertEquals(date, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationAsInt02() throws Exception + { + ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class); + ZonedDateTime date = ZonedDateTime.of( + LocalDateTime.ofEpochSecond(1, 0, ZoneOffset.UTC), + ZoneOffset.UTC); + WrapperWithReadTimestampsAsNanosEnabled actual = + reader.readValue(a2q("{'value':1}")); + assertEquals(date, actual.value, "The value is not correct."); + } + + @Test + public void testDeserializationComparedToStandard() throws Throwable + { + String inputString = "2021-02-01T19:49:04.0513486Z"; + + assertEquals(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(inputString, ZonedDateTime::from), + READER.readValue(q(inputString)), + "The value is not correct."); + } + + @Test + public void testDeserializationComparedToStandard2() throws Throwable + { + String inputString = "2021-02-01T19:49:04.0513486Z[UTC]"; + + ZonedDateTime converted = newMapperBuilder() + .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false) + .build() + .readerFor(ZonedDateTime.class).readValue(q(inputString)); + + assertEquals(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(inputString, ZonedDateTime::from), + converted, + "The value is not correct."); + } + + @Test + public void testBadDeserializationAsString01() throws Throwable + { + try { + READER.readValue(q("notazone")); + fail("Should nae pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from String"); + } + } + + @Test + public void testDeserializationAsArrayDisabled() throws Throwable + { + try { + READER.readValue("[\"2000-01-01T12:00Z\"]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from Array"); + } + } + + @Test + public void testDeserializationAsEmptyArrayDisabled() throws Throwable + { + try { + READER.readValue("[]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from Array"); + } + try { + newMapper() + .readerFor(ZonedDateTime.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[]"); + fail("expected MismatchedInputException"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from Array"); + } + } + + @Test + public void testDeserializationAsArrayEnabled() throws Throwable + { + ZonedDateTime value = newMapper() + .readerFor(ZonedDateTime.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue("[\"2000-01-01T12:00Z\"]"); + assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), + value, + "The value is not correct."); + } + + @Test + public void testDeserializationAsEmptyArrayEnabled() throws Throwable + { + ZonedDateTime value = newMapper() + .readerFor(ZonedDateTime.class) + .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, + DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .readValue("[]"); + assertNull(value); + } + + @Test + public void testDeserializationWithZonePreserved() throws Throwable + { + WrapperWithFeatures wrapper = newMapper() + .readerFor(WrapperWithFeatures.class) + .readValue("{\"value\":\"2000-01-01T12:00+01:00\"}"); + assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneOffset.ofHours(1)), + wrapper.value, + "Timezone should be preserved."); + } + + /* + /********************************************************** + /* Tests for empty string handling + /********************************************************** + */ + + @Test + public void testLenientDeserializeFromEmptyString() throws Exception { + + String key = "zoneDateTime"; + ObjectMapper mapper = newMapper(); + ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + ZonedDateTime actualDateFromNullStr = actualMapFromNullStr.get(key); + assertNull(actualDateFromNullStr); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr); + ZonedDateTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key); + assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting"); + } + + @Test + public void testStrictDeserializeFromEmptyString() throws Exception { + + final String key = "zonedDateTime"; + final ObjectMapper mapper = mapperBuilder() + .withConfigOverride(ZonedDateTime.class, + o -> o.setFormat(JsonFormat.Value.forLeniency(false))) + .build(); + final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF); + + String valueFromNullStr = mapper.writeValueAsString(asMap(key, null)); + Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr); + assertNull(actualMapFromNullStr.get(key)); + + String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); + assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr)); + } + + /* + /********************************************************** + /* Tests for ISO-8601 ZonedDateTimes that are colonless + /********************************************************** + */ + + @Test + public void testDeserializationWithoutColonInOffset() throws Throwable + { + WrapperWithFeatures wrapper = READER + .forType(WrapperWithFeatures.class) + .readValue("{\"value\":\"2000-01-01T12:00+0100\"}"); + + assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneOffset.ofHours(1)), + wrapper.value, + "Value parses as if it were with colon"); + } + + @Test + public void testDeserializationWithoutColonInTimeZoneWithTZDB() throws Throwable + { + WrapperWithFeatures wrapper = READER + .forType(WrapperWithFeatures.class) + .readValue("{\"value\":\"2000-01-01T12:00+0100[Europe/Paris]\"}"); + assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneId.of("Europe/Paris")), + wrapper.value, + "Timezone should be preserved."); + } + + @Test + public void ZonedDateTime_with_offset_can_be_deserialized() throws Exception { + ObjectReader r = newMapper().readerFor(ZonedDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + + String base = "2015-07-24T12:23:34.184"; + for (String offset : Arrays.asList("+00", "-00")) { + String time = base + offset; + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + '"')); + } + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + "00" + '"')); + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + ":00" + '"')); + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30" ), r.readValue('"' + time + "30" + '"')); + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30" ), r.readValue('"' + time + ":30" + '"')); + } + + for (String prefix : Arrays.asList("-", "+")) { + for (String hours : Arrays.asList("00", "01", "02", "03", "11", "12")) { + String time = base + prefix + hours; + ZonedDateTime expectedHour = ZonedDateTime.parse(time + ":00"); + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertEquals(expectedHour, r.readValue('"' + time + '"')); + } + assertEquals(expectedHour, r.readValue('"' + time + "00" + '"')); + assertEquals(expectedHour, r.readValue('"' + time + ":00" + '"')); + assertEquals(ZonedDateTime.parse(time + ":30"), r.readValue('"' + time + "30" + '"')); + assertEquals(ZonedDateTime.parse(time + ":30"), r.readValue('"' + time + ":30" + '"')); + } + } + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializerTest.java new file mode 100644 index 0000000000..a397b376c2 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializerTest.java @@ -0,0 +1,59 @@ +package tools.jackson.databind.ext.javatime.deser.key; + +import java.time.ZonedDateTime; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +// for [modules-java8#306] +public class ZonedDateTimeKeyDeserializerTest + extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final TypeReference> MAP_TYPE_REF + = new TypeReference>() {}; + + @Test + public void Instant_style_can_be_deserialized() throws Exception { + Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184Z"), + MAP_TYPE_REF); + Map.Entry entry = map.entrySet().iterator().next(); + assertEquals("2015-07-24T12:23:34.184Z", entry.getKey().toString()); + } + + @Test + public void ZonedDateTime_with_zone_name_can_be_deserialized() throws Exception { + Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184Z[UTC]"), + MAP_TYPE_REF); + Map.Entry entry = map.entrySet().iterator().next(); + assertEquals("2015-07-24T12:23:34.184Z[UTC]", entry.getKey().toString()); + } + + // NOTE: Java 9+ test + @Test + public void ZonedDateTime_with_place_name_can_be_deserialized() throws Exception { + Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184Z[Europe/London]"), + MAP_TYPE_REF); + Map.Entry entry = map.entrySet().iterator().next(); + assertEquals("2015-07-24T13:23:34.184+01:00[Europe/London]", entry.getKey().toString()); + } + + @Test + public void ZonedDateTime_with_offset_can_be_deserialized() throws Exception { + Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184+02:00"), + MAP_TYPE_REF); + Map.Entry entry = map.entrySet().iterator().next(); + assertEquals("2015-07-24T12:23:34.184+02:00", entry.getKey().toString()); + } + + private static String getMap(String input) { + return "{\"" + input + "\": \"This is a string\"}"; + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/DurationAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/DurationAsKeyTest.java new file mode 100644 index 0000000000..b925e88653 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/DurationAsKeyTest.java @@ -0,0 +1,34 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class DurationAsKeyTest extends DateTimeTestBase +{ + private static final Duration DURATION = Duration.ofMinutes(13).plusSeconds(37).plusNanos(120 * 1000 * 1000L); + private static final String DURATION_STRING = "PT13M37.12S"; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { }); + + @Test + public void testSerialization() throws Exception { + assertEquals(mapAsString(DURATION_STRING, "test"), + MAPPER.writeValueAsString(Collections.singletonMap(DURATION, "test"))); + } + + @Test + public void testDeserialization() throws Exception { + assertEquals(Collections.singletonMap(DURATION, "test"), READER.readValue(mapAsString(DURATION_STRING, "test"))); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/InstantAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/InstantAsKeyTest.java new file mode 100644 index 0000000000..ebb9c85c40 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/InstantAsKeyTest.java @@ -0,0 +1,50 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.Instant; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class InstantAsKeyTest extends DateTimeTestBase +{ + private static final Instant INSTANT_0 = Instant.ofEpochMilli(0); + private static final String INSTANT_0_STRING = "1970-01-01T00:00:00Z"; + private static final Instant INSTANT = Instant.ofEpochSecond(1426325213l, 590000000l); + private static final String INSTANT_STRING = "2015-03-14T09:26:53.590Z"; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { }); + + @Test + public void testSerialization0() throws Exception { + String value = MAPPER.writeValueAsString(asMap(INSTANT_0, "test")); + assertEquals(mapAsString(INSTANT_0_STRING, "test"), value); + } + + @Test + public void testSerialization1() throws Exception { + String value = MAPPER.writeValueAsString(asMap(INSTANT, "test")); + assertEquals(mapAsString(INSTANT_STRING, "test"), value); + } + + @Test + public void testDeserialization0() throws Exception { + Map value = READER.readValue(mapAsString(INSTANT_0_STRING, "test")); + Map EXP = asMap(INSTANT_0, "test"); + assertEquals(EXP, value, "Value is incorrect"); + } + + @Test + public void testDeserialization1() throws Exception { + Map value = READER.readValue(mapAsString(INSTANT_STRING, "test")); + Map EXP = asMap(INSTANT, "test"); + assertEquals(EXP, value, "Value is incorrect"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateAsKeyTest.java new file mode 100644 index 0000000000..2d062e1fab --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateAsKeyTest.java @@ -0,0 +1,33 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.LocalDate; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalDateAsKeyTest extends DateTimeTestBase +{ + private static final LocalDate DATE = LocalDate.of(2015, 3, 14); + private static final String DATE_STRING = "2015-03-14"; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { }); + + @Test + public void testSerialization() throws Exception { + assertEquals(mapAsString(DATE_STRING, "test"), + MAPPER.writeValueAsString(asMap(DATE, "test"))); + } + + @Test + public void testDeserialization() throws Exception { + assertEquals(asMap(DATE, "test"), READER.readValue(mapAsString(DATE_STRING, "test"))); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateTimeAsKeyTest.java new file mode 100644 index 0000000000..a6bc233b19 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateTimeAsKeyTest.java @@ -0,0 +1,80 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.deser.DeserializationProblemHandler; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalDateTimeAsKeyTest extends DateTimeTestBase +{ + private static final LocalDateTime DATE_TIME_0 = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC); + /* + * Current serializer is LocalDateTime.toString(), which omits seconds if it can + */ + private static final String DATE_TIME_0_STRING = "1970-01-01T00:00"; + private static final LocalDateTime DATE_TIME = LocalDateTime.of(2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000); + private static final String DATE_TIME_STRING = "2015-03-14T09:26:53.590"; + + private final TypeReference> TYPE_REF = new TypeReference>() { }; + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testSerialization0() throws Exception { + String value = MAPPER.writeValueAsString(asMap(DATE_TIME_0, "test")); + assertEquals(mapAsString(DATE_TIME_0_STRING, "test"), value); + } + + @Test + public void testSerialization1() throws Exception { + String value = MAPPER.writeValueAsString(asMap(DATE_TIME, "test")); + assertEquals(mapAsString(DATE_TIME_STRING, "test"), value); + } + + @Test + public void testDeserialization0() throws Exception { + Map value = READER.readValue( + mapAsString(DATE_TIME_0_STRING, "test")); + assertEquals(asMap(DATE_TIME_0, "test"), value, "Value is incorrect"); + } + + @Test + public void testDeserialization1() throws Exception { + Map value = READER.readValue( + mapAsString(DATE_TIME_STRING, "test")); + assertEquals(asMap(DATE_TIME, "test"), value, "Value is incorrect"); + } + + @Test + public void testDateTimeExceptionIsHandled() throws Throwable + { + LocalDateTime now = LocalDateTime.now(); + DeserializationProblemHandler handler = new DeserializationProblemHandler() { + @Override + public Object handleWeirdKey(DeserializationContext ctxt, Class targetType, + String valueToConvert, String failureMsg) { + if (LocalDateTime.class == targetType) { + if ("now".equals(valueToConvert)) { + return now; + } + } + return NOT_HANDLED; + } + }; + Map value = mapperBuilder().addHandler(handler) + .build() + .readValue(mapAsString("now", "test"), TYPE_REF); + + assertEquals(asMap(now, "test"), value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/LocalTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalTimeAsKeyTest.java new file mode 100644 index 0000000000..f3fe7c62b1 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalTimeAsKeyTest.java @@ -0,0 +1,53 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.LocalTime; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalTimeAsKeyTest extends DateTimeTestBase +{ + private static final LocalTime TIME_0 = LocalTime.ofSecondOfDay(0); + /* + * Seconds are omitted if possible + */ + private static final String TIME_0_STRING = "00:00"; + private static final LocalTime TIME = LocalTime.of(3, 14, 15, 920 * 1000 * 1000); + private static final String TIME_STRING = "03:14:15.920"; + + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testSerialization0() throws Exception { + assertEquals(mapAsString(TIME_0_STRING, "test"), + MAPPER.writeValueAsString(asMap(TIME_0, "test"))); + } + + @Test + public void testSerialization1() throws Exception { + assertEquals(mapAsString(TIME_STRING, "test"), + MAPPER.writeValueAsString(asMap(TIME, "test"))); + } + + @Test + public void testDeserialization0() throws Exception { + assertEquals(asMap(TIME_0, "test"), READER.readValue(mapAsString(TIME_0_STRING, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization1() throws Exception { + assertEquals(asMap(TIME, "test"), READER.readValue(mapAsString(TIME_STRING, "test")), + "Value is incorrect"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/MonthDayAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/MonthDayAsKeyTest.java new file mode 100644 index 0000000000..28f1d9b3ae --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/MonthDayAsKeyTest.java @@ -0,0 +1,36 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.MonthDay; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class MonthDayAsKeyTest extends DateTimeTestBase +{ + private static final MonthDay MONTH_DAY = MonthDay.of(3, 14); + private static final String MONTH_DAY_STRING = "--03-14"; + + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testSerialization() throws Exception { + assertEquals(mapAsString(MONTH_DAY_STRING, "test"), MAPPER.writeValueAsString(asMap(MONTH_DAY, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization() throws Exception { + assertEquals(asMap(MONTH_DAY, "test"), READER.readValue(mapAsString(MONTH_DAY_STRING, "test")), + "Value is incorrect"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetDateTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetDateTimeAsKeyTest.java new file mode 100644 index 0000000000..42d03deadf --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetDateTimeAsKeyTest.java @@ -0,0 +1,70 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class OffsetDateTimeAsKeyTest extends DateTimeTestBase +{ + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private static final OffsetDateTime DATE_TIME_0 = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0), ZoneOffset.UTC); + private static final String DATE_TIME_0_STRING = "1970-01-01T00:00Z"; + private static final OffsetDateTime DATE_TIME_1 = OffsetDateTime.of(2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneOffset.UTC); + private static final String DATE_TIME_1_STRING = "2015-03-14T09:26:53.590Z"; + private static final OffsetDateTime DATE_TIME_2 = OffsetDateTime.of(2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneOffset.ofHours(6)); + private static final String DATE_TIME_2_STRING = "2015-03-14T09:26:53.590+06:00"; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testSerialization0() throws Exception { + String value = MAPPER.writeValueAsString(asMap(DATE_TIME_0, "test")); + assertEquals(mapAsString(DATE_TIME_0_STRING, "test"), value, + "Value is incorrect"); + } + + @Test + public void testSerialization1() throws Exception { + assertEquals(mapAsString(DATE_TIME_1_STRING, "test"), + MAPPER.writeValueAsString(asMap(DATE_TIME_1, "test")), + "Value is incorrect"); + } + + @Test + public void testSerialization2() throws Exception { + assertEquals(mapAsString(DATE_TIME_2_STRING, "test"), + MAPPER.writeValueAsString(asMap(DATE_TIME_2, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization0() throws Exception { + assertEquals(asMap(DATE_TIME_0, "test"), READER.readValue(mapAsString(DATE_TIME_0_STRING, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization1() throws Exception { + assertEquals(asMap(DATE_TIME_1, "test"), READER.readValue(mapAsString(DATE_TIME_1_STRING, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization2() throws Exception { + assertEquals(asMap(DATE_TIME_2, "test"), + READER.readValue(mapAsString(DATE_TIME_2_STRING, "test")), + "Value is incorrect"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetTimeAsKeyTest.java new file mode 100644 index 0000000000..381e2c5e91 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetTimeAsKeyTest.java @@ -0,0 +1,70 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class OffsetTimeAsKeyTest extends DateTimeTestBase +{ + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private static final OffsetTime TIME_0 = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC); + private static final String TIME_0_STRING = "00:00Z"; + private static final OffsetTime TIME_1 = OffsetTime.of(3, 14, 15, 920 * 1000 * 1000, ZoneOffset.UTC); + private static final String TIME_1_STRING = "03:14:15.920Z"; + private static final OffsetTime TIME_2 = OffsetTime.of(3, 14, 15, 920 * 1000 * 1000, ZoneOffset.ofHours(6)); + private static final String TIME_2_STRING = "03:14:15.920+06:00"; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testSerialization0() throws Exception { + assertEquals(mapAsString(TIME_0_STRING, "test"), + MAPPER.writeValueAsString(asMap(TIME_0, "test"))); + } + + @Test + public void testSerialization1() throws Exception { + assertEquals(mapAsString(TIME_1_STRING, "test"), + MAPPER.writeValueAsString(asMap(TIME_1, "test")), + "Value is incorrect"); + } + + @Test + public void testSerialization2() throws Exception { + assertEquals(mapAsString(TIME_2_STRING, "test"), + MAPPER.writeValueAsString(asMap(TIME_2, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization0() throws Exception { + assertEquals(asMap(TIME_0, "test"), + READER.readValue(mapAsString(TIME_0_STRING, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization1() throws Exception { + assertEquals(asMap(TIME_1, "test"), + READER.readValue(mapAsString(TIME_1_STRING, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization2() throws Exception { + assertEquals(asMap(TIME_2, "test"), + READER.readValue(mapAsString(TIME_2_STRING, "test")), + "Value is incorrect"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/PeriodAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/PeriodAsKeyTest.java new file mode 100644 index 0000000000..22b9b64071 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/PeriodAsKeyTest.java @@ -0,0 +1,54 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.Period; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class PeriodAsKeyTest extends DateTimeTestBase +{ + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private static final Period PERIOD_0 = Period.of(0, 0, 0); + private static final String PERIOD_0_STRING = "P0D"; + private static final Period PERIOD = Period.of(3, 1, 4); + private static final String PERIOD_STRING = "P3Y1M4D"; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testSerialization0() throws Exception { + assertEquals(mapAsString(PERIOD_0_STRING, "test"), + MAPPER.writeValueAsString(asMap(PERIOD_0, "test")), + "Value is incorrect"); + } + + @Test + public void testSerialization1() throws Exception { + assertEquals(mapAsString(PERIOD_STRING, "test"), + MAPPER.writeValueAsString(asMap(PERIOD, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization0() throws Exception { + assertEquals(asMap(PERIOD_0, "test"), + READER.readValue(mapAsString(PERIOD_0_STRING, "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization1() throws Exception { + assertEquals(asMap(PERIOD, "test"), + READER.readValue(mapAsString(PERIOD_STRING, "test")), + "Value is incorrect"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/YearAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/YearAsKeyTest.java new file mode 100644 index 0000000000..c14d5d222a --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/YearAsKeyTest.java @@ -0,0 +1,70 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.Year; +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class YearAsKeyTest extends DateTimeTestBase +{ + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testKeySerialization() throws Exception { + assertEquals(mapAsString("3141", "test"), + MAPPER.writeValueAsString(asMap(Year.of(3141), "test")), + "Value is incorrect"); + } + + @Test + public void testKeyDeserialization() throws Exception { + assertEquals(asMap(Year.of(3141), "test"), READER.readValue(mapAsString("3141", "test")), + "Value is incorrect"); + // Test both padded, unpadded + assertEquals(asMap(Year.of(476), "test"), READER.readValue(mapAsString("0476", "test")), + "Value is incorrect"); + assertEquals(asMap(Year.of(476), "test"), READER.readValue(mapAsString("476", "test")), + "Value is incorrect"); + } + + @Test + public void deserializeYearKey_notANumber() throws Exception { + assertThrows(InvalidFormatException.class, () -> { + READER.readValue(mapAsString("10000BC", "test")); + }); + } + + @Test + public void deserializeYearKey_notAYear() throws Exception { + assertThrows(InvalidFormatException.class, () -> { + READER.readValue(mapAsString(Integer.toString(Year.MAX_VALUE+1), "test")); + }); + } + + @Test + public void serializeAndDeserializeYearKeyUnpadded() throws Exception { + // fix for issue #51 verify we can deserialize an unpadded year e.g. "1" + Map testMap = Collections.singletonMap(Year.of(1), 1F); + String serialized = MAPPER.writeValueAsString(testMap); + TypeReference> yearFloatTypeReference = new TypeReference>() {}; + Map deserialized = MAPPER.readValue(serialized, yearFloatTypeReference); + assertEquals(testMap, deserialized); + + // actually, check padded as well just to make sure + Map deserialized2 = MAPPER.readValue(a2q("{'0001':1.0}"), + yearFloatTypeReference); + assertEquals(testMap, deserialized2); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/YearMonthAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/YearMonthAsKeyTest.java new file mode 100644 index 0000000000..03bee96d1a --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/YearMonthAsKeyTest.java @@ -0,0 +1,34 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.YearMonth; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class YearMonthAsKeyTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { + }); + + @Test + public void testSerialization() throws Exception { + assertEquals(mapAsString("3141-05", "test"), + MAPPER.writeValueAsString(asMap(YearMonth.of(3141, 5), "test")), + "Value is incorrect"); + } + + @Test + public void testDeserialization() throws Exception { + assertEquals(asMap(YearMonth.of(3141, 5), "test"), + READER.readValue(mapAsString("3141-05", "test")), + "Value is incorrect"); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneIdAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneIdAsKeyTest.java new file mode 100644 index 0000000000..66e071d399 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneIdAsKeyTest.java @@ -0,0 +1,62 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.ZoneId; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ZoneIdAsKeyTest extends DateTimeTestBase +{ + private static final ZoneId ZONE_0 = ZoneId.of("UTC"); + private static final String ZONE_0_STRING = "UTC"; + private static final ZoneId ZONE_1 = ZoneId.of("+06:00"); + private static final String ZONE_1_STRING = "+06:00"; + private static final ZoneId ZONE_2 = ZoneId.of("Europe/London"); + private static final String ZONE_2_STRING = "Europe/London"; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { }); + + @Test + public void testSerialization0() throws Exception { + assertEquals(mapAsString(ZONE_0_STRING, "test"), + MAPPER.writeValueAsString(asMap(ZONE_0, "test"))); + } + + @Test + public void testSerialization1() throws Exception { + assertEquals(mapAsString(ZONE_1_STRING, "test"), + MAPPER.writeValueAsString(asMap(ZONE_1, "test"))); + } + + @Test + public void testSerialization2() throws Exception { + assertEquals(mapAsString(ZONE_2_STRING, "test"), + MAPPER.writeValueAsString(asMap(ZONE_2, "test"))); + } + + @Test + public void testDeserialization0() throws Exception { + assertEquals(asMap(ZONE_0, "test"), + READER.readValue(mapAsString(ZONE_0_STRING, "test"))); + } + + @Test + public void testDeserialization1() throws Exception { + assertEquals(asMap(ZONE_1, "test"), + READER.readValue(mapAsString(ZONE_1_STRING, "test"))); + } + + @Test + public void testDeserialization2() throws Exception { + assertEquals(asMap(ZONE_2, "test"), + READER.readValue(mapAsString(ZONE_2_STRING, "test"))); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneOffsetAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneOffsetAsKeyTest.java new file mode 100644 index 0000000000..bf857040df --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneOffsetAsKeyTest.java @@ -0,0 +1,48 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.ZoneOffset; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ZoneOffsetAsKeyTest extends DateTimeTestBase +{ + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private static final ZoneOffset OFFSET_0 = ZoneOffset.UTC; + private static final String OFFSET_0_STRING = "Z"; + private static final ZoneOffset OFFSET_1 = ZoneOffset.ofHours(6); + private static final String OFFSET_1_STRING = "+06:00"; + + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerialization0() throws Exception { + String value = MAPPER.writeValueAsString(asMap(OFFSET_0, "test")); + assertEquals(mapAsString(OFFSET_0_STRING, "test"), value); + } + + @Test + public void testSerialization1() throws Exception { + String value = MAPPER.writeValueAsString(asMap(OFFSET_1, "test")); + assertEquals(mapAsString(OFFSET_1_STRING, "test"), value); + } + + @Test + public void testDeserialization0() throws Exception { + Map value = MAPPER.readValue(mapAsString(OFFSET_0_STRING, "test"), TYPE_REF); + assertEquals(asMap(OFFSET_0, "test"), value); + } + + @Test + public void testDeserialization1() throws Exception { + Map value = MAPPER.readValue(mapAsString(OFFSET_1_STRING, "test"), TYPE_REF); + assertEquals(asMap(OFFSET_1, "test"), value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/ZonedDateTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/ZonedDateTimeAsKeyTest.java new file mode 100644 index 0000000000..90912ac382 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/key/ZonedDateTimeAsKeyTest.java @@ -0,0 +1,90 @@ +package tools.jackson.databind.ext.javatime.key; + +import java.time.*; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class ZonedDateTimeAsKeyTest extends DateTimeTestBase +{ + private static final TypeReference> TYPE_REF = new TypeReference>() { + }; + private static final ZonedDateTime DATE_TIME_0 = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0), ZoneOffset.UTC); + private static final String DATE_TIME_0_STRING = "1970-01-01T00:00:00Z"; +// private static final Instant DATE_TIME_0_INSTANT = DATE_TIME_0.toInstant(); + + private static final ZonedDateTime DATE_TIME_1 = ZonedDateTime.of( + 2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneOffset.UTC); + private static final String DATE_TIME_1_STRING = "2015-03-14T09:26:53.59Z"; + + private static final ZonedDateTime DATE_TIME_2 = ZonedDateTime.of( + 2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneId.of("Europe/Budapest")); + /** + * Value of {@link #DATE_TIME_2} after it's been serialized and read back. Serialization throws away time zone information, it only + * keeps offset data. + */ + private static final ZonedDateTime DATE_TIME_2_OFFSET = DATE_TIME_2.withZoneSameInstant(ZoneOffset.ofHours(1)); + private static final String DATE_TIME_2_STRING = "2015-03-14T09:26:53.59+01:00";; + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(TYPE_REF); + + @Test + public void testSerialization0() throws Exception { + String value = MAPPER.writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_0, "test")); + assertEquals(mapAsString(DATE_TIME_0_STRING, "test"), value); + } + + @Test + public void testSerialization1() throws Exception { + String value = MAPPER.writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_1, "test")); + assertEquals(mapAsString(DATE_TIME_1_STRING, "test"), value); + } + + @Test + public void testSerialization2() throws Exception { + String value = MAPPER.writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_2, "test")); + assertEquals(mapAsString(DATE_TIME_2_STRING, "test"), value); + } + + @Test + public void testDeserialization0() throws Exception { + assertEquals(asMap(DATE_TIME_0, "test"), + READER.readValue(mapAsString(DATE_TIME_0_STRING, "test"))); + } + + @Test + public void testDeserialization1() throws Exception { + assertEquals(asMap(DATE_TIME_1, "test"), + READER.readValue(mapAsString(DATE_TIME_1_STRING, "test"))); + } + + @Test + public void testDeserialization2() throws Exception { + assertEquals(asMap(DATE_TIME_2_OFFSET, "test"), + READER.readValue(mapAsString(DATE_TIME_2_STRING, "test"))); + } + + @Test + public void testSerializationToInstantWithNanos() throws Exception { + String value = mapperBuilder().enable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS).build() + .writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_1, "test")); + assertEquals(mapAsString(String.valueOf(DATE_TIME_1.toEpochSecond()) + '.' + DATE_TIME_1.getNano(), "test"), value); + } + + @Test + public void testSerializationToInstantWithoutNanos() throws Exception { + String value = mapperBuilder().enable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS).build() + .writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_1, "test")); + assertEquals(mapAsString(String.valueOf(DATE_TIME_1.toInstant().toEpochMilli()), "test"), value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeExceptionTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeExceptionTest.java new file mode 100644 index 0000000000..e798ed137a --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeExceptionTest.java @@ -0,0 +1,24 @@ +package tools.jackson.databind.ext.javatime.misc; + +import java.time.DateTimeException; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class DateTimeExceptionTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + + // [modules-java#319]: should not fail to ser/deser DateTimeException + @Test + public void testDateTimeExceptionRoundtrip() throws Exception + { + String json = MAPPER.writeValueAsString(new DateTimeException("Test!")); + DateTimeException result = MAPPER.readValue(json, DateTimeException.class); + assertEquals("Test!", result.getMessage()); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeSchemasTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeSchemasTest.java new file mode 100644 index 0000000000..57f85867e5 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeSchemasTest.java @@ -0,0 +1,239 @@ +package tools.jackson.databind.ext.javatime.misc; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.*; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.JsonParser; +import tools.jackson.databind.*; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.jsonFormatVisitors.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class DateTimeSchemasTest extends DateTimeTestBase +{ + static class VisitorWrapper implements JsonFormatVisitorWrapper { + SerializationContext serializationContext; + final String baseName; + final Map traversedProperties; + + public VisitorWrapper(SerializationContext ctxt, String baseName, Map traversedProperties) { + this.serializationContext = ctxt; + this.baseName = baseName; + this.traversedProperties = traversedProperties; + } + + VisitorWrapper createSubtraverser(String bn) { + return new VisitorWrapper(getContext(), bn, traversedProperties); + } + + public Map getTraversedProperties() { + return traversedProperties; + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType type) { + return new JsonObjectFormatVisitor.Base(serializationContext) { + @Override + public void property(BeanProperty prop) { + anyProperty(prop); + } + + @Override + public void optionalProperty(BeanProperty prop) { + anyProperty(prop); + } + + private void anyProperty(BeanProperty prop) { + final String propertyName = prop.getFullName().toString(); + traversedProperties.put(baseName + propertyName, ""); + serializationContext.findPrimaryPropertySerializer(prop.getType(), prop) + .acceptJsonFormatVisitor(createSubtraverser(baseName + propertyName + "."), prop.getType()); + } + }; + } + + @Override + public JsonArrayFormatVisitor expectArrayFormat(JavaType type) { + traversedProperties.put(baseName, "ARRAY/"+type.getGenericSignature()); + return null; + } + + @Override + public JsonStringFormatVisitor expectStringFormat(JavaType type) { + return new JsonStringFormatVisitor.Base() { + @Override + public void format(JsonValueFormat format) { + traversedProperties.put(baseName, "STRING/"+format.name()); + } + }; + } + + @Override + public JsonNumberFormatVisitor expectNumberFormat(JavaType type) { + return new JsonNumberFormatVisitor.Base() { + @Override + public void numberType(JsonParser.NumberType format) { + traversedProperties.put(baseName, "NUMBER/"+format.name()); + } + }; + } + + @Override + public JsonIntegerFormatVisitor expectIntegerFormat(JavaType type) { + return new JsonIntegerFormatVisitor.Base() { + @Override + public void numberType(JsonParser.NumberType numberType) { + traversedProperties.put(baseName + "numberType", "INTEGER/" + numberType.name()); + } + + @Override + public void format(JsonValueFormat format) { + traversedProperties.put(baseName + "format", "INTEGER/" + format.name()); + } + }; + } + + @Override + public JsonBooleanFormatVisitor expectBooleanFormat(JavaType type) { + traversedProperties.put(baseName, "BOOLEAN"); + return new JsonBooleanFormatVisitor.Base(); + } + + @Override + public JsonNullFormatVisitor expectNullFormat(JavaType type) { + return new JsonNullFormatVisitor.Base(); + } + + @Override + public JsonAnyFormatVisitor expectAnyFormat(JavaType type) { + traversedProperties.put(baseName, "ANY"); + return new JsonAnyFormatVisitor.Base(); + } + + @Override + public JsonMapFormatVisitor expectMapFormat(JavaType type) { + traversedProperties.put(baseName, "MAP"); + return new JsonMapFormatVisitor.Base(serializationContext); + } + + @Override + public SerializationContext getContext() { + return serializationContext; + } + + @Override + public void setContext(SerializationContext ctxt) { + this.serializationContext = ctxt; + } + } + + // 05-Feb-2025, tatu: Change defaults to Jackson 2.x wrt serialization + // shape (as Timestamps vs Strings) + private final ObjectMapper MAPPER = mapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + // // // Local date/time types + + // [modules-java8#105] + @Test + public void testLocalTimeSchema() throws Exception + { + VisitorWrapper wrapper = new VisitorWrapper(null, "", new HashMap()); + MAPPER.writer().acceptJsonFormatVisitor(LocalTime.class, wrapper); + Map properties = wrapper.getTraversedProperties(); + + // By default, serialized as an int array, so: + assertEquals(1, properties.size()); + _verifyIntArrayType(properties.get("")); + + // but becomes date/time + wrapper = new VisitorWrapper(null, "", new HashMap()); + MAPPER.writer().without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .acceptJsonFormatVisitor(LocalTime.class, wrapper); + properties = wrapper.getTraversedProperties(); + _verifyTimeType(properties.get("")); + } + + @Test + public void testLocalDateSchema() throws Exception + { + VisitorWrapper wrapper = new VisitorWrapper(null, "", new HashMap()); + MAPPER.writer().acceptJsonFormatVisitor(LocalDate.class, wrapper); + Map properties = wrapper.getTraversedProperties(); + + // By default, serialized as an int array, so: + assertEquals(1, properties.size()); + _verifyIntArrayType(properties.get("")); + + // but becomes date/time + wrapper = new VisitorWrapper(null, "", new HashMap()); + MAPPER.writer().without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .acceptJsonFormatVisitor(LocalDate.class, wrapper); + properties = wrapper.getTraversedProperties(); + _verifyDateType(properties.get("")); + } + + // // // Zoned date/time types + + @Test + public void testDateTimeSchema() throws Exception + { + VisitorWrapper wrapper = new VisitorWrapper(null, "", new HashMap()); + MAPPER.writer().acceptJsonFormatVisitor(ZonedDateTime.class, wrapper); + Map properties = wrapper.getTraversedProperties(); + + // By default, serialized as an int array, so: + assertEquals(1, properties.size()); + _verifyBigDecimalType(properties.get("")); + + // but becomes long + wrapper = new VisitorWrapper(null, "", new HashMap()); + MAPPER.writer() + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .acceptJsonFormatVisitor(ZonedDateTime.class, wrapper); + properties = wrapper.getTraversedProperties(); + _verifyLongType(properties.get("numberType")); + _verifyLongFormat(properties.get("format")); + + // but becomes date/time + wrapper = new VisitorWrapper(null, "", new HashMap()); + MAPPER.writer().without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .acceptJsonFormatVisitor(ZonedDateTime.class, wrapper); + properties = wrapper.getTraversedProperties(); + _verifyDateTimeType(properties.get("")); + } + + private void _verifyIntArrayType(String desc) { + assertEquals("ARRAY/Ljava/util/List;", desc); + } + + private void _verifyTimeType(String desc) { + assertEquals("STRING/TIME", desc); + } + + private void _verifyDateType(String desc) { + assertEquals("STRING/DATE", desc); + } + + private void _verifyDateTimeType(String desc) { + assertEquals("STRING/DATE_TIME", desc); + } + + private void _verifyBigDecimalType(String desc) { + assertEquals("NUMBER/BIG_DECIMAL", desc); + } + + private void _verifyLongType(String desc) { + assertEquals("INTEGER/LONG", desc); + } + + private void _verifyLongFormat(String desc) { + assertEquals("INTEGER/UTC_MILLISEC", desc); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/DeductionTypeSerialization296Test.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/DeductionTypeSerialization296Test.java new file mode 100644 index 0000000000..6e4a697105 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/DeductionTypeSerialization296Test.java @@ -0,0 +1,86 @@ +package tools.jackson.databind.ext.javatime.misc; + +import java.time.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +// for [modules-java8#296]: problem with `JsonTypeInfo.Id.DEDUCTION` +public class DeductionTypeSerialization296Test extends DateTimeTestBase +{ + static class Wrapper { + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) + public Object value; + + public Wrapper(Object value) { + this.value = value; + } + } + + private final ObjectMapper MAPPER = mapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + @Test + public void testLocalDate() throws Exception + { + LocalDate date = LocalDate.of(1986, Month.JANUARY, 17); + assertEquals(a2q("{'value':'1986-01-17'}"), + MAPPER.writeValueAsString(new Wrapper(date))); + } + + @Test + public void testLocalDateTime() throws Exception + { + LocalDateTime datetime = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57); + assertEquals(a2q("{'value':'2013-08-21T09:22:00.000000057'}"), + MAPPER.writeValueAsString(new Wrapper(datetime))); + } + + @Test + public void testLocalTime() throws Exception + { + LocalTime time = LocalTime.of(9, 22, 57); + assertEquals(a2q("{'value':'09:22:57'}"), + MAPPER.writeValueAsString(new Wrapper(time))); + } + + @Test + public void testMonthDate() throws Exception + { + MonthDay date = MonthDay.of(Month.JANUARY, 17); + assertEquals(a2q("{'value':'--01-17'}"), + MAPPER.writeValueAsString(new Wrapper(date))); + } + + @Test + public void testOffsetTime() throws Exception + { + OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300")); + assertEquals(a2q("{'value':'15:43+03:00'}"), + MAPPER.writeValueAsString(new Wrapper(time))); + } + + @Test + public void testYearMonth() throws Exception + { + YearMonth date = YearMonth.of(1986, Month.JANUARY); + assertEquals(a2q("{'value':'1986-01'}"), + MAPPER.writeValueAsString(new Wrapper(date))); + } + + @Test + public void testZoneId() throws Exception + { + ZoneId zone = ZoneId.of("America/Denver"); + assertEquals(a2q("{'value':'America/Denver'}"), + MAPPER.writeValueAsString(new Wrapper(zone))); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/JDKSerializabilityTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/JDKSerializabilityTest.java new file mode 100644 index 0000000000..162ef8014c --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/JDKSerializabilityTest.java @@ -0,0 +1,46 @@ +package tools.jackson.databind.ext.javatime.misc; + +import java.io.*; +import java.time.Year; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.*; +import tools.jackson.databind.ext.javatime.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class JDKSerializabilityTest extends DateTimeTestBase +{ + @Test + public void testJDKSerializability() throws Exception { + final Year input = Year.of(1986); + ObjectMapper mapper = newMapper(); + String json1 = mapper.writeValueAsString(input); + + // validate we can still use it to deserialize jackson objects + ObjectMapper thawedMapper = serializeAndDeserialize(mapper); + String json2 = thawedMapper.writeValueAsString(input); + + assertEquals(json1, json2); + + Year result = thawedMapper.readValue(json1, Year.class); + assertEquals(input, result); + } + + private ObjectMapper serializeAndDeserialize(ObjectMapper mapper) throws Exception { + //verify serialization + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream); + + outputStream.writeObject(mapper); + byte[] serializedBytes = byteArrayOutputStream.toByteArray(); + + //verify deserialization + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serializedBytes); + ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream); + + Object deserializedObject = inputStream.readObject(); + return (ObjectMapper) deserializedObject; + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/UnsupportedTypesTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/UnsupportedTypesTest.java new file mode 100644 index 0000000000..62364ec763 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/UnsupportedTypesTest.java @@ -0,0 +1,34 @@ +package tools.jackson.databind.ext.javatime.misc; + +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class UnsupportedTypesTest extends DateTimeTestBase +{ + // [modules-java8#207] + static class TAWrapper { + public TemporalAdjuster a; + + public TAWrapper(TemporalAdjuster a) { + this.a = a; + } + } + + // [modules-java#207]: should not fail on `TemporalAdjuster` + @Test + public void testTemporalAdjusterSerialization() throws Exception + { + ObjectMapper mapper = newMapper(); + + // Not 100% sure how this happens, actually; should fail on empty "POJO"? + assertEquals(a2q("{'a':{}}"), + mapper.writeValueAsString(new TAWrapper(TemporalAdjusters.firstDayOfMonth()))); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/DurationSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/DurationSerTest.java new file mode 100644 index 0000000000..6aba0e70d0 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/DurationSerTest.java @@ -0,0 +1,322 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Duration; +import java.time.temporal.TemporalAmount; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class DurationSerTest extends DateTimeTestBase +{ + private final ObjectWriter WRITER = newMapper().writer(); + + // [datetime#224] + static class MyDto224 { + @JsonFormat(pattern = "MINUTES" + // Work-around from issue: +// , without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS + ) + @JsonProperty("mins") + final Duration duration; + + public MyDto224(Duration d) { duration = d; } + + public Duration getDuration() { return duration; } + } + + // [datetime#282] + static class Bean282 { + @JsonFormat(pattern = "SECONDS") + public Duration duration; + + public Bean282(Duration d) { duration = d; } + } + + @Test + public void testSerializationAsTimestampNanoseconds01() throws Exception + { + Duration duration = Duration.ofSeconds(60L, 0); + String value = WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(duration); + assertEquals("60"+NO_NANOSECS_SUFFIX, value); + } + + @Test + public void testSerializationAsTimestampNanoseconds02() throws Exception + { + Duration duration = Duration.ofSeconds(13498L, 8374); + String value = WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(duration); + assertEquals("13498.000008374", value); + } + + // [modules-java8#165] + @Test + public void testSerializationAsTimestampNanoseconds03() throws Exception + { + ObjectWriter w = WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); + + // 20-Oct-2020, tatu: Very weird, but "use nanoseconds" actually results + // in unit being seconds, with fractions (with nanosec precision) + String value = w.writeValueAsString(Duration.ofMillis(1L)); + assertEquals("0.001000000", value); + + value = w.writeValueAsString(Duration.ofMillis(-1L)); + assertEquals("-0.001000000", value); + } + + @Test + public void testSerializationAsTimestampMilliseconds01() throws Exception + { + final ObjectWriter w = WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); + String value = w.writeValueAsString(Duration.ofSeconds(45L, 0)); + assertEquals("45000", value); + + // and with negative value too + value = w.writeValueAsString(Duration.ofSeconds(-32L, 0)); + assertEquals("-32000", value); + } + + @Test + public void testSerializationAsTimestampMilliseconds02() throws Exception + { + String value = WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(Duration.ofSeconds(13498L, 8374)); + assertEquals("13498000", value); + } + + @Test + public void testSerializationAsTimestampMilliseconds03() throws Exception + { + Duration duration = Duration.ofSeconds(13498L, 837481723); + String value = WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(duration); + assertEquals("13498837", value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + Duration duration = Duration.ofSeconds(60L, 0); + String value = WRITER + .without(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .writeValueAsString(duration); + assertEquals(q(duration.toString()), value); + } + + @Test + public void testSerializationAsString02() throws Exception + { + Duration duration = Duration.ofSeconds(13498L, 8374); + String value = WRITER + .without(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .writeValueAsString(duration); + assertEquals(q(duration.toString()), value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, + SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .build(); + Duration duration = Duration.ofSeconds(13498L, 8374); + String value = mapper.writeValueAsString(duration); + + assertEquals("[\"" + Duration.class.getName() + "\",13498.000008374]", value); + } + + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .build(); + Duration duration = Duration.ofSeconds(13498L, 837481723); + String value = mapper.writeValueAsString(duration); + + assertEquals("[\"" + Duration.class.getName() + "\",13498837]", value); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .build(); + Duration duration = Duration.ofSeconds(13498L, 8374); + String value = mapper.writeValueAsString(duration); + + assertEquals("[\"" + Duration.class.getName() + "\",\"" + duration.toString() + "\"]", value); + } + + /* + /********************************************************** + /* Tests for custom patterns (modules-java8#189) + /********************************************************** + */ + + @Test + public void shouldSerializeInNanos_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("NANOS"); + assertEquals("3600000000000", mapper.writeValueAsString(Duration.ofHours(1))); + } + + @Test + public void shouldSerializeInMicros_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MICROS"); + assertEquals("1000", mapper.writeValueAsString(Duration.ofMillis(1))); + } + + @Test + public void shouldSerializeInMicrosDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MICROS"); + assertEquals("1", mapper.writeValueAsString(Duration.ofNanos(1500))); + } + + @Test + public void shouldSerializeInMillis_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MILLIS"); + assertEquals("1000", mapper.writeValueAsString(Duration.ofSeconds(1))); + } + + @Test + public void shouldSerializeInMillisDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MILLIS"); + assertEquals("1", mapper.writeValueAsString(Duration.ofNanos(1500000))); + } + + @Test + public void shouldSerializeInSeconds_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("SECONDS"); + assertEquals("60", mapper.writeValueAsString(Duration.ofMinutes(1))); + } + + @Test + public void shouldSerializeInSecondsDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("SECONDS"); + assertEquals("1", mapper.writeValueAsString(Duration.ofMillis(1500))); + } + + @Test + public void shouldSerializeInMinutes_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MINUTES"); + assertEquals("60", mapper.writeValueAsString(Duration.ofHours(1))); + } + + @Test + public void shouldSerializeInMinutesDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MINUTES"); + assertEquals("1", mapper.writeValueAsString(Duration.ofSeconds(90))); + } + + @Test + public void shouldSerializeInHours_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("HOURS"); + assertEquals("24", mapper.writeValueAsString(Duration.ofDays(1))); + } + + @Test + public void shouldSerializeInHoursDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("HOURS"); + assertEquals("1", mapper.writeValueAsString(Duration.ofMinutes(90))); + } + + @Test + public void shouldSerializeInHalfDays_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("HALF_DAYS"); + assertEquals("2", mapper.writeValueAsString(Duration.ofDays(1))); + } + + @Test + public void shouldSerializeInHalfDaysDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("DAYS"); + assertEquals("1", mapper.writeValueAsString(Duration.ofHours(30))); + } + + @Test + public void shouldSerializeInDays_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("DAYS"); + assertEquals("1", mapper.writeValueAsString(Duration.ofDays(1))); + } + + @Test + public void shouldSerializeInDaysDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("DAYS"); + assertEquals("1", mapper.writeValueAsString(Duration.ofHours(36))); + } + + protected ObjectMapper _mapperForPatternOverride(String pattern) { + ObjectMapper mapper = mapperBuilder() + .withConfigOverride(Duration.class, + cfg -> cfg.setFormat(JsonFormat.Value.forPattern(pattern))) + .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .build(); + return mapper; + } + + // [datetime#224] + @Test + public void testDurationFormatOverrideMinutes() throws Exception + { + assertEquals(a2q("{'mins':120}"), + WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .writeValueAsString(new MyDto224(Duration.ofHours(2)))); + } + + // [datetime#282] + @Test + public void testDurationFormatOverrideSeconds() throws Exception + { + final Duration maxDuration = Duration.ofSeconds(Long.MIN_VALUE); + assertEquals(a2q("{'duration':"+Long.MIN_VALUE+"}"), + WRITER + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .writeValueAsString(new Bean282(maxDuration))); + } + +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/InstantSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/InstantSerTest.java new file mode 100644 index 0000000000..ba80bb7eb3 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/InstantSerTest.java @@ -0,0 +1,189 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; + +import static org.junit.jupiter.api.Assertions.*; + +public class InstantSerTest extends DateTimeTestBase +{ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT; + + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerializationAsTimestamp01Nanoseconds() throws Exception + { + Instant date = Instant.ofEpochSecond(0L); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + + assertNotNull(value); + assertEquals(NO_NANOSECS_SER, value); + } + + @Test + public void testSerializationAsTimestamp01Milliseconds() throws Exception + { + Instant date = Instant.ofEpochSecond(0L); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("0", value); + } + + @Test + public void testSerializationAsTimestamp02Nanoseconds() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 183917322); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("123456789.183917322", value); + } + + @Test + public void testSerializationAsTimestamp02Milliseconds() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 183917322); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("123456789183", value); + } + + @Test + public void testSerializationAsTimestamp03Nanoseconds() throws Exception + { + Instant date = Instant.now(); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals(DecimalUtils.toDecimal(date.getEpochSecond(), date.getNano()), value); + } + + @Test + public void testSerializationAsTimestamp03Milliseconds() throws Exception + { + Instant date = Instant.now(); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals(Long.toString(date.toEpochMilli()), value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + Instant date = Instant.ofEpochSecond(0L); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationAsString02() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 183917322); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationAsString03() throws Exception + { + Instant date = Instant.now(); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 183917322); + ObjectMapper m = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String value = m.writeValueAsString(date); + assertEquals("[\"" + Instant.class.getName() + "\",123456789.183917322]", value); + } + + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + Instant date = Instant.ofEpochSecond(123456789L, 183917322); + ObjectMapper m = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String value = m.writeValueAsString(date); + assertEquals("[\"" + Instant.class.getName() + "\",123456789183]", value); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + Instant date = Instant.now(); + ObjectMapper m = newMapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String value = m.writeValueAsString(date); + assertEquals("[\"" + Instant.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", value); + } + + static class Pojo1 { + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + public Instant t1 = Instant.parse("2022-04-27T12:00:00Z"); + public Instant t2 = t1; + } + + @Test + public void testShapeInt() throws Exception { + String json1 = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(new Pojo1()); + assertEquals("{\"t1\":1651060800000,\"t2\":1651060800.000000000}", json1); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerTest.java new file mode 100644 index 0000000000..642611fa6e --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalDate; +import java.time.Month; +import java.time.temporal.Temporal; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalDateSerTest + extends DateTimeTestBase +{ + final static class EpochDayWrapper { + @JsonFormat(shape=JsonFormat.Shape.NUMBER_INT) + public LocalDate value; + + public EpochDayWrapper() { } + public EpochDayWrapper(LocalDate v) { value = v; } + } + + static class VanillaWrapper { + public LocalDate value; + + public VanillaWrapper() { } + public VanillaWrapper(LocalDate v) { value = v; } + } + + // [modules-java8#46] + static class Holder46 { + public LocalDate localDate; + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public Object object; + + public Holder46(LocalDate localDate, Object object) { + this.localDate = localDate; + this.object = object; + } + } + + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerializationAsTimestamp01() throws Exception + { + LocalDate date = LocalDate.of(1986, Month.JANUARY, 17); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + + assertNotNull(value); + assertEquals("[1986,1,17]", value); + } + + @Test + public void testSerializationAsTimestamp02() throws Exception + { + LocalDate date = LocalDate.of(2013, Month.AUGUST, 21); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + + assertNotNull(value); + assertEquals("[2013,8,21]", value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + LocalDate date = LocalDate.of(1986, Month.JANUARY, 17); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + + assertNotNull(value); + assertEquals('"' + date.toString() + '"', value); + } + + @Test + public void testSerializationAsString02() throws Exception + { + LocalDate date = LocalDate.of(2013, Month.AUGUST, 21); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertNotNull(value); + assertEquals('"' + date.toString() + '"', value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + LocalDate date = LocalDate.of(2005, Month.NOVEMBER, 5); + String value = mapper.writeValueAsString(date); + + assertNotNull(value); + assertEquals("[\"" + LocalDate.class.getName() + "\",\"" + date.toString() + "\"]", value); + } + + // [modules-java8#46] + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + final LocalDate localDate = LocalDate.of(2017, 12, 5); + String json = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(new Holder46(localDate, localDate)); + assertEquals(a2q("{\"localDate\":[2017,12,5],\"object\":{\"java.time.LocalDate\":[2017,12,5]}}"), + json); + } + + @Test + public void testConfigOverrides() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(LocalDate.class, + o -> o.setFormat(JsonFormat.Value.forPattern("yyyy_MM_dd"))) + .build(); + LocalDate date = LocalDate.of(2005, Month.NOVEMBER, 5); + VanillaWrapper input = new VanillaWrapper(date); + final String EXP_DATE = "\"2005_11_05\""; + String json = mapper.writeValueAsString(input); + assertEquals("{\"value\":"+EXP_DATE+"}", json); + assertEquals(EXP_DATE, mapper.writeValueAsString(date)); + + // and read back, too + VanillaWrapper output = mapper.readValue(json, VanillaWrapper.class); + assertEquals(input.value, output.value); + LocalDate date2 = mapper.readValue(EXP_DATE, LocalDate.class); + assertEquals(date, date2); + } + + @Test + public void testConfigOverridesToEpochDay() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .withConfigOverride(LocalDate.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.NUMBER_INT))) + .build(); + LocalDate date = LocalDate.ofEpochDay(1000); + VanillaWrapper input = new VanillaWrapper(date); + final String EXP_DATE = "1000"; + String json = mapper.writeValueAsString(input); + assertEquals("{\"value\":"+EXP_DATE+"}", json); + assertEquals(EXP_DATE, mapper.writeValueAsString(date)); + + // and read back, too + VanillaWrapper output = mapper.readValue(json, VanillaWrapper.class); + assertEquals(input.value, output.value); + LocalDate date2 = mapper.readValue(EXP_DATE, LocalDate.class); + assertEquals(date, date2); + } + + @Test + public void testCustomFormatToEpochDay() throws Exception + { + EpochDayWrapper w = MAPPER.readValue("{\"value\": 1000}", EpochDayWrapper.class); + LocalDate date = w.value; + assertNotNull(date); + assertEquals(LocalDate.ofEpochDay(1000), date); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java new file mode 100644 index 0000000000..e3a84d2a1c --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java @@ -0,0 +1,194 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalDateTime; +import java.time.Month; +import java.time.temporal.Temporal; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalDateTimeSerTest + extends DateTimeTestBase +{ + static class LDTWrapper { + @JsonFormat(pattern="yyyy-MM-dd'A'HH:mm:ss") + public LocalDateTime value; + + public LDTWrapper(LocalDateTime v) { value = v; } + } + + // 05-Feb-2025, tatu: Use Jackson 2.x defaults wrt as-timestamps + // serialization + private final static ObjectMapper MAPPER = mapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + @Test + public void testSerializationAsTimestamp01() throws Exception + { + LocalDateTime time = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43); + assertEquals("[1986,1,17,15,43]", + MAPPER.writeValueAsString(time)); + } + + @Test + public void testSerializationAsTimestamp02() throws Exception + { + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57); + String value = MAPPER.writeValueAsString(time); + + assertEquals("[2013,8,21,9,22,57]", value); + } + + @Test + public void testSerializationAsTimestamp03Nanosecond() throws Exception + { + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57); + + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[2013,8,21,9,22,0,57]", value); + } + + @Test + public void testSerializationAsTimestamp03Millisecond() throws Exception + { + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[2013,8,21,9,22,0,0]", value); + } + + @Test + public void testSerializationAsTimestamp04Nanosecond() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[2005,11,5,22,31,5,829837]", value); + } + + @Test + public void testSerializationAsTimestamp04Millisecond() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 422829837); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[2005,11,5,22,31,5,422]", value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + LocalDateTime time = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43, 05); + final ObjectMapper m = newMapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + assertEquals("\"1986-01-17T15:43:05\"", m.writeValueAsString(time)); + } + + @Test + public void testSerializationAsString02() throws Exception + { + LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57); + + final ObjectMapper m = newMapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + String value = m.writeValueAsString(time); + assertEquals('"' + time.toString() + '"', value); + } + + @Test + public void testSerializationAsString03() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + final ObjectMapper m = newMapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + String value = m.writeValueAsString(time); + assertEquals('"' + time.toString() + '"', value); + } + + @Test + public void testSerializationWithFormatOverride() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 999000); + assertEquals(a2q("{'value':'2005-11-05A22:31:05'}"), + MAPPER.writeValueAsString(new LDTWrapper(time))); + + ObjectMapper m = mapperBuilder().withConfigOverride(LocalDateTime.class, + cfg -> cfg.setFormat(JsonFormat.Value.forPattern("yyyy-MM-dd'X'HH:mm"))) + .build(); + assertEquals(a2q("'2005-11-05X22:31'"), m.writeValueAsString(time)); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + + final ObjectMapper m = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String value = m.writeValueAsString(time); + assertEquals("[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,829837]]", value); + } + + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + final ObjectMapper m = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 422829837); + String value = m.writeValueAsString(time); + assertEquals("[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,422]]", value); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + final ObjectMapper m = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837); + String value = m.writeValueAsString(time); + assertEquals("[\"" + LocalDateTime.class.getName() + "\",\"" + time.toString() + "\"]", value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerTest.java new file mode 100644 index 0000000000..c51f498780 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerTest.java @@ -0,0 +1,200 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.ext.javatime.ser.LocalTimeSerializer; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocalTimeSerTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + private final ObjectWriter writer = MAPPER.writer(); + + // [modules-java8#115] + static class CustomLocalTimeSerializer extends LocalTimeSerializer { + public CustomLocalTimeSerializer() { + // Default doesn't cut it for us. + super(DateTimeFormatter.ofPattern("HH/mm")); + } + } + + static class CustomWrapper { + @JsonSerialize(using = CustomLocalTimeSerializer.class) + public LocalTime value; + + public CustomWrapper(LocalTime v) { value = v; } + } + + @Test + public void testSerializationAsTimestamp01() throws Exception + { + String json = writer.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(LocalTime.of(15, 43)); + assertEquals("[15,43]", json, "The value is not correct."); + } + + @Test + public void testSerializationAsTimestamp02() throws Exception + { + String json = writer.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(LocalTime.of(9, 22, 57)); + assertEquals("[9,22,57]", json, "The value is not correct."); + } + + @Test + public void testSerializationAsTimestamp03Nanoseconds() throws Exception + { + String json = writer.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, + SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(LocalTime.of(9, 22, 0, 57)); + assertEquals("[9,22,0,57]", json, "The value is not correct."); + } + + @Test + public void testSerializationAsTimestamp03Milliseconds() throws Exception + { + LocalTime time = LocalTime.of(9, 22, 0, 57); + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) + .build(); + String value = mapper.writeValueAsString(time); + + assertEquals("[9,22,0,0]", value, "The value is not correct."); + } + + @Test + public void testSerializationAsTimestamp04Nanoseconds() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 829837); + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true) + .build(); + String value = mapper.writeValueAsString(time); + assertEquals("[22,31,5,829837]", value, "The value is not correct."); + } + + @Test + public void testSerializationAsTimestamp04Milliseconds() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 422829837); + ObjectMapper mapper = newMapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .build(); + String value = mapper.writeValueAsString(time); + assertEquals("[22,31,5,422]", value, "The value is not correct."); + } + + @Test + public void testSerializationAsString01() throws Exception + { + LocalTime time = LocalTime.of(15, 43, 20); + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .build(); + assertEquals("\"15:43:20\"", mapper.writeValueAsString(time)); + } + + @Test + public void testSerializationAsString02() throws Exception + { + LocalTime time = LocalTime.of(9, 22, 57); + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .build(); + String value = mapper.writeValueAsString(time); + assertEquals('"' + time.toString() + '"', value, "The value is not correct."); + } + + @Test + public void testSerializationAsString03() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 829837); + ObjectMapper m = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .build(); + String value = m.writeValueAsString(time); + assertEquals('"' + time.toString() + '"', value, "The value is not correct."); + } + + // [modules-java8#115] + @Test + public void testWithCustomSerializer() throws Exception + { + String json = MAPPER.writeValueAsString(new CustomWrapper(LocalTime.of(15, 43))); + assertEquals("{\"value\":\"15/43\"}", json, "The value is not correct."); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 829837); + ObjectMapper m = newMapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String json = m.writeValueAsString(time); + + assertEquals("[\"" + LocalTime.class.getName() + "\",[22,31,5,829837]]", json, + "The value is not correct."); + } + + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 422829837); + + ObjectMapper m = newMapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String json = m.writeValueAsString(time); + assertEquals("[\"" + LocalTime.class.getName() + "\",[22,31,5,422]]", json, + "The value is not correct."); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + LocalTime time = LocalTime.of(22, 31, 5, 829837); + ObjectMapper m = newMapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String value = m.writeValueAsString(time); + + assertEquals("[\"" + LocalTime.class.getName() + "\",\"" + time.toString() + "\"]", value, + "The value is not correct."); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerTest.java new file mode 100644 index 0000000000..7a9053936a --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Month; +import java.time.MonthDay; +import java.time.temporal.TemporalAccessor; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class MonthDaySerTest + extends DateTimeTestBase +{ + private ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerialization01() throws Exception + { + assertEquals("\"--01-17\"", + MAPPER.writeValueAsString(MonthDay.of(Month.JANUARY, 17))); + } + + @Test + public void testSerialization02() throws Exception + { + assertEquals("\"--08-21\"", + MAPPER.writeValueAsString(MonthDay.of(Month.AUGUST, 21))); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + final ObjectMapper mapper = mapperBuilder() + .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class) + .build(); + MonthDay monthDay = MonthDay.of(Month.NOVEMBER, 5); + String value = mapper.writeValueAsString(monthDay); + assertEquals("[\"" + MonthDay.class.getName() + "\",\"--11-05\"]", value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerTest.java new file mode 100644 index 0000000000..f644feea05 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerTest.java @@ -0,0 +1,290 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OffsetDateTimeSerTest + extends DateTimeTestBase +{ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + private static final ZoneId Z1 = ZoneId.of("America/Chicago"); + + private static final ZoneId Z2 = ZoneId.of("America/Anchorage"); + + private static final ZoneId Z3 = ZoneId.of("America/Los_Angeles"); + + static class Wrapper { + @JsonFormat( + pattern="yyyy_MM_dd'T'HH:mm:ssZ", + shape=JsonFormat.Shape.STRING) + public OffsetDateTime value; + + public Wrapper() { } + public Wrapper(OffsetDateTime v) { value = v; } + } + + private ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerializationAsTimestamp01Nanoseconds() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("0.0", value); + } + + @Test + public void testSerializationAsTimestamp01Milliseconds() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("0", value); + } + + @Test + public void testSerializationAsTimestamp02Nanoseconds() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("123456789.183917322", value); + } + + @Test + public void testSerializationAsTimestamp02Milliseconds() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("123456789183", value); + } + + @Test + public void testSerializationAsTimestamp03Nanoseconds() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals(DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano()), value); + } + + @Test + public void testSerializationAsTimestamp03Milliseconds() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals(Long.toString(date.toInstant().toEpochMilli()), value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + + FORMATTER.withZone(Z1).format(date) + '"', value); + } + + @Test + public void testSerializationAsString02() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + + FORMATTER.withZone(Z2).format(date) + '"', value); + } + + @Test + public void testSerializationAsString03() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + + FORMATTER.withZone(Z3).format(date) + '"', value); + } + + // [modules-java#254] + @Test + public void testSerializationWithJsonFormat() throws Exception + { + OffsetDateTime t1 = OffsetDateTime.parse("2022-04-27T12:00:00+02:00"); + Wrapper input = new Wrapper(t1); + + // pattern="yyyy_MM_dd'T'HH:mm:ssZ" + assertEquals(a2q("{'value':'2022_04_27T12:00:00+0200'}"), + MAPPER.writeValueAsString(input)); + + ObjectMapper m = mapperBuilder().withConfigOverride(OffsetDateTime.class, + cfg -> cfg.setFormat(JsonFormat.Value.forPattern("yyyy.MM.dd'x'HH:mm:ss"))) + .build(); + assertEquals(a2q("'2022.04.27x12:00:00'"), m.writeValueAsString(t1)); + } + + @Test + public void testSerializationAsStringWithMapperTimeZone01() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = newMapper() + .writer() + .with(TimeZone.getTimeZone(Z1)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationAsStringWithMapperTimeZone02() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = newMapper() + .writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationAsStringWithMapperTimeZone03() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + String value = newMapper() + .writer() + .with(TimeZone.getTimeZone(Z3)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = newMapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, + SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build() + .writeValueAsString(date); + assertEquals("[\"" + OffsetDateTime.class.getName() + "\",123456789.183917322]", value); + } + + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .build() + .writeValueAsString(date); + assertEquals("[\"" + OffsetDateTime.class.getName() + "\",123456789183]", value); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + ObjectMapper m = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .build(); + String value = m.writeValueAsString(date); + assertEquals("[\"" + OffsetDateTime.class.getName() + "\",\"" + + FORMATTER.withZone(Z3).format(date) + "\"]", value); + } + + @Test + public void testSerializationWithTypeInfoAndMapperTimeZone() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + String value = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build() + .writer() + .with(TimeZone.getTimeZone(Z3)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + + assertEquals("[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", value); + } + + @Test + public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOn() throws Exception { + OffsetDateTime date = OffsetDateTime.now(Z3); + String value = MAPPER.writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE) + .writeValueAsString(date); + + // We expect to have the date written with the ZoneId Z2 + assertEquals("\"" + FORMATTER.format(date.atZoneSameInstant(Z2)) + "\"", value); + } + + @Test + public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOff() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = MAPPER.writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE) + .writeValueAsString(date); + + // We expect to have the date written with the ZoneId Z3 + assertEquals("\"" + FORMATTER.format(date) + "\"", value); + } + + static class Pojo1 { + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + public OffsetDateTime t1 = OffsetDateTime.parse("2022-04-27T12:00:00+02:00"); + public OffsetDateTime t2 = t1; + } + + @Test + public void testShapeInt() throws Exception { + String json1 = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(new Pojo1()); + assertEquals("{\"t1\":1651053600000,\"t2\":1651053600.000000000}", json1); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerTest.java new file mode 100644 index 0000000000..5635fc36d9 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.temporal.Temporal; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.*; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class OffsetTimeSerTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerializationAsTimestamp01() throws Exception + { + OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300")); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(time); + assertEquals("[15,43,\"+03:00\"]", value); + } + + @Test + public void testSerializationAsTimestamp02() throws Exception + { + OffsetTime time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630")); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(time); + assertEquals("[9,22,57,\"-06:30\"]", value); + } + + @Test + public void testSerializationAsTimestamp03Nanoseconds() throws Exception + { + OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630")); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[9,22,0,57,\"-06:30\"]", value); + } + + @Test + public void testSerializationAsTimestamp03Milliseconds() throws Exception + { + OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630")); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[9,22,0,0,\"-06:30\"]", value); + } + + @Test + public void testSerializationAsTimestamp04Nanoseconds() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[22,31,5,829837,\"+11:00\"]", value); + } + + @Test + public void testSerializationAsTimestamp04Milliseconds() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 422829837, ZoneOffset.of("+1100")); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(time); + assertEquals("[22,31,5,422,\"+11:00\"]", value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300")); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(time); + assertEquals('"' + time.toString() + '"', value); + } + + @Test + public void testSerializationAsString02() throws Exception + { + OffsetTime time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630")); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(time); + assertEquals('"' + time.toString() + '"', value); + } + + @Test + public void testSerializationAsString03() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(time); + assertEquals('"' + time.toString() + '"', value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + + final ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + assertEquals("[\"" + OffsetTime.class.getName() + "\",[22,31,5,829837,\"+11:00\"]]", + mapper.writeValueAsString(time)); + } + + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 422829837, ZoneOffset.of("+1100")); + + final ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) + .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + + assertEquals("[\"" + OffsetTime.class.getName() + "\",[22,31,5,422,\"+11:00\"]]", + mapper.writeValueAsString(time)); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100")); + + final ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + + assertEquals("[\"" + OffsetTime.class.getName() + "\",\"" + time.toString() + "\"]", + mapper.writeValueAsString(time)); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerTest.java new file mode 100644 index 0000000000..8cb58a109b --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerTest.java @@ -0,0 +1,64 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Month; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OneBasedMonthSerTest extends DateTimeTestBase +{ + static class Wrapper { + public Month month; + + public Wrapper(Month m) { month = m; } + public Wrapper() { } + } + + @Test + public void testSerializationFromEnum() throws Exception + { + assertEquals( "\"JANUARY\"" , writerForOneBased() + .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .writeValueAsString(Month.JANUARY)); + assertEquals( "\"JANUARY\"" , writerForZeroBased() + .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .writeValueAsString(Month.JANUARY)); + } + + @Test + public void testSerializationFromEnumWithPattern_oneBased() throws Exception + { + ObjectWriter w = writerForOneBased().with(SerializationFeature.WRITE_ENUMS_USING_INDEX); + assertEquals( "{\"month\":1}" , w.writeValueAsString(new Wrapper(Month.JANUARY))); + } + + @Test + public void testSerializationFromEnumWithPattern_zeroBased() throws Exception + { + ObjectWriter w = writerForZeroBased().with(SerializationFeature.WRITE_ENUMS_USING_INDEX); + assertEquals( "{\"month\":0}" , w.writeValueAsString(new Wrapper(Month.JANUARY))); + } + + + private ObjectWriter writerForZeroBased() { + return JsonMapper.builder() + .disable(DateTimeFeature.ONE_BASED_MONTHS) + .build() + .writer(); + } + + private ObjectWriter writerForOneBased() { + return JsonMapper.builder() + .enable(DateTimeFeature.ONE_BASED_MONTHS) + .build() + .writer(); + } + +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/PeriodSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/PeriodSerTest.java new file mode 100644 index 0000000000..0ff889e53d --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/PeriodSerTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Period; +import java.time.temporal.TemporalAmount; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PeriodSerTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerialization01() throws Exception + { + assertEquals(q("P1Y6M15D"), MAPPER.writeValueAsString(Period.of(1, 6, 15))); + } + + @Test + public void testSerialization02() throws Exception + { + assertEquals(q("P21D"), MAPPER.writeValueAsString(Period.of(0, 0, 21))); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + Period period = Period.of(5, 1, 12); + final ObjectMapper mapper = mapperBuilder() + .addMixIn(TemporalAmount.class, MockObjectConfiguration.class) + .build(); + String value = mapper.writeValueAsString(period); + assertEquals("[" + q(Period.class.getName()) + ",\"P5Y1M12D\"]", value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateSerializationWithCustomFormatter.java new file mode 100644 index 0000000000..e0bd2f81df --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateSerializationWithCustomFormatter.java @@ -0,0 +1,64 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import tools.jackson.core.json.JsonWriteFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.deser.LocalDateDeserializer; +import tools.jackson.databind.ext.javatime.ser.LocalDateSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestLocalDateSerializationWithCustomFormatter +{ + @ParameterizedTest + @MethodSource("customFormatters") + void testSerialization(DateTimeFormatter formatter) throws Exception { + LocalDate date = LocalDate.now(); + assertTrue(serializeWith(date, formatter).contains(date.format(formatter)), + "Serialized value should contain the formatted date"); + } + + private String serializeWith(LocalDate date, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES) + .addModule(new SimpleModule() + .addSerializer(new LocalDateSerializer(f))) + .build(); + return mapper.writeValueAsString(date); + } + + @ParameterizedTest + @MethodSource("customFormatters") + void testDeserialization(DateTimeFormatter formatter) throws Exception { + LocalDate date = LocalDate.now(); + assertEquals(date, deserializeWith(date.format(formatter), formatter), + "Deserialized value should match the original date"); + } + + private LocalDate deserializeWith(String json, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addDeserializer(LocalDate.class, new LocalDateDeserializer(f))) + .build(); + return mapper.readValue("\"" + json + "\"", LocalDate.class); + } + + static Stream customFormatters() { + return Stream.of( + DateTimeFormatter.BASIC_ISO_DATE, + DateTimeFormatter.ISO_DATE, + DateTimeFormatter.ISO_LOCAL_DATE, + DateTimeFormatter.ISO_ORDINAL_DATE, + DateTimeFormatter.ISO_WEEK_DATE, + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateTimeSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateTimeSerializationWithCustomFormatter.java new file mode 100644 index 0000000000..d95bf6bb00 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateTimeSerializationWithCustomFormatter.java @@ -0,0 +1,57 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer; +import tools.jackson.databind.ext.javatime.ser.LocalDateTimeSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestLocalDateTimeSerializationWithCustomFormatter +{ + @ParameterizedTest + @MethodSource("customFormatters") + void testSerialization(DateTimeFormatter formatter) throws Exception { + LocalDateTime dateTime = LocalDateTime.now(); + assertTrue(serializeWith(dateTime, formatter).contains(dateTime.format(formatter))); + } + + private String serializeWith(LocalDateTime dateTime, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addSerializer(new LocalDateTimeSerializer(f))) + .build(); + return mapper.writeValueAsString(dateTime); + } + + @ParameterizedTest + @MethodSource("customFormatters") + void testDeserialization(DateTimeFormatter formatter) throws Exception { + LocalDateTime dateTime = LocalDateTime.now(); + assertEquals(dateTime, deserializeWith(dateTime.format(formatter), formatter)); + } + + private LocalDateTime deserializeWith(String json, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(f))) + .build(); + return mapper.readValue("\"" + json + "\"", LocalDateTime.class); + } + + static Stream customFormatters() { + return Stream.of( + DateTimeFormatter.ISO_DATE_TIME, + DateTimeFormatter.ISO_LOCAL_DATE_TIME + ); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalTimeSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalTimeSerializationWithCustomFormatter.java new file mode 100644 index 0000000000..f0c94b7899 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalTimeSerializationWithCustomFormatter.java @@ -0,0 +1,56 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.deser.LocalTimeDeserializer; +import tools.jackson.databind.ext.javatime.ser.LocalTimeSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestLocalTimeSerializationWithCustomFormatter +{ + @ParameterizedTest + @MethodSource("customFormatters") + void testSerialization(DateTimeFormatter formatter) throws Exception { + LocalTime dateTime = LocalTime.now(); + assertTrue(serializeWith(dateTime, formatter).contains(dateTime.format(formatter))); + } + + private String serializeWith(LocalTime dateTime, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addSerializer(new LocalTimeSerializer(f))) + .build(); + return mapper.writeValueAsString(dateTime); + } + + @ParameterizedTest + @MethodSource("customFormatters") + void testDeserialization(DateTimeFormatter formatter) throws Exception { + LocalTime dateTime = LocalTime.now(); + assertEquals(dateTime, deserializeWith(dateTime.format(formatter), formatter)); + } + + private LocalTime deserializeWith(String json, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addDeserializer(LocalTime.class, new LocalTimeDeserializer(f))) + .build(); + return mapper.readValue("\"" + json + "\"", LocalTime.class); + } + + static Stream customFormatters() { + return Stream.of( + DateTimeFormatter.ISO_LOCAL_TIME, + DateTimeFormatter.ISO_TIME + ); + } +} \ No newline at end of file diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestYearMonthSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestYearMonthSerializationWithCustomFormatter.java new file mode 100644 index 0000000000..57d3bc8fca --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestYearMonthSerializationWithCustomFormatter.java @@ -0,0 +1,57 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.deser.YearMonthDeserializer; +import tools.jackson.databind.ext.javatime.ser.YearMonthSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestYearMonthSerializationWithCustomFormatter +{ + @ParameterizedTest + @MethodSource("customFormatters") + void testSerialization(DateTimeFormatter formatter) throws Exception { + YearMonth dateTime = YearMonth.now(); + assertTrue(serializeWith(dateTime, formatter).contains(dateTime.format(formatter))); + } + + private String serializeWith(YearMonth dateTime, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addSerializer(new YearMonthSerializer(f))) + .build(); + return mapper.writeValueAsString(dateTime); + } + + @ParameterizedTest + @MethodSource("customFormatters") + void testDeserialization(DateTimeFormatter formatter) throws Exception { + YearMonth dateTime = YearMonth.now(); + assertEquals(dateTime, deserializeWith(dateTime.format(formatter), formatter)); + } + + private YearMonth deserializeWith(String json, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addDeserializer(YearMonth.class, new YearMonthDeserializer(f))) + .build(); + return mapper.readValue("\"" + json + "\"", YearMonth.class); + } + + static Stream customFormatters() { + return Stream.of( + DateTimeFormatter.ofPattern("uuuu-MM"), + DateTimeFormatter.ofPattern("uu-M") + ); + } +} \ No newline at end of file diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestYearSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestYearSerializationWithCustomFormatter.java new file mode 100644 index 0000000000..2d6a13be46 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestYearSerializationWithCustomFormatter.java @@ -0,0 +1,57 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Year; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.deser.YearDeserializer; +import tools.jackson.databind.ext.javatime.ser.YearSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestYearSerializationWithCustomFormatter +{ + @ParameterizedTest + @MethodSource("customFormatters") + void testSerialization(DateTimeFormatter formatter) throws Exception { + Year year = Year.now(); + String expected = "\"" + year.format(formatter) + "\""; + assertEquals(expected, serializeWith(year, formatter)); + } + + private String serializeWith(Year dateTime, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addSerializer(new YearSerializer(f))) + .build(); + return mapper.writeValueAsString(dateTime); + } + + @ParameterizedTest + @MethodSource("customFormatters") + void testDeserialization(DateTimeFormatter formatter) throws Exception { + Year year = Year.now(); + assertEquals(year, deserializeWith(year.format(formatter), formatter)); + } + + private Year deserializeWith(String json, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule() + .addDeserializer(Year.class, new YearDeserializer(f))) + .build(); + return mapper.readValue("\"" + json + "\"", Year.class); + } + + static Stream customFormatters() { + return Stream.of( + DateTimeFormatter.ofPattern("yyyy"), + DateTimeFormatter.ofPattern("yy") + ); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestZonedDateTimeSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestZonedDateTimeSerializationWithCustomFormatter.java new file mode 100644 index 0000000000..0309516db1 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestZonedDateTimeSerializationWithCustomFormatter.java @@ -0,0 +1,47 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.ser.ZonedDateTimeSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestZonedDateTimeSerializationWithCustomFormatter +{ + @MethodSource("customFormatters") + @ParameterizedTest + public void testSerialization(DateTimeFormatter formatter) throws Exception { + ZonedDateTime zonedDateTime = ZonedDateTime.now(); + assertTrue(serializeWith(zonedDateTime, formatter).contains(zonedDateTime.format(formatter.withZone(ZoneOffset.UTC)))); + } + + private String serializeWith(ZonedDateTime zonedDateTime, DateTimeFormatter f) throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .addModule(new SimpleModule().addSerializer( + new ZonedDateTimeSerializer(f))) + .defaultTimeZone(TimeZone.getTimeZone("UTC")) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + return mapper.writeValueAsString(zonedDateTime); + } + + public static Stream customFormatters() { + return Stream.of( + DateTimeFormatter.ISO_ZONED_DATE_TIME, + DateTimeFormatter.ISO_OFFSET_DATE_TIME, + DateTimeFormatter.ISO_LOCAL_DATE_TIME, + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + ); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/WriteNanosecondsTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/WriteNanosecondsTest.java new file mode 100644 index 0000000000..ab61716eb2 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/WriteNanosecondsTest.java @@ -0,0 +1,119 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; + +public class WriteNanosecondsTest extends DateTimeTestBase +{ + public static final ZoneId UTC = ZoneId.of("UTC"); + + // 05-Feb-2025, tatu: Use Jackson 2.x defaults wrt as-timestamps + // serialization + private final static ObjectMapper MAPPER = mapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + public static class DummyClass { + @JsonFormat(with = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + private final T nanoseconds; + + @JsonFormat(without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + private final T notNanoseconds; + + DummyClass(T t) { + this.nanoseconds = t; + this.notNanoseconds = t; + } + } + + @Test + public void testSerializeDurationWithAndWithoutNanoseconds() throws Exception { + DummyClass value = new DummyClass<>(Duration.ZERO); + + String json = MAPPER.writer() + .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .writeValueAsString(value); + + assertThat(json).contains("\"nanoseconds\":0.0"); + assertThat(json).contains("\"notNanoseconds\":0"); + } + + @Test + public void testSerializeInstantWithAndWithoutNanoseconds() throws Exception { + DummyClass input = new DummyClass<>(Instant.EPOCH); + + String json = MAPPER.writeValueAsString(input); + + assertTrue(json.contains("\"nanoseconds\":0.0")); + assertTrue(json.contains("\"notNanoseconds\":0")); + } + + @Test + public void testSerializeLocalDateTimeWithAndWithoutNanoseconds() throws Exception { + DummyClass input = new DummyClass<>( + // Nanos will only be written if it's non-zero + LocalDateTime.of(1970, 1, 1, 0, 0, 0, 1) + ); + + String json = MAPPER.writeValueAsString(input); + + assertTrue(json.contains("\"nanoseconds\":[1970,1,1,0,0,0,1]")); + assertTrue(json.contains("\"notNanoseconds\":[1970,1,1,0,0,0,0]")); + } + + @Test + public void testSerializeLocalTimeWithAndWithoutNanoseconds() throws Exception { + DummyClass input = new DummyClass<>( + // Nanos will only be written if it's non-zero + LocalTime.of(0, 0, 0, 1) + ); + + String json = MAPPER.writeValueAsString(input); + + assertTrue(json.contains("\"nanoseconds\":[0,0,0,1]")); + assertTrue(json.contains("\"notNanoseconds\":[0,0,0,0]")); + } + + @Test + public void testSerializeOffsetDateTimeWithAndWithoutNanoseconds() throws Exception { + DummyClass input = new DummyClass<>(OffsetDateTime.ofInstant(Instant.EPOCH, UTC)); + + String json = MAPPER.writeValueAsString(input); + + assertTrue(json.contains("\"nanoseconds\":0.0")); + assertTrue(json.contains("\"notNanoseconds\":0")); + } + + @Test + public void testSerializeOffsetTimeWithAndWithoutNanoseconds() throws Exception { + DummyClass input = new DummyClass<>( + // Nanos will only be written if it's non-zero + OffsetTime.of(0,0,0, 1 , ZoneOffset.UTC) + ); + + String json = MAPPER.writeValueAsString(input); + + assertTrue(json.contains("\"nanoseconds\":[0,0,0,1,\"Z\"]")); + assertTrue(json.contains("\"notNanoseconds\":[0,0,0,0,\"Z\"]")); + } + + @Test + public void testSerializeZonedDateTimeWithAndWithoutNanoseconds() throws Exception { + DummyClass input = new DummyClass<>(ZonedDateTime.ofInstant(Instant.EPOCH, UTC)); + + String json = MAPPER.writeValueAsString(input); + + assertTrue(json.contains("\"nanoseconds\":0.0")); + assertTrue(json.contains("\"notNanoseconds\":0")); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/WriteZoneIdTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/WriteZoneIdTest.java new file mode 100644 index 0000000000..e98b202a30 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/WriteZoneIdTest.java @@ -0,0 +1,92 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class WriteZoneIdTest extends DateTimeTestBase +{ + static class DummyClassWithDate { + @JsonFormat(shape = JsonFormat.Shape.STRING, + pattern = "dd-MM-yyyy'T'hh:mm:ss Z", + with = JsonFormat.Feature.WRITE_DATES_WITH_ZONE_ID) + public ZonedDateTime date; + + DummyClassWithDate() { } + + public DummyClassWithDate(ZonedDateTime date) { + this.date = date; + } + } + + private static ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerialization01() throws Exception + { + ZoneId id = ZoneId.of("America/Chicago"); + String value = MAPPER.writeValueAsString(id); + assertEquals("\"America/Chicago\"", value); + } + + @Test + public void testSerialization02() throws Exception + { + ZoneId id = ZoneId.of("America/Anchorage"); + String value = MAPPER.writeValueAsString(id); + assertEquals("\"America/Anchorage\"", value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + ZoneId id = ZoneId.of("America/Denver"); + ObjectMapper mapper = mapperBuilder() + .addMixIn(ZoneId.class, MockObjectConfiguration.class) + .build(); + String value = mapper.writeValueAsString(id); + assertEquals("[\"java.time.ZoneId\",\"America/Denver\"]", value); + } + + @Test + public void testJacksonAnnotatedPOJOWithDateWithTimezoneToJson() throws Exception + { + String ZONE_ID_STR = "Asia/Krasnoyarsk"; + final ZoneId ZONE_ID = ZoneId.of(ZONE_ID_STR); + + DummyClassWithDate input = new DummyClassWithDate(ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), ZONE_ID)); + + // 30-Jun-2016, tatu: Exact time seems to vary a bit based on DST, so let's actually + // just verify appending of timezone id itself: + String json = MAPPER.writeValueAsString(input); + if (!json.contains("\"01-01-1970T")) { + fail("Should contain time prefix, did not: "+json); + } + String match = String.format("[%s]", ZONE_ID_STR); + if (!json.contains(match)) { + fail("Should contain zone id "+match+", does not: "+json); + } + } + + @Test + public void testMapSerialization() throws Exception { + final ZonedDateTime datetime = ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Warsaw]"); + final HashMap map = new HashMap<>(); + map.put(datetime, ""); + String json = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .writeValueAsString(map); + assertEquals("{\"2007-12-03T10:15:30+01:00[Europe/Warsaw]\":\"\"}", json); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializationTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializationTest.java new file mode 100644 index 0000000000..d227fe8552 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializationTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Month; +import java.time.YearMonth; +import java.time.temporal.Temporal; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class YearMonthSerializationTest + extends DateTimeTestBase +{ + private static class SimpleAggregate + { + @JsonProperty("yearMonth") + @JsonFormat(pattern = "yyMM") + final YearMonth yearMonth; + + @JsonCreator + SimpleAggregate(@JsonProperty("yearMonth") YearMonth yearMonth) + { + this.yearMonth = yearMonth; + } + } + + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerializationAsTimestamp01() throws Exception + { + YearMonth yearMonth = YearMonth.of(1986, Month.JANUARY); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(yearMonth); + + assertNotNull(value); + assertEquals("[1986,1]", value); + } + + @Test + public void testSerializationAsTmestamp02() throws Exception + { + YearMonth yearMonth = YearMonth.of(2013, Month.AUGUST); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(yearMonth); + + assertNotNull(value); + assertEquals("[2013,8]", value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + YearMonth yearMonth = YearMonth.of(1986, Month.JANUARY); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(yearMonth); + + assertNotNull(value); + assertEquals('"' + yearMonth.toString() + '"', value); + } + + @Test + public void testSerializationAsString02() throws Exception + { + YearMonth yearMonth = YearMonth.of(2013, Month.AUGUST); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(yearMonth); + assertEquals('"' + yearMonth.toString() + '"', value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + YearMonth yearMonth = YearMonth.of(2005, Month.NOVEMBER); + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String value = mapper.writeValueAsString(yearMonth); + assertEquals("[\"" + YearMonth.class.getName() + "\",\"" + yearMonth.toString() + "\"]", value); + } + + @Test + public void testDeserializationAsTimestamp01() throws Exception + { + YearMonth yearMonth = YearMonth.of(1986, Month.JANUARY); + YearMonth value = MAPPER.readValue("[1986,1]", YearMonth.class); + assertEquals(yearMonth, value); + } + + @Test + public void testDeserializationAsTimestamp02() throws Exception + { + YearMonth yearMonth = YearMonth.of(2013, Month.AUGUST); + YearMonth value = MAPPER.readValue("[2013,8]", YearMonth.class); + assertEquals(yearMonth, value); + } + + @Test + public void testDeserializationAsString01() throws Exception + { + YearMonth yearMonth = YearMonth.of(1986, Month.JANUARY); + YearMonth value = MAPPER.readValue('"' + yearMonth.toString() + '"', YearMonth.class); + + assertNotNull(value); + assertEquals(yearMonth, value); + } + + @Test + public void testDeserializationAsString02() throws Exception + { + YearMonth yearMonth = YearMonth.of(2013, Month.AUGUST); + YearMonth value = this.MAPPER.readValue('"' + yearMonth.toString() + '"', YearMonth.class); + assertEquals(yearMonth, value); + } + + @Test + public void testDeserializationWithTypeInfo01() throws Exception + { + YearMonth yearMonth = YearMonth.of(2005, Month.NOVEMBER); + + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readValue("[\"" + YearMonth.class.getName() + "\",\"" + yearMonth.toString() + "\"]", Temporal.class); + assertInstanceOf(YearMonth.class, value, "The value should be a YearMonth."); + assertEquals(yearMonth, value); + } + + @Test + public void testSerializationWithPattern01() throws Exception + { + YearMonth yearMonth = YearMonth.of(2013, Month.AUGUST); + SimpleAggregate simpleAggregate = new SimpleAggregate(yearMonth); + String value = MAPPER.writeValueAsString(simpleAggregate); + assertEquals("{\"yearMonth\":\"1308\"}", value); + } + + @Test + public void testDeserializationWithPattern01() throws Exception + { + YearMonth yearMonth = YearMonth.of(2013, Month.AUGUST); + SimpleAggregate simpleAggregate = new SimpleAggregate(yearMonth); + + SimpleAggregate value = MAPPER.readValue("{\"yearMonth\":\"1308\"}", SimpleAggregate.class); + assertEquals(simpleAggregate.yearMonth, value.yearMonth); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/YearSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/YearSerTest.java new file mode 100644 index 0000000000..f08f77872a --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/YearSerTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.Year; +import java.time.temporal.Temporal; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class YearSerTest extends DateTimeTestBase +{ + final static class YearAsStringWrapper { + @JsonFormat(shape = JsonFormat.Shape.STRING) + public Year value; + + public YearAsStringWrapper(Year value) { + this.value = value; + } + } + + // Defaults fine: year only serialized as String with explicit + // overrides + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testDefaultSerialization() throws Exception + { + assertEquals("1986", + MAPPER.writeValueAsString(Year.of(1986))); + assertEquals("2013", + MAPPER.writeValueAsString(Year.of(2013))); + } + + @Test + public void testAsStringSerializationViaAnnotation() throws Exception + { + assertEquals(a2q("{'value':'1972'}"), + MAPPER.writeValueAsString(new YearAsStringWrapper(Year.of(1972)))); + } + + @Test + public void testAsStringSerializationViaFormatConfig() throws Exception + { + final ObjectMapper asStringMapper = mapperBuilder() + .withConfigOverride(Year.class, o -> o.setFormat( + JsonFormat.Value.forShape(JsonFormat.Shape.STRING))) + .build(); + + assertEquals(q("2025"), + asStringMapper.writeValueAsString(Year.of(2025))); + } + + @Test + public void testSerializationWithTypeInfo() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + String value = mapper.writeValueAsString(Year.of(2005)); + assertEquals("[\"" + Year.class.getName() + "\",2005]", value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerTest.java new file mode 100644 index 0000000000..a4efe3c7ef --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.ZoneId; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ZoneIdSerTest extends DateTimeTestBase +{ + private ObjectMapper MAPPER = newMapper(); + + private final ObjectMapper MOCK_OBJECT_MIXIN_MAPPER = mapperBuilder() + .addMixIn(ZoneId.class, MockObjectConfiguration.class) + .build(); + + @Test + public void testSerialization01() throws Exception + { + final String value = MAPPER.writeValueAsString(ZoneId.of("America/Chicago")); + assertEquals("\"America/Chicago\"", value); + } + + @Test + public void testSerialization02() throws Exception + { + final String value = MAPPER.writeValueAsString(ZoneId.of("America/Anchorage")); + assertEquals("\"America/Anchorage\"", value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + String value = MOCK_OBJECT_MIXIN_MAPPER.writeValueAsString(ZoneId.of("America/Denver")); + assertEquals("[\"java.time.ZoneId\",\"America/Denver\"]", value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/ZoneOffsetSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZoneOffsetSerTest.java new file mode 100644 index 0000000000..beb40b0566 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZoneOffsetSerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.ZoneId; +import java.time.ZoneOffset; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +public class ZoneOffsetSerTest extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testSerialization01() throws Exception + { + ZoneOffset offset = ZoneOffset.of("Z"); + String value = MAPPER.writeValueAsString(offset); + assertEquals("\"Z\"", value); + } + + @Test + public void testSerialization02() throws Exception + { + ZoneOffset offset = ZoneOffset.of("+0300"); + String value = MAPPER.writeValueAsString(offset); + assertEquals("\"+03:00\"", value); + } + + @Test + public void testSerialization03() throws Exception + { + ZoneOffset offset = ZoneOffset.of("-0630"); + String value = MAPPER.writeValueAsString(offset); + assertEquals("\"-06:30\"", value); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + ObjectMapper mapper = newMapperBuilder() + .addMixIn(ZoneId.class, MockObjectConfiguration.class) + .build(); + ZoneOffset offset = ZoneOffset.of("+0415"); + String value = mapper.writeValueAsString(offset); + assertEquals("[\"" + ZoneOffset.class.getName() + "\",\"+04:15\"]", value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerTest.java new file mode 100644 index 0000000000..2a16e8f41c --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerTest.java @@ -0,0 +1,967 @@ +/* + * Copyright 2013 FasterXML.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package tools.jackson.databind.ext.javatime.ser; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.MockObjectConfiguration; +import tools.jackson.databind.ext.javatime.ser.ZonedDateTimeSerializer; +import tools.jackson.databind.ext.javatime.util.DecimalUtils; + +import static org.junit.jupiter.api.Assertions.*; + +public class ZonedDateTimeSerTest + extends DateTimeTestBase +{ + private static final DateTimeFormatter FORMATTER_WITHOUT_ZONEID = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + private static final ZoneId Z1 = ZoneId.of("America/Chicago"); + + private static final ZoneId Z2 = ZoneId.of("America/Anchorage"); + + private static final ZoneId Z3 = ZoneId.of("America/Los_Angeles"); + + private static final ZoneId UTC = ZoneOffset.UTC; + + private static final ZoneId DEFAULT_TZ = UTC; + + private static final ZoneId FIX_OFFSET = ZoneId.of("-08:00"); + + final static class Wrapper { + @JsonFormat(pattern="yyyy_MM_dd HH:mm:ss(Z)", + shape=JsonFormat.Shape.STRING) + public ZonedDateTime value; + + public Wrapper() { } + public Wrapper(ZonedDateTime v) { value = v; } + } + + final static class WrapperNumeric { + @JsonFormat(pattern="yyyyMMddHHmmss", + shape=JsonFormat.Shape.STRING, + timezone = "UTC") + public ZonedDateTime value; + + public WrapperNumeric() { } + public WrapperNumeric(ZonedDateTime v) { value = v; } + } + + private final ObjectMapper MAPPER = newMapper(); + private final ObjectReader READER = MAPPER.readerFor(ZonedDateTime.class); + private final ObjectMapper MAPPER_WITH_DEFAULT_TZ = newMapper(TimeZone.getDefault()); + + + @Test + public void testSerializationAsTimestamp01Nanoseconds() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("0.0", value); + } + + @Test + public void testSerializationAsTimestamp01NegativeSeconds() throws Exception + { + // test for Issue #69 + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(-14159020000L, 183917322), UTC); + String serialized = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + ZonedDateTime actual = MAPPER.readValue(serialized, ZonedDateTime.class); + assertEquals(date, actual); + } + + @Test + public void testSerializationAsTimestamp01NegativeSecondsWithDefaults() throws Exception + { + // test for Issue #69 using default mapper config + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("MMM dd yyyy HH:mm:ss.SSS zzz", Locale.ENGLISH); + ZonedDateTime original = ZonedDateTime.parse("Apr 13 1969 05:05:38.599 UTC", dtf); + String serialized = MAPPER.writeValueAsString(original); + ZonedDateTime deserialized = MAPPER.readValue(serialized, ZonedDateTime.class); + assertEquals(original.getDayOfMonth(), deserialized.getDayOfMonth(), "The day is not correct."); + assertEquals(original.getMonthValue(), deserialized.getMonthValue(), "The month is not correct."); + assertEquals(original.getYear(), deserialized.getYear(), "The year is not correct."); + assertEquals(original.getHour(), deserialized.getHour(), "The hour is not correct."); + assertEquals(original.getMinute(), deserialized.getMinute(), "The minute is not correct."); + assertEquals(original.getSecond(), deserialized.getSecond(), "The second is not correct."); + assertEquals(original.getNano(), deserialized.getNano(), "The nano is not correct."); + assertEquals(ZoneId.of("UTC").getRules(), deserialized.getZone().getRules(), "The time zone is not correct."); + } + + @Test + public void testSerializationAsTimestamp01Milliseconds() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("0", value); + } + + @Test + public void testSerializationAsTimestamp02Nanoseconds() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("123456789.183917322", value); + } + + @Test + public void testSerializationAsTimestamp02Milliseconds() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("123456789183", value); + } + + @Test + public void testSerializationAsTimestamp03Nanoseconds() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals(DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano()), value); + } + + @Test + public void testSerializationAsTimestamp03Milliseconds() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals(Long.toString(date.toInstant().toEpochMilli()), value); + } + + @Test + public void testSerializationAsString01() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + + FORMATTER.withZone(Z1).format(date) + '"', value); + } + + @Test + public void testSerializationAsString02() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + + FORMATTER.withZone(Z2).format(date) + '"', value); + } + + @Test + public void testSerializationAsString03() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + + FORMATTER.withZone(Z3).format(date) + '"', value); + } + + @Test + public void testSerializationAsStringWithMapperTimeZone01() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + String value = newMapper() + .writer() + .with(TimeZone.getTimeZone(Z1)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationAsStringWithMapperTimeZone02() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = newMapper() + .writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationAsStringWithMapperTimeZone03() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = newMapper() + .writer() + .with(TimeZone.getTimeZone(Z3)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(date); + assertEquals('"' + FORMATTER.format(date) + '"', value); + } + + @Test + public void testSerializationAsStringWithZoneIdOff() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, false) + .build(); + + assertEquals(q(FORMATTER.withZone(Z3).format(date)), + mapper.writeValueAsString(date)); + } + + @Test + public void testSerializationAsStringWithZoneIdOffAndMapperTimeZone() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = newMapper() + .writer() + .with(TimeZone.getTimeZone(Z3)) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .writeValueAsString(date); + assertEquals(q(FORMATTER.format(date)), value); + } + + @Test + public void testSerializationAsStringWithZoneIdOn() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + ObjectMapper mapper = newMapperBuilder() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true) + .build(); + String value = mapper.writeValueAsString(date); + assertEquals("\"" + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(date) + "\"", value); + } + + @Test + public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOnAndACustomFormatter() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + // With a custom DateTimeFormatter without a ZoneId. + String value = newMapperBuilder().addModule( + new SimpleModule().addSerializer(new ZonedDateTimeSerializer(FORMATTER_WITHOUT_ZONEID))) + .build() + .writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE) + .writeValueAsString(date); + + // We expect to have the date written with the datetime of ZoneId Z2 + assertEquals("\"" + date.withZoneSameInstant(Z2).format(FORMATTER_WITHOUT_ZONEID) + "\"", value); + } + + @Test + public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOffAndACustomFormatter() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + // With a custom DateTimeFormatter without a Zone. + String value = newMapperBuilder().addModule( + new SimpleModule().addSerializer(new ZonedDateTimeSerializer(FORMATTER_WITHOUT_ZONEID))) + .build() + .writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE) + .writeValueAsString(date); + + // We expect to have the date written with the datetime of ZoneId Z3 + assertEquals("\"" + date.format(FORMATTER_WITHOUT_ZONEID) + "\"", value); + } + + @Test + public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOn() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = MAPPER.writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE) + .writeValueAsString(date); + + // We expect to have the date written with the ZoneId Z2 + assertEquals("\"" + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(date.withZoneSameInstant(Z2)) + "\"", value); + } + + @Test + public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOff() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = MAPPER.writer() + .with(TimeZone.getTimeZone(Z2)) + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE) + .writeValueAsString(date); + + // We expect to have the date written with the ZoneId Z3 + assertEquals("\"" + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(date) + "\"", value); + } + + @Test + public void testSerializationWithTypeInfo01() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build() + .writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("[\"" + ZonedDateTime.class.getName() + "\",123456789.183917322]", value); + } + + @Test + public void testSerializationWithTypeInfo02() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + String value = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build() + .writer() + .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .writeValueAsString(date); + assertEquals("[\"" + ZonedDateTime.class.getName() + "\",123456789183]", value); + } + + @Test + public void testSerializationWithTypeInfo03() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = mapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build() + .writeValueAsString(date); + assertEquals("[\"" + ZonedDateTime.class.getName() + "\",\"" + + FORMATTER.withZone(Z3).format(date) + "\"]", value); + } + + @Test + public void testSerializationWithTypeInfoAndMapperTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + String value = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build() + .writer() + .with(TimeZone.getTimeZone(Z3)) + .writeValueAsString(date); + assertEquals("[\"" + ZonedDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", value); + } + + @Test + public void testDeserializationAsFloat01WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ZonedDateTime value = MAPPER.readValue("0.000000000", ZonedDateTime.class); + + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat01WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ZonedDateTime value = MAPPER + .readerFor(ZonedDateTime.class) + .with(TimeZone.getDefault()) + .readValue("0.000000000"); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat02WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ZonedDateTime value = MAPPER.readValue("123456789.183917322", ZonedDateTime.class); + + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat02WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ZonedDateTime value = MAPPER + .readerFor(ZonedDateTime.class) + .with(TimeZone.getDefault()) + .readValue("123456789.183917322"); + + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat03WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + ObjectMapper mapper = newMapper(); + ZonedDateTime value = mapper.readValue( + DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano()), ZonedDateTime.class + ); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsFloat03WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + ObjectMapper mapper = newMapper(TimeZone.getDefault()); + ZonedDateTime value = mapper.readValue( + DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano()), ZonedDateTime.class + ); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01NanosecondsWithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ZonedDateTime value = READER + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01NanosecondsWithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ZonedDateTime value = newMapper(TimeZone.getDefault()).readerFor(ZonedDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01MillisecondsWithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ZonedDateTime value = READER + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt01MillisecondsWithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ZonedDateTime value = MAPPER_WITH_DEFAULT_TZ + .readerFor(ZonedDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("0"); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02NanosecondsWithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + ZonedDateTime value = MAPPER.readerFor(ZonedDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789"); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02NanosecondsWithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + ZonedDateTime value = MAPPER_WITH_DEFAULT_TZ.readerFor(ZonedDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789"); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02MillisecondsWithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + ZonedDateTime value = MAPPER.readerFor(ZonedDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789422"); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt02MillisecondsWithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + ObjectMapper mapper = newMapper(TimeZone.getDefault()); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("123456789422"); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03NanosecondsWithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + ObjectMapper mapper = newMapper(); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toEpochSecond())); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03NanosecondsWithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + date = date.minus(date.getNano(), ChronoUnit.NANOS); + ObjectMapper mapper = newMapper(TimeZone.getDefault()); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toEpochSecond())); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03MillisecondsWithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + date = date.minus(date.getNano() - (date.get(ChronoField.MILLI_OF_SECOND) * 1_000_000L), ChronoUnit.NANOS); + ObjectMapper mapper = newMapper(); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toInstant().toEpochMilli())); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsInt03MillisecondsWithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + date = date.minus(date.getNano() - (date.get(ChronoField.MILLI_OF_SECOND) * 1_000_000L), ChronoUnit.NANOS); + ObjectMapper mapper = newMapper(TimeZone.getDefault()); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue(Long.toString(date.toInstant().toEpochMilli())); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ObjectMapper mapper = newMapper(); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ObjectMapper mapper = newMapper(TimeZone.getDefault()); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithTimeZoneTurnedOff() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), FIX_OFFSET); + ObjectMapper mapper = newMapper(TimeZone.getDefault()); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(FIX_OFFSET, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString01WithZoneId() throws Exception { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ZonedDateTime value = MAPPER.readerFor(ZonedDateTime.class).readValue( + "\"" + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(date) + "\""); + assertIsEqual(date, value); + } + + @Test + public void testDeserializationAsString02WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ZonedDateTime value = READER + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString02WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ZonedDateTime value = newMapper(TimeZone.getDefault()) + .readerFor(ZonedDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString02WithTimeZoneTurnedOff() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), FIX_OFFSET); + ObjectMapper mapper = newMapper(TimeZone.getDefault()); + ZonedDateTime value = mapper.readerFor(ZonedDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(FIX_OFFSET, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString02WithZoneId() throws Exception { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ZonedDateTime value = READER + .readValue("\"" + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(date) + "\""); + assertIsEqual(date, value); + } + + @Test + public void testDeserializationAsString03WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + ZonedDateTime value = READER + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(DEFAULT_TZ, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString03WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + ZonedDateTime value = newMapper(TimeZone.getDefault()) + .readerFor(ZonedDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(ZoneId.systemDefault().normalized(), value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString03WithTimeZoneTurnedOff() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(FIX_OFFSET); + ZonedDateTime value = newMapper(TimeZone.getDefault()) + .readerFor(ZonedDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals(FIX_OFFSET, value.getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationAsString03WithZoneId() throws Exception { + ZonedDateTime date = ZonedDateTime.now(Z3); + ZonedDateTime value = MAPPER.readValue("\"" + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(date) + "\"", ZonedDateTime.class); + assertIsEqual(date, value); + } + + @Test + public void testDeserializationWithTypeInfo01WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readValue( + "[\"" + ZonedDateTime.class.getName() + "\",123456789.183917322]", Temporal.class + ); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(DEFAULT_TZ, ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo01WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ObjectMapper mapper = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readValue( + "[\"" + ZonedDateTime.class.getName() + "\",123456789.183917322]", Temporal.class + ); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(ZoneId.systemDefault().normalized(), ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readerFor(Temporal.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[\"" + ZonedDateTime.class.getName() + "\",123456789]"); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(DEFAULT_TZ, ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo02WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2); + ObjectMapper mapper = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readerFor(Temporal.class) + .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[\"" + ZonedDateTime.class.getName() + "\",123456789]"); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(ZoneId.systemDefault().normalized(), ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper.readerFor(Temporal.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue("[\"" + ZonedDateTime.class.getName() + "\",123456789422]"); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(DEFAULT_TZ, ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo03WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2); + ObjectMapper mapper = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper + .readerFor(Temporal.class) + .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .readValue( + "[\"" + ZonedDateTime.class.getName() + "\",123456789422]"); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(ZoneId.systemDefault().normalized(), ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo04WithoutTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + ObjectMapper mapper = newMapperBuilder() + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper + .readerFor(Temporal.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue( + "[\"" + ZonedDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]"); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(DEFAULT_TZ, ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo04WithTimeZone() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(Z3); + ObjectMapper mapper = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build(); + Temporal value = mapper + .readerFor(Temporal.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue( + "[\"" + ZonedDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]"); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(ZoneId.systemDefault().normalized(), ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testDeserializationWithTypeInfo04WithTimeZoneTurnedOff() throws Exception + { + ZonedDateTime date = ZonedDateTime.now(FIX_OFFSET); + Temporal value = newMapperBuilder(TimeZone.getDefault()) + .addMixIn(Temporal.class, MockObjectConfiguration.class) + .build() + .readerFor(Temporal.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue( + "[\"" + ZonedDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]"); + assertInstanceOf(ZonedDateTime.class, value, "The value should be an ZonedDateTime."); + assertIsEqual(date, (ZonedDateTime) value); + assertEquals(FIX_OFFSET, ((ZonedDateTime) value).getZone(), "The time zone is not correct."); + } + + @Test + public void testCustomPatternWithAnnotations() throws Exception + { + ZonedDateTime inputValue = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), UTC); + final Wrapper input = new Wrapper(inputValue); + String json = MAPPER.writeValueAsString(input); + assertEquals(a2q("{'value':'1970_01_01 00:00:00(+0000)'}"), json); + + Wrapper result = MAPPER.readerFor(Wrapper.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue(json); + // looks like timezone gets converted (is that correct or not?); verify just offsets for now + assertEquals(input.value.toInstant(), result.value.toInstant()); + } + + // [modules-java#269] + @Test + public void testCustomPatternWithNumericTimestamp() throws Exception + { + String input = a2q("{'value':'3.141592653'}"); + + Wrapper result = JsonMapper.builder() + .enable(DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS) + .build() + .readerFor(Wrapper.class) + .readValue(input); + + assertEquals(Instant.ofEpochSecond(3L, 141592653L), result.value.toInstant()); + } + + @Test + public void testNumericCustomPatternWithAnnotations() throws Exception + { + ZonedDateTime inputValue = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L), UTC); + final WrapperNumeric input = new WrapperNumeric(inputValue); + ObjectMapper m = newMapper(); + String json = m.writeValueAsString(input); + assertEquals(a2q("{'value':'19700101000000'}"), json); + + WrapperNumeric result = m.readValue(json, WrapperNumeric.class); + assertEquals(input.value.toInstant(), result.value.toInstant()); + } + + @Test + public void testInstantPriorToEpochIsEqual() throws Exception + { + //Issue #120 test + final Instant original = Instant.ofEpochMilli(-1); + final String serialized = MAPPER.writeValueAsString(original); + final Instant deserialized = MAPPER.readValue(serialized, Instant.class); + assertEquals(original, deserialized); + } + + public static class Pojo1 { + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + public ZonedDateTime t1 = ZonedDateTime.parse("2022-04-27T12:00:00+02:00[Europe/Paris]"); + + public ZonedDateTime t2 = t1; + } + + @Test + public void testShapeInt() throws Exception { + String json1 = mapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build() + .writeValueAsString(new Pojo1()); + assertEquals("{\"t1\":1651053600000,\"t2\":1651053600.000000000}", json1); + } + + private static void assertIsEqual(ZonedDateTime expected, ZonedDateTime actual) + { + assertTrue(expected.isEqual(actual), + "The value is not correct. Expected timezone-adjusted <" + expected + ">, actual <" + actual + ">."); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerWithJsonFormat333Test.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerWithJsonFormat333Test.java new file mode 100644 index 0000000000..436cc6fdc5 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerWithJsonFormat333Test.java @@ -0,0 +1,43 @@ +package tools.jackson.databind.ext.javatime.ser; + +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// [module-java8#333]: ZonedDateTime serialization with @JsonFormat pattern never uses +// while WRITE_DATES_WITH_ZONE_ID enabled #333 +public class ZonedDateTimeSerWithJsonFormat333Test + extends DateTimeTestBase +{ + public static class ContainerWithPattern333 { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss z") + public ZonedDateTime value; + } + + public static class ContainerWithoutPattern333 { + @JsonFormat(shape = JsonFormat.Shape.STRING) + public ZonedDateTime value; + } + + private final ObjectMapper MAPPER = mapperBuilder().enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID).build(); + + @Test + public void testJsonFormatOverridesSerialization() throws Exception + { + // ISO-8601 string for ZonedDateTime + ZonedDateTime zonedDateTime = ZonedDateTime.parse("2024-11-15T18:27:06.921054+01:00[Europe/Berlin]"); + ContainerWithPattern333 input = new ContainerWithPattern333(); + input.value = zonedDateTime; + + assertEquals(a2q("{'value':'2024-11-15 18:27:06 CET'}"), + MAPPER.writeValueAsString(input)); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/tofix/InstantDeserializerNegative359Test.java b/src/test/java/tools/jackson/databind/ext/javatime/tofix/InstantDeserializerNegative359Test.java new file mode 100644 index 0000000000..a3d7711d65 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/tofix/InstantDeserializerNegative359Test.java @@ -0,0 +1,39 @@ +package tools.jackson.databind.ext.javatime.tofix; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// [modules-java8#359] InstantDeserializer deserializes the nanosecond portion of +// fractional timestamps incorrectly: -1.000000001 deserializes to 1969-12-31T23:59:59.000000001Z +// instead of 1969-12-31T23:59:58.999999999Z +public class InstantDeserializerNegative359Test + extends DateTimeTestBase +{ + private final ObjectReader READER = newMapper().readerFor(Instant.class); + + @JacksonTestFailureExpected + @Test + public void testDeserializationAsFloat04() + throws Exception + { + Instant actual = READER.readValue("-1.000000001"); + Instant expected = Instant.ofEpochSecond(-1L, -1L); + assertEquals(expected, actual); + } + + @Test + public void testDeserializationAsFloat05() + throws Exception + { + Instant actual = READER.readValue("-0.000000001"); + Instant expected = Instant.ofEpochSecond(0L, -1L); + assertEquals(expected, actual); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/tofix/InstantViaBigDecimal307Test.java b/src/test/java/tools/jackson/databind/ext/javatime/tofix/InstantViaBigDecimal307Test.java new file mode 100644 index 0000000000..59fecdd11b --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/tofix/InstantViaBigDecimal307Test.java @@ -0,0 +1,47 @@ +package tools.jackson.databind.ext.javatime.tofix; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; + +import static org.junit.jupiter.api.Assertions.*; + +// [modules-java8#307]: Loss of precision via JsonNode for BigDecimal-valued +// things (like Instant) +public class InstantViaBigDecimal307Test extends DateTimeTestBase +{ + public static class Wrapper307 { + public Instant value; + + public Wrapper307(Instant v) { value = v; } + public Wrapper307() { } + } + + private final Instant ISSUED_AT = Instant.ofEpochSecond(1234567890).plusNanos(123456789); + + private ObjectMapper MAPPER = mapperBuilder() + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + @Test + public void instantViaReadValue() throws Exception { + String serialized = MAPPER.writeValueAsString(new Wrapper307(ISSUED_AT)); + Wrapper307 deserialized = MAPPER.readValue(serialized, Wrapper307.class); + assertEquals(ISSUED_AT, deserialized.value); + } + + @JacksonTestFailureExpected + @Test + public void instantViaReadTree() throws Exception { + String serialized = MAPPER.writeValueAsString(new Wrapper307(ISSUED_AT)); + JsonNode tree = MAPPER.readTree(serialized); + Wrapper307 deserialized = MAPPER.treeToValue(tree, Wrapper307.class); + assertEquals(ISSUED_AT, deserialized.value); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/tofix/OffsetDateTimeDeser279Test.java b/src/test/java/tools/jackson/databind/ext/javatime/tofix/OffsetDateTimeDeser279Test.java new file mode 100644 index 0000000000..82a9c43261 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/tofix/OffsetDateTimeDeser279Test.java @@ -0,0 +1,50 @@ +package tools.jackson.databind.ext.javatime.tofix; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; + +import static org.junit.jupiter.api.Assertions.*; + +public class OffsetDateTimeDeser279Test extends DateTimeTestBase +{ + // For [modules-java8#279] + static class Wrapper279 { + OffsetDateTime date; + + public Wrapper279(OffsetDateTime d) { date = d; } + protected Wrapper279() { } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + public OffsetDateTime getDate() { + return date; + } + public void setDate(OffsetDateTime date) { + this.date = date; + } + } + + private ObjectMapper MAPPER = newMapper(); + + // For [modules-java8#279] + @JacksonTestFailureExpected + @Test + public void testWrapperWithPattern279() throws Exception + { + final OffsetDateTime date = OffsetDateTime.now(ZoneId.of("UTC")) + .truncatedTo(ChronoUnit.SECONDS); + final Wrapper279 input = new Wrapper279(date); + final String json = MAPPER.writeValueAsString(input); + + Wrapper279 result = MAPPER.readValue(json, Wrapper279.class); + assertEquals(input.date, result.date); + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/tofix/ZonedDateTimeIssue244Test.java b/src/test/java/tools/jackson/databind/ext/javatime/tofix/ZonedDateTimeIssue244Test.java new file mode 100644 index 0000000000..305518d256 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/tofix/ZonedDateTimeIssue244Test.java @@ -0,0 +1,72 @@ +package tools.jackson.databind.ext.javatime.tofix; + +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for https://github.com/FasterXML/jackson-modules-java8/issues/244 + */ +public class ZonedDateTimeIssue244Test extends DateTimeTestBase +{ + private final ObjectMapper MAPPER = mapperBuilder() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + @JacksonTestFailureExpected + @Test + public void zoneIdUTC() throws Exception + { + assertSerializeAndDeserialize(ZonedDateTime.now(ZoneId.of("UTC"))); + } + + @Test + public void zoneOffsetUTC() throws Exception + { + assertSerializeAndDeserialize(ZonedDateTime.now(ZoneOffset.UTC)); // fails! + } + + @JacksonTestFailureExpected + @Test + public void zoneOffsetNonUTC() throws Exception + { + assertSerializeAndDeserialize(ZonedDateTime.now(ZoneOffset.ofHours(-7))); // fails! + } + + private void assertSerializeAndDeserialize(final ZonedDateTime date) throws Exception + { + final Example example1 = new Example(date); + final String json = MAPPER.writeValueAsString(example1); + final Example example2 = MAPPER.readValue(json, Example.class); + + assertEquals(example1.getDate(), example2.getDate()); + } + + static class Example + { + private ZonedDateTime date; + + public Example() + { + } + + public Example(final ZonedDateTime date) + { + this.date = date; + } + + public ZonedDateTime getDate() + { + return date; + } + } +} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverterTest.java b/src/test/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverterTest.java new file mode 100644 index 0000000000..700a13cb56 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverterTest.java @@ -0,0 +1,54 @@ +package tools.jackson.databind.ext.javatime.util; + +import java.time.temporal.ChronoUnit; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ext.javatime.DateTimeTestBase; +import tools.jackson.databind.ext.javatime.util.DurationUnitConverter; + +import static org.junit.jupiter.api.Assertions.*; + +public class DurationUnitConverterTest + extends DateTimeTestBase +{ + @Test + public void shouldMapToTemporalUnit() { + for (ChronoUnit inputUnit : new ChronoUnit[] { + ChronoUnit.NANOS, + ChronoUnit.MICROS, + ChronoUnit.MILLIS, + ChronoUnit.SECONDS, + ChronoUnit.MINUTES, + ChronoUnit.HOURS, + ChronoUnit.HALF_DAYS, + ChronoUnit.DAYS, + }) { + DurationUnitConverter conv = DurationUnitConverter.from(inputUnit.name()); + assertNotNull(conv); + // is case-sensitive: + assertNull(DurationUnitConverter.from(inputUnit.name().toLowerCase())); + } + } + + @Test + public void shouldNotMapToTemporalUnit() { + for (String invalid : new String[] { + // Inaccurate units not (yet?) supported + "WEEKS", + "MONTHS", + "YEARS", + "DECADES", + "CENTURIES", + "MILLENNIA", + "ERAS", + "FOREVER", + + // Not matching at all + "DOESNOTMATCH", "", " " + }) { + assertNull(DurationUnitConverter.from(invalid), + "Should not map pattern '"+invalid+"'"); + } + } +} diff --git a/src/test/java/tools/jackson/databind/interop/DateJava8FallbacksTest.java b/src/test/java/tools/jackson/databind/interop/DateJava8FallbacksTest.java deleted file mode 100644 index 567d35cc9b..0000000000 --- a/src/test/java/tools/jackson/databind/interop/DateJava8FallbacksTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package tools.jackson.databind.interop; - -import java.time.DateTimeException; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; - -import org.junit.jupiter.api.Test; - -import tools.jackson.core.JsonParser; -import tools.jackson.databind.*; -import tools.jackson.databind.exc.InvalidDefinitionException; -import tools.jackson.databind.testutil.DatabindTestUtil; -import tools.jackson.databind.util.TokenBuffer; - -import static org.junit.jupiter.api.Assertions.*; - -// [databind#2683]: add fallback handling for Java 8 date/time types, to -// prevent accidental serialization as POJOs, as well as give more information -// on deserialization attempts -// -// @since 2.12 -public class DateJava8FallbacksTest extends DatabindTestUtil -{ - private final ObjectMapper MAPPER = newJsonMapper(); - - private final OffsetDateTime DATETIME_EPOCH = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), - ZoneOffset.of("Z")); - - // Test to prevent serialization as POJO, without Java 8 date/time module: - @Test - public void testPreventSerialization() throws Exception - { - try { - String json = MAPPER.writerWithDefaultPrettyPrinter() - .writeValueAsString(DATETIME_EPOCH); - fail("Should not pass, wrote out as\n: "+json); - } catch (InvalidDefinitionException e) { - verifyException(e, "Java 8 date/time type `java.time.OffsetDateTime` not supported by default"); - verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\""); - } - } - - @Test - public void testBetterDeserializationError() throws Exception - { - try { - OffsetDateTime result = MAPPER.readValue(" 0 ", OffsetDateTime.class); - fail("Not expecting to pass, resulted in: "+result); - } catch (InvalidDefinitionException e) { - verifyException(e, "Java 8 date/time type `java.time.OffsetDateTime` not supported by default"); - verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\""); - } - } - - // But, [databind#3091], allow deser from JsonToken.VALUE_EMBEDDED_OBJECT - @Test - public void testAllowAsEmbedded() throws Exception - { - OffsetDateTime time = OffsetDateTime.ofInstant(Instant.now(), - ZoneId.of("Z")); - try (TokenBuffer tb = TokenBuffer.forGeneration()) { - tb.writeEmbeddedObject(time); - - try (JsonParser p = tb.asParser()) { - OffsetDateTime result = MAPPER.readValue(p, OffsetDateTime.class); - assertSame(time, result); - } - } - - // but also try deser into an array - try (TokenBuffer tb = TokenBuffer.forGeneration()) { - tb.writeStartArray(); - tb.writeEmbeddedObject(time); - tb.writeEndArray(); - - try (JsonParser p = tb.asParser()) { - Object[] result = MAPPER.readValue(p, Object[].class); - assertNotNull(result); - assertEquals(1, result.length); - assertSame(time, result[0]); - } - } - } - - // [databind#4718]: should not block serialization of `DateTimeException` - @Test - public void testAllowExceptionSer() throws Exception { - String json = MAPPER.writeValueAsString(new DateTimeException("Test!")); - assertTrue(MAPPER.readTree(json).isObject()); - } - - // [databind#4718]: should not block deserialization of `DateTimeException` - @Test - public void testAllowExceptionDeser() throws Exception { - DateTimeException exc = MAPPER.readValue("{\"message\":\"test!\"}", - DateTimeException.class); - assertNotNull(exc); - } -} diff --git a/src/test/java/tools/jackson/databind/node/POJONodeTest.java b/src/test/java/tools/jackson/databind/node/POJONodeTest.java index 09b96dab2c..6fe01159e6 100644 --- a/src/test/java/tools/jackson/databind/node/POJONodeTest.java +++ b/src/test/java/tools/jackson/databind/node/POJONodeTest.java @@ -65,15 +65,13 @@ public void testPOJONodeCustomSer() throws Exception @Test public void testAddJava8DateAsPojo() throws Exception { - JsonNode node = MAPPER.createObjectNode().putPOJO("test", LocalDateTime.now()); + LocalDateTime dt = LocalDateTime.parse("2025-03-31T12:00"); + JsonNode node = MAPPER.createObjectNode().putPOJO("test", dt); String json = node.toString(); assertNotNull(json); JsonNode result = MAPPER.readTree(json); String msg = result.path("test").asString(); - assertTrue(msg.startsWith("[ERROR:"), - "Wrong fail message: "+msg); - assertTrue(msg.contains("InvalidDefinitionException"), - "Wrong fail message: "+msg); + assertEquals(dt, LocalDateTime.parse(msg)); } }