From 922175d610258e9d0cde4f801df4ad08ccb8866a Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Mon, 5 Feb 2024 14:00:29 +0000 Subject: [PATCH] feat: updated existing converters --- .../commons/beanutils2/ConvertUtilsBean.java | 6 ++ .../converters/CharacterConverter.java | 8 +- .../converters/DateTimeConverter.java | 22 ++++++ .../converters/DurationConverter.java | 2 +- .../beanutils2/converters/EnumConverter.java | 44 +++++++++-- .../converters/InstantConverter.java | 57 +++++++++++++ .../converters/PeriodConverter.java | 2 +- .../converters/CharacterConverterTest.java | 14 +--- .../converters/EnumConverterTest.java | 37 +++++++++ .../converters/InstantConverterTest.java | 79 +++++++++++++++++++ 10 files changed, 251 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/apache/commons/beanutils2/converters/InstantConverter.java create mode 100644 src/test/java/org/apache/commons/beanutils2/converters/InstantConverterTest.java diff --git a/src/main/java/org/apache/commons/beanutils2/ConvertUtilsBean.java b/src/main/java/org/apache/commons/beanutils2/ConvertUtilsBean.java index 9d81581d2..12d5ffc0f 100644 --- a/src/main/java/org/apache/commons/beanutils2/ConvertUtilsBean.java +++ b/src/main/java/org/apache/commons/beanutils2/ConvertUtilsBean.java @@ -30,6 +30,7 @@ import java.nio.file.Path; import java.sql.Timestamp; import java.time.Duration; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -68,6 +69,7 @@ import org.apache.commons.beanutils2.converters.FileConverter; import org.apache.commons.beanutils2.converters.FloatConverter; import org.apache.commons.beanutils2.converters.InetAddressConverter; +import org.apache.commons.beanutils2.converters.InstantConverter; import org.apache.commons.beanutils2.converters.IntegerConverter; import org.apache.commons.beanutils2.converters.LocalDateConverter; import org.apache.commons.beanutils2.converters.LocalDateTimeConverter; @@ -144,6 +146,7 @@ *
  • java.sql.Date (no default value)
  • *
  • java.sql.Time (no default value)
  • *
  • java.sql.Timestamp (no default value)
  • + *
  • java.time.Instant (no default value)
  • *
  • java.time.LocalDate (no default value)
  • *
  • java.time.LocalDateTime (no default value)
  • *
  • java.time.LocalTime (no default value)
  • @@ -500,6 +503,7 @@ private void registerArrays(final boolean throwException, final int defaultArray registerArrayConverter(Dimension.class, new DimensionConverter(), throwException, defaultArraySize); registerArrayConverter(File.class, new FileConverter(), throwException, defaultArraySize); registerArrayConverter(InetAddress.class, new InetAddressConverter(), throwException, defaultArraySize); + registerArrayConverter(Instant.class, new InstantConverter(), throwException, defaultArraySize); registerArrayConverter(Path.class, new PathConverter(), throwException, defaultArraySize); registerArrayConverter(java.sql.Date.class, new SqlDateConverter(), throwException, defaultArraySize); registerArrayConverter(java.sql.Time.class, new SqlTimeConverter(), throwException, defaultArraySize); @@ -536,6 +540,7 @@ private void registerArrays(final boolean throwException, final int defaultArray *
  • {@code java.util.Date.class} - {@link DateConverter}
  • *
  • {@code java.util.Calendar.class} - {@link CalendarConverter}
  • *
  • {@code File.class} - {@link FileConverter}
  • + *
  • {@code Instant.class} - {@link InstantConverter}
  • *
  • {@code Path.class} - {@link PathConverter}
  • *
  • {@code java.sql.Date.class} - {@link SqlDateConverter}
  • *
  • {@code java.sql.Time.class} - {@link SqlTimeConverter}
  • @@ -571,6 +576,7 @@ private void registerOther(final boolean throwException) { register(Calendar.class, throwException ? new CalendarConverter() : new CalendarConverter(null)); register(File.class, throwException ? new FileConverter() : new FileConverter(null)); register(InetAddress.class, throwException ? new InetAddressConverter() : new InetAddressConverter(null)); + register(Instant.class, throwException ? new InstantConverter() : new InstantConverter(null)); register(Path.class, throwException ? new PathConverter() : new PathConverter(null)); register(java.sql.Date.class, throwException ? new SqlDateConverter() : new SqlDateConverter(null)); register(java.sql.Time.class, throwException ? new SqlTimeConverter() : new SqlTimeConverter(null)); diff --git a/src/main/java/org/apache/commons/beanutils2/converters/CharacterConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/CharacterConverter.java index ade787569..a6c5a6b30 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/CharacterConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/CharacterConverter.java @@ -79,7 +79,13 @@ protected String convertToString(final Object value) { @Override protected T convertToType(final Class type, final Object value) throws Exception { if (Character.class.equals(type) || Character.TYPE.equals(type)) { - return type.cast(Character.valueOf(value.toString().charAt(0))); + final String stringValue = toString(value); + + if (stringValue.isEmpty()) { + throw new IllegalArgumentException("Value must not be empty"); + } + + return type.cast(Character.valueOf(stringValue.charAt(0))); } throw conversionException(type, value); diff --git a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java index e6826078e..1dd981db6 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java @@ -25,6 +25,7 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; import java.util.Calendar; import java.util.Date; @@ -135,6 +136,8 @@ protected String convertToString(final Object value) { } else if (value instanceof TemporalAccessor) { // Backstop for other TemporalAccessor implementations. date = Date.from(Instant.from((TemporalAccessor) value)); + } else if (value instanceof Instant) { + date = Date.from((Instant) value); } String result = null; @@ -169,6 +172,7 @@ protected String convertToString(final Object value) { *
  • {@link java.time.LocalDate}
  • *
  • {@link java.time.LocalDateTime}
  • *
  • {@link java.time.OffsetDateTime}
  • + *
  • {@link java.time.Instant}
  • *
  • {@link java.time.ZonedDateTime}
  • *
  • {@link java.sql.Date}
  • *
  • {@link java.sql.Time}
  • @@ -247,6 +251,11 @@ protected T convertToType(final Class targetType, final Object value) thr return toDate(targetType, date.toInstant().toEpochMilli()); } + if (value instanceof Instant) { + final Instant date = (Instant) value; + return toDate(targetType, date.toEpochMilli()); + } + // Convert all other types to String & handle final String stringValue = toTrim(value); if (stringValue.isEmpty()) { @@ -555,6 +564,10 @@ private T toDate(final Class type, final long value) { return type.cast(offsetDateTime); } + if (type.equals(Instant.class)) { + return type.cast(Instant.ofEpochMilli(value)); + } + // java.util.Calendar if (type.equals(Calendar.class)) { Calendar calendar = null; @@ -587,6 +600,7 @@ private T toDate(final Class type, final long value) { *
  • {@link java.sql.Date}
  • *
  • {@link java.sql.Time}
  • *
  • {@link java.sql.Timestamp}
  • + *
  • {@link java.time.Instant}
  • * *

    * N.B. No default String conversion mechanism is provided for {@link java.util.Date} and {@link java.util.Calendar} type. @@ -624,6 +638,14 @@ private T toDate(final Class type, final String value) { } } + if (type.equals(Instant.class)) { + try { + return type.cast(Instant.parse(value)); + } catch (final DateTimeParseException ex) { + throw new ConversionException("String must be in ISO-8601 format to create a java.time.Instant"); + } + } + final String msg = toString(getClass()) + " does not support default String to '" + toString(type) + "' conversion."; if (log().isWarnEnabled()) { log().warn(" " + msg); diff --git a/src/main/java/org/apache/commons/beanutils2/converters/DurationConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/DurationConverter.java index 75ce550ed..e3a9d4279 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/DurationConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/DurationConverter.java @@ -59,7 +59,7 @@ public DurationConverter(final Duration defaultValue) { @Override protected T convertToType(final Class type, final Object value) throws Throwable { if (Duration.class.equals(type)) { - return type.cast(Duration.parse(String.valueOf(value))); + return type.cast(Duration.parse(toString(value))); } throw conversionException(type, value); diff --git a/src/main/java/org/apache/commons/beanutils2/converters/EnumConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/EnumConverter.java index fd8ed8c93..84f3ede63 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/EnumConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/EnumConverter.java @@ -16,6 +16,9 @@ */ package org.apache.commons.beanutils2.converters; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from java.lang.Enum objects. *

    @@ -28,6 +31,11 @@ */ public final class EnumConverter> extends AbstractConverter> { + /** Matches if a given input is an enum string. */ + private static final Pattern ENUM_PATTERN = Pattern.compile( + "((?:[a-z\\d.]+)*)\\.([A-Za-z\\d]+)[#.]([A-Z\\d_]+)" + ); + /** * Constructs a java.lang.Enum Converter that throws a {@code ConversionException} if an error occurs. */ @@ -59,15 +67,37 @@ public EnumConverter(final Enum defaultValue) { @Override protected R convertToType(final Class type, final Object value) throws Throwable { if (Enum.class.isAssignableFrom(type)) { - final String enumValue = String.valueOf(value); - final R[] constants = type.getEnumConstants(); - if (constants == null) { - throw conversionException(type, value); + final String stringValue = toString(value); + + try { + return type.cast((Enum) Enum.valueOf((Class) type, stringValue)); + } catch (IllegalArgumentException ex) { + // Continue to check fully qualified name. + } + + Matcher matcher = ENUM_PATTERN.matcher(stringValue); + + if (!matcher.matches()) { + throw new IllegalArgumentException( + "Value doesn't follow Enum naming convention, expecting value like: java.time.DayOfWeek.MONDAY"); } - for (final R candidate : constants) { - if (((Enum) candidate).name().equalsIgnoreCase(enumValue)) { - return candidate; + + String className = matcher.group(1) + "." + matcher.group(2); + + try { + Class classForName = Class.forName(className); + + if (!classForName.isEnum()) { + throw new IllegalArgumentException("Value isn't an enumerated type."); } + + if (!type.isAssignableFrom(classForName)) { + throw new IllegalArgumentException("Class is not the required type."); + } + + return type.cast((Enum) Enum.valueOf(classForName, matcher.group(3))); + } catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Class \"" + className + "\" doesn't exist.", ex); } } diff --git a/src/main/java/org/apache/commons/beanutils2/converters/InstantConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/InstantConverter.java new file mode 100644 index 000000000..60f7105ce --- /dev/null +++ b/src/main/java/org/apache/commons/beanutils2/converters/InstantConverter.java @@ -0,0 +1,57 @@ +/* + * 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.commons.beanutils2.converters; + +import java.time.Instant; + +/** + * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from {@link Instant} objects. + *

    + * Can be configured to either return a default value or throw a {@code ConversionException} if a conversion error occurs. + *

    + * + * @since 2.0 + * @see Instant + */ +public final class InstantConverter extends DateTimeConverter { + + /** + * Constructs a {@link Instant} Converter that throws a {@code ConversionException} if an error occurs. + */ + public InstantConverter() { + super(); + } + + /** + * Constructs a {@link Instant} Converter that returns a default value if an error occurs. + * + * @param defaultValue The default value to be returned if the value to be converted is missing or an error occurs converting the value. + */ + public InstantConverter(final Instant defaultValue) { + super(defaultValue); + } + + /** + * Gets the default type this {@code Converter} handles. + * + * @return Default type this {@code Converter} handles. + */ + @Override + protected Class getDefaultType() { + return Instant.class; + } +} diff --git a/src/main/java/org/apache/commons/beanutils2/converters/PeriodConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/PeriodConverter.java index 76bfe25fd..8d612ade7 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/PeriodConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/PeriodConverter.java @@ -59,7 +59,7 @@ public PeriodConverter(final Period defaultValue) { @Override protected T convertToType(final Class type, final Object value) throws Throwable { if (Period.class.equals(type)) { - return type.cast(Period.parse(String.valueOf(value))); + return type.cast(Period.parse(toString(value))); } throw conversionException(type, value); diff --git a/src/test/java/org/apache/commons/beanutils2/converters/CharacterConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/CharacterConverterTest.java index c1d8b0133..570f36da3 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/CharacterConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/CharacterConverterTest.java @@ -30,14 +30,16 @@ */ public class CharacterConverterTest { - /** Sets Up */ + private Converter converter; + @BeforeEach public void setUp() throws Exception { + converter = new CharacterConverter(); } - /** Tear Down */ @AfterEach public void tearDown() throws Exception { + converter = null; } /** @@ -45,7 +47,6 @@ public void tearDown() throws Exception { */ @Test public void testConvertToChar() { - final Converter converter = new CharacterConverter(); assertEquals(Character.valueOf('F'), converter.convert(Character.TYPE, "FOO"), "Wrong result"); } @@ -54,7 +55,6 @@ public void testConvertToChar() { */ @Test public void testConvertToCharacter() { - final Converter converter = new CharacterConverter(); assertEquals(Character.valueOf('N'), converter.convert(Character.class, Character.valueOf('N')), "Character Test"); assertEquals(Character.valueOf('F'), converter.convert(Character.class, "FOO"), "String Test"); assertEquals(Character.valueOf('3'), converter.convert(Character.class, Integer.valueOf(321)), "Integer Test"); @@ -65,7 +65,6 @@ public void testConvertToCharacter() { */ @Test public void testConvertToCharacterNullNoDefault() { - final Converter converter = new CharacterConverter(); assertThrows(ConversionException.class, () -> converter.convert(Character.class, null)); } @@ -75,8 +74,6 @@ public void testConvertToCharacterNullNoDefault() { @Test @SuppressWarnings("unchecked") // testing raw conversion public void testConvertToString() { - - final Converter converter = new CharacterConverter(); @SuppressWarnings("rawtypes") final Converter raw = converter; @@ -90,10 +87,7 @@ public void testConvertToString() { * Tries a conversion to an unsupported type. */ @Test - @SuppressWarnings("unchecked") // tests failure so allow mismatch public void testConvertToUnsupportedType() { - @SuppressWarnings("rawtypes") // tests failure so allow mismatch - final Converter converter = new CharacterConverter(); assertThrows(ConversionException.class, () -> converter.convert(Integer.class, "Test")); } diff --git a/src/test/java/org/apache/commons/beanutils2/converters/EnumConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/EnumConverterTest.java index a4752afd8..28b28c580 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/EnumConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/EnumConverterTest.java @@ -20,6 +20,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.time.DayOfWeek; +import java.util.concurrent.TimeUnit; + import org.apache.commons.beanutils2.ConversionException; import org.apache.commons.beanutils2.Converter; import org.junit.jupiter.api.AfterEach; @@ -79,4 +82,38 @@ public void testSimpleConversion() throws Exception { public void testUnsupportedType() { assertThrows(ConversionException.class, () -> converter.convert(Integer.class, "http://www.apache.org")); } + + @Test + public void testConvertTimeUnit() { + final TimeUnit expected = TimeUnit.NANOSECONDS; + final Enum actual = converter.convert(Enum.class, "java.util.concurrent.TimeUnit.NANOSECONDS"); + assertEquals(expected, actual); + } + + @Test + public void testConvertDayOfWeek() { + final DayOfWeek expected = DayOfWeek.MONDAY; + final DayOfWeek actual = converter.convert(DayOfWeek.class, "java.time.DayOfWeek#MONDAY"); + assertEquals(expected, actual); + } + + @Test + public void testConvertMismatchingEnumType() { + assertThrows(ConversionException.class, () -> converter.convert(TimeUnit.class, "java.time.DayOfWeek#MONDAY")); + } + + @Test + public void testBrokenNamingConvention() { + assertThrows(ConversionException.class, () -> converter.convert(Enum.class, "JAVA-TIME-DAYOFWEEK#MONDAY")); + } + + @Test + public void testNonEnumClasses() { + assertThrows(ConversionException.class, () -> converter.convert(Enum.class, "java.lang.String#MONDAY")); + } + + @Test + public void testNonExistingClasses() { + assertThrows(ConversionException.class, () -> converter.convert(Enum.class, "java.lang.does.not.exist#MONDAY")); + } } diff --git a/src/test/java/org/apache/commons/beanutils2/converters/InstantConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/InstantConverterTest.java new file mode 100644 index 000000000..76373cdbb --- /dev/null +++ b/src/test/java/org/apache/commons/beanutils2/converters/InstantConverterTest.java @@ -0,0 +1,79 @@ +/* + * 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.commons.beanutils2.converters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.Instant; +import java.time.Period; + +import org.apache.commons.beanutils2.ConversionException; +import org.apache.commons.beanutils2.Converter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test Case for the {@link InstantConverter} class. +*/ +public class InstantConverterTest { + + private Converter converter; + + protected Class getExpectedType() { + return Period.class; + } + + protected Converter makeConverter() { + return new InstantConverter(); + } + + @BeforeEach + public void setUp() throws Exception { + converter = makeConverter(); + } + + @AfterEach + public void tearDown() throws Exception { + converter = null; + } + + @Test + public void testConvertingMilliseconds() { + final Instant expected = Instant.ofEpochMilli(1596500083605L); + final Instant actual = converter.convert(Instant.class, 1596500083605L); + assertEquals(expected, actual); + } + + @Test + public void testConvertingInstantString() { + final Instant expected = Instant.ofEpochMilli(1196676930000L); + final Instant actual = converter.convert(Instant.class, "2007-12-03T10:15:30.00Z"); + assertEquals(expected, actual); + } + + @Test + public void testText() { + assertThrows(ConversionException.class, () -> converter.convert(Instant.class, "Hello, world!")); + } + + @Test + public void testLocalizedNumber() { + assertThrows(ConversionException.class, () -> converter.convert(Instant.class, "200,000,000,000")); + } +}