diff --git a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java index 3eff4c1f..bf8fcba6 100644 --- a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java @@ -2,7 +2,6 @@ import java.math.BigDecimal; import java.math.BigInteger; -import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.sql.Timestamp; @@ -32,7 +31,7 @@ import com.cedarsoftware.util.ArrayUtilities; import com.cedarsoftware.util.CompactLinkedMap; -import com.cedarsoftware.util.Convention; +import com.cedarsoftware.util.DateUtilities; import com.cedarsoftware.util.StringUtilities; /** @@ -65,14 +64,20 @@ final class MapConversions { static final String MONTHS = "months"; static final String DAY = "day"; static final String DAYS = "days"; + static final String HOUR = "hour"; static final String HOURS = "hours"; + static final String MINUTE = "minute"; static final String MINUTES = "minutes"; + 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"; private static final String ID = "id"; static final String LANGUAGE = "language"; @@ -82,13 +87,20 @@ final class MapConversions { static final String URI_KEY = "URI"; static final String URL_KEY = "URL"; static final String UUID = "UUID"; + static final String JAR = "jar"; + static final String AUTHORITY = "authority"; + static final String REF = "ref"; + static final String PORT = "port"; + static final String FILE = "file"; + static final String HOST = "host"; + static final String PROTOCOL = "protocol"; private MapConversions() {} public static final String KEY_VALUE_ERROR_MESSAGE = "To convert from Map to %s the map must include one of the following: %s[_v], or [value] with associated values."; static Object toUUID(Object from, Converter converter) { - Map map = (Map) from; + Map map = (Map) from; if (map.containsKey(MapConversions.UUID)) { return converter.convert(map.get(UUID), UUID.class); @@ -100,7 +112,7 @@ static Object toUUID(Object from, Converter converter) { return new UUID(most, least); } - return fromMap(from, converter, UUID.class, UUID, MOST_SIG_BITS, LEAST_SIG_BITS); + return fromMap(from, converter, UUID.class, UUID); } static Byte toByte(Object from, Converter converter) { @@ -160,23 +172,58 @@ static AtomicBoolean toAtomicBoolean(Object from, Converter converter) { } static java.sql.Date toSqlDate(Object from, Converter converter) { + Map map = (Map) from; + if (map.containsKey(EPOCH_MILLIS)) { + return fromMap(from, converter, java.sql.Date.class, EPOCH_MILLIS); + } else if (map.containsKey(TIME) && !map.containsKey(DATE)) { + return fromMap(from, converter, java.sql.Date.class, TIME); + } else if (map.containsKey(TIME) && map.containsKey(DATE)) { + LocalDate date = converter.convert(map.get(DATE), LocalDate.class); + LocalTime time = converter.convert(map.get(TIME), LocalTime.class); + ZoneId zoneId = converter.convert(map.get(ZONE), ZoneId.class); + ZonedDateTime zdt = ZonedDateTime.of(date, time, zoneId); + return new java.sql.Date(zdt.toInstant().toEpochMilli()); + } return fromMap(from, converter, java.sql.Date.class, EPOCH_MILLIS); } static Date toDate(Object from, Converter converter) { - return fromMap(from, converter, Date.class, EPOCH_MILLIS); + Map map = (Map) from; + if (map.containsKey(EPOCH_MILLIS)) { + return fromMap(from, converter, Date.class, EPOCH_MILLIS); + } else if (map.containsKey(TIME) && !map.containsKey(DATE)) { + return fromMap(from, converter, Date.class, TIME); + } else if (map.containsKey(TIME) && map.containsKey(DATE)) { + LocalDate date = converter.convert(map.get(DATE), LocalDate.class); + LocalTime time = converter.convert(map.get(TIME), LocalTime.class); + ZoneId zoneId = converter.convert(map.get(ZONE), ZoneId.class); + ZonedDateTime zdt = ZonedDateTime.of(date, time, zoneId); + return new Date(zdt.toInstant().toEpochMilli()); + } + return fromMap(map, converter, Date.class, EPOCH_MILLIS, NANOS); } static Timestamp toTimestamp(Object from, Converter converter) { - Map map = (Map) from; + Map map = (Map) from; if (map.containsKey(EPOCH_MILLIS)) { long time = converter.convert(map.get(EPOCH_MILLIS), long.class); int ns = converter.convert(map.get(NANOS), int.class); Timestamp timeStamp = new Timestamp(time); timeStamp.setNanos(ns); return timeStamp; + } else if (map.containsKey(DATE) && map.containsKey(TIME) && map.containsKey(ZONE)) { + LocalDate date = converter.convert(map.get(DATE), LocalDate.class); + LocalTime time = converter.convert(map.get(TIME), LocalTime.class); + ZoneId zoneId = converter.convert(map.get(ZONE), ZoneId.class); + ZonedDateTime zdt = ZonedDateTime.of(date, time, zoneId); + return Timestamp.from(zdt.toInstant()); + } else if (map.containsKey(TIME) && map.containsKey(NANOS)) { + long time = converter.convert(map.get(TIME), long.class); + int ns = converter.convert(map.get(NANOS), int.class); + Timestamp timeStamp = new Timestamp(time); + timeStamp.setNanos(ns); + return timeStamp; } - return fromMap(map, converter, Timestamp.class, EPOCH_MILLIS, NANOS); } @@ -185,7 +232,7 @@ static TimeZone toTimeZone(Object from, Converter converter) { } static Calendar toCalendar(Object from, Converter converter) { - Map map = (Map) from; + Map map = (Map) from; if (map.containsKey(EPOCH_MILLIS)) { return converter.convert(map.get(EPOCH_MILLIS), Calendar.class); } else if (map.containsKey(DATE) && map.containsKey(TIME)) { @@ -209,6 +256,18 @@ static Calendar toCalendar(Object from, Converter converter) { cal.set(Calendar.MILLISECOND, zdt.getNano() / 1_000_000); cal.getTime(); return cal; + } else if (map.containsKey(TIME) && !map.containsKey(DATE)) { + TimeZone timeZone; + if (map.containsKey(ZONE)) { + timeZone = converter.convert(map.get(ZONE), TimeZone.class); + } else { + timeZone = converter.getOptions().getTimeZone(); + } + Calendar cal = Calendar.getInstance(timeZone); + String time = (String) map.get(TIME); + ZonedDateTime zdt = DateUtilities.parseDate(time, converter.getOptions().getZoneId(), true); + cal.setTimeInMillis(zdt.toInstant().toEpochMilli()); + return cal; } return fromMap(from, converter, Calendar.class, DATE, TIME, ZONE); } @@ -239,15 +298,41 @@ static Locale toLocale(Object from, Converter converter) { } static LocalDate toLocalDate(Object from, Converter converter) { + Map map = (Map) from; + if (map.containsKey(YEAR) && map.containsKey(MONTH) && map.containsKey(DAY)) { + int month = converter.convert(map.get(MONTH), int.class); + int day = converter.convert(map.get(DAY), int.class); + int year = converter.convert(map.get(YEAR), int.class); + return LocalDate.of(year, month, day); + } return fromMap(from, converter, LocalDate.class, DATE); } static LocalTime toLocalTime(Object from, Converter converter) { + Map map = (Map) from; + if (map.containsKey(HOUR) && map.containsKey(MINUTE)) { + int hour = converter.convert(map.get(HOUR), int.class); + int minute = converter.convert(map.get(MINUTE), int.class); + int second = converter.convert(map.get(SECOND), int.class); + int nano = converter.convert(map.get(NANO), int.class); + return LocalTime.of(hour, minute, second, nano); + } return fromMap(from, converter, LocalTime.class, TIME); } static OffsetTime toOffsetTime(Object from, Converter converter) { - Map map = (Map) from; + Map map = (Map) from; + if (map.containsKey(HOUR) && map.containsKey(MINUTE)) { + int hour = converter.convert(map.get(HOUR), int.class); + int minute = converter.convert(map.get(MINUTE), int.class); + int second = converter.convert(map.get(SECOND), int.class); + int nano = converter.convert(map.get(NANO), int.class); + int offsetHour = converter.convert(map.get(OFFSET_HOUR), int.class); + int offsetMinute = converter.convert(map.get(OFFSET_MINUTE), int.class); + ZoneOffset zoneOffset = ZoneOffset.ofHoursMinutes(offsetHour, offsetMinute); + return OffsetTime.of(hour, minute, second, nano, zoneOffset); + } + if (map.containsKey(TIME)) { String ot = (String) map.get(TIME); try { @@ -260,18 +345,23 @@ static OffsetTime toOffsetTime(Object from, Converter converter) { } static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { - Map map = (Map) from; + Map map = (Map) from; if (map.containsKey(DATE) && map.containsKey(TIME)) { LocalDate date = converter.convert(map.get(DATE), LocalDate.class); LocalTime time = converter.convert(map.get(TIME), LocalTime.class); ZoneOffset zoneOffset = converter.convert(map.get(OFFSET), ZoneOffset.class); return OffsetDateTime.of(date, time, zoneOffset); } + if (map.containsKey(DATE_TIME) && map.containsKey(OFFSET)) { + LocalDateTime dateTime = converter.convert(map.get(DATE_TIME), LocalDateTime.class); + ZoneOffset zoneOffset = converter.convert(map.get(OFFSET), ZoneOffset.class); + return OffsetDateTime.of(dateTime, zoneOffset); + } return fromMap(from, converter, OffsetDateTime.class, DATE, TIME, OFFSET); } static LocalDateTime toLocalDateTime(Object from, Converter converter) { - Map map = (Map) from; + Map map = (Map) from; if (map.containsKey(DATE)) { LocalDate localDate = converter.convert(map.get(DATE), LocalDate.class); LocalTime localTime = map.containsKey(TIME) ? converter.convert(map.get(TIME), LocalTime.class) : LocalTime.MIDNIGHT; @@ -282,7 +372,7 @@ static LocalDateTime toLocalDateTime(Object from, Converter converter) { } static ZonedDateTime toZonedDateTime(Object from, Converter converter) { - Map map = (Map) from; + Map map = (Map) from; if (map.containsKey(ZONE) && map.containsKey(DATE_TIME)) { ZoneId zoneId = converter.convert(map.get(ZONE), ZoneId.class); LocalDateTime localDateTime = converter.convert(map.get(DATE_TIME), LocalDateTime.class); @@ -376,31 +466,88 @@ static Year toYear(Object from, Converter converter) { } static URL toURL(Object from, Converter converter) { - Map map = (Map)from; - String url = (String) map.get(URL_KEY); - if (StringUtilities.isEmpty(url)) { - throw new IllegalArgumentException("null or empty string cannot be used to create URL"); - } + Map map = (Map) from; + + String url = null; try { + url = (String) map.get(URL_KEY); + if (StringUtilities.hasContent(url)) { + return converter.convert(url, URL.class); + } + url = (String) map.get(VALUE); + if (StringUtilities.hasContent(url)) { + return converter.convert(url, URL.class); + } + url = (String) map.get(V); + if (StringUtilities.hasContent(url)) { + return converter.convert(url, URL.class); + } + + url = mapToUrlString(map); return URI.create(url).toURL(); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Unable to create URL from: " + url, e); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot convert Map to URL. Malformed URL: '" + url + "'"); } } static URI toURI(Object from, Converter converter) { - Map map = (Map)from; - String uri = (String) map.get(URI_KEY); - if (StringUtilities.isEmpty(uri)) { - throw new IllegalArgumentException("null or empty string cannot be used to create URI"); - } + Map map = (Map) from; + String uri = null; try { + uri = (String) map.get(URI_KEY); + if (StringUtilities.hasContent(uri)) { + return converter.convert(map.get(URI_KEY), URI.class); + } + uri = (String) map.get(VALUE); + if (StringUtilities.hasContent(uri)) { + return converter.convert(map.get(VALUE), URI.class); + } + uri = (String) map.get(V); + if (StringUtilities.hasContent(uri)) { + return converter.convert(map.get(V), URI.class); + } + + uri = mapToUrlString(map); return URI.create(uri); } catch (Exception e) { - throw new IllegalArgumentException("Unable to create URI from: " + uri, e); + throw new IllegalArgumentException("Cannot convert Map to URI. Malformed URI: '" + uri + "'"); } } + private static String mapToUrlString(Map map) { + StringBuilder builder = new StringBuilder(20); + String protocol = (String) map.get(PROTOCOL); + String host = (String) map.get(HOST); + String file = (String) map.get(FILE); + String authority = (String) map.get(AUTHORITY); + String ref = (String) map.get(REF); + Long port = (Long) map.get(PORT); + + builder.append(protocol); + builder.append(':'); + if (!protocol.equalsIgnoreCase(JAR)) { + builder.append("//"); + } + if (authority != null && !authority.isEmpty()) { + builder.append(authority); + } else { + if (host != null && !host.isEmpty()) { + builder.append(host); + } + if (!port.equals(-1L)) { + builder.append(":" + port); + } + } + if (file != null && !file.isEmpty()) { + builder.append(file); + } + if (ref != null && !ref.isEmpty()) { + builder.append("#" + ref); + } + + return builder.toString(); + } + static Map initMap(Object from, Converter converter) { Map map = new CompactLinkedMap<>(); map.put(V, from); @@ -408,7 +555,7 @@ static URI toURI(Object from, Converter converter) { } private static T fromMap(Object from, Converter converter, Class type, String...keys) { - Map map = asMap(from); + Map map = (Map) from; if (keys.length == 1) { String key = keys[0]; if (map.containsKey(key)) { @@ -426,10 +573,4 @@ private static T fromMap(Object from, Converter converter, Class type, St String keyText = ArrayUtilities.isEmpty(keys) ? "" : "[" + String.join(", ", keys) + "], "; throw new IllegalArgumentException(String.format(KEY_VALUE_ERROR_MESSAGE, Converter.getShortName(type), keyText)); } - - private static Map asMap(Object o) { - Convention.throwIfFalse(o instanceof Map, "from must be an instance of map"); - return (Map)o; - } - } diff --git a/src/main/java/com/cedarsoftware/util/convert/StringConversions.java b/src/main/java/com/cedarsoftware/util/convert/StringConversions.java index 297e326d..19c845d8 100644 --- a/src/main/java/com/cedarsoftware/util/convert/StringConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/StringConversions.java @@ -257,7 +257,7 @@ static URL toURL(Object from, Converter converter) { URI uri = URI.create((String) from); return uri.toURL(); } catch (Exception e) { - throw new IllegalArgumentException("Cannot convert String '" + str, e); + throw new IllegalArgumentException("Cannot convert String '" + str + "' to URL", e); } } @@ -481,7 +481,15 @@ static OffsetTime toOffsetTime(Object from, Converter converter) { try { return OffsetTime.parse(s, DateTimeFormatter.ISO_OFFSET_TIME); } catch (Exception e) { - throw new IllegalArgumentException("Unable to parse [" + s + "] as an OffsetTime"); + try { + OffsetDateTime dateTime = toOffsetDateTime(from, converter); + if (dateTime == null) { + return null; + } + return dateTime.toOffsetTime(); + } catch (Exception ex) { + throw new IllegalArgumentException("Unable to parse '" + s + "' as an OffsetTime", e); + } } } diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index 2872e98d..43cb5d6b 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -92,6 +92,8 @@ // TODO: More exception tests (make sure IllegalArgumentException is thrown, for example, not DateTimeException) // TODO: Throwable conversions need to be added for all the popular exception types // TODO: Enum and EnumSet conversions need to be added +// TODO: URL to URI, URI to URL +// TODO: MapConversions --> Var args of Object[]'s - show as 'OR' in message: [DATE, TIME], [epochMillis], [dateTime], [_V], or [VALUE] class ConverterEverythingTest { private static final String TOKYO = "Asia/Tokyo"; private static final ZoneId TOKYO_Z = ZoneId.of(TOKYO); @@ -277,7 +279,7 @@ private static void loadUrlTests() { }); TEST_DB.put(pair(Map.class, URL.class), new Object[][]{ { mapOf(URL_KEY, "https://domain.com"), toURL("https://domain.com"), true}, - { mapOf(URL_KEY, "bad uri"), new IllegalArgumentException("Illegal character in path")}, + { mapOf(URL_KEY, "bad earl"), new IllegalArgumentException("Cannot convert Map to URL. Malformed URL: 'bad earl'")}, }); } @@ -313,7 +315,7 @@ private static void loadUriTests() { }); TEST_DB.put(pair(Map.class, URI.class), new Object[][]{ { mapOf(URI_KEY, "https://domain.com"), toURI("https://domain.com"), true}, - { mapOf(URI_KEY, "bad uri"), new IllegalArgumentException("Unable to create URI from: bad uri")}, + { mapOf(URI_KEY, "bad uri"), new IllegalArgumentException("Cannot convert Map to URI. Malformed URI: 'bad uri'")}, }); } @@ -354,7 +356,7 @@ private static void loadOffsetTimeTests() { TEST_DB.put(pair(String.class, OffsetTime.class), new Object[][]{ {"10:15:30+01:00", OffsetTime.parse("10:15:30+01:00"), true}, {"10:15:30+01:00:59", OffsetTime.parse("10:15:30+01:00:59"), true}, - {"10:15:30+01:00.001", new IllegalArgumentException("Unable to parse [10:15:30+01:00.001] as an OffsetTime")}, + {"10:15:30+01:00.001", new IllegalArgumentException("Unable to parse '10:15:30+01:00.001' as an OffsetTime")}, }); TEST_DB.put(pair(Map.class, OffsetTime.class), new Object[][]{ {mapOf(TIME, "00:00+09:00"), OffsetTime.parse("00:00+09:00"), true}, diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java index 5bce25d3..ea83284f 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java @@ -3390,7 +3390,7 @@ void testStringToUUID() assertThatThrownBy(() -> this.converter.convert("00000000", UUID.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Invalid UUID string: 00000000"); + .hasMessageContaining("Unable to convert '00000000' to UUID"); } @Test @@ -3707,8 +3707,8 @@ void testUUIDToMap() UUID uuid = new UUID(1L, 2L); Map map = this.converter.convert(uuid, Map.class); assert map.size() == 1; - assertEquals(map.get(VALUE), uuid); - assert map.get(VALUE).getClass().equals(UUID.class); + assertEquals(map.get(MapConversions.UUID), uuid.toString()); + assert map.get(MapConversions.UUID).getClass().equals(String.class); } @Test