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

#135 Support more ways to create beans (e.g. Java records) #398

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 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 pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
<dependency>
<groupId>ch.jalu</groupId>
<artifactId>typeresolver</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>
</dependency>

<!-- Annotations for @NotNull and @Nullable -->
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/ch/jalu/configme/beanmapper/IgnoreInMapping.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ch.jalu.configme.beanmapper;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* In classes used for bean properties, annotating a field with this annotation tells ConfigMe to ignore the
* property, i.e. when a bean is created, the annotated field will not be set, and when a bean is exported, fields
* with this annotation will also be ignored.
ljacqu marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* Instead of this annotation, you can also declare fields as {@code transient} to have them skipped.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IgnoreInMapping {
}
8 changes: 4 additions & 4 deletions src/main/java/ch/jalu/configme/beanmapper/Mapper.java
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably need to create another issue to change the mapping context. It should be able to have more info to support generic types in the future.

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import org.jetbrains.annotations.Nullable;

/**
* Creates JavaBeans based on the values coming from a property reader. See the JavaDoc on the default implementation,
* {@link MapperImpl}, for more details.
* Maps values of a property reader to Java classes (referred to as beans). See the JavaDoc on the default
* implementation, {@link MapperImpl}, for more details.
*/
public interface Mapper {

Expand Down Expand Up @@ -42,9 +42,9 @@ public interface Mapper {
}

/**
* Converts a complex type such as a JavaBean object to simple types suitable for exporting. This method
* Converts a complex type such as a bean to simple types suitable for exporting. This method
* typically returns a Map of values, or simple types like String / Number for scalar values.
* Used in the {@link ch.jalu.configme.properties.BeanProperty#toExportValue} method.
* Used by {@link ch.jalu.configme.properties.types.BeanPropertyType#toExportValue}.
*
* @param object the object to convert to its export value
* @return export value to use
Expand Down
115 changes: 49 additions & 66 deletions src/main/java/ch/jalu/configme/beanmapper/MapperImpl.java
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check behavior when a class has no bean instantiation: I think there was some weird behavior. Mapping should be stopped.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import ch.jalu.configme.beanmapper.context.ExportContextImpl;
import ch.jalu.configme.beanmapper.context.MappingContext;
import ch.jalu.configme.beanmapper.context.MappingContextImpl;
import ch.jalu.configme.beanmapper.instantiation.BeanInstantiation;
import ch.jalu.configme.beanmapper.instantiation.BeanInstantiationService;
import ch.jalu.configme.beanmapper.instantiation.BeanInstantiationServiceImpl;
import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandler;
import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandlerImpl;
import ch.jalu.configme.beanmapper.leafvaluehandler.MapperLeafType;
import ch.jalu.configme.beanmapper.propertydescription.BeanDescriptionFactory;
import ch.jalu.configme.beanmapper.propertydescription.BeanDescriptionFactoryImpl;
import ch.jalu.configme.beanmapper.propertydescription.BeanPropertyComments;
import ch.jalu.configme.beanmapper.propertydescription.BeanPropertyDescription;
import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder;
Expand All @@ -19,45 +19,48 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;

import static ch.jalu.configme.internal.PathUtils.OPTIONAL_SPECIFIER;
import static ch.jalu.configme.internal.PathUtils.pathSpecifierForIndex;
import static ch.jalu.configme.internal.PathUtils.pathSpecifierForMapKey;

/**
* Implementation of {@link Mapper}.
* Default implementation of {@link Mapper}.
* <p>
* Maps a section of a property resource to the provided JavaBean class. The mapping is based on the bean's properties,
* whose names must correspond with the names in the property resource. For example, if a JavaBean class has a property
* {@code length} and should be mapped from the property resource's value at path {@code definition}, the mapper will
* look up {@code definition.length} to get the value of the JavaBean property.
* Maps a section of a property resource to the provided Java class (called a "bean" type). The mapping is based on the
* bean's properties, whose names must correspond with the names in the property resource. For example, if a bean class
* has a property {@code length} and should be mapped from the property resource's value at path {@code definition},
* the mapper will look up {@code definition.length} to get the value of the bean property.
ljacqu marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* Classes must be JavaBeans. These are simple classes with private fields, accompanied by getters and setters.
* <b>The mapper only considers properties which have both a getter and a setter method.</b> Any Java class without
* at least one property with both a getter <i>and</i> a setter is not considered as a JavaBean class. Such classes can
* be supported by implementing a custom {@link MapperLeafType} that performs the conversion from the value coming
* from the property reader to an object of the class's type.
* Classes are created by the {@link BeanInstantiationService}. The {@link BeanInstantiationServiceImpl
* default implementation} supports Java classes with a zero-args constructor, as well as Java records. The service can
* be extended to support more types of classes.
* <br>For Java classes with a zero-args constructor, the class's private fields are taken as properties. The perceived
ljacqu marked this conversation as resolved.
Show resolved Hide resolved
* properties can be modified with {@link ExportName} and {@link IgnoreInMapping}.
ljacqu marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* <b>Recursion:</b> the mapping of values to a JavaBean is performed recursively, i.e. a JavaBean may have other
* JavaBeans as fields and generic types at any arbitrary "depth".
* <b>Recursion:</b> the mapping of values to a bean is performed recursively, i.e. a bean may have other beans
* as fields and generic types at any arbitrary "depth".
* <p>
* <b>Collections</b> are only supported if they are explicitly typed, i.e. a field of {@code List<String>}
* <b>Collections</b> are only supported if they have an explicit type argument, i.e. a field of {@code List<String>}
* is supported but {@code List<?>} and {@code List<T extends Number>} are not supported. Specifically, you may
* only declare fields of type {@link java.util.List} or {@link java.util.Set}, or a parent type ({@link Collection}
* or {@link Iterable}).
* or {@link Iterable}) by default.
* Fields of type <b>Map</b> are supported also, with similar limitations. Additionally, maps may only have
* {@code String} as key type, but no restrictions are imposed on the value type.
* <p>
* JavaBeans may have <b>optional fields</b>. If the mapper cannot map the property resource value to the corresponding
* Beans may have <b>optional fields</b>. If the mapper cannot map the property resource value to the corresponding
* field, it only treats it as a failure if the field's value is {@code null}. If the field has a default value assigned
* to it on initialization, the default value remains and the mapping process continues. A JavaBean field whose value is
* {@code null} signifies a failure and stops the mapping process immediately.
* to it on initialization, the default value remains and the mapping process continues. If a bean is created with a
* null property, the mapping process is stopped immediately.
* <br>Optional properties can also be defined by declaring them with {@link Optional}.
*/
public class MapperImpl implements Mapper {

Expand All @@ -68,22 +71,22 @@ public class MapperImpl implements Mapper {
// Fields and general configurable methods
// ---------

private final BeanDescriptionFactory beanDescriptionFactory;
private final LeafValueHandler leafValueHandler;
private final BeanInstantiationService beanInstantiationService;

public MapperImpl() {
this(new BeanDescriptionFactoryImpl(),
this(new BeanInstantiationServiceImpl(),
new LeafValueHandlerImpl(LeafValueHandlerImpl.createDefaultLeafTypes()));
}

public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory,
public MapperImpl(@NotNull BeanInstantiationService beanInstantiationService,
@NotNull LeafValueHandler leafValueHandler) {
this.beanDescriptionFactory = beanDescriptionFactory;
this.beanInstantiationService = beanInstantiationService;
this.leafValueHandler = leafValueHandler;
}

protected final @NotNull BeanDescriptionFactory getBeanDescriptionFactory() {
return beanDescriptionFactory;
protected final @NotNull BeanInstantiationService getBeanInstantiationService() {
return beanInstantiationService;
}

protected final @NotNull LeafValueHandler getLeafValueHandler() {
Expand Down Expand Up @@ -131,7 +134,7 @@ public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory,

// Step 3: treat as bean
Map<String, Object> mappedBean = new LinkedHashMap<>();
for (BeanPropertyDescription property : beanDescriptionFactory.getAllProperties(value.getClass())) {
for (BeanPropertyDescription property : getBeanProperties(value)) {
Object exportValueOfProperty = toExportValue(property.getValue(value), exportContext);
if (exportValueOfProperty != null) {
BeanPropertyComments propComments = property.getComments();
Expand All @@ -146,6 +149,12 @@ public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory,
return mappedBean;
}

protected @NotNull List<BeanPropertyDescription> getBeanProperties(@NotNull Object value) {
return beanInstantiationService.findInstantiation(value.getClass())
.map(BeanInstantiation::getProperties)
.orElse(Collections.emptyList());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting to note that here a bean instantiation is retrieved during the export to find out what properties it has. Maybe "instantiation" is not a good name? No better idea at the time of writing

}

/**
* Handles values of types which need special handling (such as Optional). Null means the value is not
* a special type and that the export value should be built differently. Use {@link #RETURN_NULL} to
Expand Down Expand Up @@ -358,7 +367,7 @@ public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory,
// -- Bean

/**
* Converts the provided value to the requested JavaBeans class if possible.
* Converts the provided value to the requested bean class if possible.
*
* @param context mapping context (incl. desired type)
* @param value the value from the property resource
Expand All @@ -369,46 +378,20 @@ public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory,
if (!(value instanceof Map<?, ?>)) {
return null;
}
ljacqu marked this conversation as resolved.
Show resolved Hide resolved

Collection<BeanPropertyDescription> properties =
beanDescriptionFactory.getAllProperties(context.getTargetTypeAsClassOrThrow());
// Check that we have properties (or else we don't have a bean)
if (properties.isEmpty()) {
return null;
}

Map<?, ?> entries = (Map<?, ?>) value;
Object bean = createBeanMatchingType(context);
for (BeanPropertyDescription property : properties) {
Object result = convertValueForType(
context.createChild(property.getName(), property.getTypeInformation()),
entries.get(property.getName()));
if (result == null) {
if (property.getValue(bean) == null) {
return null; // We do not support beans with a null value
}
context.registerError("No value found, fallback to field default value");
} else {
property.setValue(bean, result);
}
}
return bean;
}

/**
* Creates an object matching the given type information.
*
* @param mappingContext current mapping context
* @return new instance of the given type
*/
protected @NotNull Object createBeanMatchingType(@NotNull MappingContext mappingContext) {
// clazz is never null given the only path that leads to this method already performs that check
final Class<?> clazz = mappingContext.getTargetTypeAsClassOrThrow();
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new ConfigMeMapperException(mappingContext, "Could not create object of type '"
+ clazz.getName() + "'. It is required to have a default constructor", e);
Optional<BeanInstantiation> instantiation =
beanInstantiationService.findInstantiation(context.getTargetTypeAsClassOrThrow());
if (instantiation.isPresent()) {
List<Object> propertyValues = instantiation.get().getProperties().stream()
.map(prop -> {
MappingContext childContext = context.createChild(prop.getName(), prop.getTypeInformation());
return convertValueForType(childContext, entries.get(prop.getName()));
})
.collect(Collectors.toList());

return instantiation.get().create(propertyValues, context.getErrorRecorder());
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ch.jalu.configme.beanmapper.instantiation;

import ch.jalu.configme.beanmapper.propertydescription.BeanPropertyDescription;
import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

/**
* Creation method for a given bean type. A bean instantiation returns the properties that are needed to create it
* and allows to create beans of the given type. Objects implementing this interface are stateless.
*/
public interface BeanInstantiation {

/**
* Returns the properties which the bean has.
ljacqu marked this conversation as resolved.
Show resolved Hide resolved
*
* @return the bean's properties
*/
@NotNull List<BeanPropertyDescription> getProperties();

/**
* Creates a new bean with the given property values. The provided property values must be in the same order as
* returned by this instantiation in {@link #getProperties()}.
ljacqu marked this conversation as resolved.
Show resolved Hide resolved
* Null is returned if the bean cannot be created, e.g. because a property value was null and it is not supported
* by this instantiation.
*
* @param propertyValues the values to set to the bean (can contain null entries)
* @param errorRecorder error recorder for errors if the bean can be created, but the values weren't fully valid
* @return the bean, if possible, otherwise null
*/
@Nullable Object create(@NotNull List<Object> propertyValues, @NotNull ConvertErrorRecorder errorRecorder);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ch.jalu.configme.beanmapper.instantiation;

import org.jetbrains.annotations.NotNull;

import java.util.Optional;

/**
* Service for the creation of beans.
*
* @see BeanInstantiationServiceImpl
*/
public interface BeanInstantiationService {

/**
* Inspects the given class and returns an optional with an object defining how to instantiate the bean;
* an empty optional is returned if the class cannot be treated as a bean.
*
* @param clazz the class to inspect
* @return optional with the instantiation, empty optional if not possible
*/
@NotNull Optional<BeanInstantiation> findInstantiation(@NotNull Class<?> clazz);
ljacqu marked this conversation as resolved.
Show resolved Hide resolved

}
Loading