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}