Skip to content

Commit

Permalink
Implement Diagnostics
Browse files Browse the repository at this point in the history
Implemented Diagnostics, allowing developers to get advanced insight
into the mappings and conversion performed by BeanMapper. This is
especially useful when a developer feels that a mapping (usually a
collection mapping) is taking an exorbitant amount of time.
  • Loading branch information
marcus-talbot42 committed May 8, 2024
1 parent 0a15cad commit c98a705
Show file tree
Hide file tree
Showing 32 changed files with 1,431 additions and 105 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Added diagnostics, allowing users to check what mappings and conversion are performed as part of a given mapping.

### NB

Diagnostics are only collected when enabled. Diagnostics can be enabled by using the `BeanMapper.wrap(DiagnosticsDetailLevel)`-method.

Each DiagnosticsDetailLevel comes with a different logging strategy.
* `DiagnosticsDetailLevel.COUNT_TOTAL`: Logs the total number of mappings and conversion performed as part of the top-level mapping, in the format: `[Mapping ] SourceClass -> TargetClass {total mappings: 38, total conversions: 70, max depth: 7}`
* `DiagnosticsDetailLevel.COUNT_PER_PAIR`: Logs the total number of mappings and conversion per type pair.
* `DiagnosticsDetailLevel.TREE_COMPLETE`: Logs the entire mapping and conversion tree.


## [4.1.5]

### Fixed
Expand Down
27 changes: 14 additions & 13 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>io.beanmapper</groupId>
Expand Down Expand Up @@ -52,21 +53,21 @@
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<javassist.version>3.29.2-GA</javassist.version>
<slf4j.version>2.0.3</slf4j.version>
<javassist.version>3.30.0-GA</javassist.version>
<slf4j.version>2.0.13</slf4j.version>

<jackson-databind.version>2.13.4.2</jackson-databind.version>
<junit.version>5.9.0</junit.version>
<jackson-databind.version>2.17.0</jackson-databind.version>
<junit.version>5.10.2</junit.version>

<maven.compiler.plugin.version>3.10.1</maven.compiler.plugin.version>
<maven.surefire.plugin.version>2.22.2</maven.surefire.plugin.version>
<maven.release.plugin.version>2.5.3</maven.release.plugin.version>
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
<maven.surefire.plugin.version>3.2.5</maven.surefire.plugin.version>
<maven.release.plugin.version>3.0.1</maven.release.plugin.version>
<nexus.staging.plugin.version>1.6.13</nexus.staging.plugin.version>
<jacoco.maven.plugin.version>0.8.8</jacoco.maven.plugin.version>
<owasp.dependency.check.version>7.2.1</owasp.dependency.check.version>
<maven.javadoc.plugin.version>3.4.1</maven.javadoc.plugin.version>
<maven.source.plugin.version>3.2.1</maven.source.plugin.version>
<maven.gpg.plugin.version>3.0.1</maven.gpg.plugin.version>
<jacoco.maven.plugin.version>0.8.12</jacoco.maven.plugin.version>
<owasp.dependency.check.version>9.1.0</owasp.dependency.check.version>
<maven.javadoc.plugin.version>3.6.3</maven.javadoc.plugin.version>
<maven.source.plugin.version>3.3.1</maven.source.plugin.version>
<maven.gpg.plugin.version>3.2.3</maven.gpg.plugin.version>
</properties>

<dependencies>
Expand Down
113 changes: 81 additions & 32 deletions src/main/java/io/beanmapper/BeanMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@

import io.beanmapper.config.BeanMapperBuilder;
import io.beanmapper.config.Configuration;
import io.beanmapper.config.DiagnosticsConfiguration;
import io.beanmapper.config.DiagnosticsConfigurationImpl;
import io.beanmapper.config.OverrideConfiguration;
import io.beanmapper.strategy.MapStrategyType;
import io.beanmapper.utils.diagnostics.DiagnosticsDetailLevel;
import io.beanmapper.utils.diagnostics.tree.CollectionMappingDiagnosticNode;
import io.beanmapper.utils.diagnostics.tree.DiagnosticsNode;
import io.beanmapper.utils.diagnostics.tree.MapMappingDiagnosticsNode;
import io.beanmapper.utils.diagnostics.tree.MappingDiagnosticsNode;

/**
* Class that is responsible first for understanding the semantics of the source and target
Expand All @@ -23,26 +31,58 @@ public final class BeanMapper {

private final Configuration configuration;

/**
*/
public BeanMapper(Configuration configuration) {
this.configuration = configuration;
}

