diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/DbClientMapperProvider.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/DbClientMapperProvider.java new file mode 100644 index 00000000000..61f5a324e12 --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/DbClientMapperProvider.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.dbclient.common; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import io.helidon.common.mapper.Mapper; +import io.helidon.common.mapper.spi.MapperProvider; + +/** + * Java Service loader service to get database types mappers. + */ +public class DbClientMapperProvider implements MapperProvider { + + /** + * Mappers index {@code [Class, Class] -> Mapper}. + */ + private static final Map, Map, Mapper>> MAPPERS = initMappers(); + + private static Map, Map, Mapper>> initMappers() { + // All mappers index + Map, Map, Mapper>> mappers = new HashMap<>(3); + // * Mappers for java.sql.Timestamp source + Map, Mapper> sqlTimestampMap = new HashMap<>(4); + // - Mapper for java.sql.Timestamp to java.util.Date + sqlTimestampMap.put( + Date.class, + source -> source); + // - Mapper for java.sql.Timestamp to java.time.LocalDateTime + sqlTimestampMap.put( + LocalDateTime.class, + (Timestamp source) -> source != null + ? source.toLocalDateTime() + : null); + // - Mapper for java.sql.Timestamp to java.time.ZonedDateTime + sqlTimestampMap.put( + ZonedDateTime.class, + (Timestamp source) -> source != null + ? ZonedDateTime.ofInstant(source.toInstant(), ZoneOffset.UTC) + : null); + // - Mapper for java.sql.Timestamp to java.util.Calendar + sqlTimestampMap.put( + Calendar.class, + DbClientMapperProvider::sqlTimestampToGregorianCalendar); + // - Mapper for java.sql.Timestamp to java.util.GregorianCalendar + sqlTimestampMap.put( + GregorianCalendar.class, + DbClientMapperProvider::sqlTimestampToGregorianCalendar); + mappers.put( + Timestamp.class, + Collections.unmodifiableMap(sqlTimestampMap)); + // * Mappers for java.sql.Date source + Map, Mapper> sqlDateMap = new HashMap<>(2); + // - Mapper for java.sql.Date to java.util.Date + sqlDateMap.put( + Date.class, + source -> source); + // - Mapper for java.sql.Date to java.time.LocalDate + sqlDateMap.put( + LocalDate.class, + (java.sql.Date source) -> source != null + ? source.toLocalDate() + : null); + mappers.put( + java.sql.Date.class, + Collections.unmodifiableMap(sqlDateMap)); + // * Mappers for java.sql.Time source + Map, Mapper> sqlTimeMap = new HashMap<>(2); + // - Mapper for java.sql.Time to java.util.Date + sqlTimeMap.put( + Date.class, + source -> source); + // - Mapper for java.sql.Time to java.time.LocalTime + sqlTimeMap.put( + LocalTime.class, + (java.sql.Time source) -> source != null + ? source.toLocalTime() + : null); + mappers.put( + java.sql.Time.class, + Collections.unmodifiableMap(sqlTimeMap)); + return Collections.unmodifiableMap(mappers); + } + + @Override + public Optional> mapper(Class sourceClass, Class targetClass) { + Map, Mapper> targetMap = MAPPERS.get(sourceClass); + if (targetMap == null) { + return Optional.empty(); + } + Mapper mapper = targetMap.get(targetClass); + return mapper == null ? Optional.empty() : Optional.of(mapper); + } + + /** + * Maps {@link java.sql.Timestamp} to {@link java.util.Calendar} with zone set to UTC. + */ + private static GregorianCalendar sqlTimestampToGregorianCalendar(Timestamp source) { + return source != null + ? GregorianCalendar.from(ZonedDateTime.ofInstant(source.toInstant(), ZoneOffset.UTC)) + : null; + } + +} diff --git a/dbclient/common/src/main/java/module-info.java b/dbclient/common/src/main/java/module-info.java index becd92c8256..af548ffc74d 100644 --- a/dbclient/common/src/main/java/module-info.java +++ b/dbclient/common/src/main/java/module-info.java @@ -26,4 +26,6 @@ requires transitive io.helidon.dbclient; exports io.helidon.dbclient.common; + + provides io.helidon.common.mapper.spi.MapperProvider with io.helidon.dbclient.common.DbClientMapperProvider; } diff --git a/dbclient/common/src/main/resources/META-INF/services/io.helidon.common.mapper.spi.MapperProvider b/dbclient/common/src/main/resources/META-INF/services/io.helidon.common.mapper.spi.MapperProvider new file mode 100644 index 00000000000..87951883442 --- /dev/null +++ b/dbclient/common/src/main/resources/META-INF/services/io.helidon.common.mapper.spi.MapperProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +io.helidon.dbclient.common.DbClientMapperProvider diff --git a/dbclient/common/src/test/java/io/helidon/dbclient/common/mapper/MapperTest.java b/dbclient/common/src/test/java/io/helidon/dbclient/common/mapper/MapperTest.java new file mode 100644 index 00000000000..6d2f7b5668b --- /dev/null +++ b/dbclient/common/src/test/java/io/helidon/dbclient/common/mapper/MapperTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.dbclient.common.mapper; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import io.helidon.common.mapper.MapperManager; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test database types {@link Mapper}s. + */ +public class MapperTest { + + private static MapperManager mm; + + @BeforeAll + public static void init() { + mm = MapperManager.create(); + } + + /** + * Current date with time set to 00:00:00. + * + * @return current date with time set to 00:00:00 + */ + private static OffsetDateTime currentDate() { + return OffsetDateTime.now() + .with(ChronoField.HOUR_OF_DAY, 0) + .with(ChronoField.MINUTE_OF_HOUR, 0) + .with(ChronoField.SECOND_OF_MINUTE, 0) + .with(ChronoField.NANO_OF_SECOND, 0); + } + + /** + * Current time with date set to 1. 1. 1970 (epoch). + * + * @return current time with date set to 1. 1. 1970 (epoch) + */ + private static OffsetDateTime currentTime() { + return OffsetDateTime.now() + .with(ChronoField.YEAR, 1970) + .with(ChronoField.MONTH_OF_YEAR, 1) + .with(ChronoField.DAY_OF_MONTH, 1); + } + + @Test + public void testSqlDateToLocalDate() { + OffsetDateTime dt = currentDate(); + java.sql.Date source = new java.sql.Date(dt.toInstant().toEpochMilli()); + LocalDate target = mm.map(source, java.sql.Date.class, LocalDate.class); + assertThat(target.toEpochSecond(LocalTime.MIN, dt.getOffset()), is(source.getTime()/1000)); + } + + @Test + public void testSqlDateToUtilDate() { + OffsetDateTime dt = OffsetDateTime.now(); + java.sql.Date source = new java.sql.Date(dt.toInstant().toEpochMilli()); + Date target = mm.map(source, java.sql.Date.class, Date.class); + assertThat(target.getTime(), is(source.getTime())); + } + + @Test + public void testSqlTimeToLocalTime() { + OffsetDateTime dt = currentTime(); + java.sql.Time source = new java.sql.Time(dt.toInstant().toEpochMilli()); + LocalTime target = mm.map(source, java.sql.Time.class, LocalTime.class); + assertThat(target.toEpochSecond(LocalDate.EPOCH, dt.getOffset()), is(source.getTime()/1000)); + } + + @Test + public void testSqlTimeToUtilDate() { + OffsetDateTime dt = OffsetDateTime.now(); + java.sql.Time source = new java.sql.Time(dt.toInstant().toEpochMilli()); + Date target = mm.map(source, java.sql.Time.class, Date.class); + assertThat(target.getTime(), is(source.getTime())); + } + + @Test + public void testSqlTimestampToGregorianCalendar() { + Timestamp source = new Timestamp(System.currentTimeMillis()); + GregorianCalendar target = mm.map(source, Timestamp.class, GregorianCalendar.class); + assertThat(target.getTimeInMillis(), is(source.getTime())); + } + + @Test + public void testSqlTimestampToCalendar() { + Timestamp source = new Timestamp(System.currentTimeMillis()); + Calendar target = mm.map(source, Timestamp.class, Calendar.class); + assertThat(target.getTimeInMillis(), is(source.getTime())); + } + + @Test + public void testSqlTimestampToLocalDateTime() { + // Need to know time zone too. + OffsetDateTime dt = OffsetDateTime.now(); + Timestamp source = new Timestamp(dt.toInstant().toEpochMilli()); + LocalDateTime target = mm.map(source, Timestamp.class, LocalDateTime.class); + assertThat(target.atOffset(dt.getOffset()).toInstant().toEpochMilli(), is(source.getTime())); + } + + @Test + public void testSqlTimestampToUtilDate() { + Timestamp source = new Timestamp(System.currentTimeMillis()); + Date target = mm.map(source, Timestamp.class, Date.class); + assertThat(target.getTime(), is(source.getTime())); + } + + @Test + public void testSqlTimestampToZonedDateTime() { + Timestamp source = new Timestamp(System.currentTimeMillis()); + ZonedDateTime target = mm.map(source, Timestamp.class, ZonedDateTime.class); + assertThat(target.toInstant().toEpochMilli(), is(source.getTime())); + } + +}