Skip to content

Commit

Permalink
Support custom type conversion from INI to object (#132)
Browse files Browse the repository at this point in the history
* Support custom type conversion from INI to object

* Making parameters final

* Adding JUnits
  • Loading branch information
oswaldobapvicjr authored Jun 8, 2023
1 parent 90ec525 commit 7627192
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Optional;

import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

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.TypeFactory;
import net.obvj.confectory.util.Property;
import net.obvj.confectory.util.ReflectionUtils;
import net.obvj.confectory.util.*;

/**
* A specialized {@code Mapper} that loads the contents of a valid INI {@code Source}
Expand Down Expand Up @@ -63,6 +59,9 @@
*/
public class INIToObjectMapper<T> extends AbstractINIMapper<T> implements Mapper<T>
{
private static final String DELIMITER_LEFT = "['";
private static final String DELIMITER_RIGHT = "']";

private static final String MSG_UNABLE_TO_BUILD_OBJECT = "Unable to build object of type: %s";
private static final String MSG_UNPARSABLE_PROPERTY_VALUE = "Unable to parse the value of the property %s into a field of type '%s'";

Expand Down Expand Up @@ -102,12 +101,13 @@ Object newObject(Context context)
@Override
Object parseValue(Context context, String value)
{
Field field = findField(getCurrentType(context), context.currentKey);
Class<?> currentType = getCurrentType(context);
Field field = PropertyUtils.findFieldByPropertyKeyOrName(currentType, context.currentKey);
try
{
return field != null ? TypeFactory.parse(field.getType(), value) : null;
return field != null ? PropertyUtils.parseValue(value, field) : null;
}
catch (ParseException exception)
catch (ParseException | ReflectiveOperationException exception)
{
throw new ConfigurationException(exception, MSG_UNPARSABLE_PROPERTY_VALUE,
currentFieldIdentifierToString(context), field.getType().getCanonicalName());
Expand All @@ -117,7 +117,7 @@ Object parseValue(Context context, String value)
@Override
void put(Object target, String name, Object value)
{
Field field = findField(target.getClass(), name);
Field field = PropertyUtils.findFieldByPropertyKeyOrName(target.getClass(), name);
if (field != null && !ReflectionUtils.isTransient(field))
{
try
Expand All @@ -142,61 +142,10 @@ private Class<?> getCurrentType(Context context)
{
return targetType;
}
Field field = findField(targetType, context.currentSectionName);
Field field = PropertyUtils.findFieldByPropertyKeyOrName(targetType, context.currentSectionName);
return field != null ? field.getType() : null;
}

/**
* Find a field matching any of the following criteria in the specified type, or
* {@code null} if no match found:
* <ul>
* <li>the field is marked with the {@code @}{@link Property} annotation, and the
* annotation defines a custom name equal to the one specified in the parameter; or</li>
* <li>the field name is equal to the specified {@code name} parameter</li>
* </ul>
*
* @param type the class to reflect; not null
* @param name the field name to obtain; not null
* @return the {@link Field} object; can be null
*/
private static Field findField(Class<?> type, String name)
{
return findFieldByAnnotation(type, name).orElseGet(() -> FieldUtils.getField(type, name, true));
}

/**
* Find a field matching the following criteria in the specified type, or {@code null} if
* no match found:
* <ul>
* <li>the field is marked with the {@code @}{@link Property} annotation, and the
* annotation defines a custom name equal to the one specified in the parameter</li>
* </ul>
*
* @param type the class to reflect; not null
* @param name the field name to obtain; not null
* @return an Optional possibly containing a {@link Field} object; can be empty
*/
private static Optional<Field> findFieldByAnnotation(Class<?> type, String name)
{
return FieldUtils.getFieldsListWithAnnotation(type, Property.class).stream()
.filter(field -> isFieldAnnotated(field, name)).findAny();
}

/**
* Returns {@code true} if the field is annotated with a Property annotation which value
* matches the specified name.
*
* @param field the field to be checked; not null
* @param name the name to be tested; not null
* @return {@code true} if the field is annotated with a Property annotation which value
* matches the specified name
*/
private static boolean isFieldAnnotated(Field field, String name)
{
Property property = field.getDeclaredAnnotation(Property.class);
return property != null && name.equals(property.value());
}

/**
* @return a string representing the current field for exception/troubleshooting purposes
*/
Expand All @@ -205,9 +154,9 @@ private String currentFieldIdentifierToString(Context context)
StringBuilder builder = new StringBuilder();
if (context.currentSectionName != null)
{
builder.append("['").append(context.currentSectionName).append("']");
builder.append(DELIMITER_LEFT).append(context.currentSectionName).append(DELIMITER_RIGHT);
}
builder.append("['").append(context.currentKey).append("']");
builder.append(DELIMITER_LEFT).append(context.currentKey).append(DELIMITER_RIGHT);
return builder.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,15 @@ private void writeField(T targetObject, Field field, Properties properties)
{
return; // Ignore transient fields
}
Property anootation = getPropertyAnnotation(field);
String propertyKey = PropertyUtils.getPropertyOrFieldName(anootation, field);
Property annotation = getPropertyAnnotation(field);
String propertyKey = PropertyUtils.getPropertyKeyOrFieldName(annotation, field);
String propertyValue = properties.getProperty(propertyKey);
if (propertyValue != null)
{
Class<?> fieldType = field.getType();
try
{
Object parsedValue = PropertyUtils.parseValue(propertyValue, fieldType, anootation);
Object parsedValue = PropertyUtils.parseValue(propertyValue, fieldType, annotation);
FieldUtils.writeDeclaredField(targetObject, field.getName(), parsedValue, true);
}
catch (ParseException exception)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private DateUtils()
* @param string the string to parse; not null
* @return the parsed {@link Date}; not null
*/
public static Date parseDateRfc3339(String string)
public static Date parseDateRfc3339(final String string)
{
Instant instant = parseInstantRfc3339(string);
return Date.from(instant);
Expand All @@ -83,7 +83,7 @@ public static Date parseDateRfc3339(String string)
* @param string the string to parse; not null
* @return the parsed {@link Instant}; not null
*/
public static Instant parseInstantRfc3339(String string)
public static Instant parseInstantRfc3339(final String string)
{
TemporalAccessor temporalAccessor = RFC_3339_DATETIME_FORMATTER.parse(string);
return Instant.from(temporalAccessor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@

package net.obvj.confectory.util;

import static net.obvj.confectory.util.StringUtils.*;

import java.lang.reflect.Field;
import java.util.Optional;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

/**
* Common methods for working with the {@link Property} annotation.
Expand All @@ -36,8 +40,8 @@ private PropertyUtils()
}

/**
* Returns either the field name, or the value specified in the {@code @}{@link Property}
* annotation (if not null).
* Returns either the property key specified in the {@code @}{@link Property} annotation
* or, if not present, the field name.
*
* @param property the {@link Property} annotation to be evaluated (null is allowed)
* @param field the {@link Field} to be evaluated (not null)
Expand All @@ -47,17 +51,97 @@ private PropertyUtils()
*
* @throws NullPointerException if the field is null
*/
public static String getPropertyOrFieldName(Property property, Field field)
public static String getPropertyKeyOrFieldName(final Property property, final Field field)
{
if (property != null)
{
String key = StringUtils.defaultIfEmpty(property.value(), property.key());
if (StringUtils.isNotEmpty(key))
{
return key;
}
}
return field.getName();
return defaultIfEmpty(getPropertyKey(property), field::getName);
}

/**
* Safely returns the property key defined in the {@code @}{@link Property} annotation, or
* an empty string if the annotation is null.
*
* @param property the {@link Property} annotation to be evaluated (null is allowed)
* @return the property key defined in the annotation, or an empty string (not null)
*/
public static String getPropertyKey(final Property property)
{
return property != null
? StringUtils.defaultIfEmpty(property.value(), property.key())
: StringUtils.EMPTY;
}

/**
* Find a field matching any of the following criteria in the specified type, or
* {@code null} if no match found:
* <ul>
* <li>the field is marked with the {@code @}{@link Property} annotation, and the
* annotation defines a custom name equal to the one specified in the parameter; or</li>
* <li>the field name is equal to the specified {@code name} parameter</li>
* </ul>
*
* @param type the class to reflect; not null
* @param name the field name to obtain; not null
* @return the {@link Field} object; can be null
*/
public static Field findFieldByPropertyKeyOrName(final Class<?> type, final String name)
{
return findFieldByPropertyKey(type, name)
.orElseGet(() -> FieldUtils.getField(type, name, true));
}

/**
* Find a field matching the following criteria in the specified type, or {@code null} if
* no match found:
* <ul>
* <li>the field is marked with the {@code @}{@link Property} annotation; and</li>
* <li>the annotation defines a property key which is equal to the one specified in the
* parameter</li>
* </ul>
*
* @param type the class to check; not {@code null}
* @param name the field name to obtain; {@code null}
* @return an Optional possibly containing a {@link Field} object; can be empty
*/
public static Optional<Field> findFieldByPropertyKey(final Class<?> type, final String name)
{
return FieldUtils.getFieldsListWithAnnotation(type, Property.class).stream()
.filter(field -> isFieldAnnotated(field, name)).findAny();
}

/**
* Returns {@code true} if the field is annotated with a Property annotation which key
* matches the specified name.
*
* @param field the field to be checked; not null
* @param name the name to be tested
* @return {@code true} if the field is annotated with a Property annotation which key
* matches the specified name
*/
public static boolean isFieldAnnotated(final Field field, final String name)
{
Property property = field.getDeclaredAnnotation(Property.class);
return PropertyUtils.getPropertyKey(property).equals(name);
}

/**
* Parse the specified value into an object based on the field type.
* <p>
* This method may apply custom conversion logic by invoking the {@link TypeConverter}
* class configured in the {@link Property} annotation (if not null), or standard
* conversion by using the {@link TypeFactory} by default.
*
* @param string the string to be parsed
* @param field the target field (not null)
*
* @return the object resulting from the parse operation
*
* @throws ReflectiveOperationException if an error occurs in the reflective operation
* @throws ParseException if an error is encountered while parsing
*/
public static Object parseValue(final String string, final Field field)
throws ReflectiveOperationException, ParseException
{
return parseValue(string, field.getType(), field.getAnnotation(Property.class));
}

/**
Expand All @@ -73,12 +157,11 @@ public static String getPropertyOrFieldName(Property property, Field field)
*
* @return the object resulting from the parse operation
*
* @throws ReflectiveOperationException if an error occurs in the reflective operation
* @throws UnsupportedOperationException if the specified type is not supported
* @throws ParseException if an error is encountered while parsing
* @throws ReflectiveOperationException if an error occurs in the reflective operation
* @throws ParseException if an error is encountered while parsing
*/
public static Object parseValue(String string, Class<?> targetType, Property property)
throws ReflectiveOperationException, ParseException
public static Object parseValue(final String string, final Class<?> targetType,
final Property property) throws ReflectiveOperationException, ParseException
{
if (property != null && property.converter().length > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private ReflectionUtils()
* {@code false}, otherwise
* @throws NullPointerException if the field is null
*/
public static boolean isTransient(Field field)
public static boolean isTransient(final Field field)
{
return Modifier.isTransient(field.getModifiers());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package net.obvj.confectory.util;

import java.util.function.Supplier;

import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.lookup.StringLookupFactory;

Expand Down Expand Up @@ -52,9 +54,37 @@ private StringUtils()
* @param string the string to be expanded; {@code null} returns {@code null}
* @return the expanded string
*/
public static String expandEnvironmentVariables(String string)
public static String expandEnvironmentVariables(final String string)
{
return ENVIRONMENT_VARIABLE_SUBSTITUTOR.replace(string);
}

/**
* Returns either the passed in string, or if the string is empty ({@code ""}) or
* {@code null}, the value returned by the specified supplier.
* <p>
* For example:
* <blockquote>
*
* <pre>
* StringUtils.defaultIfEmpty(null,() -&gt; "default") = "default"
* StringUtils.defaultIfEmpty("", () -&gt; "default") = "default"
* StringUtils.defaultIfEmpty("a", () -&gt; "default") = "a"
* </pre>
*
* </blockquote>
*
* @param string the string to check; may be {@code null}
* @param defaultSupplier a string supplier function to be executed only if the original
* string is empty or {@code null}
*
* @return the passed in string, or the value returned by the specified supplier
* @throws NullPointerException if the passed in supplier is null
* @since 2.5.0
*/
public static String defaultIfEmpty(final String string, final Supplier<String> defaultSupplier)
{
return string == null || string.isEmpty() ? defaultSupplier.get() : string;
}

}
Loading

0 comments on commit 7627192

Please sign in to comment.