This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-beanutils.git
The following commit(s) were added to refs/heads/master by this push: new dc3985c2 feat: updated existing converters (#49) dc3985c2 is described below commit dc3985c20a9597715f579234ae0c7203bc135ff9 Author: Seth Falco <s...@falco.fun> AuthorDate: Fri Aug 8 23:05:19 2025 +0100 feat: updated existing converters (#49) --- .../commons/beanutils2/ConvertUtilsBean.java | 6 +++ .../beanutils2/converters/CharacterConverter.java | 8 ++- .../beanutils2/converters/DateTimeConverter.java | 22 +++++++++ .../beanutils2/converters/DurationConverter.java | 2 +- .../beanutils2/converters/EnumConverter.java | 38 ++++++++++++--- .../beanutils2/converters/InstantConverter.java | 57 ++++++++++++++++++++++ .../beanutils2/converters/PeriodConverter.java | 2 +- .../converters/CharacterConverterTest.java | 14 ++---- .../beanutils2/converters/EnumConverterTest.java | 37 ++++++++++++++ ...onverterTest.java => InstantConverterTest.java} | 57 ++++++++++------------ 10 files changed, 193 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/apache/commons/beanutils2/ConvertUtilsBean.java b/src/main/java/org/apache/commons/beanutils2/ConvertUtilsBean.java index 6692bfb5..4f0b1f6e 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.net.URL; 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.EnumConverter; 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 @@ import org.apache.commons.logging.LogFactory; * <li>java.sql.Date (no default value)</li> * <li>java.sql.Time (no default value)</li> * <li>java.sql.Timestamp (no default value)</li> + * <li>java.time.Instant (no default value)</li> * <li>java.time.LocalDate (no default value)</li> * <li>java.time.LocalDateTime (no default value)</li> * <li>java.time.LocalTime (no default value)</li> @@ -500,6 +503,7 @@ public class ConvertUtilsBean { 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 @@ public class ConvertUtilsBean { * <li>{@code java.util.Date.class} - {@link DateConverter}</li> * <li>{@code java.util.Calendar.class} - {@link CalendarConverter}</li> * <li>{@code File.class} - {@link FileConverter}</li> + * <li>{@code Instant.class} - {@link InstantConverter}</li> * <li>{@code Path.class} - {@link PathConverter}</li> * <li>{@code java.sql.Date.class} - {@link SqlDateConverter}</li> * <li>{@code java.sql.Time.class} - {@link SqlTimeConverter}</li> @@ -571,6 +576,7 @@ public class ConvertUtilsBean { 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 427f0248..ae58cdf9 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 @@ public final class CharacterConverter extends AbstractConverter<Character> { @Override protected <T> T convertToType(final Class<T> 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 d6e93c2a..636f956d 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.LocalDateTime; 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 @@ public abstract class DateTimeConverter<D> extends AbstractConverter<D> { } 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 @@ public abstract class DateTimeConverter<D> extends AbstractConverter<D> { * <li>{@link java.time.LocalDate}</li> * <li>{@link java.time.LocalDateTime}</li> * <li>{@link java.time.OffsetDateTime}</li> + * <li>{@link java.time.Instant}</li> * <li>{@link java.time.ZonedDateTime}</li> * <li>{@link java.sql.Date}</li> * <li>{@link java.sql.Time}</li> @@ -247,6 +251,11 @@ public abstract class DateTimeConverter<D> extends AbstractConverter<D> { 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 @@ public abstract class DateTimeConverter<D> extends AbstractConverter<D> { 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 @@ public abstract class DateTimeConverter<D> extends AbstractConverter<D> { * <li>{@link java.sql.Date}</li> * <li>{@link java.sql.Time}</li> * <li>{@link java.sql.Timestamp}</li> + * <li>{@link java.time.Instant}</li> * </ul> * <p> * <strong>N.B.</strong> No default String conversion mechanism is provided for {@link java.util.Date} and {@link java.util.Calendar} type. @@ -624,6 +638,14 @@ public abstract class DateTimeConverter<D> extends AbstractConverter<D> { } } + 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 2943386a..fd03cd8c 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 final class DurationConverter extends AbstractConverter<Duration> { @Override protected <T> T convertToType(final Class<T> 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 bd93a145..3f98e795 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/EnumConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/EnumConverter.java @@ -59,15 +59,39 @@ public final class EnumConverter<E extends Enum<E>> extends AbstractConverter<En @Override protected <R> R convertToType(final Class<R> 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. } - for (final R candidate : constants) { - if (((Enum) candidate).name().equalsIgnoreCase(enumValue)) { - return candidate; + + final int lastHash = stringValue.lastIndexOf('#'); + final int lastDot = stringValue.lastIndexOf('.'); + + if (lastDot == -1 && lastHash == -1) { + throw new IllegalArgumentException( + "Expected fully qualified name for Enum constant like: java.time.DayOfWeek.MONDAY"); + } + + final String enumValue = stringValue.substring(Math.max(lastHash, lastDot) + 1); + final String className = stringValue.substring(0, stringValue.length() - enumValue.length() - 1); + + 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, enumValue)); + } 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 00000000..60f7105c --- /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. + * <p> + * Can be configured to either return a <i>default value</i> or throw a {@code ConversionException} if a conversion error occurs. + * </p> + * + * @since 2.0 + * @see Instant + */ +public final class InstantConverter extends DateTimeConverter<Instant> { + + /** + * Constructs a {@link Instant} <i>Converter</i> that throws a {@code ConversionException} if an error occurs. + */ + public InstantConverter() { + super(); + } + + /** + * Constructs a {@link Instant} <i>Converter</i> 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<Instant> 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 3946bf0c..8b663890 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 final class PeriodConverter extends AbstractConverter<Period> { @Override protected <T> T convertToType(final Class<T> 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 800260ab..e21b1646 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 @@ import org.junit.jupiter.api.Test; */ class CharacterConverterTest { - /** Sets Up */ + private Converter<Character> converter; + @BeforeEach public void setUp() throws Exception { + converter = new CharacterConverter(); } - /** Tear Down */ @AfterEach public void tearDown() throws Exception { + converter = null; } /** @@ -45,7 +47,6 @@ class CharacterConverterTest { */ @Test void testConvertToChar() { - final Converter<Character> converter = new CharacterConverter(); assertEquals(Character.valueOf('F'), converter.convert(Character.TYPE, "FOO"), "Wrong result"); } @@ -54,7 +55,6 @@ class CharacterConverterTest { */ @Test void testConvertToCharacter() { - final Converter<Character> 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 @@ class CharacterConverterTest { */ @Test void testConvertToCharacterNullNoDefault() { - final Converter<Character> converter = new CharacterConverter(); assertThrows(ConversionException.class, () -> converter.convert(Character.class, null)); } @@ -75,8 +74,6 @@ class CharacterConverterTest { @Test @SuppressWarnings("unchecked") // testing raw conversion void testConvertToString() { - - final Converter<Character> converter = new CharacterConverter(); @SuppressWarnings("rawtypes") final Converter raw = converter; @@ -90,10 +87,7 @@ class CharacterConverterTest { * Tries a conversion to an unsupported type. */ @Test - @SuppressWarnings("unchecked") // tests failure so allow mismatch 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 3af78814..dde2dc7b 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 @@ 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.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 @@ class EnumConverterTest { void testUnsupportedType() { assertThrows(ConversionException.class, () -> converter.convert(Integer.class, "http://www.apache.org")); } + + @Test + void testConvertTimeUnit() { + final TimeUnit expected = TimeUnit.NANOSECONDS; + final Enum actual = converter.convert(Enum.class, "java.util.concurrent.TimeUnit.NANOSECONDS"); + assertEquals(expected, actual); + } + + @Test + void testConvertDayOfWeek() { + final DayOfWeek expected = DayOfWeek.MONDAY; + final DayOfWeek actual = converter.convert(DayOfWeek.class, "java.time.DayOfWeek#MONDAY"); + assertEquals(expected, actual); + } + + @Test + void testConvertMismatchingEnumType() { + assertThrows(ConversionException.class, () -> converter.convert(TimeUnit.class, "java.time.DayOfWeek#MONDAY")); + } + + @Test + void testBrokenNamingConvention() { + assertThrows(ConversionException.class, () -> converter.convert(Enum.class, "JAVA-TIME-DAYOFWEEK#MONDAY")); + } + + @Test + void testNonEnumClasses() { + assertThrows(ConversionException.class, () -> converter.convert(Enum.class, "java.lang.String#MONDAY")); + } + + @Test + 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/EnumConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/InstantConverterTest.java similarity index 54% copy from src/test/java/org/apache/commons/beanutils2/converters/EnumConverterTest.java copy to src/test/java/org/apache/commons/beanutils2/converters/InstantConverterTest.java index 3af78814..fd1b54ce 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/EnumConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/InstantConverterTest.java @@ -6,7 +6,7 @@ * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,12 +14,14 @@ * 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; @@ -27,22 +29,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** - * Test Case for the EnumConverter class. - */ -class EnumConverterTest { + * Test Case for the {@link InstantConverter} class. +*/ +class InstantConverterTest { - public enum PizzaStatus { - ORDERED, READY, DELIVERED; - } - - private Converter<Enum<PizzaStatus>> converter; + private Converter<Instant> converter; protected Class<?> getExpectedType() { - return Enum.class; + return Period.class; } - protected Converter<Enum<PizzaStatus>> makeConverter() { - return new EnumConverter<>(); + protected Converter<Instant> makeConverter() { + return new InstantConverter(); } @BeforeEach @@ -56,27 +54,26 @@ class EnumConverterTest { } @Test - void testSimpleConversion() throws Exception { - final String[] message = { "from String", "from String", "from String", "from String", "from String", "from String", "from String", "from String", }; - - final Object[] input = { "DELIVERED", "ORDERED", "READY" }; - - final PizzaStatus[] expected = { PizzaStatus.DELIVERED, PizzaStatus.ORDERED, PizzaStatus.READY }; + void testConvertingMilliseconds() { + final Instant expected = Instant.ofEpochMilli(1596500083605L); + final Instant actual = converter.convert(Instant.class, 1596500083605L); + assertEquals(expected, actual); + } - for (int i = 0; i < expected.length; i++) { - assertEquals(expected[i], converter.convert(PizzaStatus.class, input[i]), message[i] + " to Enum"); - } + @Test + void testConvertingInstantString() { + final Instant expected = Instant.ofEpochMilli(1196676930000L); + final Instant actual = converter.convert(Instant.class, "2007-12-03T10:15:30.00Z"); + assertEquals(expected, actual); + } - for (int i = 0; i < expected.length; i++) { - assertEquals(input[i], converter.convert(String.class, expected[i]), input[i] + " to String"); - } + @Test + void testText() { + assertThrows(ConversionException.class, () -> converter.convert(Instant.class, "Hello, world!")); } - /** - * Tests a conversion to an unsupported type. - */ @Test - void testUnsupportedType() { - assertThrows(ConversionException.class, () -> converter.convert(Integer.class, "http://www.apache.org")); + void testLocalizedNumber() { + assertThrows(ConversionException.class, () -> converter.convert(Instant.class, "200,000,000,000")); } }