001/* 002 * Copyright 2023 the original author or authors. 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.test.valueobjects; 017 018import static de.cuioss.test.valueobjects.util.ReflectionHelper.handlePostProcessConfig; 019import static de.cuioss.test.valueobjects.util.ReflectionHelper.handlePropertyMetadata; 020import static de.cuioss.test.valueobjects.util.ReflectionHelper.scanBeanTypeForProperties; 021import static de.cuioss.test.valueobjects.util.ReflectionHelper.shouldScanClass; 022import static de.cuioss.tools.collect.CollectionLiterals.immutableList; 023import static org.junit.jupiter.api.Assertions.assertFalse; 024import static org.junit.jupiter.api.Assertions.assertNotNull; 025import static org.junit.jupiter.api.Assertions.assertTrue; 026 027import java.lang.reflect.Type; 028import java.util.ArrayList; 029import java.util.List; 030import java.util.Optional; 031import java.util.SortedSet; 032import java.util.TreeSet; 033import java.util.function.Function; 034 035import org.junit.jupiter.api.Test; 036 037import de.cuioss.test.generator.TypedGenerator; 038import de.cuioss.test.valueobjects.api.VerifyMapperConfiguration; 039import de.cuioss.test.valueobjects.api.property.PropertyConfig; 040import de.cuioss.test.valueobjects.api.property.PropertyConfigs; 041import de.cuioss.test.valueobjects.api.property.PropertyReflectionConfig; 042import de.cuioss.test.valueobjects.contract.MapperContractImpl; 043import de.cuioss.test.valueobjects.generator.dynamic.GeneratorResolver; 044import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator; 045import de.cuioss.test.valueobjects.objects.RuntimeProperties; 046import de.cuioss.test.valueobjects.objects.TestObjectProvider; 047import de.cuioss.test.valueobjects.objects.impl.BeanInstantiator; 048import de.cuioss.test.valueobjects.objects.impl.DefaultInstantiator; 049import de.cuioss.test.valueobjects.property.PropertyMetadata; 050import de.cuioss.test.valueobjects.util.AnnotationHelper; 051import de.cuioss.test.valueobjects.util.GeneratorRegistry; 052import de.cuioss.test.valueobjects.util.PropertyHelper; 053import de.cuioss.tools.base.Preconditions; 054import de.cuioss.tools.reflect.MoreReflection; 055import lombok.AccessLevel; 056import lombok.Getter; 057 058/** 059 * Allows to test a mapper implementing a {@link Function} to map a (pseudo-)DTO 060 * object based on whatever technology (FHIR, ...) to a DTO object. The actual 061 * test-method is {@link #verifyMapper()}. The mapper-test-configuration is 062 * defined with {@link VerifyMapperConfiguration} 063 * 064 * For simple uses-case, like well designed beans there is no special 065 * configuration needed 066 * <ul> 067 * <li>In case the mapper needs to be configured in a special way (not using the 068 * default constructor) you can overwrite {@link #getUnderTest()}</li> 069 * <li>The metadata / creation of the <em>source</em> objects can be adjusted in 070 * multiple ways: 071 * <ul> 072 * <li>{@link PropertyMetadata}: The annotations on class level affect the 073 * metadata for the source-objects</li> 074 * <li>{@link #resolveSourcePropertyMetadata()}: can be overwritten as an 075 * alternative for using {@link PropertyMetadata} annotations</li> 076 * <li>{@link #getSourceInstantiator(RuntimeProperties)}: If not overwritten the 077 * default implementation chooses the {@link BeanInstantiator} in order to 078 * generate source-objects.</li> 079 * </ul> 080 * </li> 081 * <li>The metadata <em>target</em> objects is derived by reflection. 082 * <em>Caution</em>: For more complex objects that can not be created by the 083 * generator framework you must provide either a {@link TypedGenerator}, see 084 * {@link GeneratorRegistry} or overwrite {@link #anyTargetObject()}</li> 085 * </ul> 086 * 087 * @author Oliver Wolff 088 * @param <M> Mapper: The type of the Mapper 089 * @param <S> Source: The type of the source-Objects to be mapped from 090 * @param <T> Target: The type of the source-Objects to be mapped to 091 */ 092@SuppressWarnings("squid:S2187") // Base class for tests 093public class MapperTest<M extends Function<S, T>, S, T> implements GeneratorRegistry, TestObjectProvider<M> { 094 095 @Getter(AccessLevel.PROTECTED) 096 private Class<M> mapperClass; 097 098 @Getter(AccessLevel.PROTECTED) 099 private Class<? extends S> sourceClass; 100 101 @Getter(AccessLevel.PROTECTED) 102 private Class<T> targetClass; 103 104 /** 105 * Reads the type information and fills the fields {@link #getMapperClass()}, 106 * {@link #getSourceClass()}, {@link #getTargetClass()}. It runs it checks only 107 * once 108 */ 109 @SuppressWarnings({ "unchecked" }) 110 protected void intializeTypeInformation() { 111 if (null == mapperClass) { 112 var parameterized = MoreReflection.extractParameterizedType(getClass()).orElseThrow( 113 () -> new IllegalArgumentException("Given type defines no generic Type: " + getClass())); 114 List<Type> types = immutableList(parameterized.getActualTypeArguments()); 115 Preconditions.checkArgument(3 == types.size(), "Super Class must provide 3 generic types in order to work"); 116 mapperClass = (Class<M>) MoreReflection.extractGenericTypeCovariantly(types.get(0)) 117 .orElseThrow(() -> new AssertionError("Unable to determine mapperClass from type" + getClass())); 118 assertNotNull(mapperClass, "Unable to determine mapperClass"); 119 assertFalse(mapperClass.isInterface(), 120 "This type only works with concrete implementations, but was the interface " + mapperClass); 121 sourceClass = (Class<S>) MoreReflection.extractGenericTypeCovariantly(types.get(1)) 122 .orElseThrow(() -> new AssertionError("Unable to determine sourceClass from type" + getClass())); 123 assertNotNull(sourceClass, "Unable to determine sourceClass"); 124 targetClass = (Class<T>) MoreReflection.extractGenericTypeCovariantly(types.get(2)) 125 .orElseThrow(() -> new AssertionError("Unable to determine targetClass from type" + getClass())); 126 assertNotNull(targetClass, "Unable to determine targetClass"); 127 } 128 } 129 130 /** 131 * Shorthand for calling 132 * {@link MapperTest#verifyMapper(PropertyReflectionConfig)} with {@code null} 133 */ 134 @Test 135 public void verifyMapper() { 136 verifyMapper(null); 137 } 138 139 /** 140 * The actual test-method to be run 141 * 142 * @param targetConfig providing configuration, may be null 143 */ 144 public void verifyMapper(PropertyReflectionConfig targetConfig) { 145 intializeTypeInformation(); 146 Optional<VerifyMapperConfiguration> config = MoreReflection.extractAnnotation(getClass(), 147 VerifyMapperConfiguration.class); 148 149 assertTrue(config.isPresent(), 150 "The mapper test must be annotated with " + VerifyMapperConfiguration.class.getName()); 151 152 @SuppressWarnings("squid:S3655") // owolff: false positive, checked above 153 var processedSourceProperties = AnnotationHelper.handleMetadataForMapperTest(config.get(), 154 resolveSourcePropertyMetadata()); 155 156 ParameterizedInstantiator<? extends S> sourceInstantiator = getSourceInstantiator( 157 new RuntimeProperties(processedSourceProperties)); 158 159 var targetProperties = new RuntimeProperties(resolveTargetPropertyMetadata(targetConfig)); 160 161 new MapperContractImpl<>(config.get(), sourceInstantiator, targetProperties, getUnderTest()).assertContract(); 162 } 163 164 @Override 165 public M getUnderTest() { 166 intializeTypeInformation(); 167 return new DefaultInstantiator<>(mapperClass).newInstance(); 168 } 169 170 /** 171 * Resolves the {@link PropertyMetadata} for the <em>source</em> objects by 172 * using reflection and the annotations {@link PropertyConfig} and / 173 * {@link PropertyConfigs} if provided. 174 * 175 * @return a {@link List} of {@link PropertyMetadata} defining the base line for 176 * the configured attributes 177 */ 178 public List<PropertyMetadata> resolveSourcePropertyMetadata() { 179 intializeTypeInformation(); 180 return handlePropertyMetadata(getClass(), getSourceClass()); 181 } 182 183 /** 184 * Resolves the {@link PropertyMetadata} for the <em>target</em> objects by 185 * using reflection 186 * 187 * @param config providing configuration, may be null 188 * 189 * @return a {@link List} of {@link PropertyMetadata} defining the base line for 190 * the configured attributes 191 */ 192 public List<PropertyMetadata> resolveTargetPropertyMetadata(PropertyReflectionConfig config) { 193 intializeTypeInformation(); 194 final List<PropertyMetadata> builder = new ArrayList<>(); 195 if (shouldScanClass(getClass())) { 196 final SortedSet<PropertyMetadata> scanned = new TreeSet<>( 197 scanBeanTypeForProperties(anyTargetObject().getClass(), config)); 198 builder.addAll(handlePostProcessConfig(config, scanned)); 199 } 200 final var handled = PropertyHelper.handlePrimitiveAsDefaults(PropertyHelper.toMapView(builder).values()); 201 PropertyHelper.logMessageForTargetPropertyMetadata(handled); 202 return immutableList(handled); 203 } 204 205 /** 206 * @return a target object to be used for reflection based resolving of 207 * {@link PropertyMetadata}. The default implementation uses the 208 * {@link GeneratorRegistry} in order to instantiate a corresponding 209 * object. For more complex objects you should add a corresponding 210 * {@link TypedGenerator}, see {@link GeneratorRegistry} 211 */ 212 public T anyTargetObject() { 213 intializeTypeInformation(); 214 return GeneratorResolver.resolveGenerator(targetClass).next(); 215 } 216 217 /** 218 * @param runtimeProperties to be used for creating the 219 * {@link ParameterizedInstantiator} 220 * 221 * @return the {@link ParameterizedInstantiator} to be used for instantiating 222 * source-object. If not overwritten it default to the beanInstantiator 223 */ 224 @SuppressWarnings("java:S1452") // owolff: using wildcards here is the only way 225 public ParameterizedInstantiator<? extends S> getSourceInstantiator(RuntimeProperties runtimeProperties) { 226 intializeTypeInformation(); 227 return new BeanInstantiator<>(new DefaultInstantiator<>(getSourceClass()), runtimeProperties); 228 } 229}