complete support for DateFormat Project: http://git-wip-us.apache.org/repos/asf/johnzon/repo Commit: http://git-wip-us.apache.org/repos/asf/johnzon/commit/fb05f282 Tree: http://git-wip-us.apache.org/repos/asf/johnzon/tree/fb05f282 Diff: http://git-wip-us.apache.org/repos/asf/johnzon/diff/fb05f282
Branch: refs/heads/master Commit: fb05f282c9d0ec0f03426cfe6be1688ff9aa078f Parents: 8583655 Author: amoscatelli <[email protected]> Authored: Fri Mar 30 15:59:37 2018 +0200 Committer: amoscatelli <[email protected]> Committed: Fri Mar 30 15:59:37 2018 +0200 ---------------------------------------------------------------------- .../apache/johnzon/jsonb/JohnzonBuilder.java | 245 ++++++++++++------- .../apache/johnzon/jsonb/JsonbTypesTest.java | 134 +++++++++- 2 files changed, 287 insertions(+), 92 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/johnzon/blob/fb05f282/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java index 1ae554a..3af5efc 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java @@ -90,8 +90,19 @@ import java.util.function.Supplier; import java.util.stream.Stream; import static java.time.format.DateTimeFormatter.ofPattern; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MILLI_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; import static java.util.Collections.emptyMap; +import java.util.Objects; import static java.util.Optional.ofNullable; +import java.util.concurrent.TimeUnit; import static javax.json.bind.config.PropertyNamingStrategy.IDENTITY; import static javax.json.bind.config.PropertyOrderStrategy.LEXICOGRAPHICAL; @@ -141,52 +152,52 @@ public class JohnzonBuilder implements JsonbBuilder { final String orderValue = config.getProperty(JsonbConfig.PROPERTY_ORDER_STRATEGY).map(String::valueOf).orElse(LEXICOGRAPHICAL); final PropertyVisibilityStrategy visibilityStrategy = config.getProperty(JsonbConfig.PROPERTY_VISIBILITY_STRATEGY) .map(PropertyVisibilityStrategy.class::cast).orElse(new PropertyVisibilityStrategy() { - private final ConcurrentMap<Class<?>, PropertyVisibilityStrategy> strategies = new ConcurrentHashMap<>(); + private final ConcurrentMap<Class<?>, PropertyVisibilityStrategy> strategies = new ConcurrentHashMap<>(); - @Override - public boolean isVisible(final Field field) { - if (field.getAnnotation(JsonbProperty.class) != null) { - return true; - } - final PropertyVisibilityStrategy strategy = strategies.computeIfAbsent(field.getDeclaringClass(), this::visibilityStrategy); - return strategy == this ? Modifier.isPublic(field.getModifiers()) : strategy.isVisible(field); - } + @Override + public boolean isVisible(final Field field) { + if (field.getAnnotation(JsonbProperty.class) != null) { + return true; + } + final PropertyVisibilityStrategy strategy = strategies.computeIfAbsent(field.getDeclaringClass(), this::visibilityStrategy); + return strategy == this ? Modifier.isPublic(field.getModifiers()) : strategy.isVisible(field); + } - @Override - public boolean isVisible(final Method method) { - final PropertyVisibilityStrategy strategy = strategies.computeIfAbsent(method.getDeclaringClass(), this::visibilityStrategy); - return strategy == this ? Modifier.isPublic(method.getModifiers()) : strategy.isVisible(method); - } + @Override + public boolean isVisible(final Method method) { + final PropertyVisibilityStrategy strategy = strategies.computeIfAbsent(method.getDeclaringClass(), this::visibilityStrategy); + return strategy == this ? Modifier.isPublic(method.getModifiers()) : strategy.isVisible(method); + } - private PropertyVisibilityStrategy visibilityStrategy(final Class<?> type) { // can be cached - JsonbVisibility visibility = type.getAnnotation(JsonbVisibility.class); - if (visibility != null) { - try { - return visibility.value().newInstance(); - } catch (final InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } - } - Package p = type.getPackage(); - while (p != null) { - visibility = p.getAnnotation(JsonbVisibility.class); - if (visibility != null) { - try { - return visibility.value().newInstance(); - } catch (final InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } - } - final String name = p.getName(); - final int end = name.lastIndexOf('.'); - if (end < 0) { - break; - } - p = Package.getPackage(name.substring(0, end)); + private PropertyVisibilityStrategy visibilityStrategy(final Class<?> type) { // can be cached + JsonbVisibility visibility = type.getAnnotation(JsonbVisibility.class); + if (visibility != null) { + try { + return visibility.value().newInstance(); + } catch (final InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + } + Package p = type.getPackage(); + while (p != null) { + visibility = p.getAnnotation(JsonbVisibility.class); + if (visibility != null) { + try { + return visibility.value().newInstance(); + } catch (final InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException(e); } - return this; } - }); + final String name = p.getName(); + final int end = name.lastIndexOf('.'); + if (end < 0) { + break; + } + p = Package.getPackage(name.substring(0, end)); + } + return this; + } + }); config.getProperty("johnzon.attributeOrder").ifPresent(comp -> builder.setAttributeOrder(Comparator.class.cast(comp))); config.getProperty("johnzon.enforceQuoteString") @@ -227,13 +238,13 @@ public class JohnzonBuilder implements JsonbBuilder { final AccessMode accessMode = config.getProperty("johnzon.accessMode") .map(this::toAccessMode) .orElseGet(() -> new JsonbAccessMode( - propertyNamingStrategy, orderValue, visibilityStrategy, - !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), - defaultConverters, - factory, parserFactoryProvider, - config.getProperty("johnzon.accessModeDelegate") - .map(this::toAccessMode) - .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false)))); + propertyNamingStrategy, orderValue, visibilityStrategy, + !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), + defaultConverters, + factory, parserFactoryProvider, + config.getProperty("johnzon.accessModeDelegate") + .map(this::toAccessMode) + .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false)))); builder.setAccessMode(accessMode); @@ -309,7 +320,7 @@ public class JohnzonBuilder implements JsonbBuilder { final ParameterizedType pt = findPT(s, JsonbSerializer.class); if (pt == null) { throw new IllegalArgumentException(s + " doesn't implement JsonbSerializer"); - } + } final Type[] args = pt.getActualTypeArguments(); // TODO: support PT in ObjectConverter (list) if (args.length != 1 || !Class.class.isInstance(args[0])) { @@ -325,7 +336,7 @@ public class JohnzonBuilder implements JsonbBuilder { final ParameterizedType pt = findPT(d, JsonbDeserializer.class); if (pt == null) { throw new IllegalArgumentException(d + " doesn't implement JsonbDeserializer"); - } + } final Type[] args = pt.getActualTypeArguments(); if (args.length != 1 || !Class.class.isInstance(args[0])) { throw new IllegalArgumentException("We only support deserializer on Class for now"); @@ -619,7 +630,6 @@ public class JohnzonBuilder implements JsonbBuilder { })); addDateFormatConfigConverters(converters, zoneIDUTC); - converters.forEach((k, v) -> builder.addAdapter(k.getFrom(), k.getTo(), v)); return converters; } @@ -630,98 +640,151 @@ public class JohnzonBuilder implements JsonbBuilder { final Optional<Locale> locale = config.getProperty(JsonbConfig.LOCALE).map(Locale.class::cast); final DateTimeFormatter formatter = locale.isPresent() ? ofPattern(dateFormat, locale.get()) : ofPattern(dateFormat); - // Note: we try and fallback in the parsing cause we don't know if the date format provided is - // for date, datetime, time - converters.put(new AdapterKey(Date.class, String.class), new ConverterAdapter<>(new Converter<Date>() { - private volatile boolean useFormatter = true; @Override public String toString(final Date instance) { - return LocalDateTime.ofInstant(instance.toInstant(), zoneIDUTC).toString(); + return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); } @Override public Date fromString(final String text) { - if (useFormatter) { - try { - return Date.from(LocalDateTime.parse(text, formatter).toInstant(ZoneOffset.UTC)); - } catch (final DateTimeParseException dpe) { - useFormatter = false; - } + try { + return Date.from(parseZonedDateTime(text, formatter, zoneIDUTC).toInstant()); + } catch (final DateTimeParseException dpe) { + return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)); } - return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)); } })); converters.put(new AdapterKey(LocalDateTime.class, String.class), new ConverterAdapter<>(new Converter<LocalDateTime>() { - private volatile boolean useFormatter = true; @Override public String toString(final LocalDateTime instance) { - return instance.toString(); + return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(ZoneOffset.UTC), zoneIDUTC)); } @Override public LocalDateTime fromString(final String text) { - if (useFormatter) { - try { - return LocalDateTime.parse(text, formatter); - } catch (final DateTimeParseException dpe) { - useFormatter = false; - } + try { + return parseZonedDateTime(text, formatter, zoneIDUTC).toLocalDateTime(); + } catch (final DateTimeParseException dpe) { + return LocalDateTime.parse(text); } - return LocalDateTime.parse(text); } })); converters.put(new AdapterKey(LocalDate.class, String.class), new ConverterAdapter<>(new Converter<LocalDate>() { - private volatile boolean useFormatter = true; @Override public String toString(final LocalDate instance) { - return instance.toString(); + return formatter.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(instance.toEpochDay())), zoneIDUTC)); } @Override public LocalDate fromString(final String text) { - if (useFormatter) { - try { - return LocalDate.parse(text, formatter); - } catch (final DateTimeParseException dpe) { - useFormatter = false; - } + try { + return parseZonedDateTime(text, formatter, zoneIDUTC).toLocalDate(); + } catch (final DateTimeParseException dpe) { + return LocalDate.parse(text); + } + } + })); + converters.put(new AdapterKey(OffsetDateTime.class, String.class), new ConverterAdapter<>(new Converter<OffsetDateTime>() { + + @Override + public String toString(final OffsetDateTime instance) { + return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); + } + + @Override + public OffsetDateTime fromString(final String text) { + try { + return parseZonedDateTime(text, formatter, zoneIDUTC).toOffsetDateTime(); + } catch (final DateTimeParseException dpe) { + return OffsetDateTime.parse(text); } - return LocalDate.parse(text); } })); converters.put(new AdapterKey(ZonedDateTime.class, String.class), new ConverterAdapter<>(new Converter<ZonedDateTime>() { - private volatile boolean useFormatter = true; @Override public String toString(final ZonedDateTime instance) { - return instance.toString(); + return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); } @Override public ZonedDateTime fromString(final String text) { - if (useFormatter) { - try { - return ZonedDateTime.parse(text, formatter); - } catch (final DateTimeParseException dpe) { - useFormatter = false; - } + try { + return parseZonedDateTime(text, formatter, zoneIDUTC); + } catch (final DateTimeParseException dpe) { + return ZonedDateTime.parse(text); } - return ZonedDateTime.parse(text); + } + })); + converters.put(new AdapterKey(Calendar.class, String.class), new ConverterAdapter<>(new Converter<Calendar>() { + + @Override + public String toString(final Calendar instance) { + return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); + } + + @Override + public Calendar fromString(final String text) { + Calendar instance = Calendar.getInstance(); + instance.setTime(Date.from(parseZonedDateTime(text, formatter, zoneIDUTC).toInstant())); + return instance; + } + })); + converters.put(new AdapterKey(GregorianCalendar.class, String.class), new ConverterAdapter<>(new Converter<GregorianCalendar>() { + + @Override + public String toString(final GregorianCalendar instance) { + return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); + } + + @Override + public GregorianCalendar fromString(final String text) { + Calendar instance = GregorianCalendar.getInstance(); + instance.setTime(Date.from(parseZonedDateTime(text, formatter, zoneIDUTC).toInstant())); + return GregorianCalendar.class.cast(instance); + } + })); + converters.put(new AdapterKey(Instant.class, String.class), new ConverterAdapter<>(new Converter<Instant>() { + + @Override + public String toString(final Instant instance) { + return formatter.format(ZonedDateTime.ofInstant(instance, zoneIDUTC)); + } + + @Override + public Instant fromString(final String text) { + return parseZonedDateTime(text, formatter, zoneIDUTC).toInstant(); } })); }); } + + private static ZonedDateTime parseZonedDateTime(final String text, final DateTimeFormatter formatter, final ZoneId defaultZone){ + TemporalAccessor parse = formatter.parse(text); + ZoneId zone = parse.query(TemporalQueries.zone()); + if (Objects.isNull(zone)) { + zone = defaultZone; + } + int year = parse.isSupported(YEAR) ? parse.get(YEAR) : 0; + int month = parse.isSupported(MONTH_OF_YEAR) ? parse.get(MONTH_OF_YEAR) : 0; + int day = parse.isSupported(DAY_OF_MONTH) ? parse.get(DAY_OF_MONTH) : 0; + int hour = parse.isSupported(HOUR_OF_DAY) ? parse.get(HOUR_OF_DAY) : 0; + int minute = parse.isSupported(MINUTE_OF_HOUR) ? parse.get(MINUTE_OF_HOUR) : 0; + int second = parse.isSupported(SECOND_OF_MINUTE) ? parse.get(SECOND_OF_MINUTE) : 0; + int millisecond = parse.isSupported(MILLI_OF_SECOND) ? parse.get(MILLI_OF_SECOND) : 0; + return ZonedDateTime.of(year, month, day, hour, minute, second, millisecond, zone); + } private static void logIfDeprecatedTimeZone(final String text) { /* TODO: get the list, UTC is clearly not deprecated but uses 3 letters if (text.length() == 3) { // don't fail but log it Logger.getLogger(JohnzonBuilder.class.getName()).severe("Deprecated timezone: " + text); } - */ + */ } private Map<String, ?> generatorConfig() { http://git-wip-us.apache.org/repos/asf/johnzon/blob/fb05f282/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbTypesTest.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbTypesTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbTypesTest.java index 0f792df..ad5b6d2 100644 --- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbTypesTest.java +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbTypesTest.java @@ -36,11 +36,13 @@ import java.time.Period; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Calendar; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; +import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.OptionalDouble; @@ -49,6 +51,7 @@ import java.util.OptionalLong; import java.util.SimpleTimeZone; import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import org.apache.cxf.common.util.StringUtils; import static org.junit.Assert.assertEquals; @@ -109,8 +112,44 @@ public class JsonbTypesTest { assertEquals(expected, jsonb.toJson(types)); } + @Test + public void readAndWriteWithDateFormats() { + readAndWriteWithDateFormat(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"), "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + readAndWriteWithDateFormat(DateTimeFormatter.ofPattern("yyyyMMdd+HHmmssZ"), "yyyyMMdd+HHmmssZ"); + readAndWriteWithDateFormat(DateTimeFormatter.ofPattern("yyyy-MM-dd"), "yyyy-MM-dd"); + } + + private void readAndWriteWithDateFormat(DateTimeFormatter dateTimeFormatter, String dateFormat) { + final LocalDate localDate = LocalDate.of(2015, 1, 1); + final LocalDateTime localDateTime = LocalDateTime.of(2015, 1, 1, 1, 1); + final ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("UTC")); + final String expected = "{" + + "\"calendar\":\"" + dateTimeFormatter.format(zonedDateTime) + "\"," + + "\"date\":\"" + dateTimeFormatter.format(ZonedDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.UTC), ZoneId.of("UTC"))) + "\"," + + "\"gregorianCalendar\":\"" + dateTimeFormatter.format(zonedDateTime) + "\"," + + "\"instant\":\"" + dateTimeFormatter.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(localDate.toEpochDay())), ZoneId.of("UTC"))) + "\"," + + "\"localDate\":\"" + dateTimeFormatter.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(localDate.toEpochDay())), ZoneId.of("UTC"))) + "\"," + + "\"localDateTime\":\"" + dateTimeFormatter.format(ZonedDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.UTC), ZoneId.of("UTC"))) + "\"," + + "\"offsetDateTime\":\"" + dateTimeFormatter.format(ZonedDateTime.ofInstant(OffsetDateTime.of(localDateTime, ZoneOffset.UTC).toInstant(), ZoneId.of("UTC"))) + "\"" + + "}"; + + final Jsonb jsonb = newJsonb(dateFormat); + + final DateTypes types = jsonb.fromJson(new StringReader(expected), DateTypes.class); + assertEquals(localDate, types.localDate); + assertEquals(expected, jsonb.toJson(types)); + } + private static Jsonb newJsonb() { - return JsonbProvider.provider().create().withConfig(new JsonbConfig().setProperty("johnzon.attributeOrder", new Comparator<String>() { + return newJsonb(null); + } + + private static Jsonb newJsonb(String dateFormat) { + JsonbConfig jsonbConfig = new JsonbConfig(); + if (!StringUtils.isEmpty(dateFormat)){ + jsonbConfig.withDateFormat(dateFormat, Locale.getDefault()); + } + return JsonbProvider.provider().create().withConfig(jsonbConfig.setProperty("johnzon.attributeOrder", new Comparator<String>() { @Override public int compare(final String o1, final String o2) { return o1.compareTo(o2); @@ -339,4 +378,97 @@ public class JsonbTypesTest { period, localDateTime, localDate, offsetDateTime, offsetTime); } } + + public static class DateTypes { + private Date date; + private Calendar calendar; + private GregorianCalendar gregorianCalendar; + private Instant instant; + private LocalDateTime localDateTime; + private LocalDate localDate; + private OffsetDateTime offsetDateTime; + + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Calendar getCalendar() { + return calendar; + } + + public void setCalendar(Calendar calendar) { + this.calendar = calendar; + } + + public GregorianCalendar getGregorianCalendar() { + return gregorianCalendar; + } + + public void setGregorianCalendar(GregorianCalendar gregorianCalendar) { + this.gregorianCalendar = gregorianCalendar; + } + + public Instant getInstant() { + return instant; + } + + public void setInstant(Instant instant) { + this.instant = instant; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Types types = Types.class.cast(o); + return + Objects.equals(date, types.date) && + Objects.equals(calendar, types.calendar) && + Objects.equals(gregorianCalendar, types.gregorianCalendar) && + Objects.equals(instant, types.instant) && + Objects.equals(localDateTime, types.localDateTime) && + Objects.equals(localDate, types.localDate) && + Objects.equals(offsetDateTime, types.offsetDateTime); + } + + @Override + public int hashCode() { + return Objects.hash( + date, calendar, gregorianCalendar, instant, localDateTime, localDate, offsetDateTime); + } + } }