private <S> void addDiagnosticsNode(S source) {
if (configuration instanceof DiagnosticsConfiguration dc && dc.isInDiagnosticsMode()) {
if (dc.getParentConfiguration().orElse(null) instanceof DiagnosticsConfigurationImpl dci) {
dci.getBeanMapperDiagnostics().ifPresent(bd -> bd.getDiagnostics().clear());
}
Class<S> sourceClass = source != null ? (Class<S>) configuration.getBeanUnproxy().unproxy(source.getClass()) : (Class<S>) Void.class;
DiagnosticsNode<?, ?> node = getsDiagnosticsNode(source, sourceClass);
dc.setBeanMapperDiagnostics(node);
if (configuration instanceof OverrideConfiguration) {
dc.getParentConfiguration()
.map(DiagnosticsConfiguration.class::cast)
.orElseThrow()
.getBeanMapperDiagnostics().ifPresent(bd -> bd.add(node));
}
}
}

private <S> DiagnosticsNode<?, ?> getsDiagnosticsNode(S source, Class<S> sourceClass) {
DiagnosticsNode<?, ?> node = new MappingDiagnosticsNode<S, Object>(sourceClass, configuration.getTargetClass());
if (source instanceof Collection<?>) {
node = new CollectionMappingDiagnosticNode<>(source, sourceClass, configuration.getPreferredCollectionClass(), configuration.getTargetClass());
}
if (source instanceof Map<?, ?>) {
node = new MapMappingDiagnosticsNode<>((Map) source, (Class<Map>) sourceClass, configuration.getPreferredCollectionClass(), configuration.getTargetClass());
}
return node;
}

public <S, T> T map(S source) {
addDiagnosticsNode(source);
if (source == null && !configuration.getUseNullValue()) {
// noinspection unchecked
return (T) this.getConfiguration().getDefaultValueForClass(this.getConfiguration().getTargetClass());
}
return MapStrategyType.getStrategy(this, configuration).map(source);
T result = MapStrategyType.getStrategy(this, configuration).map(source);
if (this.configuration.getParentConfiguration().orElseThrow() instanceof DiagnosticsConfigurationImpl dc) {
dc.getDiagnosticsLogger().ifPresent(dl -> dl.log(((DiagnosticsConfiguration) configuration).getBeanMapperDiagnostics().orElse(null)));
}
return result;
}

