diff --git a/pom.xml b/pom.xml index 29e399b8..c43cf6e5 100644 --- a/pom.xml +++ b/pom.xml @@ -193,7 +193,7 @@ ch.jalu typeresolver - 0.1.0 + 0.2.0-SNAPSHOT diff --git a/src/main/java/ch/jalu/configme/beanmapper/Ignore.java b/src/main/java/ch/jalu/configme/beanmapper/Ignore.java new file mode 100644 index 00000000..ab2aef59 --- /dev/null +++ b/src/main/java/ch/jalu/configme/beanmapper/Ignore.java @@ -0,0 +1,11 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Ignore { +} diff --git a/src/main/java/ch/jalu/configme/beanmapper/MapperImpl.java b/src/main/java/ch/jalu/configme/beanmapper/MapperImpl.java index b57d30cd..6404f708 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/MapperImpl.java +++ b/src/main/java/ch/jalu/configme/beanmapper/MapperImpl.java @@ -9,8 +9,6 @@ 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; @@ -21,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -71,23 +70,22 @@ public class MapperImpl implements Mapper { // Fields and general configurable methods // --------- - private final BeanDescriptionFactory beanDescriptionFactory; private final LeafValueHandler leafValueHandler; - private final BeanInspector beanInspector = new BeanInspector(); // TODO: Make configurable, add interface... + private final BeanInspector beanInspector; public MapperImpl() { - this(new BeanDescriptionFactoryImpl(), + this(new BeanInspector(), new LeafValueHandlerImpl(LeafValueHandlerImpl.createDefaultLeafTypes())); } - public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory, + public MapperImpl(@NotNull BeanInspector beanInspector, @NotNull LeafValueHandler leafValueHandler) { - this.beanDescriptionFactory = beanDescriptionFactory; + this.beanInspector = beanInspector; this.leafValueHandler = leafValueHandler; } - protected final @NotNull BeanDescriptionFactory getBeanDescriptionFactory() { - return beanDescriptionFactory; + protected final @NotNull BeanInspector getBeanInspector() { + return beanInspector; } protected final @NotNull LeafValueHandler getLeafValueHandler() { @@ -135,8 +133,7 @@ public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory, // Step 3: treat as bean Map mappedBean = new LinkedHashMap<>(); - // TODO: Adapt me - 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(); @@ -151,6 +148,13 @@ public MapperImpl(@NotNull BeanDescriptionFactory beanDescriptionFactory, return mappedBean; } + @NotNull + private List getBeanProperties(@NotNull Object value) { + return beanInspector.findInstantiation(value.getClass()) + .map(BeanInstantiation::getProperties) + .orElse(Collections.emptyList()); + } + /** * 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 diff --git a/src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanInspector.java b/src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanInspector.java index 84dfb530..5b97f814 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanInspector.java +++ b/src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanInspector.java @@ -2,26 +2,15 @@ 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.beanmapper.propertydescription.FieldProperty; -import ch.jalu.configme.exception.ConfigMeException; import ch.jalu.configme.internal.record.RecordComponent; import ch.jalu.configme.internal.record.RecordInspector; import ch.jalu.configme.internal.record.ReflectionHelper; -import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; -import ch.jalu.typeresolver.TypeInfo; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.lang.reflect.Constructor; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; import java.util.List; -import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; public class BeanInspector { @@ -31,140 +20,27 @@ public class BeanInspector { public @NotNull Optional findInstantiation(@NotNull Class clazz) { if (recordInspector.isRecord(clazz)) { RecordComponent[] recordComponents = recordInspector.getRecordComponents(clazz); - return Optional.of(new RecordInstantiation(clazz, Arrays.asList(recordComponents))); + List properties = + beanDescriptionFactory.createRecordProperties(clazz, recordComponents); + + return Optional.of(new RecordInstantiation(clazz, properties)); } Optional> zeroArgConstructor = getConstructor(clazz); if (zeroArgConstructor.isPresent()) { - List properties = beanDescriptionFactory.getAllProperties2(clazz); - return Optional.of(new BeanNoArgConstructor(zeroArgConstructor.get(), properties)); + List properties = beanDescriptionFactory.getAllProperties(clazz); + return Optional.of(new BeanZeroArgConstrInstantiation(zeroArgConstructor.get(), properties)); } return Optional.empty(); } - private static @NotNull Optional> getConstructor(@NotNull Class declarer, - Class @NotNull ... parameters) { + static @NotNull Optional> getConstructor(@NotNull Class declarer, + Class @NotNull ... parameters) { try { return Optional.of(declarer.getDeclaredConstructor(parameters)); } catch (NoSuchMethodException ignore) { return Optional.empty(); } } - - private static final class BeanNoArgConstructor implements BeanInstantiation { - - private final Constructor zeroArgsConstructor; - private final List properties; - - private BeanNoArgConstructor(@NotNull Constructor zeroArgsConstructor, - @NotNull List properties) { - this.zeroArgsConstructor = zeroArgsConstructor; - this.properties = properties; - } - - @Override - public @NotNull List getProperties() { - return Collections.unmodifiableList(properties); - } - - @Override - public @Nullable Object create(@NotNull List propertyValues, - @NotNull ConvertErrorRecorder errorRecorder) { - final Object bean; - try { - bean = zeroArgsConstructor.newInstance(); - } catch (ReflectiveOperationException e) { - throw new ConfigMeException("Failed to call constructor for " - + zeroArgsConstructor.getDeclaringClass()); - } - - if (propertyValues.size() != properties.size()) { - throw new ConfigMeException("Invalid property values, " + propertyValues.size() + " were given, but " - + zeroArgsConstructor.getDeclaringClass() + " has " + properties.size() + " properties"); - } - - Iterator propIt = properties.iterator(); - Iterator valuesIt = propertyValues.iterator(); - while (propIt.hasNext() && valuesIt.hasNext()) { - FieldProperty property = propIt.next(); - Object value = valuesIt.next(); - if (value == null) { - if (property.getValue(bean) == null) { - return null; // No default value on field, return null -> no bean with a null value - } - errorRecorder.setHasError("Fallback to default value"); - } else { - property.setValue(bean, value); - } - } - return bean; - } - } - - private static final class RecordInstantiation implements BeanInstantiation { - - private final Constructor canonicalConstructor; - private final List properties; - - public RecordInstantiation(@NotNull Class clazz, @NotNull List components) { - this.properties = components.stream() - .map(RecordProperty::new) - .collect(Collectors.toList()); - Class[] recordTypes = components.stream().map(RecordComponent::getType).toArray(Class[]::new); - this.canonicalConstructor = getConstructor(clazz, recordTypes) - .orElseThrow(() -> new ConfigMeException("Could not get canonical constructor of " + clazz)); - } - - @Override - public @NotNull List getProperties() { - return properties; - } - - @Override - public @Nullable Object create(@NotNull List propertyValues, - @NotNull ConvertErrorRecorder errorRecorder) { - if (propertyValues.stream().anyMatch(Objects::isNull)) { - return null; // No support for null values in records - } - - Object[] properties = propertyValues.toArray(); - try { - return canonicalConstructor.newInstance(properties); - } catch (IllegalArgumentException | ReflectiveOperationException e) { - // TODO: Separate clause for InvocationTargetException? - throw new ConfigMeException("Error calling record constructor for " - + canonicalConstructor.getDeclaringClass(), e); - } - } - } - - private static final class RecordProperty implements BeanPropertyDescription { - - private final RecordComponent recordComponent; - - private RecordProperty(@NotNull RecordComponent recordComponent) { - this.recordComponent = recordComponent; - } - - @Override - public @NotNull String getName() { - return recordComponent.getName(); - } - - @Override - public @NotNull TypeInfo getTypeInformation() { - return TypeInfo.of(recordComponent.getGenericType()); - } - - @Override - public @Nullable Object getValue(@NotNull Object bean) { - throw new UnsupportedOperationException(); // TODO - } - - @Override - public @NotNull BeanPropertyComments getComments() { - return BeanPropertyComments.EMPTY; // TODO - } - } } diff --git a/src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanZeroArgConstrInstantiation.java b/src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanZeroArgConstrInstantiation.java new file mode 100644 index 00000000..5c59847a --- /dev/null +++ b/src/main/java/ch/jalu/configme/beanmapper/instantiation/BeanZeroArgConstrInstantiation.java @@ -0,0 +1,63 @@ +package ch.jalu.configme.beanmapper.instantiation; + +import ch.jalu.configme.beanmapper.propertydescription.BeanPropertyDescription; +import ch.jalu.configme.beanmapper.propertydescription.FieldProperty; +import ch.jalu.configme.exception.ConfigMeException; +import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +final class BeanZeroArgConstrInstantiation implements BeanInstantiation { + + private final Constructor zeroArgsConstructor; + private final List properties; + + public BeanZeroArgConstrInstantiation(@NotNull Constructor zeroArgsConstructor, + @NotNull List properties) { + this.zeroArgsConstructor = zeroArgsConstructor; + this.properties = properties; + } + + @Override + public @NotNull List getProperties() { + return Collections.unmodifiableList(properties); + } + + @Override + public @Nullable Object create(@NotNull List propertyValues, + @NotNull ConvertErrorRecorder errorRecorder) { + final Object bean; + try { + bean = zeroArgsConstructor.newInstance(); + } catch (ReflectiveOperationException e) { + throw new ConfigMeException("Failed to call constructor for " + + zeroArgsConstructor.getDeclaringClass()); + } + + if (propertyValues.size() != properties.size()) { + throw new ConfigMeException("Invalid property values, " + propertyValues.size() + " were given, but " + + zeroArgsConstructor.getDeclaringClass() + " has " + properties.size() + " properties"); + } + + Iterator propIt = properties.iterator(); + Iterator valuesIt = propertyValues.iterator(); + while (propIt.hasNext() && valuesIt.hasNext()) { + FieldProperty property = propIt.next(); + Object value = valuesIt.next(); + if (value == null) { + if (property.getValue(bean) == null) { + return null; // No default value on field, return null -> no bean with a null value + } + errorRecorder.setHasError("Fallback to default value"); + } else { + property.setValue(bean, value); + } + } + return bean; + } +} diff --git a/src/main/java/ch/jalu/configme/beanmapper/instantiation/RecordInstantiation.java b/src/main/java/ch/jalu/configme/beanmapper/instantiation/RecordInstantiation.java new file mode 100644 index 00000000..4609b831 --- /dev/null +++ b/src/main/java/ch/jalu/configme/beanmapper/instantiation/RecordInstantiation.java @@ -0,0 +1,48 @@ +package ch.jalu.configme.beanmapper.instantiation; + +import ch.jalu.configme.beanmapper.propertydescription.BeanPropertyDescription; +import ch.jalu.configme.beanmapper.propertydescription.FieldProperty; +import ch.jalu.configme.exception.ConfigMeException; +import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +final class RecordInstantiation implements BeanInstantiation { + + private final Constructor canonicalConstructor; + private final List properties; + + public RecordInstantiation(@NotNull Class clazz, @NotNull List properties) { + this.properties = properties; + Class[] recordTypes = properties.stream().map(FieldProperty::getType).toArray(Class[]::new); + this.canonicalConstructor = BeanInspector.getConstructor(clazz, recordTypes) + .orElseThrow(() -> new ConfigMeException("Could not get canonical constructor of " + clazz)); + } + + @Override + public @NotNull List getProperties() { + return Collections.unmodifiableList(properties); + } + + @Override + public @Nullable Object create(@NotNull List propertyValues, + @NotNull ConvertErrorRecorder errorRecorder) { + if (propertyValues.stream().anyMatch(Objects::isNull)) { + return null; // No support for null values in records + } + + Object[] properties = propertyValues.toArray(); + try { + return canonicalConstructor.newInstance(properties); + } catch (IllegalArgumentException | ReflectiveOperationException e) { + // TODO: Separate clause for InvocationTargetException? + throw new ConfigMeException("Error calling record constructor for " + + canonicalConstructor.getDeclaringClass(), e); + } + } +} diff --git a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactory.java b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactory.java index ac5e0a07..5819aaf6 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactory.java +++ b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactory.java @@ -1,5 +1,6 @@ package ch.jalu.configme.beanmapper.propertydescription; +import ch.jalu.configme.internal.record.RecordComponent; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -11,15 +12,9 @@ */ public interface BeanDescriptionFactory { - /** - * Returns all properties on the given class which should be considered while creating a bean of the - * given type. This is usually all properties which can be read from and written to. - * - * @param clazz the class whose properties should be returned - * @return the relevant properties on the class - */ - @NotNull List getAllProperties(@NotNull Class clazz); + @NotNull List getAllProperties(@NotNull Class clazz); - @NotNull List getAllProperties2(@NotNull Class clazz); + @NotNull List createRecordProperties(@NotNull Class clazz, + RecordComponent @NotNull [] components); } diff --git a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java index 3474ac21..8e185123 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java +++ b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java @@ -3,22 +3,21 @@ import ch.jalu.configme.Comment; import ch.jalu.configme.beanmapper.ConfigMeMapperException; import ch.jalu.configme.beanmapper.ExportName; -import ch.jalu.typeresolver.TypeInfo; +import ch.jalu.configme.beanmapper.Ignore; +import ch.jalu.configme.exception.ConfigMeException; +import ch.jalu.configme.internal.record.RecordComponent; +import ch.jalu.typeresolver.FieldUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -32,46 +31,40 @@ * This description factory returns property descriptions for all properties on a class * for which a getter and setter is associated. Inherited properties are considered. *

- * This implementation supports {@link ExportName} and transient properties, declared either - * with the {@code transient} keyword or by adding the {@link java.beans.Transient} annotation. + * This implementation supports {@link ExportName} and transient properties, declared + * with the {@code transient} keyword or by adding the {@link Ignore} annotation. */ public class BeanDescriptionFactoryImpl implements BeanDescriptionFactory { - private final Map, List> classProperties = new HashMap<>(); + private final Map, List> classProperties = new HashMap<>(); - /** - * Returns all properties of the given bean class for which there exists a getter and setter. - * - * @param clazz the bean property to process - * @return the bean class's properties to handle - */ @Override - public @NotNull List getAllProperties(@NotNull Class clazz) { + public @NotNull List getAllProperties(@NotNull Class clazz) { return classProperties.computeIfAbsent(clazz, this::collectAllProperties); } - @Override - public @NotNull List getAllProperties2(@NotNull Class clazz) { - // TODO: Caching. Here, or elsewhere? - return collectAllProperties2(clazz); - } - - /** - * Collects all properties available on the given class. - * - * @param clazz the class to process - * @return properties of the class - */ - protected @NotNull List collectAllProperties(@NotNull Class clazz) { - List descriptors = getWritableProperties(clazz); - - List properties = descriptors.stream() - .map(this::convert) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + public @NotNull List createRecordProperties(@NotNull Class clazz, + RecordComponent @NotNull [] components) { + // TODO: Caching here. + LinkedHashMap instanceFieldsByName = FieldUtils.getAllFields(clazz) + .filter(FieldUtils::isRegularInstanceField) + .collect(FieldUtils.collectByName(false)); - validateProperties(clazz, properties); - return properties; + List relevantFields = new ArrayList<>(); + for (RecordComponent component : components) { + Field field = instanceFieldsByName.get(component.getName()); + if (field == null) { + throw new ConfigMeException("Record component '" + component.getName() + "' for " + clazz.getName() + + " does not have a field with the same name"); + } + FieldProperty property = convert(field); + if (property == null) { + throw new ConfigMeException("Record component '" + component.getName() + "' for " + clazz.getName() + + " has a field defined to be ignored: this is not supported for records"); + } + relevantFields.add(property); + } + return relevantFields; } /** @@ -80,14 +73,13 @@ public class BeanDescriptionFactoryImpl implements BeanDescriptionFactory { * @param clazz the class to process * @return properties of the class */ - protected @NotNull List collectAllProperties2(@NotNull Class clazz) { - List> parentsAndClass = collectClassAndAllParents(clazz); + protected @NotNull List collectAllProperties(@NotNull Class clazz) { + LinkedHashMap instanceFieldsByName = FieldUtils.getAllFields(clazz) + .filter(FieldUtils::isRegularInstanceField) + .collect(FieldUtils.collectByName(false)); - // TODO: Use field utils - List properties = parentsAndClass.stream() - .flatMap(clz -> Arrays.stream(clz.getDeclaredFields())) - .filter(field -> !Modifier.isStatic(field.getModifiers()) && !field.isSynthetic()) - .map(this::convert2) + List properties = instanceFieldsByName.values().stream() + .map(this::convert) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -95,30 +87,11 @@ public class BeanDescriptionFactoryImpl implements BeanDescriptionFactory { return properties; } - /** - * Converts a {@link PropertyDescriptor} to a {@link BeanPropertyDescription} object. - * - * @param descriptor the descriptor to convert - * @return the converted object, or null if the property should be skipped - */ - protected @Nullable BeanPropertyDescription convert(@NotNull PropertyDescriptor descriptor) { - if (Boolean.TRUE.equals(descriptor.getValue("transient"))) { + protected @Nullable FieldProperty convert(@NotNull Field field) { + if (Modifier.isTransient(field.getModifiers()) || field.isAnnotationPresent(Ignore.class)) { return null; } - Field field = tryGetField(descriptor.getWriteMethod().getDeclaringClass(), descriptor.getName()); - BeanPropertyComments comments = getComments(field); - return new BeanPropertyDescriptionImpl( - getPropertyName(descriptor, field), - createTypeInfo(descriptor), - descriptor.getReadMethod(), - descriptor.getWriteMethod(), - comments); - } - - protected @Nullable FieldProperty convert2(@NotNull Field field) { - // TODO: Annotation to ignore field - return new FieldProperty(field, getCustomExportName(field), getComments(field)); @@ -140,21 +113,6 @@ public class BeanDescriptionFactoryImpl implements BeanDescriptionFactory { return BeanPropertyComments.EMPTY; } - /** - * Returns the field with the given name on the provided class, or null if it doesn't exist. - * - * @param clazz the class to search in - * @param name the field name to look for - * @return the field if matched, otherwise null - */ - protected @Nullable Field tryGetField(@NotNull Class clazz, @NotNull String name) { - try { - return clazz.getDeclaredField(name); - } catch (NoSuchFieldException ignore) { - } - return null; - } - /** * Validates the class's properties. * @@ -175,108 +133,9 @@ protected void validateProperties(@NotNull Class clazz, }); } - /** - * Returns the name which is used in the export files for the given property descriptor. - * - * @param descriptor the descriptor to get the name for - * @param field the field associated with the property (may be null) - * @return the property name - */ - protected @NotNull String getPropertyName(@NotNull PropertyDescriptor descriptor, @Nullable Field field) { - if (field != null && field.isAnnotationPresent(ExportName.class)) { - return field.getAnnotation(ExportName.class).value(); - } - return descriptor.getName(); - } - protected @Nullable String getCustomExportName(@NotNull Field field) { return field.isAnnotationPresent(ExportName.class) ? field.getAnnotation(ExportName.class).value() : null; } - - protected @NotNull TypeInfo createTypeInfo(@NotNull PropertyDescriptor descriptor) { - return new TypeInfo(descriptor.getWriteMethod().getGenericParameterTypes()[0]); - } - - /** - * Returns all properties of the given class that are writable - * (all bean properties with an associated read and write method). - * - * @param clazz the class to process - * @return all writable properties of the bean class - */ - protected @NotNull List getWritableProperties(@NotNull Class clazz) { - PropertyDescriptor[] descriptors; - try { - descriptors = Introspector.getBeanInfo(clazz).getPropertyDescriptors(); - } catch (IntrospectionException e) { - throw new IllegalStateException(e); - } - List writableProperties = new ArrayList<>(descriptors.length); - for (PropertyDescriptor descriptor : descriptors) { - if (descriptor.getWriteMethod() != null && descriptor.getReadMethod() != null) { - writableProperties.add(descriptor); - } - } - return sortPropertiesList(clazz, writableProperties); - } - - /** - * Returns a sorted list of the given properties which will be used for further processing and whose - * order will be maintained. May return the same list. - * - * @param clazz the class from which the properties come from - * @param properties the properties to sort - * @return sorted properties - */ - protected @NotNull List sortPropertiesList(@NotNull Class clazz, - @NotNull List properties) { - Map fieldNameByIndex = createFieldNameOrderMap(clazz); - int maxIndex = fieldNameByIndex.size(); - - properties.sort(Comparator.comparing(property -> { - Integer index = fieldNameByIndex.get(property.getName()); - return index == null ? maxIndex : index; - })); - return properties; - } - - /** - * Creates a map of index (encounter number) by field name for all fields of the given class, - * including its superclasses. Fields are sorted by declaration order in the classes; sorted - * by top-most class in the inheritance hierarchy to the lowest (the class provided as parameter). - * - * @param clazz the class to create the field index map for - * @return map with all field names as keys and its index as value - */ - protected @NotNull Map createFieldNameOrderMap(@NotNull Class clazz) { - Map nameByIndex = new HashMap<>(); - int i = 0; - for (Class currentClass : collectClassAndAllParents(clazz)) { - for (Field field : currentClass.getDeclaredFields()) { - nameByIndex.put(field.getName(), i); - ++i; - } - } - return nameByIndex; - } - - /** - * Returns a list of the class's parents, including the given class itself, with the top-most parent - * coming first. Does not include the Object class. - * - * @param clazz the class whose parents should be collected - * @return list with all of the class's parents, sorted by highest class in the hierarchy to lowest - */ - protected @NotNull List> collectClassAndAllParents(@NotNull Class clazz) { - List> parents = new ArrayList<>(); - Class curClass = clazz; - while (curClass != null && curClass != Object.class) { - parents.add(curClass); - curClass = curClass.getSuperclass(); - } - Collections.reverse(parents); - return parents; - } } diff --git a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImpl.java b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImpl.java index e7ef0e7c..69258b02 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImpl.java +++ b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImpl.java @@ -11,7 +11,7 @@ /** * Default implementation of {@link BeanPropertyDescription}. */ -public class BeanPropertyDescriptionImpl implements BeanPropertyDescription { +public class BeanPropertyDescriptionImpl implements BeanPropertyDescription { // TODO: Remove me private final String name; private final TypeInfo typeInformation; diff --git a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/FieldProperty.java b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/FieldProperty.java index 3b632d31..1373b6a7 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/FieldProperty.java +++ b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/FieldProperty.java @@ -2,6 +2,7 @@ import ch.jalu.configme.beanmapper.ConfigMeMapperException; import ch.jalu.configme.exception.ConfigMeException; +import ch.jalu.typeresolver.FieldUtils; import ch.jalu.typeresolver.TypeInfo; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,6 +35,10 @@ public FieldProperty(@NotNull Field field, return TypeInfo.of(field); } + public @NotNull Class getType() { + return field.getType(); + } + public void setValue(@NotNull Object bean, @NotNull Object value) { if (!field.isAccessible()) { field.setAccessible(true); // todo: exception handling @@ -41,8 +46,8 @@ public void setValue(@NotNull Object bean, @NotNull Object value) { try { field.set(bean, value); } catch (IllegalArgumentException | IllegalAccessException e) { - // todo: Use field utils for field name - throw new ConfigMeMapperException("Failed to set value to field " + field + ". Value: " + value, e); + String fieldName = FieldUtils.formatField(field); + throw new ConfigMeMapperException("Failed to set value to field " + fieldName + ". Value: " + value, e); } } @@ -54,8 +59,7 @@ public void setValue(@NotNull Object bean, @NotNull Object value) { try { return field.get(bean); } catch (IllegalArgumentException | IllegalAccessException e) { - // TODO: use field utils for field name - throw new ConfigMeException("Failed to get value for field " + field, e); + throw new ConfigMeException("Failed to get value for field " + FieldUtils.formatField(field), e); } } diff --git a/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomLeafTypeTest.java b/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomLeafTypeTest.java index cc5523b3..39fcedfc 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomLeafTypeTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomLeafTypeTest.java @@ -4,9 +4,9 @@ import ch.jalu.configme.SettingsManager; import ch.jalu.configme.SettingsManagerBuilder; import ch.jalu.configme.TestUtils; -import ch.jalu.configme.beanmapper.leafvaluehandler.MapperLeafType; +import ch.jalu.configme.beanmapper.instantiation.BeanInspector; import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandlerImpl; -import ch.jalu.configme.beanmapper.propertydescription.BeanDescriptionFactoryImpl; +import ch.jalu.configme.beanmapper.leafvaluehandler.MapperLeafType; import ch.jalu.configme.properties.BeanProperty; import ch.jalu.configme.properties.Property; import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; @@ -84,7 +84,7 @@ private MyTestSettings() { public static final class MapperWithCustomIntSupport extends MapperImpl { MapperWithCustomIntSupport() { - super(new BeanDescriptionFactoryImpl(), + super(new BeanInspector(), LeafValueHandlerImpl.builder().addDefaults().addType(new CustomIntegerLeafType()).build()); } } diff --git a/src/test/java/ch/jalu/configme/beanmapper/MapperImplTest.java b/src/test/java/ch/jalu/configme/beanmapper/MapperImplTest.java index 356584d4..63f6fa29 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/MapperImplTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/MapperImplTest.java @@ -10,9 +10,9 @@ import ch.jalu.configme.beanmapper.command.optionalproperties.ComplexOptionalTypeConfig; import ch.jalu.configme.beanmapper.context.MappingContext; import ch.jalu.configme.beanmapper.context.MappingContextImpl; +import ch.jalu.configme.beanmapper.instantiation.BeanInspector; import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandler; import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandlerImpl; -import ch.jalu.configme.beanmapper.propertydescription.BeanDescriptionFactory; import ch.jalu.configme.beanmapper.typeissues.GenericCollection; import ch.jalu.configme.beanmapper.typeissues.MapWithNonStringKeys; import ch.jalu.configme.beanmapper.typeissues.UnsupportedCollection; @@ -453,16 +453,16 @@ void shouldForwardException() { @Test void shouldReturnFields() { // given - BeanDescriptionFactory descriptionFactory = mock(BeanDescriptionFactory.class); + BeanInspector beanInspector = mock(BeanInspector.class); LeafValueHandlerImpl leafValueHandler = mock(LeafValueHandlerImpl.class); - MapperImpl mapper = new MapperImpl(descriptionFactory, leafValueHandler); + MapperImpl mapper = new MapperImpl(beanInspector, leafValueHandler); // when - BeanDescriptionFactory returnedDescriptionFactory = mapper.getBeanDescriptionFactory(); + BeanInspector returnedBeanInspector = mapper.getBeanInspector(); LeafValueHandler returnedLeafValueHandler = mapper.getLeafValueHandler(); // then - assertThat(returnedDescriptionFactory, sameInstance(descriptionFactory)); + assertThat(returnedBeanInspector, sameInstance(beanInspector)); assertThat(returnedLeafValueHandler, sameInstance(leafValueHandler)); } diff --git a/src/test/java/ch/jalu/configme/beanmapper/MapperTypeInfoWithNoClassEquivTest.java b/src/test/java/ch/jalu/configme/beanmapper/MapperTypeInfoWithNoClassEquivTest.java index 33011a83..bca4a4ca 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/MapperTypeInfoWithNoClassEquivTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/MapperTypeInfoWithNoClassEquivTest.java @@ -2,9 +2,9 @@ import ch.jalu.configme.beanmapper.context.MappingContext; import ch.jalu.configme.beanmapper.context.MappingContextImpl; +import ch.jalu.configme.beanmapper.instantiation.BeanInspector; import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandlerImpl; import ch.jalu.configme.beanmapper.leafvaluehandler.MapperLeafType; -import ch.jalu.configme.beanmapper.propertydescription.BeanDescriptionFactoryImpl; import ch.jalu.configme.exception.ConfigMeException; import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; import ch.jalu.typeresolver.TypeInfo; @@ -43,7 +43,7 @@ void shouldNotThrowForTypeInfoWithNoClassEquivalentTooEarly() { .addDefaults() .addType(extNumberLeafType) .build(); - MapperImpl mapper = new MapperImpl(new BeanDescriptionFactoryImpl(), leafValueHandler); + MapperImpl mapper = new MapperImpl(new BeanInspector(), leafValueHandler); // when Object result = mapper.convertToBean(3.2, targetType, errorRecorder); diff --git a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java index df496a3b..ff6956cc 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java @@ -14,7 +14,6 @@ import ch.jalu.typeresolver.TypeInfo; import org.junit.jupiter.api.Test; -import java.beans.Transient; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -23,7 +22,6 @@ import static ch.jalu.configme.TestUtils.transform; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -42,10 +40,10 @@ class BeanDescriptionFactoryImplTest { @Test void shouldReturnWritableProperties() { // given / when - Collection descriptions = factory.getAllProperties(SampleBean.class); + Collection descriptions = factory.getAllProperties(SampleBean.class); // then - assertThat(descriptions, hasSize(2)); + assertThat(descriptions, hasSize(4)); BeanPropertyDescription sizeProperty = getDescription("size", descriptions); assertThat(sizeProperty.getTypeInformation(), equalTo(new TypeInfo(int.class))); @@ -54,6 +52,14 @@ void shouldReturnWritableProperties() { BeanPropertyDescription nameProperty = getDescription("name", descriptions); assertThat(nameProperty.getTypeInformation(), equalTo(new TypeInfo(String.class))); assertThat(nameProperty.getComments(), sameInstance(BeanPropertyComments.EMPTY)); + + BeanPropertyDescription longFieldProperty = getDescription("longField", descriptions); + assertThat(longFieldProperty.getTypeInformation(), equalTo(new TypeInfo(long.class))); + assertThat(longFieldProperty.getComments(), sameInstance(BeanPropertyComments.EMPTY)); + + BeanPropertyDescription uuidProperty = getDescription("uuid", descriptions); + assertThat(uuidProperty.getTypeInformation(), equalTo(new TypeInfo(UUID.class))); + assertThat(uuidProperty.getComments(), sameInstance(BeanPropertyComments.EMPTY)); } @Test @@ -62,35 +68,20 @@ void shouldReturnEmptyListForNonBeanClass() { assertThat(factory.getAllProperties(List.class), empty()); } - @Test - void shouldHandleBooleanMethodsAndMatchWithFields() { - // given / when - List properties = new ArrayList<>(factory.getAllProperties(BooleanTestBean.class)); - - // then - assertThat(properties, hasSize(4)); - assertThat(transform(properties, BeanPropertyDescription::getName), - containsInAnyOrder("active", "isField", "empty", "isNotMatched")); - - // First two elements can be mapped to fields, so check their order. For the two unknown ones, we don't make any guarantees - assertThat(properties.get(0).getName(), equalTo("active")); - assertThat(properties.get(1).getName(), equalTo("isField")); - } - @Test void shouldNotConsiderTransientFields() { // given / when - Collection properties = factory.getAllProperties(BeanWithTransientFields.class); + Collection properties = factory.getAllProperties(BeanWithTransientFields.class); // then assertThat(properties, hasSize(2)); - assertThat(transform(properties, BeanPropertyDescription::getName), contains("name", "mandatory")); + assertThat(transform(properties, BeanPropertyDescription::getName), contains("name", "isMandatory")); } @Test void shouldBeAwareOfInheritanceAndRespectOrder() { // given / when - Collection properties = factory.getAllProperties(Middle.class); + Collection properties = factory.getAllProperties(Middle.class); // then assertThat(properties, hasSize(3)); @@ -100,7 +91,7 @@ void shouldBeAwareOfInheritanceAndRespectOrder() { @Test void shouldLetChildFieldsOverrideParentFields() { // given / when - Collection properties = factory.getAllProperties(Child.class); + Collection properties = factory.getAllProperties(Child.class); // then assertThat(properties, hasSize(5)); @@ -111,7 +102,7 @@ void shouldLetChildFieldsOverrideParentFields() { @Test void shouldUseExportName() { // given / when - Collection properties = factory.getAllProperties(AnnotatedEntry.class); + Collection properties = factory.getAllProperties(AnnotatedEntry.class); // then assertThat(properties, hasSize(2)); @@ -144,8 +135,8 @@ void shouldThrowForWhenExportNameIsNullForProperty() { @Test void shouldReturnCommentsWithUuidIfNotRepeatable() { // given / when - Collection sampleBeanProperties = factory.getAllProperties(SampleBean.class); - Collection sampleBeanProperties2 = factory.getAllProperties(SampleBean.class); + Collection sampleBeanProperties = factory.getAllProperties(SampleBean.class); + Collection sampleBeanProperties2 = factory.getAllProperties(SampleBean.class); // then BeanPropertyComments sizeComments = getDescription("size", sampleBeanProperties).getComments(); @@ -161,7 +152,7 @@ void shouldReturnCommentsWithUuidIfNotRepeatable() { @Test void shouldReturnCommentsWithoutUuid() { // given / when - Collection execDetailsProperties = factory.getAllProperties(ExecutionDetails.class); + Collection execDetailsProperties = factory.getAllProperties(ExecutionDetails.class); // then BeanPropertyComments executorComments = getDescription("executor", execDetailsProperties).getComments(); @@ -175,7 +166,7 @@ void shouldReturnCommentsWithoutUuid() { @Test void shouldPickUpCustomNameFromField() { // given / when - List properties = new ArrayList<>(factory.getAllProperties(BeanWithExportName.class)); + List properties = new ArrayList<>(factory.getAllProperties(BeanWithExportName.class)); // then assertThat(properties, hasSize(3)); @@ -190,7 +181,7 @@ void shouldPickUpCustomNameFromField() { @Test void shouldPickUpCustomNameFromFieldsIncludingInheritance() { // given / when - List properties = new ArrayList<>(factory.getAllProperties(BeanWithExportNameExtension.class)); + List properties = new ArrayList<>(factory.getAllProperties(BeanWithExportNameExtension.class)); // then assertThat(properties, hasSize(4)); @@ -205,7 +196,7 @@ void shouldPickUpCustomNameFromFieldsIncludingInheritance() { } private static BeanPropertyDescription getDescription(String name, - Collection descriptions) { + Collection descriptions) { for (BeanPropertyDescription description : descriptions) { if (name.equals(description.getName())) { return description; @@ -219,138 +210,17 @@ private static final class SampleBean { private String name; @Comment("Size of this entry (cm)") private int size; - private long longField; // static "getter" method - private UUID uuid = UUID.randomUUID(); // no setter - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getSize() { - return size; - } - - public void setSize(int size) { - this.size = size; - } - - public UUID getUuid() { - return uuid; - } - - public static long getLongField() { - // Method with normal getter name is static! - return 0; - } - - public void setLongField(long longField) { - this.longField = longField; - } - } - - private static final class BooleanTestBean { - private boolean isEmpty; - private Boolean isReference; - private boolean active; - private String isString; - private boolean isField; - private boolean notMatched; - - public boolean isEmpty() { - return isEmpty; - } - - public void setEmpty(boolean empty) { - isEmpty = empty; - } - - public Boolean isReference() { // "is" getter only supported for primitive boolean - return isReference; - } + private long longField; + private UUID uuid = UUID.randomUUID(); - public void setReference(Boolean isReference) { - this.isReference = isReference; - } - - public boolean isActive() { - return active; - } - - public void setActive(boolean active) { - this.active = active; - } - - public String isString() { // "is" only supported for boolean - return isString; - } - - public void setString(String isString) { - this.isString = isString; - } - - public boolean getIsField() { - return isField; - } - - public void setIsField(boolean field) { - this.isField = field; - } - - // ----------------- - // notMatched: creates a valid property "isNotMatched" picked up by the introspector, - // but we should not match this to the field `notMatched`. - // ----------------- - public boolean getIsNotMatched() { - return notMatched; - } - - public void setIsNotMatched(boolean notMatched) { - this.notMatched = notMatched; - } } private static final class BeanWithTransientFields { + private String name; private transient long tempId; private transient boolean isSaved; private boolean isMandatory; - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public long getTempId() { - return tempId; - } - - @Transient - public void setTempId(long tempId) { - this.tempId = tempId; - } - - @Transient - public boolean isSaved() { - return isSaved; - } - - public void setSaved(boolean saved) { - isSaved = saved; - } - - public boolean isMandatory() { - return isMandatory; - } - - public void setMandatory(boolean mandatory) { - isMandatory = mandatory; - } } } diff --git a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java index af7f07c3..17691ac2 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java @@ -43,7 +43,7 @@ void shouldHandlePropertyGetError() { @Test void shouldHaveAppropriateStringRepresentation() { // given - Collection properties = new BeanDescriptionFactoryImpl() + Collection properties = new BeanDescriptionFactoryImpl() .collectAllProperties(AnnotatedEntry.class); BeanPropertyDescription hasIdProperty = properties.stream() .filter(prop -> "has-id".equals(prop.getName())).findFirst().get(); diff --git a/src/test/java/ch/jalu/configme/demo/beans/Country.java b/src/test/java/ch/jalu/configme/demo/beans/Country.java index e041055c..e6239ca5 100644 --- a/src/test/java/ch/jalu/configme/demo/beans/Country.java +++ b/src/test/java/ch/jalu/configme/demo/beans/Country.java @@ -1,6 +1,5 @@ package ch.jalu.configme.demo.beans; -import java.beans.Transient; import java.util.List; /** @@ -37,7 +36,6 @@ public void setNeighbors(List neighbors) { this.neighbors = neighbors; } - @Transient public boolean isTemporary() { return isTemporary; } diff --git a/src/test/java/ch/jalu/configme/demo/beans/UserBase.java b/src/test/java/ch/jalu/configme/demo/beans/UserBase.java index fcdf537c..a5d32847 100644 --- a/src/test/java/ch/jalu/configme/demo/beans/UserBase.java +++ b/src/test/java/ch/jalu/configme/demo/beans/UserBase.java @@ -1,6 +1,6 @@ package ch.jalu.configme.demo.beans; -import java.beans.Transient; +import ch.jalu.configme.beanmapper.Ignore; /** * User base bean. @@ -11,6 +11,7 @@ public class UserBase { private User richie; private User lionel; private double version; + @Ignore private transient int build; public User getBobby() { @@ -45,10 +46,6 @@ public void setVersion(double version) { this.version = version; } - @Transient - public int getBuild() { - return build; - } public void setBuild(int build) { this.build = build; diff --git a/src/test/java/ch/jalu/configme/samples/inheritance/Child.java b/src/test/java/ch/jalu/configme/samples/inheritance/Child.java index 52010c90..f682e02a 100644 --- a/src/test/java/ch/jalu/configme/samples/inheritance/Child.java +++ b/src/test/java/ch/jalu/configme/samples/inheritance/Child.java @@ -1,30 +1,11 @@ package ch.jalu.configme.samples.inheritance; -import java.beans.Transient; - /** * Child class. */ public class Child extends Middle { private int importance; + private boolean temporary; - @Override - public boolean isTemporary() { - return super.isTemporary(); - } - - @Override - @Transient(false) - public void setTemporary(boolean temporary) { - super.setTemporary(temporary); - } - - public int getImportance() { - return importance; - } - - public void setImportance(int importance) { - this.importance = importance; - } } diff --git a/src/test/java/ch/jalu/configme/samples/inheritance/Middle.java b/src/test/java/ch/jalu/configme/samples/inheritance/Middle.java index c4a6688e..961bdccc 100644 --- a/src/test/java/ch/jalu/configme/samples/inheritance/Middle.java +++ b/src/test/java/ch/jalu/configme/samples/inheritance/Middle.java @@ -8,19 +8,4 @@ public class Middle extends Parent { private String name; private float ratio; - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public float getRatio() { - return ratio; - } - - public void setRatio(float ratio) { - this.ratio = ratio; - } } diff --git a/src/test/java/ch/jalu/configme/samples/inheritance/Parent.java b/src/test/java/ch/jalu/configme/samples/inheritance/Parent.java index 1fcde387..3dc966a6 100644 --- a/src/test/java/ch/jalu/configme/samples/inheritance/Parent.java +++ b/src/test/java/ch/jalu/configme/samples/inheritance/Parent.java @@ -1,7 +1,5 @@ package ch.jalu.configme.samples.inheritance; -import java.beans.Transient; - /** * Parent class. */ @@ -10,20 +8,4 @@ public class Parent { private long id; private transient boolean temporary; - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public boolean isTemporary() { - return temporary; - } - - @Transient - public void setTemporary(boolean temporary) { - this.temporary = temporary; - } }