-
Notifications
You must be signed in to change notification settings - Fork 17
#135 Support more ways to create beans (e.g. Java records) #398
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
Open
ljacqu
wants to merge
18
commits into
master
Choose a base branch
from
135-support-data-classes-draft
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
3b46d03
Create record inspector (for future use in bean mapping)
ljacqu e2f32ca
Rough POC - support different bean instantiations
ljacqu 1eaa86f
Add support for ignore annotation / shuffle things around -- WIP
ljacqu 9094e23
Update --
ljacqu d4c0646
Move & rename more
ljacqu 4f66522
Add tests
ljacqu f875c49
Extract methods / add javadoc / introduce interfaces
ljacqu c4af00c
Small Javadoc revisions
ljacqu 2e42127
Merge remote-tracking branch 'origin/master' into 135-support-data-cl…
ljacqu c5f81dd
Update for typeresolver changes
ljacqu bd5f2a7
Remove snapshot version of JavaTypeResolver
ljacqu 8eedc86
Merge remote-tracking branch 'origin/master' into 135-support-data-cl…
ljacqu c033256
Update JavaDocs (no longer mention JavaBeans)
ljacqu f6bcb82
Add tests / revise final fields / remove unused method
ljacqu 3dde809
Rename Ignore annotation to IgnoreInMapping
ljacqu 0a8e9bd
Add tests for record properties
ljacqu e3a2686
Change phrasing
ljacqu a7e1ada
Add getters for instantiation fields
ljacqu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
src/main/java/ch/jalu/configme/beanmapper/IgnoreInMapping.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
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; | ||
|
||
/** | ||
* Annotation to tell ConfigMe to ignore the field during bean mapping. In other words, when a bean is created, | ||
* a field with this annotation will not be written to or read. | ||
* <p> | ||
* Fields declared as {@code transient} are also ignored by ConfigMe. | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.FIELD) | ||
public @interface IgnoreInMapping { | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -19,45 +19,49 @@ | |
|
||
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 to the names in the property resource. For example, if a bean | ||
* has a property {@code length} and it should be mapped from the property resource's value at path {@code definition}, | ||
* the mapper will look up {@code definition.length} in the resource to determine the value. | ||
* <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 instance fields are taken as properties. You can | ||
* change the behavior of the fields with @{@link ExportName} and @{@link IgnoreInMapping}. There must be at | ||
* least one property for the class to be treated as a bean. | ||
* <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 { | ||
|
||
|
@@ -68,22 +72,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() { | ||
|
@@ -131,7 +135,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(); | ||
|
@@ -146,6 +150,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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -358,7 +368,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 | ||
|
@@ -369,46 +379,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; | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanInstantiation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 of the bean. | ||
* | ||
* @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's {@link #getProperties()}. | ||
* 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); | ||
|
||
} |
23 changes: 23 additions & 0 deletions
23
src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanInstantiationService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
|
||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.