From 899087cc0f62c1e74063e6b84b992173f511e0f0 Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Sat, 30 Mar 2024 13:58:13 -0400 Subject: [PATCH] Added Throwable to Map and Map to Throwable conversion Finished 100% testing on conversions. --- README.md | 4 +- pom.xml | 28 ++-- .../com/cedarsoftware/util/DateUtilities.java | 2 +- .../cedarsoftware/util/convert/Converter.java | 5 + .../util/convert/MapConversions.java | 142 +++++++++++------- .../util/convert/StringConversions.java | 7 +- .../util/convert/ThrowableConversions.java | 44 ++++++ .../util/CollectionUtilitiesTests.java | 6 +- .../util/TestGraphComparator.java | 39 +++-- .../convert/CharArrayConversionsTests.java | 7 +- .../util/convert/ConverterEverythingTest.java | 102 +++++-------- .../util/convert/ConverterTest.java | 60 ++++++-- 12 files changed, 278 insertions(+), 168 deletions(-) create mode 100644 src/main/java/com/cedarsoftware/util/convert/ThrowableConversions.java diff --git a/README.md b/README.md index 0497af25..964e492d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The classes in the`.jar`file are version 52 (`JDK 1.8`). To include in your project: ##### GradleF ``` -implementation 'com.cedarsoftware:java-util:2.4.6' +implementation 'com.cedarsoftware:java-util:2.4.7' ``` ##### Maven @@ -23,7 +23,7 @@ implementation 'com.cedarsoftware:java-util:2.4.6' com.cedarsoftware java-util - 2.4.6 + 2.4.7 ``` --- diff --git a/pom.xml b/pom.xml index 53da8aa3..a7462042 100644 --- a/pom.xml +++ b/pom.xml @@ -30,25 +30,25 @@ - 5.10.1 - 5.10.1 - 3.25.1 - 4.19.1 + 5.10.2 + 5.10.2 + 3.25.3 + 4.19.11 4.11.0 - 1.20.0 + 1.21.0 1.6.13 3.3.0 - 3.1.0 - 3.12.1 + 3.2.2 + 3.13.0 3.6.3 - 3.2.3 + 3.2.5 3.3.0 - 1.26.4 - 5.1.9 + 1.26.4 + 5.1.9 UTF-8 @@ -139,13 +139,13 @@ org.apache.felix maven-scr-plugin - ${version.plugin.felix.scr} + ${version.plugin.scr} org.apache.felix maven-bundle-plugin - ${version.plugin.felix.bundle} + ${version.plugin.bundle} true @@ -261,10 +261,10 @@ com.cedarsoftware json-io - ${version.json.io} + 4.19.11 test - + org.agrona agrona diff --git a/src/main/java/com/cedarsoftware/util/DateUtilities.java b/src/main/java/com/cedarsoftware/util/DateUtilities.java index 847fd299..4c0d7335 100644 --- a/src/main/java/com/cedarsoftware/util/DateUtilities.java +++ b/src/main/java/com/cedarsoftware/util/DateUtilities.java @@ -80,7 +80,7 @@ public final class DateUtilities { private static final Pattern allDigits = Pattern.compile("^\\d+$"); private static final String days = "monday|mon|tuesday|tues|tue|wednesday|wed|thursday|thur|thu|friday|fri|saturday|sat|sunday|sun"; // longer before shorter matters private static final String mos = "January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|July|Jul|August|Aug|September|Sept|Sep|October|Oct|November|Nov|December|Dec"; - private static final String yr = "\\d{4}"; + private static final String yr = "[+-]?\\d{4,5}\\b"; private static final String d1or2 = "\\d{1,2}"; private static final String d2 = "\\d{2}"; private static final String ord = "st|nd|rd|th"; diff --git a/src/main/java/com/cedarsoftware/util/convert/Converter.java b/src/main/java/com/cedarsoftware/util/convert/Converter.java index ed8ae5ba..1979914b 100644 --- a/src/main/java/com/cedarsoftware/util/convert/Converter.java +++ b/src/main/java/com/cedarsoftware/util/convert/Converter.java @@ -831,6 +831,10 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(String.class, Year.class), StringConversions::toYear); CONVERSION_DB.put(pair(Map.class, Year.class), MapConversions::toYear); + // Throwable conversions supported + CONVERSION_DB.put(pair(Void.class, Throwable.class), VoidConversions::toNull); + CONVERSION_DB.put(pair(Map.class, Throwable.class), MapConversions::toThrowable); + // Map conversions supported CONVERSION_DB.put(pair(Void.class, Map.class), VoidConversions::toNull); CONVERSION_DB.put(pair(Byte.class, Map.class), MapConversions::initMap); @@ -872,6 +876,7 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(Locale.class, Map.class), LocaleConversions::toMap); CONVERSION_DB.put(pair(URI.class, Map.class), UriConversions::toMap); CONVERSION_DB.put(pair(URL.class, Map.class), UrlConversions::toMap); + CONVERSION_DB.put(pair(Throwable.class, Map.class), ThrowableConversions::toMap); } public Converter(ConverterOptions options) { diff --git a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java index 2a1acbd1..061ff1d5 100644 --- a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java @@ -1,5 +1,6 @@ package com.cedarsoftware.util.convert; +import java.lang.reflect.Constructor; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; @@ -71,14 +72,12 @@ final class MapConversions { static final String SECOND = "second"; static final String SECONDS = "seconds"; static final String EPOCH_MILLIS = "epochMillis"; - static final String NANO = "nano"; static final String NANOS = "nanos"; static final String MOST_SIG_BITS = "mostSigBits"; static final String LEAST_SIG_BITS = "leastSigBits"; static final String OFFSET = "offset"; static final String OFFSET_HOUR = "offsetHour"; static final String OFFSET_MINUTE = "offsetMinute"; - static final String DATE_TIME = "dateTime"; static final String ID = "id"; static final String LANGUAGE = "language"; static final String COUNTRY = "country"; @@ -87,6 +86,10 @@ final class MapConversions { static final String URI_KEY = "URI"; static final String URL_KEY = "URL"; static final String UUID = "UUID"; + static final String CLASS = "class"; + static final String MESSAGE = "message"; + static final String CAUSE = "cause"; + static final String CAUSE_MESSAGE = "causeMessage"; static final String OPTIONAL = " (optional)"; private MapConversions() {} @@ -169,7 +172,7 @@ static AtomicBoolean toAtomicBoolean(Object from, Converter converter) { static java.sql.Date toSqlDate(Object from, Converter converter) { Map.Entry epochTime = toEpochMillis(from, converter); if (epochTime == null) { - return fromMap(from, converter, java.sql.Date.class, new String[]{EPOCH_MILLIS}, new String[]{DATE, TIME, ZONE + OPTIONAL}, new String[]{TIME, ZONE + OPTIONAL}); + return fromMap(from, converter, java.sql.Date.class, new String[]{EPOCH_MILLIS}, new String[]{TIME, ZONE + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}); } return new java.sql.Date(epochTime.getKey()); } @@ -177,45 +180,44 @@ static java.sql.Date toSqlDate(Object from, Converter converter) { static Date toDate(Object from, Converter converter) { Map.Entry epochTime = toEpochMillis(from, converter); if (epochTime == null) { - return fromMap(from, converter, Date.class, new String[]{EPOCH_MILLIS}, new String[]{DATE, TIME, ZONE + OPTIONAL}, new String[]{TIME, ZONE + OPTIONAL}); + return fromMap(from, converter, Date.class, new String[]{EPOCH_MILLIS}, new String[]{TIME, ZONE + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}); } return new Date(epochTime.getKey()); } + /** + * If the time String contains seconds resolution better than milliseconds, it will be kept. For example, + * If the time was "08.37:16.123456789" the sub-millisecond portion here will take precedence over a separate + * key/value of "nanos" mapped to a value. However, if "nanos" is specific as a key/value, and the time does + * not include nanosecond resolution, then a value > 0 specified in the "nanos" key will be incorporated into + * the resolution of the time. + */ static Timestamp toTimestamp(Object from, Converter converter) { Map map = (Map) from; Object epochMillis = map.get(EPOCH_MILLIS); + int ns = converter.convert(map.get(NANOS), int.class); // optional if (epochMillis != null) { long time = converter.convert(epochMillis, long.class); - int ns = converter.convert(map.get(NANOS), int.class); // optional Timestamp timeStamp = new Timestamp(time); - timeStamp.setNanos(ns); + if (map.containsKey(NANOS) && ns != 0) { + timeStamp.setNanos(ns); + } return timeStamp; } - + + // Map.Entry return has key of epoch-millis and value of nanos-of-second Map.Entry epochTime = toEpochMillis(from, converter); - if (epochTime == null) { - return fromMap(from, converter, Timestamp.class, new String[]{EPOCH_MILLIS, NANOS + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}, new String[]{TIME, ZONE + OPTIONAL}); + if (epochTime == null) { // specified as "value" or "_v" are not at all and will give nice exception error message. + return fromMap(from, converter, Timestamp.class, new String[]{EPOCH_MILLIS, NANOS + OPTIONAL}, new String[]{TIME, ZONE + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}); } Timestamp timestamp = new Timestamp(epochTime.getKey()); - int ns = converter.convert(epochTime.getValue(), int.class); - setNanosPreserveMillis(timestamp, ns); + if (timestamp.getTime() % 1000 == 0) { // Add nanoseconds *if* Timestamp time was only to second resolution + timestamp.setNanos(epochTime.getValue()); + } return timestamp; } - static void setNanosPreserveMillis(Timestamp timestamp, int nanoToSet) { - // Extract the current milliseconds and nanoseconds - int currentNanos = timestamp.getNanos(); - int milliPart = currentNanos / 1_000_000; // Milliseconds part of the current nanoseconds - - // Preserve the millisecond part of the current time and add the new nanoseconds - int newNanos = milliPart * 1_000_000 + (nanoToSet % 1_000_000); - - // Set the new nanoseconds value, preserving the milliseconds - timestamp.setNanos(newNanos); - } - static TimeZone toTimeZone(Object from, Converter converter) { return fromMap(from, converter, TimeZone.class, new String[]{ZONE}); } @@ -229,16 +231,17 @@ static Calendar toCalendar(Object from, Converter converter) { Object date = map.get(DATE); Object time = map.get(TIME); - Object zone = map.get(ZONE); + Object zone = map.get(ZONE); // optional + ZoneId zoneId; + if (zone != null) { + zoneId = converter.convert(zone, ZoneId.class); + } else { + zoneId = converter.getOptions().getZoneId(); + } + if (date != null && time != null) { LocalDate localDate = converter.convert(date, LocalDate.class); LocalTime localTime = converter.convert(time, LocalTime.class); - ZoneId zoneId; - if (zone != null) { - zoneId = converter.convert(zone, ZoneId.class); - } else { - zoneId = converter.getOptions().getZoneId(); - } LocalDateTime ldt = LocalDateTime.of(localDate, localTime); ZonedDateTime zdt = ldt.atZone(zoneId); Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(zoneId)); @@ -254,12 +257,6 @@ static Calendar toCalendar(Object from, Converter converter) { } if (time != null && date == null) { - ZoneId zoneId; - if (zone != null) { - zoneId = converter.convert(zone, ZoneId.class); - } else { - zoneId = converter.getOptions().getZoneId(); - } Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(zoneId)); ZonedDateTime zdt = DateUtilities.parseDate((String)time, zoneId, true); cal.setTimeInMillis(zdt.toInstant().toEpochMilli()); @@ -268,12 +265,14 @@ static Calendar toCalendar(Object from, Converter converter) { return fromMap(from, converter, Calendar.class, new String[]{EPOCH_MILLIS}, new String[]{TIME, ZONE + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}); } + // Map.Entry return has key of epoch-millis and value of nanos-of-second private static Map.Entry toEpochMillis(Object from, Converter converter) { Map map = (Map) from; Object epochMillis = map.get(EPOCH_MILLIS); + int ns = converter.convert(map.get(NANOS), int.class); // optional if (epochMillis != null) { - return new AbstractMap.SimpleImmutableEntry<>(converter.convert(epochMillis, long.class), 0); + return new AbstractMap.SimpleImmutableEntry<>(converter.convert(epochMillis, long.class), ns); } Object time = map.get(TIME); @@ -286,13 +285,13 @@ private static Map.Entry toEpochMillis(Object from, Converter con LocalTime lt = converter.convert(time, LocalTime.class); ZoneId zoneId = converter.convert(zone, ZoneId.class); ZonedDateTime zdt = ZonedDateTime.of(ld, lt, zoneId); - Instant instant = zdt.toInstant(); - return new AbstractMap.SimpleImmutableEntry<>(instant.toEpochMilli(), instant.getNano()); + return nanoRule(zdt, ns); } // Time only if (time != null && date == null && zone == null) { - return new AbstractMap.SimpleImmutableEntry<>(converter.convert(time, Date.class).getTime(), 0); + ZonedDateTime zdt = converter.convert(time, ZonedDateTime.class); + return nanoRule(zdt, ns); } // Time & Zone, no Date @@ -300,8 +299,7 @@ private static Map.Entry toEpochMillis(Object from, Converter con LocalDateTime ldt = converter.convert(time, LocalDateTime.class); ZoneId zoneId = converter.convert(zone, ZoneId.class); ZonedDateTime zdt = ZonedDateTime.of(ldt, zoneId); - Instant instant = zdt.toInstant(); - return new AbstractMap.SimpleImmutableEntry<>(instant.toEpochMilli(), instant.getNano()); + return nanoRule(zdt, ns); } // Time & Date, no zone @@ -309,13 +307,20 @@ private static Map.Entry toEpochMillis(Object from, Converter con LocalDate ld = converter.convert(date, LocalDate.class); LocalTime lt = converter.convert(time, LocalTime.class); ZonedDateTime zdt = ZonedDateTime.of(ld, lt, converter.getOptions().getZoneId()); - Instant instant = zdt.toInstant(); - return new AbstractMap.SimpleImmutableEntry<>(instant.toEpochMilli(), instant.getNano()); + return nanoRule(zdt, ns); } return null; } + private static Map.Entry nanoRule(ZonedDateTime zdt, int nanosFromMap) { + int nanos = zdt.getNano(); + if (nanos != 0) { + nanosFromMap = nanos; + } + return new AbstractMap.SimpleImmutableEntry<>(zdt.toEpochSecond() * 1000, nanosFromMap); + } + static Locale toLocale(Object from, Converter converter) { Map map = (Map) from; @@ -376,7 +381,7 @@ static LocalTime toLocalTime(Object from, Converter converter) { Object hour = map.get(HOUR); Object minute = map.get(MINUTE); Object second = map.get(SECOND); - Object nano = map.get(NANO); + Object nano = map.get(NANOS); if (hour != null && minute != null) { int h = converter.convert(hour, int.class); int m = converter.convert(minute, int.class); @@ -384,7 +389,7 @@ static LocalTime toLocalTime(Object from, Converter converter) { int n = converter.convert(nano, int.class); return LocalTime.of(h, m, s, n); } - return fromMap(from, converter, LocalTime.class, new String[]{TIME}, new String[]{HOUR, MINUTE, SECOND + OPTIONAL, NANO + OPTIONAL}); + return fromMap(from, converter, LocalTime.class, new String[]{TIME}, new String[]{HOUR, MINUTE, SECOND + OPTIONAL, NANOS + OPTIONAL}); } static OffsetTime toOffsetTime(Object from, Converter converter) { @@ -392,7 +397,7 @@ static OffsetTime toOffsetTime(Object from, Converter converter) { Object hour = map.get(HOUR); Object minute = map.get(MINUTE); Object second = map.get(SECOND); - Object nano = map.get(NANO); + Object nano = map.get(NANOS); Object oh = map.get(OFFSET_HOUR); Object om = map.get(OFFSET_MINUTE); if (hour != null && minute != null) { @@ -417,7 +422,7 @@ static OffsetTime toOffsetTime(Object from, Converter converter) { if (time != null) { return converter.convert(time, OffsetTime.class); } - return fromMap(from, converter, OffsetTime.class, new String[] {TIME}, new String[] {HOUR, MINUTE, SECOND + OPTIONAL, NANO + OPTIONAL, OFFSET_HOUR, OFFSET_MINUTE}); + return fromMap(from, converter, OffsetTime.class, new String[] {TIME}, new String[] {HOUR, MINUTE, SECOND + OPTIONAL, NANOS + OPTIONAL, OFFSET_HOUR, OFFSET_MINUTE}); } static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { @@ -475,7 +480,7 @@ static ZonedDateTime toZonedDateTime(Object from, Converter converter) { LocalDateTime localDateTime = converter.convert(time, LocalDateTime.class); return ZonedDateTime.of(localDateTime, zoneId); } - return fromMap(from, converter, ZonedDateTime.class, new String[] {EPOCH_MILLIS}, new String[] {DATE_TIME, ZONE}, new String[] {DATE, TIME, ZONE}); + return fromMap(from, converter, ZonedDateTime.class, new String[] {EPOCH_MILLIS}, new String[] {TIME, ZONE}, new String[] {DATE, TIME, ZONE}); } static Class toClass(Object from, Converter converter) { @@ -580,10 +585,45 @@ static URL toURL(Object from, Converter converter) { return fromMap(from, converter, URL.class, new String[] {URL_KEY}); } + static Throwable toThrowable(Object from, Converter converter) { + Map map = (Map) from; + try { + String className = (String) map.get(CLASS); + String message = (String) map.get(MESSAGE); + String causeClassName = (String) map.get(CAUSE); + String causeMessage = (String) map.get(CAUSE_MESSAGE); + + Class clazz = Class.forName(className); + Throwable cause = null; + + if (causeClassName != null && !causeClassName.isEmpty()) { + Class causeClass = Class.forName(causeClassName); + // Assuming the cause class has a constructor that takes a String message. + Constructor causeConstructor = causeClass.getConstructor(String.class); + cause = (Throwable) causeConstructor.newInstance(causeMessage); + } + + // Check for appropriate constructor based on whether a cause is present. + Constructor constructor; + Throwable exception; + + if (cause != null) { + constructor = clazz.getConstructor(String.class, Throwable.class); + exception = (Throwable) constructor.newInstance(message, cause); + } else { + constructor = clazz.getConstructor(String.class); + exception = (Throwable) constructor.newInstance(message); + } + + return exception; + } catch (Exception e) { + throw new IllegalArgumentException("Unable to reconstruct exception instance from map: " + map); + } + } + static URI toURI(Object from, Converter converter) { Map map = (Map) from; - String uri = null; - uri = (String) map.get(URI_KEY); + String uri = (String) map.get(URI_KEY); if (StringUtilities.hasContent(uri)) { return converter.convert(map.get(URI_KEY), URI.class); } diff --git a/src/main/java/com/cedarsoftware/util/convert/StringConversions.java b/src/main/java/com/cedarsoftware/util/convert/StringConversions.java index 4a9957d6..3f95bb23 100644 --- a/src/main/java/com/cedarsoftware/util/convert/StringConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/StringConversions.java @@ -434,7 +434,12 @@ static ZoneId toZoneId(Object from, Converter converter) { try { return ZoneId.of(str); } catch (Exception e) { - throw new IllegalArgumentException("Unknown time-zone ID: '" + str + "'", e); + TimeZone tz = TimeZone.getTimeZone(str); + if ("GMT".equals(tz.getID())) { + throw new IllegalArgumentException("Unknown time-zone ID: '" + str + "'", e); + } else { + return tz.toZoneId(); + } } } diff --git a/src/main/java/com/cedarsoftware/util/convert/ThrowableConversions.java b/src/main/java/com/cedarsoftware/util/convert/ThrowableConversions.java new file mode 100644 index 00000000..7fe68927 --- /dev/null +++ b/src/main/java/com/cedarsoftware/util/convert/ThrowableConversions.java @@ -0,0 +1,44 @@ +package com.cedarsoftware.util.convert; + +import java.util.Map; + +import com.cedarsoftware.util.CompactLinkedMap; + +import static com.cedarsoftware.util.convert.MapConversions.CAUSE; +import static com.cedarsoftware.util.convert.MapConversions.CAUSE_MESSAGE; +import static com.cedarsoftware.util.convert.MapConversions.CLASS; +import static com.cedarsoftware.util.convert.MapConversions.MESSAGE; + +/** + * @author John DeRegnaucourt (jdereg@gmail.com) + *
+ * Copyright (c) Cedar Software LLC + *

