Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to date parsing out-of-the-box #124

Merged
merged 8 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion confectory-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@

<properties>
<commons-lang.version>3.12.0</commons-lang.version>
<commons-io.version>2.11.0</commons-io.version>
<commons-text.version>1.10.0</commons-text.version>
<commons-io.version>2.11.0</commons-io.version>
<jsonmerge.version>1.2.1</jsonmerge.version>
<json-path.version>2.8.0</json-path.version>
<json-smart.version>2.4.10</json-smart.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import net.obvj.confectory.ConfigurationException;
import net.obvj.confectory.internal.helper.BeanConfigurationHelper;
import net.obvj.confectory.internal.helper.ConfigurationHelper;
import net.obvj.confectory.util.ParseException;
import net.obvj.confectory.util.ParseFactory;
import net.obvj.confectory.util.Property;
import net.obvj.confectory.util.ReflectionUtils;
Expand Down Expand Up @@ -63,7 +64,7 @@
public class INIToObjectMapper<T> extends AbstractINIMapper<T> implements Mapper<T>
{
private static final String MSG_UNABLE_TO_BUILD_OBJECT = "Unable to build object of type: %s";
private static final String MSG_UNPARSABLE_PROPERTY_VALUE = "The value defined for property %s cannot be parsed as '%s'";
private static final String MSG_UNPARSABLE_PROPERTY_VALUE = "Unable to parse the value of the property %s into a field of type '%s'";

private final Class<T> targetType;

Expand Down Expand Up @@ -106,10 +107,10 @@ Object parseValue(Context context, String value)
{
return field != null ? ParseFactory.parse(field.getType(), value) : null;
}
catch (NumberFormatException exception)
catch (ParseException exception)
{
throw new ConfigurationException(exception, MSG_UNPARSABLE_PROPERTY_VALUE,
currentFieldIdentifierToString(context), field.getType());
currentFieldIdentifierToString(context), field.getType().getCanonicalName());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import net.obvj.confectory.ConfigurationException;
import net.obvj.confectory.internal.helper.BeanConfigurationHelper;
import net.obvj.confectory.internal.helper.ConfigurationHelper;
import net.obvj.confectory.util.ParseException;
import net.obvj.confectory.util.ParseFactory;
import net.obvj.confectory.util.Property;
import net.obvj.confectory.util.ReflectionUtils;
Expand Down Expand Up @@ -64,6 +65,8 @@
*/
public class PropertiesToObjectMapper<T> implements Mapper<T>
{
private static final String MSG_UNABLE_TO_PARSE_PROPERTY = "Unable to parse the value of the property '%s' into a field of type '%s'";

private final Class<T> targetType;

/**
Expand Down Expand Up @@ -128,8 +131,16 @@ private void writeField(T targetObject, Field field, Properties properties) thro
if (propertyValue != null)
{
Class<?> fieldType = field.getType();
Object parsedValue = ParseFactory.parse(fieldType, propertyValue);
FieldUtils.writeDeclaredField(targetObject, field.getName(), parsedValue, true);
try
{
Object parsedValue = ParseFactory.parse(fieldType, propertyValue);
FieldUtils.writeDeclaredField(targetObject, field.getName(), parsedValue, true);
}
catch (ParseException exception)
{
throw new ConfigurationException(exception, MSG_UNABLE_TO_PARSE_PROPERTY,
propertyKey, fieldType.getCanonicalName());
}
}
// Do nothing if the property is not found
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2023 obvj.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.obvj.confectory.util;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.TemporalAccessor;
import java.util.Date;

/**
* Common methods for working with dates.
*
* @author oswaldo.bapvic.jr
* @since 2.5.0
*/
public class DateUtils
{

private static final DateTimeFormatter RFC_3339_DATETIME_FORMATTER = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.optionalStart().appendLiteral('T').optionalEnd()
.optionalStart().appendLiteral(' ').optionalEnd()
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.optionalStart().appendOffset("+HH:MM", "Z").optionalEnd()
.toFormatter();

/**
* Private constructor to hide the public, implicit one.
*/
private DateUtils()
{
throw new UnsupportedOperationException("Instantiation not allowed");
}

/**
* Parses the given date-time representation string based in RFC-3339 format, producing a
* {@link Date}.
* <p>
* Either of the following examples are acceptable:
* <ul>
* <li>{@code "2019-09-07T13:21:59Z"}</li>
* <li>{@code "2020-10-08 13:22:58.654Z"}</li>
* <li>{@code "2021-11-09T10:23:57.321+00:00"}</li>
* <li>{@code "2022-12-10 10:24:56.987654321-03:00"}</li>
* </ul>
*
* @param string the string to parse; not null
* @return the parsed {@link Date}; not null
*/
public static Date parseDateRfc3339(String string)
{
Instant instant = parseInstantRfc3339(string);
return Date.from(instant);
}

/**
* Parses the given date-time representation string based in RFC-3339 format, producing a
* {@link Date}.
* <p>
* Either of the following examples are acceptable:
* <ul>
* <li>{@code "2019-09-07T13:21:59.987Z"}</li>
* <li>{@code "2020-10-08 13:22:58.654Z"}</li>
* <li>{@code "2021-11-09T10:23:57.321+00:00"}</li>
* <li>{@code "2022-12-10 10:24:56.987654321-03:00"}</li>
* </ul>
*
* @param string the string to parse; not null
* @return the parsed {@link Instant}; not null
*/
public static Instant parseInstantRfc3339(String string)
{
TemporalAccessor temporalAccessor = RFC_3339_DATETIME_FORMATTER.parse(string);
return Instant.from(temporalAccessor);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2023 obvj.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.obvj.confectory.util;

/**
* A specialized runtime exception that signals that an error has been reached
* unexpectedly while parsing.
*
* @author oswaldo.bapvic.jr (Oswaldo Junior)
* @since 2.5.0
*/
public class ParseException extends RuntimeException
{
private static final long serialVersionUID = -4528618103182440263L;

/**
* Constructs a new exception with the specified detail message. A detail message is a
* String that describes this particular exception.
*
* @param message the detailed message, which is saved for later retrieval by the
* {@link Throwable#getMessage()} method
* @param args arguments to the message format, as in
* {@link String#format(String, Object...)}
*/
public ParseException(String message, Object... args)
{
super(String.format(message, args));
}

/**
* Constructs a new exception with the specified detail message and cause.
*
* @param cause the cause, which is saved for later retrieval by the
* {@link Throwable#getCause()} method. (A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown)
* @param message the detail message, which is saved for later retrieval by the
* {@link Throwable#getMessage()} method
* @param args arguments to the message format, as in
* {@link String#format(String, Object...)}
*/
public ParseException(Throwable cause, String message, Object... args)
{
super(String.format(message, args), cause);
}

/**
* Constructs a new exception with the specified cause. This constructor is useful for
* exceptions that are wrappers for other throwables.
*
* @param cause the cause (which is saved for later retrieval by the
* {@link Throwable#getCause()} method). A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown.
*/
public ParseException(Throwable cause)
{
super(cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,80 @@

package net.obvj.confectory.util;

import java.sql.Timestamp;
import java.time.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import org.apache.commons.lang3.ClassUtils;

/**
* A class that contains built-in parsers from string into common object types, typically
* for Reflection purposes.
* <p>
* It supports all of the primitive types (including their wrappers) as well as the
* following object types:
* <p>
* <b><i>Since 2.5.0:</i></b>
* <ul>
* <li>{@code java.sql.Date} such as {@code "2007-12-03"}</li>
* <li>{@code java.sql.Timestamp} such as {@code "2007-12-03 10:15:30.998"}</li>
* <li>{@code java.time.DayOfWeek} such as {@code "FRIDAY"}</li>
* <li>{@code java.time.Duration} such as {@code "PT15M"} (15 minutes)</li>
* <li>{@code java.time.Instant} such as {@code "2007-12-03T13:15:30Z"}</li>
* <li>{@code java.time.LocalDate} such as {@code "2007-12-03"}</li>
* <li>{@code java.time.LocalDateTime} such as {@code "2007-12-03T10:15:30"}</li>
* <li>{@code java.time.Month} such as {@code "DECEMBER"}</li>
* <li>{@code java.time.OffsetDateTime} such as {@code "2007-12-03T10:15:30-03:00"}</li>
* <li>{@code java.time.ZonedDateTime} such as {@code "2007-12-03T10:15:30-03:00[America/Sao_Paulo]"}</li>
* <li>{@code java.util.Date} such as {@code "2007-12-03T10:15:30+01:00"} (accepting valid
* date-time representations in RFC 3339 formats)</li>
* </ul>
*
* @author oswaldo.bapvic.jr
* @since 1.2.0
*/
public class ParseFactory
{
private static final Map<Class<?>, Function<String, ?>> factory = new HashMap<>();
private static final Map<Class<?>, Function<String, ?>> PARSERS = new HashMap<>();

static
{
factory.put(Boolean.class, Boolean::valueOf);
factory.put(Byte.class, Byte::valueOf);
factory.put(Short.class, Short::valueOf);
factory.put(Integer.class, Integer::valueOf);
factory.put(Long.class, Long::valueOf);
factory.put(Float.class, Float::valueOf);
factory.put(Double.class, Double::valueOf);
factory.put(Character.class, string -> string.isEmpty() ? 0 : Character.valueOf(string.charAt(0)));
factory.put(String.class, Function.identity());
PARSERS.put(Boolean.class, Boolean::valueOf);
PARSERS.put(Byte.class, Byte::valueOf);
PARSERS.put(Short.class, Short::valueOf);
PARSERS.put(Integer.class, Integer::valueOf);
PARSERS.put(Long.class, Long::valueOf);
PARSERS.put(Float.class, Float::valueOf);
PARSERS.put(Double.class, Double::valueOf);
PARSERS.put(Character.class, string -> string.isEmpty() ? 0 : Character.valueOf(string.charAt(0)));
PARSERS.put(String.class, Function.identity());

// Legacy java.util.Date may accept the either of the formats
// "2007-12-03T10:15:30+01:00", or "2007-12-03T09:15:30Z"
PARSERS.put(java.util.Date.class, DateUtils::parseDateRfc3339);

// java.sql.Date, such as: "2007-2-28" or "2005-12-1"
PARSERS.put(java.sql.Date.class, java.sql.Date::valueOf);
// java.sql.Timestamp, such as: "2007-12-03 09:15:30.99"
PARSERS.put(Timestamp.class, Timestamp::valueOf);

// LocalDate, such as: "2007-12-03"
PARSERS.put(LocalDate.class, LocalDate::parse);
// LocalDateTime, such as: "2007-12-03T10:15:30"
PARSERS.put(LocalDateTime.class, LocalDateTime::parse);
// OffsetDateTime, such as: "2007-12-03T10:15:30+01:00"
PARSERS.put(OffsetDateTime.class, OffsetDateTime::parse);
// ZonedDateTime, such as: "2007-12-03T10:15:30+01:00[Europe/Paris]"
PARSERS.put(ZonedDateTime.class, ZonedDateTime::parse);
// Instant, such as: "2007-12-03T10:15:30.00Z"
PARSERS.put(Instant.class, Instant::parse);
// Duration, such as: "PT15M" (15 minutes)
PARSERS.put(Duration.class, Duration::parse);

PARSERS.put(Month.class, Month::valueOf);
PARSERS.put(DayOfWeek.class, DayOfWeek::valueOf);
}

/**
Expand All @@ -63,28 +108,40 @@ private ParseFactory()
* @param string the string to be parsed
* @return an object containing the result of the parsing of the specified string into the
* specified type
* @throws UnsupportedOperationException if the requested target type is not supported
* @throws UnsupportedOperationException if the specified type is not supported
* @throws ParseException if an error is encountered while parsing
*/
@SuppressWarnings("unchecked")
public static <T> T parse(Class<T> type, String string)
{
Class<?> objectType = ClassUtils.primitiveToWrapper(type);
Function<String, ?> function = getFunction(objectType)
.orElseThrow(() -> new UnsupportedOperationException("Unsupported type: " + type));
Object object = function.apply(string);
return (T) object;
Function<String, ?> parser = getParser(objectType);
try
{
Object object = parser.apply(string);
return (T) object;
}
catch (Exception exception)
{
throw new ParseException(exception, "Unparsable %s: \"%s\"", type.getCanonicalName(), string);
}
}

/**
* Returns an {@link Optional} possibly containing a {@link Function} to parse the
* specified target type.
* Returns a {@link Function} to parse the specified type.
*
* @param type the target type
* @return the {@link Function} to be applied for the specified type, or
* {@link Optional#empty()} if the specified type is not supported
* @return the {@link Function} to be applied for the specified type; not null
* @throws UnsupportedOperationException if the specified type is not supported
*/
private static Optional<Function<String, ?>> getFunction(Class<?> type)
private static Function<String, ?> getParser(Class<?> type)
{
return Optional.ofNullable(factory.get(type));
Function<String, ?> parser = PARSERS.get(type);
if (parser == null)
{
throw new UnsupportedOperationException("Unsupported type: " + type);
}
return parser;
}

}
Loading