diff --git a/README.md b/README.md index 4d7916d5..68416bc6 100644 --- a/README.md +++ b/README.md @@ -94,3 +94,4 @@ Find the rest of the documentation on [beanmapper.io](http://beanmapper.io). WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + diff --git a/src/main/java/io/beanmapper/annotations/BeanCollectionUsage.java b/src/main/java/io/beanmapper/annotations/BeanCollectionUsage.java index 45dae5af..dbfdc87b 100644 --- a/src/main/java/io/beanmapper/annotations/BeanCollectionUsage.java +++ b/src/main/java/io/beanmapper/annotations/BeanCollectionUsage.java @@ -1,5 +1,7 @@ package io.beanmapper.annotations; +import static io.beanmapper.utils.CanonicalClassName.determineCanonicalClassName; + /** * Determines how to deal with the target collection. If the value is set to CONSTRUCT, it * will also be recreated, even if it already exists. If REUSE is set, it will attempt to @@ -34,7 +36,7 @@ public boolean mustClear() { public boolean mustConstruct(Object targetCollection) { if (targetCollection != null && - targetCollection.getClass().getCanonicalName().startsWith("java.util.Collections.")) { + determineCanonicalClassName(targetCollection.getClass()).startsWith("java.util.Collections.")) { return true; } return this.construct || targetCollection == null; diff --git a/src/main/java/io/beanmapper/config/OverrideField.java b/src/main/java/io/beanmapper/config/OverrideField.java index fde4bcaf..502d641f 100644 --- a/src/main/java/io/beanmapper/config/OverrideField.java +++ b/src/main/java/io/beanmapper/config/OverrideField.java @@ -38,5 +38,4 @@ public T get() { } return value; } - } diff --git a/src/main/java/io/beanmapper/config/StrictMappingProperties.java b/src/main/java/io/beanmapper/config/StrictMappingProperties.java index ea87a74e..d6014960 100644 --- a/src/main/java/io/beanmapper/config/StrictMappingProperties.java +++ b/src/main/java/io/beanmapper/config/StrictMappingProperties.java @@ -4,6 +4,8 @@ import io.beanmapper.core.unproxy.SkippingBeanUnproxy; import io.beanmapper.utils.BeanMapperPerformanceLogger; +import static io.beanmapper.utils.CanonicalClassName.determineCanonicalClassName; + public class StrictMappingProperties { private BeanUnproxy beanUnproxy; @@ -89,11 +91,11 @@ public BeanPair createBeanPair(Class sourceClass, Class targetClass) { return beanPair; } if (strictSourceSuffix != null && - beanPair.getSourceClass().getCanonicalName().endsWith(strictSourceSuffix)) { + determineCanonicalClassName(sourceClass).endsWith(strictSourceSuffix)) { beanPair = beanPair.withStrictSource(); } if (strictTargetSuffix != null && - beanPair.getTargetClass().getCanonicalName().endsWith(strictTargetSuffix)) { + determineCanonicalClassName(targetClass).endsWith(strictTargetSuffix)) { beanPair = beanPair.withStrictTarget(); } return beanPair; diff --git a/src/main/java/io/beanmapper/core/BeanMatch.java b/src/main/java/io/beanmapper/core/BeanMatch.java index 5437ed16..85123648 100644 --- a/src/main/java/io/beanmapper/core/BeanMatch.java +++ b/src/main/java/io/beanmapper/core/BeanMatch.java @@ -11,6 +11,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static io.beanmapper.utils.CanonicalClassName.determineCanonicalClassName; + public class BeanMatch { private static final Logger log = LoggerFactory.getLogger(BeanMatch.class); @@ -91,8 +93,8 @@ private List validateMappingRequirements(Map BeanProperty targetBeanProperty = matchedField.targetBeanProperty(); if (sourceBeanProperty == null || targetBeanProperty == null) { missingMatches.add(sourceBeanProperty == null ? - targetBeanProperty : - sourceBeanProperty); + targetBeanProperty : + sourceBeanProperty); } } return missingMatches; @@ -111,8 +113,9 @@ private void checkForMandatoryUnmatchedNodes(String side, Class containingCla for (Map.Entry entry : nodes.entrySet()) { BeanProperty currentField = entry.getValue(); if (currentField.isUnmatched()) { - log.error("{} {} has no match for property {}", side, containingClass.getCanonicalName() != null ? containingClass.getCanonicalName() : containingClass.getDeclaringClass().getCanonicalName(), entry.getKey()); - throw new BeanNoSuchPropertyException(side + " " + containingClass.getCanonicalName() + " has no match for property " + entry.getKey()); + String canonicalClassName = determineCanonicalClassName(containingClass); + log.error("{} {} has no match for property {}", side, canonicalClassName, entry.getKey()); + throw new BeanNoSuchPropertyException(side + " " + canonicalClassName + " has no match for property " + entry.getKey()); } } } diff --git a/src/main/java/io/beanmapper/core/BeanMatchStore.java b/src/main/java/io/beanmapper/core/BeanMatchStore.java index f13fc854..a4942cbf 100644 --- a/src/main/java/io/beanmapper/core/BeanMatchStore.java +++ b/src/main/java/io/beanmapper/core/BeanMatchStore.java @@ -4,6 +4,7 @@ import static io.beanmapper.core.converter.collections.CollectionElementType.EMPTY_COLLECTION_ELEMENT_TYPE; import static io.beanmapper.core.converter.collections.CollectionElementType.derived; import static io.beanmapper.core.converter.collections.CollectionElementType.set; +import static io.beanmapper.utils.CanonicalClassName.determineCanonicalClassName; import java.beans.IntrospectionException; import java.beans.Introspector; @@ -74,29 +75,29 @@ public BeanMatch getBeanMatch(BeanPair beanPair) { } private boolean containsKeyForTargetClass(Map targetsForSource, BeanPair beanPair) { - return targetsForSource.containsKey(beanPair.getTargetClass().getCanonicalName()); + return targetsForSource.containsKey(determineCanonicalClassName(beanPair.getTargetClass())); } public BeanMatch addBeanMatch(BeanMatch beanMatch) { Map targetsForSource = getTargetsForSource(beanMatch.getSourceClass()); if (targetsForSource == null) { targetsForSource = new TreeMap<>(); - store.put(beanMatch.getSourceClass().getCanonicalName(), targetsForSource); + store.put(determineCanonicalClassName(beanMatch.getSourceClass()), targetsForSource); } storeTarget(targetsForSource, beanMatch.getTargetClass(), beanMatch); return beanMatch; } private Map getTargetsForSource(Class sourceClass) { - return store.get(sourceClass.getCanonicalName()); + return store.get(determineCanonicalClassName(sourceClass)); } private BeanMatch getTarget(Map targetsForSource, Class target) { - return targetsForSource.get(target.getCanonicalName()); + return targetsForSource.get(determineCanonicalClassName(target)); } private void storeTarget(Map targetsForSource, Class target, BeanMatch beanMatch) { - targetsForSource.put(target.getCanonicalName(), beanMatch); + targetsForSource.put(determineCanonicalClassName(target), beanMatch); } private BeanMatch determineBeanMatch(BeanPair beanPair) { @@ -283,11 +284,9 @@ private BeanPropertyWrapper dealWithBeanProperty(BeanPropertyMatchupDirection ma new BeanPropertyCreator(matchupDirection.getInverse(), otherType, wrapper.getName()) .determineNodesForPath()); } catch (BeanNoSuchPropertyException err) { - BeanMapperTraceLogger.log(""" BeanNoSuchPropertyException thrown by BeanMatchStore#dealWithBeanProperty(BeanPropertyMatchupDirection, Map, Class, PropertyAccessor), for {}. {}""", wrapper.getName(), err.getMessage()); - } } return wrapper; diff --git a/src/main/java/io/beanmapper/core/BeanStrictMappingRequirementsException.java b/src/main/java/io/beanmapper/core/BeanStrictMappingRequirementsException.java index 77319a25..5ebdd77e 100644 --- a/src/main/java/io/beanmapper/core/BeanStrictMappingRequirementsException.java +++ b/src/main/java/io/beanmapper/core/BeanStrictMappingRequirementsException.java @@ -8,6 +8,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static io.beanmapper.utils.CanonicalClassName.determineCanonicalClassName; + public class BeanStrictMappingRequirementsException extends RuntimeException { private static final Logger log = LoggerFactory.getLogger(BeanStrictMappingRequirementsException.class); @@ -30,9 +32,9 @@ private void logErrors(List validationMessages) { log.error(""" Missing matching properties for source [{}] {} > target [{}] {} for fields: """, - validationMessage.getSourceClass().getCanonicalName(), + determineCanonicalClassName(validationMessage.getSourceClass()), (validationMessage.isSourceStrict() ? "*" : ""), - validationMessage.getTargetClass().getCanonicalName(), + determineCanonicalClassName(validationMessage.getTargetClass()), (validationMessage.isTargetStrict() ? "*" : "")); for (BeanProperty field : validationMessage.getFields()) { diff --git a/src/main/java/io/beanmapper/core/collections/AbstractCollectionHandler.java b/src/main/java/io/beanmapper/core/collections/AbstractCollectionHandler.java index e0bf205e..56b6bc64 100644 --- a/src/main/java/io/beanmapper/core/collections/AbstractCollectionHandler.java +++ b/src/main/java/io/beanmapper/core/collections/AbstractCollectionHandler.java @@ -91,4 +91,4 @@ public int getGenericParameterIndex() { return 0; } -} +} \ No newline at end of file diff --git a/src/main/java/io/beanmapper/core/constructor/DefaultBeanInitializer.java b/src/main/java/io/beanmapper/core/constructor/DefaultBeanInitializer.java index db608e0c..f626d407 100644 --- a/src/main/java/io/beanmapper/core/constructor/DefaultBeanInitializer.java +++ b/src/main/java/io/beanmapper/core/constructor/DefaultBeanInitializer.java @@ -54,5 +54,4 @@ private Object[] mapParameterizedArguments(Type[] constructorParameterTypes, Obj } return mappedArguments; } - } diff --git a/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java b/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java index ce357150..efe5736a 100644 --- a/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java +++ b/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java @@ -76,8 +76,8 @@ public T map(final S source) { * present. * * @param sourceClass The class of the source-object. - * @param The type of the sourceClass. * @return A Map containing the fields of the source-class, mapped by the name of the field, or the value of an available BeanAlias. + * @param The type of the sourceClass. */ private Map getFieldsOfClass(final Class sourceClass) { return Arrays.stream(sourceClass.getDeclaredFields()) @@ -176,7 +176,7 @@ private Object[] getConstructorArgumentsMappedToCorrectTargetType(final Paramete } else { var parameterType = parameters[i].getType(); if (Collection.class.isAssignableFrom(parameterType) || Optional.class.isAssignableFrom(parameterType) - || Map.class.isAssignableFrom(parameterType)) { + || Map.class.isAssignableFrom(parameterType)) { arguments[i] = this.getBeanMapper().map(values.get(i), (ParameterizedType) parameters[i].getParameterizedType()); } else { arguments[i] = values.get(i) != null && parameters[i].getType().equals(values.get(i).getClass()) ? @@ -232,8 +232,8 @@ private Constructor getSuitableConstructor(final Map sourc var recordConstruct = (BeanRecordConstruct) canonicalConstructor.getAnnotation(BeanRecordConstruct.class); if (recordConstruct.constructMode() == BeanRecordConstructMode.EXCLUDE) throw new RecordNoAvailableConstructorsExceptions((Class) targetClass, "All available constructors have been " - + "annotated with @RecordConstruct(constructMode = RecordConstructMode.EXCLUDE). To enable mapping to the target record, please make at " - + "least one constructor available."); + + "annotated with @RecordConstruct(constructMode = RecordConstructMode.EXCLUDE). To enable mapping to the target record, please make at " + + "least one constructor available."); } return canonicalConstructor; } diff --git a/src/main/java/io/beanmapper/utils/CanonicalClassName.java b/src/main/java/io/beanmapper/utils/CanonicalClassName.java new file mode 100644 index 00000000..b08030e2 --- /dev/null +++ b/src/main/java/io/beanmapper/utils/CanonicalClassName.java @@ -0,0 +1,17 @@ +package io.beanmapper.utils; + +public class CanonicalClassName { + public static String determineCanonicalClassName(Class clazz) { + return clazz.getCanonicalName() == null ? + fallbackWhenCanonicalNameIsNull(clazz) : + clazz.getCanonicalName(); + } + + // See bug 190, https://github.com/42BV/beanmapper/issues/190 + // When an enum has abstract methods, calling getCanonicalName() on its class will return a null value. + // This fallback method will allow for the expected canonical name value to be returned. getCanonicalName + // must no longer be called directly. + private static String fallbackWhenCanonicalNameIsNull(Class clazz) { + return clazz.getSuperclass().getCanonicalName(); + } +} diff --git a/src/test/java/io/beanmapper/BeanMapperTest.java b/src/test/java/io/beanmapper/BeanMapperTest.java index 6cd64abc..7f9ab62b 100644 --- a/src/test/java/io/beanmapper/BeanMapperTest.java +++ b/src/test/java/io/beanmapper/BeanMapperTest.java @@ -130,19 +130,7 @@ import io.beanmapper.testmodel.encapsulate.source_annotated.Car; import io.beanmapper.testmodel.encapsulate.source_annotated.CarDriver; import io.beanmapper.testmodel.encapsulate.source_annotated.Driver; -import io.beanmapper.testmodel.enums.ColorEntity; -import io.beanmapper.testmodel.enums.ColorResult; -import io.beanmapper.testmodel.enums.ColorStringResult; -import io.beanmapper.testmodel.enums.Day; -import io.beanmapper.testmodel.enums.DayEnumSourceArraysAsList; -import io.beanmapper.testmodel.enums.EnumSourceArraysAsList; -import io.beanmapper.testmodel.enums.EnumTargetList; -import io.beanmapper.testmodel.enums.RGB; -import io.beanmapper.testmodel.enums.UserRole; -import io.beanmapper.testmodel.enums.UserRoleResult; -import io.beanmapper.testmodel.enums.WeekEntity; -import io.beanmapper.testmodel.enums.WeekResult; -import io.beanmapper.testmodel.enums.WeekStringResult; +import io.beanmapper.testmodel.enums.*; import io.beanmapper.testmodel.ignore.IgnoreSource; import io.beanmapper.testmodel.ignore.IgnoreTarget; import io.beanmapper.testmodel.initially_unmatched_source.SourceWithUnmatchedField; @@ -1977,6 +1965,13 @@ void testMapEntityWithMapToEntityResultWithMap() { assertNull(result.children.get("Klaas").children); } + @Test + void testEnumWithAbstractMethod() { + WithAbstractMethod enumValue = WithAbstractMethod.class.getEnumConstants()[0]; + ComplexEnumResult result = beanMapper.map(enumValue, ComplexEnumResult.class); + assertEquals(enumValue.name(), result.name); + } + private MyEntity createMyEntity() { MyEntity child = new MyEntity(); child.value = "Piet"; diff --git a/src/test/java/io/beanmapper/testmodel/enums/ComplexEnumResult.java b/src/test/java/io/beanmapper/testmodel/enums/ComplexEnumResult.java new file mode 100644 index 00000000..7552ec6d --- /dev/null +++ b/src/test/java/io/beanmapper/testmodel/enums/ComplexEnumResult.java @@ -0,0 +1,6 @@ +package io.beanmapper.testmodel.enums; + +public class ComplexEnumResult { + + public String name; +} \ No newline at end of file diff --git a/src/test/java/io/beanmapper/testmodel/enums/WithAbstractMethod.java b/src/test/java/io/beanmapper/testmodel/enums/WithAbstractMethod.java new file mode 100644 index 00000000..7c753ea1 --- /dev/null +++ b/src/test/java/io/beanmapper/testmodel/enums/WithAbstractMethod.java @@ -0,0 +1,13 @@ +package io.beanmapper.testmodel.enums; + +public enum WithAbstractMethod { + ONE_VALUE() { + @Override + public void someAbstractMethod() {} + }; + public abstract void someAbstractMethod(); + + public String getName() { + return name(); + } +} diff --git a/src/test/java/io/beanmapper/utils/CanonicalClassNameTest.java b/src/test/java/io/beanmapper/utils/CanonicalClassNameTest.java new file mode 100644 index 00000000..d3f23b5a --- /dev/null +++ b/src/test/java/io/beanmapper/utils/CanonicalClassNameTest.java @@ -0,0 +1,21 @@ +package io.beanmapper.utils; + +import io.beanmapper.testmodel.enums.Day; +import io.beanmapper.testmodel.enums.WithAbstractMethod; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CanonicalClassNameTest { + @Test + public void determineCanonicalClassNameOfOrdinaryEnum() { + assertEquals("io.beanmapper.testmodel.enums.Day", CanonicalClassName.determineCanonicalClassName(Day.MONDAY.getClass())); + } + + @Test + public void determineCanonicalClassNameOfEnumWithAbstractMethod() { + assertEquals("io.beanmapper.testmodel.enums.WithAbstractMethod", + CanonicalClassName.determineCanonicalClassName(WithAbstractMethod.ONE_VALUE.getClass())); + } +}