+ * 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 + *

+ * License + *

+ * 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. + */ +final class ThrowableConversions { + + private ThrowableConversions() {} + + static Map toMap(Object from, Converter converter) { + Throwable throwable = (Throwable) from; + Map target = new CompactLinkedMap<>(); + target.put(CLASS, throwable.getClass().getName()); + target.put(MESSAGE, throwable.getMessage()); + if (throwable.getCause() != null) { + target.put(CAUSE, throwable.getCause().getClass().getName()); + target.put(CAUSE_MESSAGE, throwable.getCause().getMessage()); + } + return target; + } +} \ No newline at end of file diff --git a/src/test/java/com/cedarsoftware/util/CollectionUtilitiesTests.java b/src/test/java/com/cedarsoftware/util/CollectionUtilitiesTests.java index 79fa70b3..bbc1fe6f 100644 --- a/src/test/java/com/cedarsoftware/util/CollectionUtilitiesTests.java +++ b/src/test/java/com/cedarsoftware/util/CollectionUtilitiesTests.java @@ -1,13 +1,11 @@ package com.cedarsoftware.util; -import com.cedarsoftware.util.io.MetaUtils; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.Test; + import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/com/cedarsoftware/util/TestGraphComparator.java b/src/test/java/com/cedarsoftware/util/TestGraphComparator.java index c0a89f93..2d9a16c1 100644 --- a/src/test/java/com/cedarsoftware/util/TestGraphComparator.java +++ b/src/test/java/com/cedarsoftware/util/TestGraphComparator.java @@ -17,9 +17,11 @@ import java.util.TreeMap; import java.util.TreeSet; -import com.cedarsoftware.util.io.JsonIo; -import com.cedarsoftware.util.io.ReadOptionsBuilder; -import com.cedarsoftware.util.io.WriteOptions; +import com.cedarsoftware.io.JsonIo; +import com.cedarsoftware.io.ReadOptions; +import com.cedarsoftware.io.ReadOptionsBuilder; +import com.cedarsoftware.io.WriteOptions; +import com.cedarsoftware.io.WriteOptionsBuilder; import org.junit.jupiter.api.Test; import static com.cedarsoftware.util.GraphComparator.Delta.Command.ARRAY_RESIZE; @@ -51,7 +53,6 @@ public class TestGraphComparator private static final int SET_TYPE_HASH = 1; private static final int SET_TYPE_TREE = 2; private static final int SET_TYPE_LINKED = 3; - public interface HasId { Object getId(); @@ -2173,28 +2174,26 @@ private Dude getDude(String name, int age) return dude; } - private Object clone(Object source) throws Exception - { - return JsonIo.deepCopy(source, new ReadOptionsBuilder().build(), new WriteOptions()); + private Object clone(Object source) { + + WriteOptions writeOptions = new WriteOptionsBuilder().showTypeInfoAlways().build(); + ReadOptions readOptions = new ReadOptionsBuilder().build(); + return JsonIo.deepCopy(source, readOptions, writeOptions); } private GraphComparator.ID getIdFetcher() { - return new GraphComparator.ID() - { - public Object getId(Object objectToId) + return objectToId -> { + if (objectToId instanceof HasId) { - if (objectToId instanceof HasId) - { - HasId obj = (HasId) objectToId; - return obj.getId(); - } - else if (objectToId instanceof Collection || objectToId instanceof Map) - { - return null; - } - throw new RuntimeException("Object does not support getId(): " + (objectToId != null ? objectToId.getClass().getName() : "null")); + HasId obj = (HasId) objectToId; + return obj.getId(); + } + else if (objectToId instanceof Collection || objectToId instanceof Map) + { + return null; } + throw new RuntimeException("Object does not support getId(): " + (objectToId != null ? objectToId.getClass().getName() : "null")); }; } } diff --git a/src/test/java/com/cedarsoftware/util/convert/CharArrayConversionsTests.java b/src/test/java/com/cedarsoftware/util/convert/CharArrayConversionsTests.java index dcefa904..0f471e8d 100644 --- a/src/test/java/com/cedarsoftware/util/convert/CharArrayConversionsTests.java +++ b/src/test/java/com/cedarsoftware/util/convert/CharArrayConversionsTests.java @@ -1,15 +1,12 @@ package com.cedarsoftware.util.convert; -import com.cedarsoftware.util.io.ReadOptionsBuilder; +import java.util.stream.Stream; + import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.Map; -import java.util.stream.Stream; - import static org.assertj.core.api.Assertions.assertThat; class CharArrayConversionsTests { diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index 09db991c..fe69cc63 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -54,6 +54,9 @@ import static com.cedarsoftware.util.MapUtilities.mapOf; import static com.cedarsoftware.util.convert.Converter.VALUE; import static com.cedarsoftware.util.convert.Converter.pair; +import static com.cedarsoftware.util.convert.MapConversions.CAUSE; +import static com.cedarsoftware.util.convert.MapConversions.CAUSE_MESSAGE; +import static com.cedarsoftware.util.convert.MapConversions.CLASS; import static com.cedarsoftware.util.convert.MapConversions.COUNTRY; import static com.cedarsoftware.util.convert.MapConversions.DATE; import static com.cedarsoftware.util.convert.MapConversions.DAY; @@ -63,11 +66,11 @@ import static com.cedarsoftware.util.convert.MapConversions.ID; import static com.cedarsoftware.util.convert.MapConversions.LANGUAGE; import static com.cedarsoftware.util.convert.MapConversions.LEAST_SIG_BITS; +import static com.cedarsoftware.util.convert.MapConversions.MESSAGE; import static com.cedarsoftware.util.convert.MapConversions.MINUTE; import static com.cedarsoftware.util.convert.MapConversions.MINUTES; import static com.cedarsoftware.util.convert.MapConversions.MONTH; import static com.cedarsoftware.util.convert.MapConversions.MOST_SIG_BITS; -import static com.cedarsoftware.util.convert.MapConversions.NANO; import static com.cedarsoftware.util.convert.MapConversions.NANOS; import static com.cedarsoftware.util.convert.MapConversions.OFFSET; import static com.cedarsoftware.util.convert.MapConversions.OFFSET_HOUR; @@ -105,7 +108,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// TODO: Throwable conversions need to be added for all the popular exception types // TODO: EnumSet conversions need to be added? class ConverterEverythingTest { private static final String TOKYO = "Asia/Tokyo"; @@ -206,6 +208,7 @@ public ZoneId getZoneId() { loadUrlTests(); loadUuidTests(); loadEnumTests(); + loadThrowableTests(); } /** @@ -220,6 +223,15 @@ private static void loadEnumTests() { }); } + /** + * Throwable + */ + private static void loadThrowableTests() { + TEST_DB.put(pair(Void.class, Throwable.class), new Object[][]{ + {null, null} + }); + } + /** * UUID */ @@ -425,11 +437,11 @@ private static void loadOffsetTimeTests() { {mapOf(TIME, "00:00:00+09:00"), OffsetTime.parse("00:00+09:00")}, // no reverse {mapOf(TIME, "00:00:00+09:00:00"), OffsetTime.parse("00:00+09:00")}, // no reverse {mapOf(TIME, "garbage"), new IllegalArgumentException("Unable to parse 'garbage' as an OffsetTime")}, // no reverse - {mapOf(HOUR, 1, MINUTE,30), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nano (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, - {mapOf(HOUR, 1, MINUTE,30, SECOND, 59), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nano (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, - {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANO, 123456789), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nano (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, - {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANO, 123456789, OFFSET_HOUR, -5, OFFSET_MINUTE, -30), OffsetTime.parse("01:30:59.123456789-05:30")}, - {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANO, 123456789, OFFSET_HOUR, -5, OFFSET_MINUTE, 30), new IllegalArgumentException("Offset 'hour' and 'minute' are not correct")}, + {mapOf(HOUR, 1, MINUTE,30), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nanos (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nanos (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANOS, 123456789), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nanos (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANOS, 123456789, OFFSET_HOUR, -5, OFFSET_MINUTE, -30), OffsetTime.parse("01:30:59.123456789-05:30")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANOS, 123456789, OFFSET_HOUR, -5, OFFSET_MINUTE, 30), new IllegalArgumentException("Offset 'hour' and 'minute' are not correct")}, {mapOf(VALUE, "16:20:00-05:00"), OffsetTime.parse("16:20:00-05:00") }, }); TEST_DB.put(pair(OffsetDateTime.class, OffsetTime.class), new Object[][]{ @@ -500,6 +512,10 @@ private static void loadMapTests() { TEST_DB.put(pair(Map.class, Map.class), new Object[][]{ { new HashMap<>(), new IllegalArgumentException("Unsupported conversion") } }); + TEST_DB.put(pair(Throwable.class, Map.class), new Object[][]{ + { new Throwable("divide by 0", new IllegalArgumentException("root issue")), mapOf(MESSAGE, "divide by 0", CLASS, Throwable.class.getName(), CAUSE, IllegalArgumentException.class.getName(), CAUSE_MESSAGE, "root issue")}, + { new IllegalArgumentException("null not allowed"), mapOf(MESSAGE, "null not allowed", CLASS, IllegalArgumentException.class.getName())}, + }); } /** @@ -768,18 +784,6 @@ private static void loadStringTests() { {ZonedDateTime.parse("1970-01-01T00:00:00Z"), "1970-01-01T00:00:00Z", true}, {ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z"), "1970-01-01T00:00:00.000000001Z", true}, }); - TEST_DB.put(pair(Number.class, String.class), new Object[][]{ - {(byte) 1, "1"}, - {(short) 2, "2"}, - {3, "3"}, - {4L, "4"}, - {5f, "5.0"}, - {6.0, "6.0"}, - {new AtomicInteger(7), "7"}, - {new AtomicLong(8L), "8"}, - {new BigInteger("9"), "9"}, - {new BigDecimal("10"), "10"}, - }); TEST_DB.put(pair(Map.class, String.class), new Object[][]{ {mapOf("_v", "alpha"), "alpha"}, {mapOf("value", "alpha"), "alpha"}, @@ -1093,7 +1097,7 @@ private static void loadLocalTimeTests() { {mapOf(VALUE, "23:59:59.999999999"), LocalTime.parse("23:59:59.999999999") }, {mapOf(HOUR, 23, MINUTE, 59), LocalTime.parse("23:59") }, {mapOf(HOUR, 23, MINUTE, 59, SECOND, 59), LocalTime.parse("23:59:59") }, - {mapOf(HOUR, 23, MINUTE, 59, SECOND, 59, NANO, 999999999), LocalTime.parse("23:59:59.999999999") }, + {mapOf(HOUR, 23, MINUTE, 59, SECOND, 59, NANOS, 999999999), LocalTime.parse("23:59:59.999999999") }, }); } @@ -1280,20 +1284,30 @@ private static void loadTimestampTests() { }); // No symmetry checks - because an OffsetDateTime of "2024-02-18T06:31:55.987654321+00:00" and "2024-02-18T15:31:55.987654321+09:00" are equivalent but not equals. They both describe the same Instant. TEST_DB.put(pair(Map.class, Timestamp.class), new Object[][] { - { mapOf(EPOCH_MILLIS, -1L, NANOS, 999999999, DATE, "1970-01-01", TIME, "08:59:59.999999999", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.999999999Z"), true}, // redundant DATE, TIME, and ZONE fields for reverse test + { mapOf(EPOCH_MILLIS, -1L, DATE, "1970-01-01", TIME, "08:59:59.987654321", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.999Z") }, // Epoch millis take precedence + { mapOf(EPOCH_MILLIS, -1L, NANOS, 1, DATE, "1970-01-01", TIME, "08:59:59.987654321", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.000000001Z") }, // Epoch millis and nanos take precedence + { mapOf(EPOCH_MILLIS, -1L, ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.999Z") }, // save as above + { mapOf(DATE, "1970-01-01", TIME, "08:59:59.987654321", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.987654321Z") }, // Epoch millis take precedence + { mapOf(NANOS, 123456789, DATE, "1970-01-01", TIME, "08:59:59.987654321", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.987654321Z") }, // time trumps nanos when it is better than second resolution + { mapOf(EPOCH_MILLIS, -1L, NANOS, 123456789, DATE, "1970-01-01", TIME, "08:59:59.999999999", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.123456789Z") }, // Epoch millis and nanos trump time + { mapOf(EPOCH_MILLIS, -1L, NANOS, 999000000, DATE, "1970-01-01", TIME, "08:59:59.999999999", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.999Z")}, // redundant, conflicting nanos + DATE, TIME, and ZONE fields for reverse test + { mapOf(EPOCH_MILLIS, -1L, NANOS, 888888888, DATE, "1970-01-01", TIME, "08:59:59.999999999", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.888888888Z")}, // redundant, conflicting nanos { mapOf(EPOCH_MILLIS, -1L, NANOS, 999000000, DATE, "1970-01-01", TIME, "08:59:59.999", ZONE, TOKYO_Z.toString()), new Timestamp(-1L), true}, { mapOf(EPOCH_MILLIS, 0L, NANOS, 0, DATE, "1970-01-01", TIME, "09:00", ZONE, TOKYO_Z.toString()), timestamp("1970-01-01T00:00:00Z"), true}, { mapOf(EPOCH_MILLIS, 0L, NANOS, 0, DATE, "1970-01-01", TIME, "09:00", ZONE, TOKYO_Z.toString()), new Timestamp(0L), true}, { mapOf(EPOCH_MILLIS, 0L, NANOS, 1, DATE, "1970-01-01", TIME, "09:00:00.000000001", ZONE, TOKYO_Z.toString()), timestamp("1970-01-01T00:00:00.000000001Z"), true}, { mapOf(EPOCH_MILLIS, 1L, NANOS, 1000000, DATE, "1970-01-01", TIME, "09:00:00.001", ZONE, TOKYO_Z.toString()), new Timestamp(1L), true}, { mapOf(EPOCH_MILLIS, 1710714535152L, NANOS, 152000000, DATE, "2024-03-18", TIME, "07:28:55.152", ZONE, TOKYO_Z.toString()), new Timestamp(1710714535152L), true}, - { mapOf(TIME, "2024-03-18T07:28:55.152", ZONE, TOKYO_Z.toString()), new Timestamp(1710714535152L)}, + { mapOf(TIME, "1970-01-01T00:00:00.000000001Z", NANOS, 1234), timestamp("1970-01-01T00:00:00.000000001Z")}, // fractional seconds in time, ignore "nanos" value if it exists + { mapOf(TIME, "1970-01-01T00:00:00Z", NANOS, 1234), (Supplier) () -> timestamp("1970-01-01T00:00:00.000001234Z")}, // No fractional seconds in time, use "nanos" value if it exists + { mapOf(DATE, "1970-01-01", TIME, "00:00:00.000000001", NANOS, 1234), timestamp("1970-01-01T00:00:00.000000001+09:00")}, // fractional seconds in time, ignore "nanos" value if it exists + { mapOf(DATE, "1970-01-01", TIME, "00:00:00", NANOS, 1234), (Supplier) () -> timestamp("1970-01-01T00:00:00.000001234+09:00")}, // No fractional seconds in time, use "nanos" value if it exists { mapOf(TIME, "2024-03-18T07:28:55.152000001", ZONE, TOKYO_Z.toString()), (Supplier) () -> { Timestamp ts = new Timestamp(1710714535152L); - MapConversions.setNanosPreserveMillis(ts, 1); + ts.setNanos(152000001); return ts; }}, - { mapOf("bad key", "2024-03-18T07:28:55.152", ZONE, TOKYO_Z.toString()), new IllegalArgumentException("Map to 'Timestamp' the map must include: [epochMillis, nanos (optional)], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values")}, + { mapOf("bad key", "2024-03-18T07:28:55.152", ZONE, TOKYO_Z.toString()), new IllegalArgumentException("Map to 'Timestamp' the map must include: [epochMillis, nanos (optional)], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values")}, }); } @@ -1329,6 +1343,7 @@ private static void loadZoneIdTests() { {"Z", ZoneId.of("Z"), true}, {"UTC", ZoneId.of("UTC"), true}, {"GMT", ZoneId.of("GMT"), true}, + {"EST", ZoneId.of("-05:00")}, }); TEST_DB.put(pair(Map.class, ZoneId.class), new Object[][]{ {mapOf("_v", "America/New_York"), NY_Z}, @@ -1748,7 +1763,7 @@ private static void loadSqlDateTests() { { mapOf(TIME, "1970-01-01T00:00", ZONE, "Z"), new java.sql.Date(0L)}, { mapOf(TIME, "X1970-01-01T00:00", ZONE, "Z"), new IllegalArgumentException("Issue parsing date-time, other characters present: X")}, { mapOf(TIME, "1970-01-01T00:00", ZONE, "bad zone"), new IllegalArgumentException("Unknown time-zone ID: 'bad zone'")}, - { mapOf("foo", "bar"), new IllegalArgumentException("Map to 'java.sql.Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values")}, + { mapOf("foo", "bar"), new IllegalArgumentException("Map to 'java.sql.Date' the map must include: [epochMillis], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values")}, }); } @@ -1845,7 +1860,7 @@ private static void loadDateTests() { { mapOf(TIME, "1970-01-01T00:00", ZONE, "Z"), new Date(0L)}, { mapOf(TIME, "X1970-01-01T00:00", ZONE, "Z"), new IllegalArgumentException("Issue parsing date-time, other characters present: X")}, { mapOf(TIME, "1970-01-01T00:00", ZONE, "bad zone"), new IllegalArgumentException("Unknown time-zone ID: 'bad zone'")}, - { mapOf("foo", "bar"), new IllegalArgumentException("Map to 'Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values")}, + { mapOf("foo", "bar"), new IllegalArgumentException("Map to 'Date' the map must include: [epochMillis], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values")}, }); } @@ -2241,9 +2256,6 @@ private static void loadBigIntegerTests() { {cal(0), BigInteger.ZERO, true}, {cal(1), BigInteger.valueOf(1000000), true}, }); - TEST_DB.put(pair(Number.class, BigInteger.class), new Object[][]{ - {0, BigInteger.ZERO}, - }); TEST_DB.put(pair(Map.class, BigInteger.class), new Object[][]{ {mapOf("_v", 0), BigInteger.ZERO}, {mapOf("_v", BigInteger.valueOf(0)), BigInteger.ZERO, true}, @@ -2358,13 +2370,6 @@ private static void loadCharacterTests() { {BigDecimal.valueOf(65535), (char) 65535, true}, {BigDecimal.valueOf(65536), new IllegalArgumentException("Value '65536' out of range to be converted to character")}, }); - TEST_DB.put(pair(Number.class, Character.class), new Object[][]{ - {BigDecimal.valueOf(-1), new IllegalArgumentException("Value '-1' out of range to be converted to character")}, - {BigDecimal.ZERO, (char) 0}, - {BigInteger.valueOf(1), (char) 1}, - {BigInteger.valueOf(65535), (char) 65535}, - {BigInteger.valueOf(65536), new IllegalArgumentException("Value '65536' out of range to be converted to character")}, - }); TEST_DB.put(pair(Map.class, Character.class), new Object[][]{ {mapOf("_v", -1), new IllegalArgumentException("Value '-1' out of range to be converted to character")}, {mapOf("value", 0), (char) 0}, @@ -2493,13 +2498,6 @@ private static void loadBooleanTests() { {BigDecimal.valueOf(1L), true, true}, {BigDecimal.valueOf(2L), true}, }); - TEST_DB.put(pair(Number.class, Boolean.class), new Object[][]{ - {-2, true}, - {-1L, true}, - {0.0, false}, - {1.0f, true}, - {BigInteger.valueOf(2), true}, - }); TEST_DB.put(pair(Map.class, Boolean.class), new Object[][]{ {mapOf("_v", 16), true}, {mapOf("_v", 0), false}, @@ -2640,9 +2638,6 @@ private static void loadDoubleTests() { {new BigDecimal("-9007199254740991"), -9007199254740991.0, true}, {new BigDecimal("9007199254740991"), 9007199254740991.0, true}, }); - TEST_DB.put(pair(Number.class, Double.class), new Object[][]{ - {2.5f, 2.5} - }); TEST_DB.put(pair(Map.class, Double.class), new Object[][]{ {mapOf("_v", "-1"), -1.0}, {mapOf("_v", -1.0), -1.0, true}, @@ -2756,9 +2751,6 @@ private static void loadFloatTests() { {new BigDecimal("-16777216"), -16777216f, true}, {new BigDecimal("16777216"), 16777216f, true}, }); - TEST_DB.put(pair(Number.class, Float.class), new Object[][]{ - {-2.2, -2.2f} - }); TEST_DB.put(pair(Map.class, Float.class), new Object[][]{ {mapOf("_v", "-1"), -1f}, {mapOf("_v", -1f), -1f, true}, @@ -2884,9 +2876,6 @@ private static void loadLongTests() { {new BigDecimal("-9223372036854775809"), Long.MAX_VALUE}, // wrap around {new BigDecimal("9223372036854775808"), Long.MIN_VALUE}, // wrap around }); - TEST_DB.put(pair(Number.class, Long.class), new Object[][]{ - {-2, -2L}, - }); TEST_DB.put(pair(Map.class, Long.class), new Object[][]{ {mapOf("_v", "-1"), -1L}, {mapOf("_v", -1L), -1L, true}, @@ -3105,9 +3094,6 @@ private static void loadIntegerTests() { {new BigDecimal("-2147483649"), Integer.MAX_VALUE}, // wrap around test {new BigDecimal("2147483648"), Integer.MIN_VALUE}, // wrap around test }); - TEST_DB.put(pair(Number.class, Integer.class), new Object[][]{ - {-2L, -2}, - }); TEST_DB.put(pair(Map.class, Integer.class), new Object[][]{ {mapOf("_v", "-1"), -1}, {mapOf("_v", -1), -1, true}, @@ -3262,9 +3248,6 @@ private static void loadShortTests() { {new BigDecimal("-32769"), Short.MAX_VALUE}, {new BigDecimal("32768"), Short.MIN_VALUE}, }); - TEST_DB.put(pair(Number.class, Short.class), new Object[][]{ - {-2L, (short) -2}, - }); TEST_DB.put(pair(Map.class, Short.class), new Object[][]{ {mapOf("_v", "-1"), (short) -1}, {mapOf("_v", -1), (short) -1}, @@ -3440,9 +3423,6 @@ private static void loadByteTest() { {new BigDecimal("-129"), Byte.MAX_VALUE}, {new BigDecimal("128"), Byte.MIN_VALUE}, }); - TEST_DB.put(pair(Number.class, Byte.class), new Object[][]{ - {-2L, (byte) -2}, - }); TEST_DB.put(pair(Map.class, Byte.class), new Object[][]{ {mapOf("_v", "-1"), (byte) -1}, {mapOf("_v", -1), (byte) -1}, diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java index ecb7c3f7..b98128f7 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java @@ -1,5 +1,6 @@ package com.cedarsoftware.util.convert; +import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -43,12 +44,17 @@ import static com.cedarsoftware.util.ArrayUtilities.EMPTY_BYTE_ARRAY; import static com.cedarsoftware.util.ArrayUtilities.EMPTY_CHAR_ARRAY; import static com.cedarsoftware.util.Converter.zonedDateTimeToMillis; +import static com.cedarsoftware.util.MapUtilities.mapOf; import static com.cedarsoftware.util.StringUtilities.EMPTY; import static com.cedarsoftware.util.convert.Converter.VALUE; import static com.cedarsoftware.util.convert.ConverterTest.fubar.bar; import static com.cedarsoftware.util.convert.ConverterTest.fubar.foo; +import static com.cedarsoftware.util.convert.MapConversions.CAUSE; +import static com.cedarsoftware.util.convert.MapConversions.CAUSE_MESSAGE; +import static com.cedarsoftware.util.convert.MapConversions.CLASS; import static com.cedarsoftware.util.convert.MapConversions.DATE; import static com.cedarsoftware.util.convert.MapConversions.EPOCH_MILLIS; +import static com.cedarsoftware.util.convert.MapConversions.MESSAGE; import static com.cedarsoftware.util.convert.MapConversions.TIME; import static com.cedarsoftware.util.convert.MapConversions.ZONE; import static org.assertj.core.api.Assertions.assertThat; @@ -93,12 +99,9 @@ class ConverterTest private static final LocalDateTime LDT_MILLENNIUM_LA = LocalDateTime.of(1999, 12, 31, 20, 59, 59, 959000000); private Converter converter; - private static final LocalDate LD_MILLENNIUM_NY = LocalDate.of(1999, 12, 31); private static final LocalDate LD_MILLENNIUM_TOKYO = LocalDate.of(2000, 1, 1); - private static final LocalDate LD_MILLENNIUM_CHICAGO = LocalDate.of(1999, 12, 31); - private static final LocalDate LD_2023_NY = LocalDate.of(2023, 6, 24); enum fubar @@ -106,6 +109,12 @@ enum fubar foo, bar, baz, quz } + private class GnarlyException extends RuntimeException { + public GnarlyException(int x) { + super("" + x); + } + } + @BeforeEach public void before() { // create converter with default options @@ -180,12 +189,10 @@ private static Stream paramsForFloatingPointTypes return arguments.stream(); } - private static Stream toByteParams() { return paramsForIntegerTypes(Byte.MIN_VALUE, Byte.MAX_VALUE); } - @ParameterizedTest @MethodSource("toByteParams") void toByte(Object source, Number number) @@ -2949,7 +2956,7 @@ void testMapToDate() { map.clear(); assertThatThrownBy(() -> this.converter.convert(map, Date.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Map to 'Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values"); + .hasMessageContaining("Map to 'Date' the map must include: [epochMillis], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -2972,7 +2979,7 @@ void testMapToSqlDate() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, java.sql.Date.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Map to 'java.sql.Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values"); + .hasMessageContaining("Map to 'java.sql.Date' the map must include: [epochMillis], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -2995,7 +3002,7 @@ void testMapToTimestamp() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, Timestamp.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Map to 'Timestamp' the map must include: [epochMillis, nanos (optional)], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values"); + .hasMessageContaining("Map to 'Timestamp' the map must include: [epochMillis, nanos (optional)], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -3060,7 +3067,7 @@ void testMapToZonedDateTime() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, ZonedDateTime.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Map to 'ZonedDateTime' the map must include: [epochMillis], [dateTime, zone], [date, time, zone], [value], or [_v] as keys with associated values"); + .hasMessageContaining("Map to 'ZonedDateTime' the map must include: [epochMillis], [time, zone], [date, time, zone], [value], or [_v] as keys with associated values"); } @@ -4328,6 +4335,41 @@ void testNullTypeInput() .hasMessageContaining("toType cannot be null"); } + @Test + void testMapToThrowable() + { + Map map = mapOf(MESSAGE, "divide by 0", CLASS, Throwable.class.getName(), CAUSE, IllegalArgumentException.class.getName(), CAUSE_MESSAGE, "root issue"); + Throwable expected = new Throwable("divide by 0", new IllegalArgumentException("root issue")); + Throwable actual = converter.convert(map, Throwable.class); + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(expected.getClass(), actual.getClass()); + assertEquals(expected.getCause().getClass(), actual.getCause().getClass()); + assertEquals(expected.getCause().getMessage(), actual.getCause().getMessage()); + + map = mapOf(MESSAGE, "null not allowed", CLASS, IllegalArgumentException.class.getName()); + expected = new IllegalArgumentException("null not allowed"); + actual = converter.convert(map, IllegalArgumentException.class); + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(expected.getClass(), actual.getClass()); + + map = mapOf(MESSAGE, "null not allowed", CLASS, IllegalArgumentException.class.getName(), CAUSE, IOException.class.getName(), CAUSE_MESSAGE, "port not open"); + expected = new IllegalArgumentException("null not allowed", new IOException("port not open", new IllegalAccessException("foo"))); + actual = converter.convert(map, IllegalArgumentException.class); + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(expected.getClass(), actual.getClass()); + assertEquals(expected.getCause().getClass(), actual.getCause().getClass()); + assertEquals(expected.getCause().getMessage(), actual.getCause().getMessage()); + } + + @Test + void testMapToThrowableFail() { + Map map = mapOf(MESSAGE, "5", CLASS, GnarlyException.class.getName()); + Throwable expected = new GnarlyException(5); + assertThatThrownBy(() -> converter.convert(map, Throwable.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unable to reconstruct exception instance from map"); + } + private ConverterOptions createCharsetOptions(final Charset charset) { return new ConverterOptions() { @Override