/**
* Copies the values from the source object to an existing target instance
*
* @param source source instance of the properties
* @param target target instance for the properties
* @param <S> the instance from which the properties get copied.
* @param <T> the instance to which the properties get copied
* @param <S> the instance from which the properties get copied.
* @param <T> the instance to which the properties get copied
* @return the original target instance containing all applicable properties
*/
public <S, T> T map(S source, T target) {
Expand All @@ -54,10 +94,11 @@ public <S, T> T map(S source, T target) {

/**
* Copies the values from the optional source object to a newly constructed target instance
* @param source optional source instance of the properties
*
* @param source optional source instance of the properties
* @param targetClass class of the target, needs to be constructed as the target instance
* @param <S> the instance from which the properties get copied
* @param <T> the instance to which the properties get copied
* @param <S> the instance from which the properties get copied
* @param <T> the instance to which the properties get copied
* @return the optional target instance containing all applicable properties
*/
public <S, T> Optional<T> map(Optional<S> source, Class<T> targetClass) {
Expand All @@ -76,9 +117,9 @@ public <S, T> Optional<T> map(Optional<S> source, Class<T> targetClass) {
* @param source Source instance of the properties.
* @param target Implementation of ParameterizedType, which provides the information necessary to map elements to
* the correct type.
* @param <S> Type of the source.
* @param <P> Type of the specific implementation of ParameterizedType used as the target.
* @return The result of the mapping.
* @param <S> Type of the source.
* @param <P> Type of the specific implementation of ParameterizedType used as the target.
*/
public <S, P extends ParameterizedType> Object map(S source, P target) {
if (source instanceof Collection<?> collection) {
Expand All @@ -94,10 +135,11 @@ public <S, P extends ParameterizedType> Object map(S source, P target) {

/**
* Copies the values from the source object to a newly constructed target instance
* @param source source instance of the properties
*
* @param source source instance of the properties
* @param targetClass class of the target, needs to be constructed as the target instance
* @param <S> The instance from which the properties get copied
* @param <T> the instance to which the properties get copied
* @param <S> The instance from which the properties get copied
* @param <T> the instance to which the properties get copied
* @return the target instance containing all applicable properties
*/
public <S, T> T map(S source, Class<T> targetClass) {
Expand All @@ -112,9 +154,9 @@ public <S, T> T map(S source, Class<T> targetClass) {
*
* @param sourceArray The source array.
* @param targetClass The class to which the elements from the source array will be converted to.
* @param <S> The type of the elements in the source array.
* @param <T> The type of the elements in the target array.
* @return A newly constructed array of the type of the target class.
* @param <S> The type of the elements in the source array.
* @param <T> The type of the elements in the target array.
*/
public <S, T> T[] map(S[] sourceArray, Class<T> targetClass) {
return Arrays.stream(sourceArray)
Expand All @@ -128,22 +170,23 @@ public <S, T> T[] map(S[] sourceArray, Class<T> targetClass) {
* <p>The type of the target collection is determined automatically from the actual type of the source collection.
* If the source </p>
*
* @param collection - The source collection
* @param collection - The source collection
* @param elementInCollectionClass - The class of each element in the target list.
* @param <S> The type up the elements in the source collection.
* @param <T> The type of the target, to which the source elements will be mapped.
* @return The target collection with mapped source collection elements.
* @param <S> The type up the elements in the source collection.
* @param <T> The type of the target, to which the source elements will be mapped.
*/
public <S, T> Collection<T> map(Collection<S> collection, Class<T> elementInCollectionClass) {
return mapCollection(collection, elementInCollectionClass);
}

/**
* Maps the source list of elements to a new target list. Convenience operator
* @param list the source list
*
* @param list the source list
* @param elementInListClass the class of each element in the target list
* @param <S> the class type of the source list
* @param <T> the class type of an element in the target list
* @param <S> the class type of the source list
* @param <T> the class type of an element in the target list
* @return the target list with mapped source list elements
*/
public <S, T> List<T> map(List<S> list, Class<T> elementInListClass) {
Expand All @@ -152,10 +195,11 @@ public <S, T> List<T> map(List<S> list, Class<T> elementInListClass) {

/**
* Maps the source set of elements to a new target set. Convenience operator
* @param set the source set
*
* @param set the source set
* @param elementInSetClass the class of each element in the target set
* @param <S> the class type of the source set
* @param <T> the class type of an element in the target set
* @param <S> the class type of the source set
* @param <T> the class type of an element in the target set
* @return the target set with mapped source set elements
*/
public <S, T> Set<T> map(Set<S> set, Class<T> elementInSetClass) {
Expand All @@ -165,23 +209,24 @@ public <S, T> Set<T> map(Set<S> set, Class<T> elementInSetClass) {
/**
* Maps the source queue to a new target queue. Convenience operator.
*
* @param queue The source queue.
* @param queue The source queue.
* @param elementInQueueClass The class of each element in the target queue.
* @param <S> The class type of the source queue.
* @param <T> The class type of the elements in the target queue.
* @return The target queue with mapped source queue elements.
* @param <S> The class type of the source queue.
* @param <T> The class type of the elements in the target queue.
*/
public <S, T> Queue<T> map(Queue<S> queue, Class<T> elementInQueueClass) {
return mapCollection(queue, elementInQueueClass);
}

/**
* Maps the source map of elements to a new target map. Convenience operator
* @param map the source map
*
* @param map the source map
* @param mapValueClass the class of each value in the target map
* @param <K> the class type of a key in both source and target map
* @param <T> the class type of a value in the target map
* @param <S> the class type of the source map
* @param <K> the class type of a key in both source and target map
* @param <T> the class type of a value in the target map
* @param <S> the class type of the source map
* @return the target map with literal source set keys and mapped source set values
*/
public <K, S, T> Map<K, T> map(Map<K, S> map, Class<T> mapValueClass) {
Expand All @@ -197,7 +242,11 @@ private <S, T, E> T mapCollection(S collection, Class<E> elementInCollection) {
}

public BeanMapperBuilder wrap() {
return new BeanMapperBuilder(configuration);
return wrap(DiagnosticsDetailLevel.DISABLED);
}

public BeanMapperBuilder wrap(DiagnosticsDetailLevel detailLevel) {
return new BeanMapperBuilder(configuration, detailLevel);
}

public Configuration getConfiguration() {
Expand Down
27 changes: 22 additions & 5 deletions src/main/java/io/beanmapper/config/BeanMapperBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import io.beanmapper.core.converter.impl.StringToIntegerConverter;
import io.beanmapper.core.converter.impl.StringToLongConverter;
import io.beanmapper.core.unproxy.BeanUnproxy;
import io.beanmapper.exceptions.BeanConfigurationOperationNotAllowedException;
import io.beanmapper.utils.Trinary;
import io.beanmapper.utils.diagnostics.DiagnosticsDetailLevel;

public class BeanMapperBuilder {

Expand All @@ -50,7 +52,21 @@ public BeanMapperBuilder() {
}

public BeanMapperBuilder(Configuration configuration) {
this.configuration = new OverrideConfiguration(configuration);
this(configuration, DiagnosticsDetailLevel.DISABLED);
}

public BeanMapperBuilder(Configuration configuration, DiagnosticsDetailLevel detailLevel) {
if (!detailLevel.isEnabled()) {
this.configuration = new OverrideConfiguration(configuration);
return;
}
if (detailLevel != DiagnosticsDetailLevel.DISABLED && configuration instanceof DiagnosticsConfigurationImpl) {
throw new BeanConfigurationOperationNotAllowedException(
"Cannot set detail-level for diagnostics when the detail-level is already set. Please make sure you did not set a detail-level in your application settings.");
}
this.configuration = !(configuration instanceof DiagnosticsConfiguration dc) || (!dc.isInDiagnosticsMode())
? new DiagnosticsConfigurationImpl(configuration, detailLevel)
: new OverrideConfiguration(configuration);
}

public BeanMapperBuilder withoutDefaultConverters() {
Expand Down Expand Up @@ -204,10 +220,10 @@ public BeanMapperBuilder setUseNullValue() {
* Adds a mapping for a default value to the configuration.
*
* @param targetClass The class that the value is the default for.
* @param value The value that will serve as the default for the target-class.
* @param value The value that will serve as the default for the target-class.
* @param <T> The type op the targetClass.
* @param <V> The type of the value.
* @return This instance.
* @param <T> The type op the targetClass.
* @param <V> The type of the value.
*/
public <T, V> BeanMapperBuilder addCustomDefaultValue(Class<T> targetClass, V value) {
this.configuration.addCustomDefaultValueForClass(targetClass, value);
Expand All @@ -216,6 +232,7 @@ public <T, V> BeanMapperBuilder addCustomDefaultValue(Class<T> targetClass, V va

public BeanMapper build() {
BeanMapper beanMapper = new BeanMapper(configuration);

// Custom collection handlers must be registered before default ones
addCollectionHandlers(customCollectionHandlers);
if (this.configuration instanceof CoreConfiguration)
Expand Down Expand Up @@ -255,7 +272,7 @@ private void addDefaultConverters() {
attachConverter(new RecordToAnyConverter());
attachConverter(new ObjectToOptionalConverter());

for (CollectionHandler collectionHandler : configuration.getCollectionHandlers()) {
for (CollectionHandler<?> collectionHandler : configuration.getCollectionHandlers()) {
attachConverter(new CollectionConverter(collectionHandler));
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/io/beanmapper/config/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.beanmapper.annotations.BeanCollectionUsage;
import io.beanmapper.annotations.LogicSecuredCheck;
Expand Down Expand Up @@ -467,4 +468,8 @@ public interface Configuration {
BeanConverterStore getBeanConverterStore();

<S, T> BeanConverter getBeanConverter(Class<S> source, Class<T> target);

default Optional<Configuration> getParentConfiguration() {
return Optional.empty();
}
}
25 changes: 25 additions & 0 deletions src/main/java/io/beanmapper/config/DiagnosticsConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.beanmapper.config;

import java.util.Optional;

import io.beanmapper.utils.diagnostics.DiagnosticsDetailLevel;
import io.beanmapper.utils.diagnostics.logging.DiagnosticsLogger;
import io.beanmapper.utils.diagnostics.tree.DiagnosticsNode;

public interface DiagnosticsConfiguration extends Configuration {

boolean isInDiagnosticsMode();

<S, T> Optional<DiagnosticsNode<S, T>> getBeanMapperDiagnostics();

<S, T> void setBeanMapperDiagnostics(DiagnosticsNode<S, T> diagnostics);

default Optional<DiagnosticsLogger> getDiagnosticsLogger(){
return Optional.empty();
}

int getMappingDepth();

DiagnosticsDetailLevel getDiagnosticsDetailLevel();

}
Loading

0 comments on commit c98a705

Please sign in to comment.