This is an automated email from the ASF dual-hosted git repository. rmannibucau pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/johnzon.git
The following commit(s) were added to refs/heads/master by this push: new 0b8c0d1 [JOHNZON-317] lazy lookup of implicit converters 0b8c0d1 is described below commit 0b8c0d13efad98654d258bee1a1ef623ac091ebb Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Mon Jul 13 13:04:52 2020 +0200 [JOHNZON-317] lazy lookup of implicit converters --- .../org/apache/johnzon/jsonb/JohnzonBuilder.java | 515 ++--------------- .../org/apache/johnzon/jsonb/JsonbAccessMode.java | 16 +- .../jsonb/converter/JohnzonJsonbAdapter.java | 8 + .../org/apache/johnzon/jsonb/JohnzonJsonbTest.java | 28 + .../org/apache/johnzon/mapper/MapperBuilder.java | 90 ++- .../org/apache/johnzon/mapper/MapperConfig.java | 11 +- .../apache/johnzon/mapper/MappingParserImpl.java | 18 +- .../java/org/apache/johnzon/mapper/Mappings.java | 42 +- .../apache/johnzon/mapper/TypeAwareAdapter.java | 6 + .../johnzon/mapper/access/BaseAccessMode.java | 2 +- .../mapper/converter/DateWithCopyConverter.java | 2 +- .../johnzon/mapper/converter/LocaleConverter.java | 23 +- .../johnzon/mapper/internal/ConverterAdapter.java | 25 +- .../johnzon/mapper/map/LazyConverterMap.java | 637 +++++++++++++++++++++ .../apache/johnzon/mapper/MapperConfigTest.java | 4 +- .../apache/johnzon/mapper/ObjectConverterTest.java | 17 + .../test/java/org/superbiz/ExtendMappingTest.java | 4 +- 17 files changed, 872 insertions(+), 576 deletions(-) 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 26f380b..5bbfa2c 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 @@ -18,72 +18,6 @@ */ package org.apache.johnzon.jsonb; -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 static java.util.Collections.emptyMap; -import static java.util.Optional.ofNullable; -import static javax.json.bind.config.PropertyNamingStrategy.IDENTITY; -import static javax.json.bind.config.PropertyOrderStrategy.ANY; - -import java.io.Closeable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.Period; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.temporal.TemporalAccessor; -import java.time.temporal.TemporalQueries; -import java.util.Base64; -import java.util.Calendar; -import java.util.Comparator; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.SimpleTimeZone; -import java.util.TimeZone; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import javax.json.JsonBuilderFactory; -import javax.json.bind.Jsonb; -import javax.json.bind.JsonbBuilder; -import javax.json.bind.JsonbConfig; -import javax.json.bind.JsonbException; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.config.BinaryDataStrategy; -import javax.json.bind.config.PropertyNamingStrategy; -import javax.json.bind.config.PropertyVisibilityStrategy; -import javax.json.bind.serializer.JsonbDeserializer; -import javax.json.bind.serializer.JsonbSerializer; -import javax.json.spi.JsonProvider; -import javax.json.stream.JsonGenerator; -import javax.json.stream.JsonParserFactory; - import org.apache.johnzon.core.AbstractJsonFactory; import org.apache.johnzon.core.JsonGeneratorFactoryImpl; import org.apache.johnzon.core.JsonParserFactoryImpl; @@ -95,7 +29,6 @@ import org.apache.johnzon.jsonb.factory.SimpleJohnzonAdapterFactory; import org.apache.johnzon.jsonb.serializer.JohnzonDeserializationContext; import org.apache.johnzon.jsonb.serializer.JohnzonSerializationContext; import org.apache.johnzon.jsonb.spi.JohnzonAdapterFactory; -import org.apache.johnzon.mapper.Adapter; import org.apache.johnzon.mapper.Converter; import org.apache.johnzon.mapper.Mapper; import org.apache.johnzon.mapper.MapperBuilder; @@ -103,8 +36,42 @@ import org.apache.johnzon.mapper.ObjectConverter; import org.apache.johnzon.mapper.SerializeValueFilter; import org.apache.johnzon.mapper.access.AccessMode; import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode; +import org.apache.johnzon.mapper.converter.LocaleConverter; import org.apache.johnzon.mapper.internal.AdapterKey; -import org.apache.johnzon.mapper.internal.ConverterAdapter; + +import javax.json.JsonBuilderFactory; +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.config.BinaryDataStrategy; +import javax.json.bind.config.PropertyNamingStrategy; +import javax.json.bind.config.PropertyVisibilityStrategy; +import javax.json.bind.serializer.JsonbDeserializer; +import javax.json.bind.serializer.JsonbSerializer; +import javax.json.spi.JsonProvider; +import javax.json.stream.JsonGenerator; +import javax.json.stream.JsonParserFactory; +import java.io.Closeable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.time.format.DateTimeFormatter.ofPattern; +import static java.util.Collections.emptyMap; +import static java.util.Optional.ofNullable; +import static javax.json.bind.config.PropertyNamingStrategy.IDENTITY; +import static javax.json.bind.config.PropertyOrderStrategy.ANY; public class JohnzonBuilder implements JsonbBuilder { private static final Object NO_BM = new Object(); @@ -212,8 +179,13 @@ public class JohnzonBuilder implements JsonbBuilder { builder.setUseJsRange(toBool( // https://github.com/eclipse-ee4j/jsonb-api/issues/180 System.getProperty("johnzon.use-js-range", config.getProperty("johnzon.use-js-range") .map(String::valueOf).orElse("false")))); - - final Map<AdapterKey, Adapter<?, ?>> defaultConverters = createJava8Converters(builder); + builder.setUseShortISO8601Format(false); + config.getProperty(JsonbConfig.DATE_FORMAT) + .map(String.class::cast) + .ifPresent(dateFormat -> builder.setAdaptersDateTimeFormatter(config.getProperty(JsonbConfig.LOCALE) + .map(it -> String.class.isInstance(it) ? new LocaleConverter().to(it.toString()) : Locale.class.cast(it)) + .map(value -> ofPattern(dateFormat, value)) + .orElseGet(() -> ofPattern(dateFormat)))); final JohnzonAdapterFactory factory = config.getProperty("johnzon.factory").map(val -> { if (JohnzonAdapterFactory.class.isInstance(val)) { @@ -241,7 +213,7 @@ public class JohnzonBuilder implements JsonbBuilder { .orElseGet(() -> new JsonbAccessMode( propertyNamingStrategy, orderValue, visibilityStrategy, !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), - defaultConverters, + builder.getAdapters(), factory, jsonp, builderFactorySupplier, parserFactoryProvider, config.getProperty("johnzon.accessModeDelegate") .map(this::toAccessMode) @@ -266,7 +238,7 @@ public class JohnzonBuilder implements JsonbBuilder { final Type[] args = pt.getActualTypeArguments(); final JohnzonJsonbAdapter johnzonJsonbAdapter = new JohnzonJsonbAdapter(adapter, args[0], args[1]); builder.addAdapter(args[0], args[1], johnzonJsonbAdapter); - defaultConverters.put(new AdapterKey(args[0], args[1]), johnzonJsonbAdapter); + builder.getAdapters().put(new AdapterKey(args[0], args[1]), johnzonJsonbAdapter); })); ofNullable(config.getProperty("johnzon.fail-on-unknown-properties") @@ -448,405 +420,6 @@ public class JohnzonBuilder implements JsonbBuilder { return ofNullable(Thread.currentThread().getContextClassLoader()).orElseGet(ClassLoader::getSystemClassLoader); } - // TODO: move these converters in converter package - private Map<AdapterKey, Adapter<?, ?>> createJava8Converters(final MapperBuilder builder) { - final Map<AdapterKey, Adapter<?, ?>> converters = new HashMap<>(); - - final ZoneId zoneIDUTC = ZoneId.of("UTC"); - - // built-in converters not in mapper - converters.put(new AdapterKey(Period.class, String.class), new ConverterAdapter<>(new Converter<Period>() { - @Override - public String toString(final Period instance) { - return instance.toString(); - } - - @Override - public Period fromString(final String text) { - return Period.parse(text); - } - })); - converters.put(new AdapterKey(Duration.class, String.class), new ConverterAdapter<>(new Converter<Duration>() { - @Override - public String toString(final Duration instance) { - return instance.toString(); - } - - @Override - public Duration fromString(final String text) { - return Duration.parse(text); - } - })); - converters.put(new AdapterKey(Date.class, String.class), new ConverterAdapter<>(new Converter<Date>() { - @Override - public String toString(final Date instance) { - return ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC) - .format(DateTimeFormatter.ISO_ZONED_DATE_TIME); - } - - @Override - public Date fromString(final String text) { - try { - return Date.from(ZonedDateTime.parse(text).toInstant()); - } catch (final DateTimeParseException dte) { - return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)); - } - } - })); - converters.put(new AdapterKey(Calendar.class, String.class), new ConverterAdapter<>(new Converter<Calendar>() { - @Override - public String toString(final Calendar instance) { - return toStringCalendar(instance); - } - - @Override - public Calendar fromString(final String text) { - return fromCalendar(text, zdt -> { - final Calendar instance = Calendar.getInstance(); - instance.clear(); - instance.setTimeZone(TimeZone.getTimeZone(zdt.getZone())); - instance.setTimeInMillis(zdt.toInstant().toEpochMilli()); - return instance; - }); - } - })); - converters.put(new AdapterKey(GregorianCalendar.class, String.class), new ConverterAdapter<>(new Converter<GregorianCalendar>() { - @Override - public String toString(final GregorianCalendar instance) { - return toStringCalendar(instance); - } - - @Override - public GregorianCalendar fromString(final String text) { - return fromCalendar(text, GregorianCalendar::from); - } - })); - converters.put(new AdapterKey(TimeZone.class, String.class), new ConverterAdapter<>(new Converter<TimeZone>() { - @Override - public String toString(final TimeZone instance) { - return instance.getID(); - } - - @Override - public TimeZone fromString(final String text) { - checkForDeprecatedTimeZone(text); - return TimeZone.getTimeZone(text); - } - })); - converters.put(new AdapterKey(ZoneId.class, String.class), new ConverterAdapter<>(new Converter<ZoneId>() { - @Override - public String toString(final ZoneId instance) { - return instance.getId(); - } - - @Override - public ZoneId fromString(final String text) { - return ZoneId.of(text); - } - })); - converters.put(new AdapterKey(ZoneOffset.class, String.class), new ConverterAdapter<>(new Converter<ZoneOffset>() { - @Override - public String toString(final ZoneOffset instance) { - return instance.getId(); - } - - @Override - public ZoneOffset fromString(final String text) { - return ZoneOffset.of(text); - } - })); - converters.put(new AdapterKey(SimpleTimeZone.class, String.class), new ConverterAdapter<>(new Converter<SimpleTimeZone>() { - @Override - public String toString(final SimpleTimeZone instance) { - return instance.getID(); - } - - @Override - public SimpleTimeZone fromString(final String text) { - checkForDeprecatedTimeZone(text); - final TimeZone timeZone = TimeZone.getTimeZone(text); - return new SimpleTimeZone(timeZone.getRawOffset(), timeZone.getID()); - } - })); - converters.put(new AdapterKey(Instant.class, String.class), new ConverterAdapter<>(new Converter<Instant>() { - @Override - public String toString(final Instant instance) { - return instance.toString(); - } - - @Override - public Instant fromString(final String text) { - return Instant.parse(text); - } - })); - converters.put(new AdapterKey(LocalDate.class, String.class), new ConverterAdapter<>(new Converter<LocalDate>() { - @Override - public String toString(final LocalDate instance) { - return instance.toString(); - } - - @Override - public LocalDate fromString(final String text) { - return LocalDate.parse(text); - } - })); - converters.put(new AdapterKey(LocalTime.class, String.class), new ConverterAdapter<>(new Converter<LocalTime>() { - @Override - public String toString(final LocalTime instance) { - return instance.toString(); - } - - @Override - public LocalTime fromString(final String text) { - return LocalTime.parse(text); - } - })); - converters.put(new AdapterKey(LocalDateTime.class, String.class), new ConverterAdapter<>(new Converter<LocalDateTime>() { - @Override - public String toString(final LocalDateTime instance) { - return instance.toString(); - } - - @Override - public LocalDateTime fromString(final String text) { - return LocalDateTime.parse(text); - } - })); - converters.put(new AdapterKey(ZonedDateTime.class, String.class), new ConverterAdapter<>(new Converter<ZonedDateTime>() { - @Override - public String toString(final ZonedDateTime instance) { - return instance.toString(); - } - - @Override - public ZonedDateTime fromString(final String text) { - return ZonedDateTime.parse(text); - } - })); - converters.put(new AdapterKey(OffsetDateTime.class, String.class), new ConverterAdapter<>(new Converter<OffsetDateTime>() { - @Override - public String toString(final OffsetDateTime instance) { - return instance.toString(); - } - - @Override - public OffsetDateTime fromString(final String text) { - return OffsetDateTime.parse(text); - } - })); - converters.put(new AdapterKey(OffsetTime.class, String.class), new ConverterAdapter<>(new Converter<OffsetTime>() { - @Override - public String toString(final OffsetTime instance) { - return instance.toString(); - } - - @Override - public OffsetTime fromString(final String text) { - return OffsetTime.parse(text); - } - })); - addDateFormatConfigConverters(converters, zoneIDUTC); - - converters.forEach((k, v) -> builder.addAdapter(k.getFrom(), k.getTo(), v)); - return converters; - } - - private String toStringCalendar(final Calendar instance) { - if (!hasTime(instance)) { // spec - final LocalDate localDate = LocalDate.of( - instance.get(Calendar.YEAR), - instance.get(Calendar.MONTH) + 1, - instance.get(Calendar.DAY_OF_MONTH)); - return localDate.toString() + - (instance.getTimeZone() != null ? - instance.getTimeZone().toZoneId().getRules() - .getOffset(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(localDate.toEpochDay()))) : ""); - } - return ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId()) - .format(DateTimeFormatter.ISO_DATE_TIME); - } - - private boolean hasTime(final Calendar instance) { - if (!instance.isSet(Calendar.HOUR_OF_DAY)) { - return false; - } - return instance.get(Calendar.HOUR_OF_DAY) != 0 || - (instance.isSet(Calendar.MINUTE)&& instance.get(Calendar.MINUTE) != 0) || - (instance.isSet(Calendar.SECOND) && instance.get(Calendar.SECOND) != 0); - } - - private <T extends Calendar> T fromCalendar(final String text, final Function<ZonedDateTime, T> calendarSupplier) { - switch (text.length()) { - case 10: { - final ZonedDateTime date = LocalDate.parse(text) - .atTime(0, 0, 0) - .atZone(ZoneId.of("UTC")); - return calendarSupplier.apply(date); - } - default: - final ZonedDateTime zonedDateTime = ZonedDateTime.parse(text); - return calendarSupplier.apply(zonedDateTime); - } - } - - - private void addDateFormatConfigConverters(final Map<AdapterKey, Adapter<?, ?>> converters, final ZoneId zoneIDUTC) { - // config, override defaults - config.getProperty(JsonbConfig.DATE_FORMAT).map(String.class::cast).ifPresent(dateFormat -> { - final Optional<Locale> locale = config.getProperty(JsonbConfig.LOCALE).map(Locale.class::cast); - final DateTimeFormatter formatter = locale.isPresent() ? ofPattern(dateFormat, locale.get()) : ofPattern(dateFormat); - - converters.put(new AdapterKey(Date.class, String.class), new ConverterAdapter<>(new Converter<Date>() { - - @Override - public String toString(final Date instance) { - return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); - } - - @Override - public Date fromString(final String text) { - try { - return Date.from(parseZonedDateTime(text, formatter, zoneIDUTC).toInstant()); - } catch (final DateTimeParseException dpe) { - return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)); - } - } - })); - converters.put(new AdapterKey(LocalDateTime.class, String.class), new ConverterAdapter<>(new Converter<LocalDateTime>() { - - @Override - public String toString(final LocalDateTime instance) { - return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(ZoneOffset.UTC), zoneIDUTC)); - } - - @Override - public LocalDateTime fromString(final String text) { - try { - return parseZonedDateTime(text, formatter, zoneIDUTC).toLocalDateTime(); - } catch (final DateTimeParseException dpe) { - return LocalDateTime.parse(text); - } - } - })); - converters.put(new AdapterKey(LocalDate.class, String.class), new ConverterAdapter<>(new Converter<LocalDate>() { - - @Override - public String toString(final LocalDate instance) { - return formatter.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(instance.toEpochDay())), zoneIDUTC)); - } - - @Override - public LocalDate fromString(final String text) { - 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); - } - } - })); - converters.put(new AdapterKey(ZonedDateTime.class, String.class), new ConverterAdapter<>(new Converter<ZonedDateTime>() { - - @Override - public String toString(final ZonedDateTime instance) { - return formatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); - } - - @Override - public ZonedDateTime fromString(final String text) { - try { - return parseZonedDateTime(text, formatter, zoneIDUTC); - } catch (final DateTimeParseException dpe) { - 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(), instance.getTimeZone().toZoneId())); - } - - @Override - public Calendar fromString(final String text) { - final ZonedDateTime zonedDateTime = parseZonedDateTime(text, formatter, zoneIDUTC); - final Calendar instance = Calendar.getInstance(); - instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone())); - instance.setTime(Date.from(zonedDateTime.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(), instance.getTimeZone().toZoneId())); - } - - @Override - public GregorianCalendar fromString(final String text) { - final ZonedDateTime zonedDateTime = parseZonedDateTime(text, formatter, zoneIDUTC); - final Calendar instance = GregorianCalendar.getInstance(); - instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone())); - instance.setTime(Date.from(zonedDateTime.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 checkForDeprecatedTimeZone(final String text) { - switch (text) { - case "CST": // really for TCK, this sucks for end users so we don't fail for all deprecated zones - throw new JsonbException("Deprecated timezone: '" + text + '"'); - default: - } - } - private Map<String, ?> generatorConfig() { final Map<String, Object> map = new HashMap<>(); if (config == null) { diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java index 1130f67..363f9b6 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java @@ -441,20 +441,20 @@ public class JsonbAccessMode implements AccessMode, Closeable { converter = new JohnzonJsonbAdapter(instance.getValue(), actualTypeArguments[0], actualTypeArguments[1]); } else if (dateFormat != null) { // TODO: support lists, LocalDate? if (Date.class == type) { - converter = new ConverterAdapter<>(new JsonbDateConverter(dateFormat)); + converter = new ConverterAdapter<>(new JsonbDateConverter(dateFormat), Date.class); } else if (LocalDateTime.class == type) { - converter = new ConverterAdapter<>(new JsonbLocalDateTimeConverter(dateFormat)); + converter = new ConverterAdapter<>(new JsonbLocalDateTimeConverter(dateFormat), LocalDateTime.class); } else if (LocalDate.class == type) { - converter = new ConverterAdapter<>(new JsonbLocalDateConverter(dateFormat)); + converter = new ConverterAdapter<>(new JsonbLocalDateConverter(dateFormat), LocalDate.class); } else if (ZonedDateTime.class == type) { - converter = new ConverterAdapter<>(new JsonbZonedDateTimeConverter(dateFormat)); + converter = new ConverterAdapter<>(new JsonbZonedDateTimeConverter(dateFormat), ZonedDateTime.class); } else { // can happen if set on the class, todo: refine the checks converter = null; // todo: should we fallback on numberformat? } } else if (numberFormat != null) { // TODO: support lists? - converter = new ConverterAdapter<>(new JsonbNumberConverter(numberFormat)); + converter = new ConverterAdapter<>(new JsonbNumberConverter(numberFormat), Number.class); } else { - converter = new ConverterAdapter<>(new JsonbValueConverter()); + converter = new ConverterAdapter<>(new JsonbValueConverter(), Object.class); } return converter; } @@ -985,7 +985,7 @@ public class JsonbAccessMode implements AccessMode, Closeable { try { MapperConverter mapperConverter = johnzonConverter.value().newInstance(); if (mapperConverter instanceof Converter) { - converter = new ConverterAdapter<>((Converter) mapperConverter); + converter = new ConverterAdapter<>((Converter) mapperConverter, annotationHolder.getType()); } else if (mapperConverter instanceof ObjectConverter.Reader) { reader = (ObjectConverter.Reader) mapperConverter; } @@ -1045,7 +1045,7 @@ public class JsonbAccessMode implements AccessMode, Closeable { try { MapperConverter mapperConverter = johnzonConverter.value().newInstance(); if (mapperConverter instanceof Converter) { - converter = new ConverterAdapter<>((Converter) mapperConverter) ; + converter = new ConverterAdapter<>((Converter) mapperConverter, reader.getType()) ; } else if (mapperConverter instanceof ObjectConverter.Writer) { writer = (ObjectConverter.Writer) mapperConverter; } diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java index 7fc505f..079c782 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/converter/JohnzonJsonbAdapter.java @@ -19,6 +19,7 @@ package org.apache.johnzon.jsonb.converter; import org.apache.johnzon.mapper.TypeAwareAdapter; +import org.apache.johnzon.mapper.internal.AdapterKey; import javax.json.bind.JsonbException; import javax.json.bind.adapter.JsonbAdapter; @@ -28,11 +29,13 @@ public class JohnzonJsonbAdapter<JsonType, OriginalType> implements TypeAwareAda private final JsonbAdapter<OriginalType, JsonType> delegate; private final Type from; private final Type to; + private final AdapterKey key; public JohnzonJsonbAdapter(final JsonbAdapter<OriginalType, JsonType> delegate, final Type from, final Type to) { this.delegate = delegate; this.from = from; this.to = to; + this.key = new AdapterKey(from, to); } @Override @@ -60,6 +63,11 @@ public class JohnzonJsonbAdapter<JsonType, OriginalType> implements TypeAwareAda } @Override + public AdapterKey getKey() { + return key; + } + + @Override public Type getTo() { return to; } diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java index debf27f..757359b 100644 --- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonJsonbTest.java @@ -24,9 +24,14 @@ import javax.json.JsonArray; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; +import org.apache.johnzon.jsonb.test.JsonbRule; +import org.junit.Rule; import org.junit.Test; public class JohnzonJsonbTest { + @Rule + public final JsonbRule rule = new JsonbRule(); + @Test public void jsonArray() throws Exception { try (final Jsonb jsonb = JsonbBuilder.create()) { @@ -35,4 +40,27 @@ public class JohnzonJsonbTest { assertEquals(json, array.toString()); } } + + @Test + public void longBounds() { + final String max = rule.toJson(new LongWrapper(Long.MAX_VALUE)); + assertEquals("{\"value\":9223372036854775807}", max); + assertEquals(Long.MAX_VALUE, rule.fromJson(max, LongWrapper.class).value, 0); + + final String min = rule.toJson(new LongWrapper(Long.MIN_VALUE)); + assertEquals("{\"value\":-9223372036854775808}", min); + assertEquals(Long.MIN_VALUE, rule.fromJson(min, LongWrapper.class).value, 0); + } + + public static class LongWrapper { + public Long value; + + public LongWrapper() { + // no-op + } + + public LongWrapper(final Long value) { + this.value = value; + } + } } diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java index 81cc049..ade0e26 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java @@ -28,25 +28,18 @@ import org.apache.johnzon.mapper.access.BaseAccessMode; import org.apache.johnzon.mapper.access.FieldAccessMode; import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode; import org.apache.johnzon.mapper.access.MethodAccessMode; -import org.apache.johnzon.mapper.converter.BigDecimalConverter; -import org.apache.johnzon.mapper.converter.BigIntegerConverter; import org.apache.johnzon.mapper.converter.BooleanConverter; import org.apache.johnzon.mapper.converter.ByteConverter; import org.apache.johnzon.mapper.converter.CachedDelegateConverter; import org.apache.johnzon.mapper.converter.CharacterConverter; -import org.apache.johnzon.mapper.converter.ClassConverter; -import org.apache.johnzon.mapper.converter.DateConverter; import org.apache.johnzon.mapper.converter.DoubleConverter; import org.apache.johnzon.mapper.converter.FloatConverter; import org.apache.johnzon.mapper.converter.IntegerConverter; -import org.apache.johnzon.mapper.converter.LocaleConverter; import org.apache.johnzon.mapper.converter.LongConverter; import org.apache.johnzon.mapper.converter.ShortConverter; -import org.apache.johnzon.mapper.converter.StringConverter; -import org.apache.johnzon.mapper.converter.URIConverter; -import org.apache.johnzon.mapper.converter.URLConverter; import org.apache.johnzon.mapper.internal.AdapterKey; import org.apache.johnzon.mapper.internal.ConverterAdapter; +import org.apache.johnzon.mapper.map.LazyConverterMap; import javax.json.JsonBuilderFactory; import javax.json.JsonReaderFactory; @@ -56,58 +49,20 @@ import javax.json.stream.JsonGeneratorFactory; import java.io.Closeable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.URI; -import java.net.URL; import java.nio.charset.Charset; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; -import java.util.Date; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.function.Predicate; // this class is responsible to hold any needed config // to build the runtime public class MapperBuilder { - private static final Map<AdapterKey, Adapter<?, ?>> DEFAULT_CONVERTERS = new HashMap<AdapterKey, Adapter<?, ?>>(24); - - static { - //DEFAULT_CONVERTERS.put(Date.class, new DateConverter("yyyy-MM-dd'T'HH:mm:ssZ")); // ISO8601 long RFC822 zone - DEFAULT_CONVERTERS.put(new AdapterKey(Date.class, String.class), new ConverterAdapter<Date>(new DateConverter("yyyyMMddHHmmssZ"))); // ISO8601 short - DEFAULT_CONVERTERS.put(new AdapterKey(URL.class, String.class), new ConverterAdapter<URL>(new URLConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(URI.class, String.class), new ConverterAdapter<URI>(new URIConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(Class.class, String.class), new ConverterAdapter<Class<?>>(new ClassConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(String.class, String.class), new ConverterAdapter<String>(new StringConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(BigDecimal.class, String.class), new ConverterAdapter<BigDecimal>(new BigDecimalConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(BigInteger.class, String.class), new ConverterAdapter<BigInteger>(new BigIntegerConverter())); - /* primitives should be hanlded low level and adapters will wrap them in string which is unlikely - DEFAULT_CONVERTERS.put(new AdapterKey(Byte.class, String.class), new ConverterAdapter<Byte>(new CachedDelegateConverter<Byte>(new ByteConverter()))); - DEFAULT_CONVERTERS.put(new AdapterKey(Character.class, String.class), new ConverterAdapter<Character>(new CharacterConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(Double.class, String.class), new ConverterAdapter<Double>(new DoubleConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(Float.class, String.class), new ConverterAdapter<Float>(new FloatConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(Integer.class, String.class), new ConverterAdapter<Integer>(new IntegerConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(Long.class, String.class), new ConverterAdapter<Long>(new LongConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(Short.class, String.class), new ConverterAdapter<Short>(new ShortConverter())); - DEFAULT_CONVERTERS.put(new AdapterKey(Boolean.class, String.class), new ConverterAdapter<Boolean>(new CachedDelegateConverter<Boolean>(new BooleanConverter()))); - DEFAULT_CONVERTERS.put(new AdapterKey(byte.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Byte.class, String.class))); - DEFAULT_CONVERTERS.put(new AdapterKey(char.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Character.class, String.class))); - DEFAULT_CONVERTERS.put(new AdapterKey(double.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Double.class, String.class))); - DEFAULT_CONVERTERS.put(new AdapterKey(float.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Float.class, String.class))); - DEFAULT_CONVERTERS.put(new AdapterKey(int.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Integer.class, String.class))); - DEFAULT_CONVERTERS.put(new AdapterKey(long.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Long.class, String.class))); - DEFAULT_CONVERTERS.put(new AdapterKey(short.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Short.class, String.class))); - DEFAULT_CONVERTERS.put(new AdapterKey(boolean.class, String.class), DEFAULT_CONVERTERS.get(new AdapterKey(Boolean.class, String.class))); - */ - DEFAULT_CONVERTERS.put(new AdapterKey(Locale.class, String.class), new LocaleConverter()); - } - private JsonReaderFactory readerFactory; private JsonGeneratorFactory generatorFactory; private JsonProvider provider; @@ -134,7 +89,7 @@ public class MapperBuilder { private boolean enforceQuoteString; private AccessMode accessMode; private Charset encoding = Charset.forName(System.getProperty("johnzon.mapper.encoding", "UTF-8")); - private ConcurrentMap<AdapterKey, Adapter<?, ?>> adapters = new ConcurrentHashMap<AdapterKey, Adapter<?, ?>>(DEFAULT_CONVERTERS); + private LazyConverterMap adapters = new LazyConverterMap(); private Map<Class<?>, ObjectConverter.Reader<?>> objectConverterReaders = new HashMap<Class<?>, ObjectConverter.Reader<?>>(); private Map<Class<?>, ObjectConverter.Writer<?>> objectConverterWriters = new HashMap<Class<?>, ObjectConverter.Writer<?>>(); private Map<Class<?>, String[]> ignoredForFields = new HashMap<Class<?>, String[]>(); @@ -244,14 +199,14 @@ public class MapperBuilder { } if (primitiveConverters) { - adapters.put(new AdapterKey(Byte.class, String.class), new ConverterAdapter<Byte>(new CachedDelegateConverter<Byte>(new ByteConverter()))); - adapters.put(new AdapterKey(Character.class, String.class), new ConverterAdapter<Character>(new CharacterConverter())); - adapters.put(new AdapterKey(Double.class, String.class), new ConverterAdapter<Double>(new DoubleConverter())); - adapters.put(new AdapterKey(Float.class, String.class), new ConverterAdapter<Float>(new FloatConverter())); - adapters.put(new AdapterKey(Integer.class, String.class), new ConverterAdapter<Integer>(new IntegerConverter())); - adapters.put(new AdapterKey(Long.class, String.class), new ConverterAdapter<Long>(new LongConverter())); - adapters.put(new AdapterKey(Short.class, String.class), new ConverterAdapter<Short>(new ShortConverter())); - adapters.put(new AdapterKey(Boolean.class, String.class), new ConverterAdapter<Boolean>(new CachedDelegateConverter<Boolean>(new BooleanConverter()))); + adapters.put(new AdapterKey(Byte.class, String.class), new ConverterAdapter<>(new CachedDelegateConverter<>(new ByteConverter()), Byte.class)); + adapters.put(new AdapterKey(Character.class, String.class), new ConverterAdapter<>(new CharacterConverter(), Character.class)); + adapters.put(new AdapterKey(Double.class, String.class), new ConverterAdapter<>(new DoubleConverter(), Double.class)); + adapters.put(new AdapterKey(Float.class, String.class), new ConverterAdapter<>(new FloatConverter(), Float.class)); + adapters.put(new AdapterKey(Integer.class, String.class), new ConverterAdapter<>(new IntegerConverter(), Integer.class)); + adapters.put(new AdapterKey(Long.class, String.class), new ConverterAdapter<>(new LongConverter(), Long.class)); + adapters.put(new AdapterKey(Short.class, String.class), new ConverterAdapter<>(new ShortConverter(), Short.class)); + adapters.put(new AdapterKey(Boolean.class, String.class), new ConverterAdapter<>(new CachedDelegateConverter<>(new BooleanConverter()), Boolean.class)); adapters.put(new AdapterKey(byte.class, String.class), adapters.get(new AdapterKey(Byte.class, String.class))); adapters.put(new AdapterKey(char.class, String.class), adapters.get(new AdapterKey(Character.class, String.class))); adapters.put(new AdapterKey(double.class, String.class), adapters.get(new AdapterKey(Double.class, String.class))); @@ -278,6 +233,25 @@ public class MapperBuilder { closeables); } + public ConcurrentHashMap<AdapterKey, Adapter<?,?>> getAdapters() { + return adapters; + } + + public MapperBuilder setUseShortISO8601Format(final boolean useShortISO8601Format) { + adapters.setUseShortISO8601Format(useShortISO8601Format); + return this; + } + + public MapperBuilder setAdaptersDateTimeFormatter(final DateTimeFormatter dateTimeFormatter) { + adapters.setDateTimeFormatter(dateTimeFormatter); + return this; + } + + public MapperBuilder setAdaptersDateTimeFormatterString(final String dateTimeFormatter) { + adapters.setDateTimeFormatter(DateTimeFormatter.ofPattern(dateTimeFormatter)); + return this; + } + public MapperBuilder setInterfaceImplementationMapping(final Map<Class<?>, Class<?>> interfaceImplementationMapping) { this.interfaceImplementationMapping = interfaceImplementationMapping; return this; @@ -395,13 +369,13 @@ public class MapperBuilder { @Deprecated // use addAdapter public MapperBuilder addPropertyEditor(final Class<?> clazz, final Converter<?> converter) { - adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter)); + adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter, clazz)); return this; } @Deprecated // use addAdapter public MapperBuilder addConverter(final Type clazz, final Converter<?> converter) { - adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter)); + adapters.put(new AdapterKey(clazz, String.class), new ConverterAdapter(converter, clazz)); return this; } diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java index 3046376..d0b17d5 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java @@ -22,6 +22,7 @@ import org.apache.johnzon.mapper.access.AccessMode; import org.apache.johnzon.mapper.converter.EnumConverter; import org.apache.johnzon.mapper.internal.AdapterKey; import org.apache.johnzon.mapper.internal.ConverterAdapter; +import org.apache.johnzon.mapper.map.LazyConverterMap; import javax.json.JsonValue; import java.lang.reflect.Type; @@ -67,7 +68,7 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { private final boolean supportEnumMapDeserialization; // for tck private final AccessMode accessMode; private final Charset encoding; - private final ConcurrentMap<AdapterKey, Adapter<?, ?>> adapters; + private final LazyConverterMap adapters; private final ConcurrentMap<Adapter<?, ?>, AdapterKey> reverseAdapters; private final Map<Class<?>, ObjectConverter.Writer<?>> objectConverterWriters; @@ -95,7 +96,7 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { //disable checkstyle for 10+ parameters //CHECKSTYLE:OFF - public MapperConfig(final ConcurrentMap<AdapterKey, Adapter<?, ?>> adapters, + public MapperConfig(final LazyConverterMap adapters, final Map<Class<?>, ObjectConverter.Writer<?>> objectConverterWriters, final Map<Class<?>, ObjectConverter.Reader<?>> objectConverterReaders, final int version, final boolean close, @@ -218,12 +219,12 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { if (Class.class.isInstance(aClass)) { final Class<?> clazz = Class.class.cast(aClass); if (Enum.class.isAssignableFrom(clazz)) { - final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz)); + final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz), clazz); adapters.putIfAbsent(new AdapterKey(String.class, aClass), enumConverter); return enumConverter; } } - final List<AdapterKey> matched = adapters.keySet().stream() + final List<AdapterKey> matched = adapters.adapterKeys().stream() .filter(k -> k.isAssignableFrom(aClass)) .collect(toList()); if (matched.size() == 1) { @@ -382,7 +383,7 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { return encoding; } - public ConcurrentMap<AdapterKey, Adapter<?, ?>> getAdapters() { + public LazyConverterMap getAdapters() { return adapters; } diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java index 1e90acf..25a5134 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java @@ -503,15 +503,9 @@ public class MappingParserImpl implements MappingParser { if (JsonObject.class == key.getTo() || JsonStructure.class == key.getTo()) { return converter.to(jsonValue.asJsonObject()); } - - //X TODO maybe we can put this into MapperConfig? - //X config.getAdapter(AdapterKey) - //X config.getAdapterKey(Adapter) - final AdapterKey adapterKey = getAdapterKey(converter); - final Object param; try { - Type to = adapterKey.getTo(); + Type to = key.getTo(); param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class, jsonPointer); } catch (final Exception e) { throw new MapperException(e); @@ -575,9 +569,7 @@ public class MappingParserImpl implements MappingParser { if (adapterKey == null) { if (converter instanceof TypeAwareAdapter) { - TypeAwareAdapter typeAwareAdapter = (TypeAwareAdapter) converter; - adapterKey = new AdapterKey(typeAwareAdapter.getFrom(), typeAwareAdapter.getTo()); - config.getReverseAdapters().putIfAbsent(converter, adapterKey); + return TypeAwareAdapter.class.cast(converter).getKey(); } else { Class<?> current = converter.getClass(); @@ -655,7 +647,7 @@ public class MappingParserImpl implements MappingParser { if (JsonObject.class == type || JsonStructure.class == type) { return jsonValue; } - final boolean typedAdapter = TypeAwareAdapter.class.isInstance(itemConverter); + final boolean typedAdapter = !ConverterAdapter.class.isInstance(itemConverter) && TypeAwareAdapter.class.isInstance(itemConverter); final Object object = buildObject( baseInstance != null ? baseInstance.getClass() : ( typedAdapter ? TypeAwareAdapter.class.cast(itemConverter).getTo() : type), @@ -1136,12 +1128,12 @@ public class MappingParserImpl implements MappingParser { if (Class.class.isInstance(aClass)) { final Class<?> clazz = Class.class.cast(aClass); if (Enum.class.isAssignableFrom(clazz)) { - final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz)); + final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz), clazz); config.getAdapters().putIfAbsent(new AdapterKey(String.class, aClass), enumConverter); return enumConverter; } } - final List<AdapterKey> matched = config.getAdapters().keySet().stream() + final List<AdapterKey> matched = config.getAdapters().adapterKeys().stream() .filter(k -> k.isAssignableFrom(aClass)) .collect(toList()); if (matched.size() == 1) { diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java index 1c5db42..1c5b045 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java @@ -160,16 +160,27 @@ public class Mappings { } if (theObjectConverter == null) { Adapter adapter; + final Type readerType = reader.getType(); if (converter instanceof Converter) { - adapter = new ConverterAdapter((Converter) converter); + adapter = new ConverterAdapter((Converter) converter, readerType); } else { adapter = (Adapter) converter; } - if (matches(reader.getType(), adapter)) { + if (matches(readerType, adapter)) { theConverter = adapter; } else { - theItemConverter = adapter; + if (converter instanceof Converter) { + if (ParameterizedType.class.isInstance(readerType) && ParameterizedType.class.cast(readerType).getActualTypeArguments().length > 0) { + final Type[] args = ParameterizedType.class.cast(readerType).getActualTypeArguments(); + // List<A> or Map<String, A> lead to read the last arg in all cases + theItemConverter = new ConverterAdapter((Converter) converter, args[args.length - 1]); + } else { + theItemConverter = adapter; + } + } else { + theItemConverter = adapter; + } } } } @@ -237,18 +248,29 @@ public class Mappings { if (converter instanceof ObjectConverter.Reader) { theObjectConverter = (ObjectConverter.Reader) converter; } + final Type writerType = writer.getType(); if (theObjectConverter == null) { Adapter adapter; if (converter instanceof Converter) { - adapter = new ConverterAdapter((Converter) converter); + adapter = new ConverterAdapter((Converter) converter, writerType); } else { adapter = (Adapter) converter; } - if (matches(writer.getType(), adapter)) { + if (matches(writerType, adapter)) { theConverter = adapter; } else { - theItemConverter = adapter; + if (converter instanceof Converter) { + if (ParameterizedType.class.isInstance(writerType) && ParameterizedType.class.cast(writerType).getActualTypeArguments().length > 0) { + final Type[] args = ParameterizedType.class.cast(writerType).getActualTypeArguments(); + // List<A> or Map<String, A> lead to read the last arg in all cases + theItemConverter = new ConverterAdapter((Converter) converter, args[args.length - 1]); + } else { + theItemConverter = adapter; + } + } else { + theItemConverter = adapter; + } } } } @@ -677,7 +699,7 @@ public class Mappings { final AdapterKey key = new AdapterKey(String.class, type); converter = adapters.get(key); // first ensure user didnt override it if (converter == null) { - converter = new ConverterAdapter(new EnumConverter(type)); + converter = new ConverterAdapter(new EnumConverter(type), type); adapters.put(key, (Adapter<?, ?>) converter); } } @@ -694,12 +716,12 @@ public class Mappings { public MapBuilderReader(final Map<String, Getter> objectGetters, final String[] paths, final int version) { this.getters = objectGetters; this.paths = paths; - this.template = new LinkedHashMap<String, Object>(); + this.template = new LinkedHashMap<>(); this.version = version; Map<String, Object> last = this.template; for (int i = 1; i < paths.length; i++) { - final Map<String, Object> newLast = new LinkedHashMap<String, Object>(); + final Map<String, Object> newLast = new LinkedHashMap<>(); last.put(paths[i], newLast); last = newLast; } @@ -707,7 +729,7 @@ public class Mappings { @Override public Object read(final Object instance) { - final Map<String, Object> map = new LinkedHashMap<String, Object>(template); + final Map<String, Object> map = new LinkedHashMap<>(template); Map<String, Object> nested = map; for (int i = 1; i < paths.length; i++) { nested = Map.class.cast(nested.get(paths[i])); diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java index cff2fc0..b956803 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/TypeAwareAdapter.java @@ -18,9 +18,15 @@ */ package org.apache.johnzon.mapper; +import org.apache.johnzon.mapper.internal.AdapterKey; + import java.lang.reflect.Type; public interface TypeAwareAdapter<A, B> extends Adapter<A, B> { Type getTo(); Type getFrom(); + + default AdapterKey getKey() { + return new AdapterKey(getFrom(), getTo()); + } } diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java index edbc379..7bd011e 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java @@ -197,7 +197,7 @@ public abstract class BaseAccessMode implements AccessMode { try { MapperConverter mapperConverter = JohnzonConverter.class.cast(a).value().newInstance(); if (mapperConverter instanceof Converter) { - final Adapter<?, ?> converter = new ConverterAdapter((Converter) mapperConverter); + final Adapter<?, ?> converter = new ConverterAdapter((Converter) mapperConverter, constructor.getGenericParameterTypes()[i]); if (matches(constructor.getParameterTypes()[i], converter)) { constructorParameterConverters[i] = converter; constructorItemParameterConverters[i] = null; diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java index 431f133..85abb26 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/DateWithCopyConverter.java @@ -28,7 +28,7 @@ public class DateWithCopyConverter implements Adapter<Date, String> { private final Adapter<Date, String> delegate; public DateWithCopyConverter(final Adapter<Date, String> delegate) { - this.delegate = delegate == null ? new ConverterAdapter<Date>(new DateConverter("yyyyMMddHHmmssZ")) : delegate; + this.delegate = delegate == null ? new ConverterAdapter<>(new DateConverter("yyyyMMddHHmmssZ"), Date.class) : delegate; } @Override diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java index a03b2a1..6dd330f 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/LocaleConverter.java @@ -18,12 +18,31 @@ */ package org.apache.johnzon.mapper.converter; -import org.apache.johnzon.mapper.Adapter; +import org.apache.johnzon.mapper.TypeAwareAdapter; +import org.apache.johnzon.mapper.internal.AdapterKey; +import java.lang.reflect.Type; import java.util.Locale; // from [lang] -public class LocaleConverter implements Adapter<Locale, String> { +public class LocaleConverter implements TypeAwareAdapter<Locale, String> { + private final AdapterKey key = new AdapterKey(Locale.class, String.class); + + @Override + public Type getTo() { + return key.getTo(); + } + + @Override + public Type getFrom() { + return key.getFrom(); + } + + @Override + public AdapterKey getKey() { + return key; + } + @Override public String from(final Locale instance) { return instance.toString(); diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java index c3bb90e..afea76b 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/internal/ConverterAdapter.java @@ -18,14 +18,18 @@ */ package org.apache.johnzon.mapper.internal; -import org.apache.johnzon.mapper.Adapter; import org.apache.johnzon.mapper.Converter; +import org.apache.johnzon.mapper.TypeAwareAdapter; -public class ConverterAdapter<A> implements Adapter<A, String> { +import java.lang.reflect.Type; + +public class ConverterAdapter<A> implements TypeAwareAdapter<A, String> { private final Converter<A> converter; + private final AdapterKey key; - public ConverterAdapter(final Converter<A> converter) { + public ConverterAdapter(final Converter<A> converter, final Type from) { this.converter = converter; + this.key = new AdapterKey(from, String.class); } public Converter<A> getConverter() { @@ -41,4 +45,19 @@ public class ConverterAdapter<A> implements Adapter<A, String> { public String from(final A a) { return converter.toString(a); } + + @Override + public Type getTo() { + return key.getTo(); + } + + @Override + public Type getFrom() { + return key.getFrom(); + } + + @Override + public AdapterKey getKey() { + return key; + } } diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java new file mode 100644 index 0000000..f1a4e42 --- /dev/null +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java @@ -0,0 +1,637 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.johnzon.mapper.map; + +import org.apache.johnzon.mapper.Adapter; +import org.apache.johnzon.mapper.Converter; +import org.apache.johnzon.mapper.MapperException; +import org.apache.johnzon.mapper.converter.BigDecimalConverter; +import org.apache.johnzon.mapper.converter.BigIntegerConverter; +import org.apache.johnzon.mapper.converter.ClassConverter; +import org.apache.johnzon.mapper.converter.DateConverter; +import org.apache.johnzon.mapper.converter.LocaleConverter; +import org.apache.johnzon.mapper.converter.StringConverter; +import org.apache.johnzon.mapper.converter.URIConverter; +import org.apache.johnzon.mapper.converter.URLConverter; +import org.apache.johnzon.mapper.internal.AdapterKey; +import org.apache.johnzon.mapper.internal.ConverterAdapter; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.SimpleTimeZone; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Stream; + +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 static java.util.stream.Collectors.toSet; + +// important: override all usages, +// mainly org.apache.johnzon.mapper.MapperConfig.findAdapter and +// org.apache.johnzon.mapper.MappingParserImpl.findAdapter today +public class LazyConverterMap extends ConcurrentHashMap<AdapterKey, Adapter<?, ?>> { + private static final Adapter<?, ?> NO_ADAPTER = new Adapter<Object, Object>() { + @Override + public Object to(final Object b) { + throw new UnsupportedOperationException("shouldn't be called"); + } + + @Override + public Object from(final Object a) { + return to(null); // just fail + } + }; + + private boolean useShortISO8601Format = true; + private DateTimeFormatter dateTimeFormatter; + + public void setUseShortISO8601Format(final boolean useShortISO8601Format) { + this.useShortISO8601Format = useShortISO8601Format; + } + + public void setDateTimeFormatter(final DateTimeFormatter dateTimeFormatter) { + this.dateTimeFormatter = dateTimeFormatter; + } + + @Override + public Adapter<?, ?> get(final Object key) { + final Adapter<?, ?> found = super.get(key); + if (found == NO_ADAPTER) { + return null; + } + if (found == null) { + if (!AdapterKey.class.isInstance(key)) { + return null; + } + final AdapterKey k = AdapterKey.class.cast(key); + if (k.getTo() == String.class) { + final Adapter<?, ?> adapter = doLazyLookup(k); + if (adapter != null) { + return adapter; + } // else let's cache we don't need to go through lazy lookups + } + add(k, NO_ADAPTER); + return null; + } + return found; + } + + @Override + public Set<Entry<AdapterKey, Adapter<?, ?>>> entrySet() { + return super.entrySet().stream() + .filter(it -> it.getValue() != NO_ADAPTER) + .collect(toSet()); + } + + public Set<AdapterKey> adapterKeys() { + return Stream.concat( + super.keySet().stream() + .filter(it -> super.get(it) != NO_ADAPTER), + Stream.of(Date.class, URI.class, URL.class, Class.class, String.class, BigDecimal.class, BigInteger.class, + Locale.class, Period.class, Duration.class, Calendar.class, GregorianCalendar.class, TimeZone.class, + ZoneId.class, ZoneOffset.class, SimpleTimeZone.class, Instant.class, LocalDateTime.class, LocalDate.class, + ZonedDateTime.class, OffsetDateTime.class, OffsetTime.class) + .map(it -> new AdapterKey(it, String.class, true))) + .collect(toSet()); + } + + private Adapter<?, ?> doLazyLookup(final AdapterKey key) { + final Type from = key.getFrom(); + if (from == Date.class) { + return addDateConverter(key); + } + if (from == URI.class) { + return add(key, new ConverterAdapter<>(new URIConverter(), URI.class)); + } + if (from == URL.class) { + return add(key, new ConverterAdapter<>(new URLConverter(), URL.class)); + } + if (from == Class.class) { + return add(key, new ConverterAdapter<>(new ClassConverter(), Class.class)); + } + if (from == String.class) { + return add(key, new ConverterAdapter<>(new StringConverter(), String.class)); + } + if (from == BigDecimal.class) { + return add(key, new ConverterAdapter<>(new BigDecimalConverter(), BigDecimal.class)); + } + if (from == BigInteger.class) { + return add(key, new ConverterAdapter<>(new BigIntegerConverter(), BigInteger.class)); + } + if (from == Locale.class) { + return add(key, new LocaleConverter()); + } + if (from == Period.class) { + return add(key, new ConverterAdapter<>(new Converter<Period>() { + @Override + public String toString(final Period instance) { + return instance.toString(); + } + + @Override + public Period fromString(final String text) { + return Period.parse(text); + } + }, Period.class)); + } + if (from == Duration.class) { + return add(key, new ConverterAdapter<>(new Converter<Duration>() { + @Override + public String toString(final Duration instance) { + return instance.toString(); + } + + @Override + public Duration fromString(final String text) { + return Duration.parse(text); + } + }, Duration.class)); + } + if (from == Calendar.class) { + return addCalendarConverter(key); + } + if (from == GregorianCalendar.class) { + return addGregorianCalendar(key); + } + if (from == TimeZone.class) { + return add(key, new ConverterAdapter<>(new Converter<TimeZone>() { + @Override + public String toString(final TimeZone instance) { + return instance.getID(); + } + + @Override + public TimeZone fromString(final String text) { + checkForDeprecatedTimeZone(text); + return TimeZone.getTimeZone(text); + } + }, TimeZone.class)); + } + if (from == ZoneId.class) { + return add(key, new ConverterAdapter<>(new Converter<ZoneId>() { + @Override + public String toString(final ZoneId instance) { + return instance.getId(); + } + + @Override + public ZoneId fromString(final String text) { + return ZoneId.of(text); + } + }, ZoneId.class)); + } + if (from == ZoneOffset.class) { + return add(key, new ConverterAdapter<>(new Converter<ZoneOffset>() { + @Override + public String toString(final ZoneOffset instance) { + return instance.getId(); + } + + @Override + public ZoneOffset fromString(final String text) { + return ZoneOffset.of(text); + } + }, ZoneOffset.class)); + } + if (from == SimpleTimeZone.class) { + return add(key, new ConverterAdapter<>(new Converter<SimpleTimeZone>() { + @Override + public String toString(final SimpleTimeZone instance) { + return instance.getID(); + } + + @Override + public SimpleTimeZone fromString(final String text) { + checkForDeprecatedTimeZone(text); + final TimeZone timeZone = TimeZone.getTimeZone(text); + return new SimpleTimeZone(timeZone.getRawOffset(), timeZone.getID()); + } + }, SimpleTimeZone.class)); + } + if (from == Instant.class) { + return addInstantConverter(key); + } + if (from == LocalDate.class) { + return addLocalDateConverter(key); + } + if (from == LocalTime.class) { + return add(key, new ConverterAdapter<>(new Converter<LocalTime>() { + @Override + public String toString(final LocalTime instance) { + return instance.toString(); + } + + @Override + public LocalTime fromString(final String text) { + return LocalTime.parse(text); + } + }, LocalTime.class)); + } + if (from == LocalDateTime.class) { + return addLocalDateTimeConverter(key); + } + if (from == ZonedDateTime.class) { + return addZonedDateTimeConverter(key); + } + if (from == OffsetDateTime.class) { + return addOffsetDateTimeConverter(key); + } + if (from == OffsetTime.class) { + return add(key, new ConverterAdapter<>(new Converter<OffsetTime>() { + @Override + public String toString(final OffsetTime instance) { + return instance.toString(); + } + + @Override + public OffsetTime fromString(final String text) { + return OffsetTime.parse(text); + } + }, OffsetTime.class)); + } + return null; + } + + private Adapter<?, ?> addOffsetDateTimeConverter(final AdapterKey key) { + if (dateTimeFormatter != null) { + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + return add(key, new ConverterAdapter<>(new Converter<OffsetDateTime>() { + @Override + public String toString(final OffsetDateTime instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); + } + + @Override + public OffsetDateTime fromString(final String text) { + try { + return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toOffsetDateTime(); + } catch (final DateTimeParseException dpe) { + return OffsetDateTime.parse(text); + } + } + }, OffsetDateTime.class)); + } + return add(key, new ConverterAdapter<>(new Converter<OffsetDateTime>() { + @Override + public String toString(final OffsetDateTime instance) { + return instance.toString(); + } + + @Override + public OffsetDateTime fromString(final String text) { + return OffsetDateTime.parse(text); + } + }, OffsetDateTime.class)); + } + + private Adapter<?, ?> addZonedDateTimeConverter(final AdapterKey key) { + if (dateTimeFormatter != null) { + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + return add(key, new ConverterAdapter<>(new Converter<ZonedDateTime>() { + @Override + public String toString(final ZonedDateTime instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); + } + + @Override + public ZonedDateTime fromString(final String text) { + try { + return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC); + } catch (final DateTimeParseException dpe) { + return ZonedDateTime.parse(text); + } + } + }, ZonedDateTime.class)); + } + return add(key, new ConverterAdapter<>(new Converter<ZonedDateTime>() { + @Override + public String toString(final ZonedDateTime instance) { + return instance.toString(); + } + + @Override + public ZonedDateTime fromString(final String text) { + return ZonedDateTime.parse(text); + } + }, ZonedDateTime.class)); + } + + private Adapter<?, ?> addLocalDateTimeConverter(final AdapterKey key) { + if (dateTimeFormatter != null) { + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + return add(key, new ConverterAdapter<>(new Converter<LocalDateTime>() { + + @Override + public String toString(final LocalDateTime instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(ZoneOffset.UTC), zoneIDUTC)); + } + + @Override + public LocalDateTime fromString(final String text) { + try { + return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toLocalDateTime(); + } catch (final DateTimeParseException dpe) { + return LocalDateTime.parse(text); + } + } + }, LocalDateTime.class)); + } + return add(key, new ConverterAdapter<>(new Converter<LocalDateTime>() { + @Override + public String toString(final LocalDateTime instance) { + return instance.toString(); + } + + @Override + public LocalDateTime fromString(final String text) { + return LocalDateTime.parse(text); + } + }, LocalDateTime.class)); + } + + private Adapter<?, ?> addLocalDateConverter(final AdapterKey key) { + if (dateTimeFormatter != null) { + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + return add(key, new ConverterAdapter<>(new Converter<LocalDate>() { + @Override + public String toString(final LocalDate instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(instance.toEpochDay())), zoneIDUTC)); + } + + @Override + public LocalDate fromString(final String text) { + try { + return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toLocalDate(); + } catch (final DateTimeParseException dpe) { + return LocalDate.parse(text); + } + } + }, LocalDate.class)); + } + return add(key, new ConverterAdapter<>(new Converter<LocalDate>() { + @Override + public String toString(final LocalDate instance) { + return instance.toString(); + } + + @Override + public LocalDate fromString(final String text) { + return LocalDate.parse(text); + } + }, LocalDate.class)); + } + + private Adapter<?, ?> addInstantConverter(final AdapterKey key) { + if (dateTimeFormatter != null) { + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + return add(key, new ConverterAdapter<>(new Converter<Instant>() { + @Override + public String toString(final Instant instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance, zoneIDUTC)); + } + + @Override + public Instant fromString(final String text) { + return parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toInstant(); + } + }, Instant.class)); + } + return add(key, new ConverterAdapter<>(new Converter<Instant>() { + @Override + public String toString(final Instant instance) { + return instance.toString(); + } + + @Override + public Instant fromString(final String text) { + return Instant.parse(text); + } + }, Instant.class)); + } + + private Adapter<?, ?> addGregorianCalendar(final AdapterKey key) { + if (dateTimeFormatter != null) { + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + return add(key, new ConverterAdapter<>(new Converter<GregorianCalendar>() { + @Override + public String toString(final GregorianCalendar instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId())); + } + + @Override + public GregorianCalendar fromString(final String text) { + final ZonedDateTime zonedDateTime = parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC); + final Calendar instance = GregorianCalendar.getInstance(); + instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone())); + instance.setTime(Date.from(zonedDateTime.toInstant())); + return GregorianCalendar.class.cast(instance); + } + }, GregorianCalendar.class)); + } + return add(key, new ConverterAdapter<>(new Converter<GregorianCalendar>() { + @Override + public String toString(final GregorianCalendar instance) { + return toStringCalendar(instance); + } + + @Override + public GregorianCalendar fromString(final String text) { + return fromCalendar(text, GregorianCalendar::from); + } + }, GregorianCalendar.class)); + } + + private Adapter<?, ?> addCalendarConverter(final AdapterKey key) { + if (dateTimeFormatter != null) { + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + return add(key, new ConverterAdapter<>(new Converter<Calendar>() { + @Override + public String toString(final Calendar instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId())); + } + + @Override + public Calendar fromString(final String text) { + final ZonedDateTime zonedDateTime = parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC); + final Calendar instance = Calendar.getInstance(); + instance.setTimeZone(TimeZone.getTimeZone(zonedDateTime.getZone())); + instance.setTime(Date.from(zonedDateTime.toInstant())); + return instance; + } + }, Calendar.class)); + } + return add(key, new ConverterAdapter<>(new Converter<Calendar>() { + @Override + public String toString(final Calendar instance) { + return toStringCalendar(instance); + } + + @Override + public Calendar fromString(final String text) { + return fromCalendar(text, zdt -> { + final Calendar instance = Calendar.getInstance(); + instance.clear(); + instance.setTimeZone(TimeZone.getTimeZone(zdt.getZone())); + instance.setTimeInMillis(zdt.toInstant().toEpochMilli()); + return instance; + }); + } + }, Calendar.class)); + } + + private Adapter<?, ?> addDateConverter(final AdapterKey key) { + if (useShortISO8601Format) { + return add(key, new ConverterAdapter<>(new DateConverter("yyyyMMddHHmmssZ"), Date.class)); + } + final ZoneId zoneIDUTC = ZoneId.of("UTC"); + if (dateTimeFormatter != null) { + return add(key, new ConverterAdapter<>(new Converter<Date>() { + + @Override + public String toString(final Date instance) { + return dateTimeFormatter.format(ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC)); + } + + @Override + public Date fromString(final String text) { + try { + return Date.from(parseZonedDateTime(text, dateTimeFormatter, zoneIDUTC).toInstant()); + } catch (final DateTimeParseException dpe) { + return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)); + } + } + }, Date.class)); + } + return add(key, new ConverterAdapter<>(new Converter<Date>() { + @Override + public String toString(final Date instance) { + return ZonedDateTime.ofInstant(instance.toInstant(), zoneIDUTC) + .format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + } + + @Override + public Date fromString(final String text) { + try { + return Date.from(ZonedDateTime.parse(text).toInstant()); + } catch (final DateTimeParseException dte) { + return Date.from(LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)); + } + } + }, Date.class)); + } + + private static ZonedDateTime parseZonedDateTime(final String text, final DateTimeFormatter formatter, final ZoneId defaultZone) { + final TemporalAccessor parse = formatter.parse(text); + ZoneId zone = parse.query(TemporalQueries.zone()); + if (Objects.isNull(zone)) { + zone = defaultZone; + } + final int year = parse.isSupported(YEAR) ? parse.get(YEAR) : 0; + final int month = parse.isSupported(MONTH_OF_YEAR) ? parse.get(MONTH_OF_YEAR) : 0; + final int day = parse.isSupported(DAY_OF_MONTH) ? parse.get(DAY_OF_MONTH) : 0; + final int hour = parse.isSupported(HOUR_OF_DAY) ? parse.get(HOUR_OF_DAY) : 0; + final int minute = parse.isSupported(MINUTE_OF_HOUR) ? parse.get(MINUTE_OF_HOUR) : 0; + final int second = parse.isSupported(SECOND_OF_MINUTE) ? parse.get(SECOND_OF_MINUTE) : 0; + final 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 checkForDeprecatedTimeZone(final String text) { + switch (text) { + case "CST": // really for TCK, this sucks for end users so we don't fail for all deprecated zones + throw new MapperException("Deprecated timezone: '" + text + '"'); + default: + } + } + + private String toStringCalendar(final Calendar instance) { + if (!hasTime(instance)) { // spec + final LocalDate localDate = LocalDate.of( + instance.get(Calendar.YEAR), + instance.get(Calendar.MONTH) + 1, + instance.get(Calendar.DAY_OF_MONTH)); + return localDate.toString() + + (instance.getTimeZone() != null ? + instance.getTimeZone().toZoneId().getRules() + .getOffset(Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(localDate.toEpochDay()))) : ""); + } + return ZonedDateTime.ofInstant(instance.toInstant(), instance.getTimeZone().toZoneId()) + .format(DateTimeFormatter.ISO_DATE_TIME); + } + + private boolean hasTime(final Calendar instance) { + if (!instance.isSet(Calendar.HOUR_OF_DAY)) { + return false; + } + return instance.get(Calendar.HOUR_OF_DAY) != 0 || + (instance.isSet(Calendar.MINUTE) && instance.get(Calendar.MINUTE) != 0) || + (instance.isSet(Calendar.SECOND) && instance.get(Calendar.SECOND) != 0); + } + + private <T extends Calendar> T fromCalendar(final String text, final Function<ZonedDateTime, T> calendarSupplier) { + switch (text.length()) { + case 10: { + final ZonedDateTime date = LocalDate.parse(text) + .atTime(0, 0, 0) + .atZone(ZoneId.of("UTC")); + return calendarSupplier.apply(date); + } + default: + final ZonedDateTime zonedDateTime = ZonedDateTime.parse(text); + return calendarSupplier.apply(zonedDateTime); + } + } + + private Adapter<?, ?> add(final AdapterKey key, final Adapter<?, ?> converter) { + final Adapter<?, ?> existing = putIfAbsent(key, converter); + return existing == null ? converter : existing; + } +} diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java index f0f4020..4044781 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java @@ -21,6 +21,7 @@ package org.apache.johnzon.mapper; import static java.util.Collections.emptyMap; import org.apache.johnzon.mapper.access.FieldAccessMode; +import org.apache.johnzon.mapper.map.LazyConverterMap; import org.junit.Assert; import org.junit.Test; @@ -31,7 +32,6 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; public class MapperConfigTest { @@ -156,7 +156,7 @@ public class MapperConfigTest { private MapperConfig createConfig(Map<Class<?>, ObjectConverter.Codec<?>> converter) { - return new MapperConfig(new ConcurrentHashMap<>(0), + return new MapperConfig(new LazyConverterMap(), Map.class.cast(converter), Map.class.cast(converter), -1, true, diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java index 62e9495..8e5e059 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ObjectConverterTest.java @@ -108,6 +108,15 @@ public class ObjectConverterTest { result = 31 * result + Arrays.hashCode(linkedPersonsArray); return result; } + + @Override + public String toString() { + return "Contact{" + + "linkedPersons=" + linkedPersons + + ", linkedPersonsArray=" + Arrays.toString(linkedPersonsArray) + + ", personMap=" + personMap + + '}'; + } } public static class Person { @@ -156,5 +165,13 @@ public class ObjectConverterTest { result = 31 * result + (lastName != null ? lastName.hashCode() : 0); return result; } + + @Override + public String toString() { + return "Person{" + + "firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + '}'; + } } } \ No newline at end of file diff --git a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java index 8e5264c..27fdd0d 100644 --- a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java +++ b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java @@ -21,13 +21,13 @@ package org.superbiz; import org.apache.johnzon.mapper.MapperConfig; import org.apache.johnzon.mapper.Mappings; import org.apache.johnzon.mapper.access.FieldAccessMode; +import org.apache.johnzon.mapper.map.LazyConverterMap; import org.junit.Test; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import static java.util.Collections.emptyMap; import static org.junit.Assert.assertEquals; @@ -57,7 +57,7 @@ public class ExtendMappingTest { public static class MyMappings extends Mappings { public MyMappings() { super(new MapperConfig( - new ConcurrentHashMap<>(), new HashMap<>(), new HashMap<>(), + new LazyConverterMap(), new HashMap<>(), new HashMap<>(), -1, true, true, true, false, false, false, new FieldAccessMode(false, false), StandardCharsets.UTF_8, String::compareTo, false, false, null, false, false,