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.contract;
017
018import static de.cuioss.tools.collect.CollectionLiterals.mutableList;
019import static java.util.Objects.requireNonNull;
020
021import java.util.List;
022import java.util.Optional;
023
024import de.cuioss.test.valueobjects.api.TestContract;
025import de.cuioss.test.valueobjects.api.contracts.VerifyBuilder;
026import de.cuioss.test.valueobjects.objects.BuilderInstantiator;
027import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator;
028import de.cuioss.test.valueobjects.objects.RuntimeProperties;
029import de.cuioss.test.valueobjects.objects.impl.BuilderConstructorBasedInstantiator;
030import de.cuioss.test.valueobjects.objects.impl.BuilderFactoryBasedInstantiator;
031import de.cuioss.test.valueobjects.objects.impl.BuilderParameterizedInstantiator;
032import de.cuioss.test.valueobjects.property.PropertyMetadata;
033import de.cuioss.test.valueobjects.property.PropertySupport;
034import de.cuioss.test.valueobjects.util.AnnotationHelper;
035import de.cuioss.tools.logging.CuiLogger;
036import de.cuioss.tools.reflect.MoreReflection;
037
038/**
039 * Defines basic tests for builder. In essence it will try to create a builder
040 * with a minimal set (required only) and one with all properties set. It sets
041 * the properties, build the actual object and verifies whether the properties
042 * are set correctly
043 *
044 * @author Oliver Wolff
045 * @param <T> identifying the type of objects to be tested
046 */
047public class BuilderContractImpl<T> implements TestContract<T> {
048
049    private static final CuiLogger log = new CuiLogger(BuilderContractImpl.class);
050
051    private final BuilderInstantiator<T> builderInstantiator;
052
053    private final RuntimeProperties runtimeProperties;
054
055    /** The usually chosen name for the actual build method. */
056    public static final String DEFAULT_BUILD_METHOD_NAME = "build";
057
058    /** The usually chosen name for a factory builder method. */
059    public static final String DEFAULT_BUILDER_FACTORY_METHOD_NAME = "builder";
060
061    /**
062     * @param instantiator      must not be null
063     * @param runtimeProperties must not be null
064     */
065    public BuilderContractImpl(final BuilderInstantiator<T> instantiator, final RuntimeProperties runtimeProperties) {
066        builderInstantiator = requireNonNull(instantiator, "builderInstantiator must not be null");
067        this.runtimeProperties = requireNonNull(runtimeProperties, "runtimeProperties must not be null.");
068    }
069
070    @Override
071    public void assertContract() {
072
073        final var builder = new StringBuilder("Verifying ");
074        builder.append(getClass().getName()).append("\nWith configuration: ").append(builderInstantiator.toString());
075        log.info(builder.toString());
076        setAndVerifyProperties(runtimeProperties.getRequiredProperties());
077        setAndVerifyProperties(runtimeProperties.getAllProperties());
078        shouldFailOnMissingRequiredAttributes();
079    }
080
081    private void setAndVerifyProperties(final List<PropertyMetadata> propertiesToBeChecked) {
082        final var properties = propertiesToBeChecked.stream().map(PropertySupport::new).toList();
083
084        properties.forEach(PropertySupport::generateTestValue);
085
086        final var builder = builderInstantiator.newBuilderInstance();
087        for (final PropertySupport support : properties) {
088            support.apply(builder);
089        }
090        final var built = builderInstantiator.build(builder);
091
092        for (final PropertySupport support : properties) {
093            if (support.isReadable()) {
094                support.assertValueSet(built);
095            }
096        }
097    }
098
099    private void shouldFailOnMissingRequiredAttributes() {
100        if (runtimeProperties.getRequiredProperties().isEmpty()) {
101            return;
102        }
103        for (final PropertyMetadata property : runtimeProperties.getRequiredProperties()) {
104            final List<PropertyMetadata> requiredMinusOne = mutableList(runtimeProperties.getRequiredProperties());
105            requiredMinusOne.remove(property);
106            var failed = false;
107            try {
108                setAndVerifyProperties(requiredMinusOne);
109                failed = true;
110            } catch (final AssertionError e) {
111                // Expected: Should have been thrown
112            }
113            if (failed) {
114                throw new AssertionError("Property is marked as required but the builder accepts if it is missing: "
115                        + property.toString());
116            }
117        }
118    }
119
120    @Override
121    public ParameterizedInstantiator<T> getInstantiator() {
122        return new BuilderParameterizedInstantiator<>(builderInstantiator, runtimeProperties);
123    }
124
125    /**
126     * Factory method for creating an instance of {@link BuilderContractImpl}
127     * depending on the given parameter
128     *
129     * @param beanType                identifying the type to be tested. Must not be
130     *                                null
131     * @param annotated               the annotated unit-test-class. It is expected
132     *                                to be annotated with {@link VerifyBuilder},
133     *                                otherwise the method will return
134     *                                {@link Optional#empty()}
135     * @param initialPropertyMetadata identifying the complete set of
136     *                                {@link PropertyMetadata}, where the actual
137     *                                {@link PropertyMetadata} for the bean tests
138     *                                will be filtered by using the attributes
139     *                                defined within {@link VerifyBuilder}. Must not
140     *                                be null.
141     * @return an instance Of {@link BeanPropertyContractImpl} in case all
142     *         requirements for the parameters are correct, otherwise it will return
143     *         {@link Optional#empty()}
144     */
145    public static final <T> Optional<BuilderContractImpl<T>> createBuilderTestContract(final Class<T> beanType,
146            final Class<?> annotated, final List<PropertyMetadata> initialPropertyMetadata) {
147
148        requireNonNull(beanType, "beantype must not be null");
149        requireNonNull(annotated, "annotated must not be null");
150        requireNonNull(initialPropertyMetadata, "initialPropertyMetadata must not be null");
151
152        final Optional<VerifyBuilder> config = MoreReflection.extractAnnotation(annotated, VerifyBuilder.class);
153
154        if (config.isEmpty()) {
155            log.debug("No annotation of type BuilderTestContract available on class: " + annotated);
156            return Optional.empty();
157        }
158        final var metadata = AnnotationHelper.handleMetadataForBuilderTest(annotated, initialPropertyMetadata);
159
160        final var contract = config.get();
161        BuilderInstantiator<T> instantiator;
162        if (VerifyBuilder.class.equals(contract.builderClass())) {
163            if (VerifyBuilder.class.equals(contract.builderFactoryProvidingClass())) {
164                instantiator = new BuilderFactoryBasedInstantiator<>(beanType, contract.builderFactoryMethodName(),
165                        contract.builderMethodName());
166            } else {
167                instantiator = new BuilderFactoryBasedInstantiator<>(contract.builderFactoryProvidingClass(),
168                        contract.builderFactoryMethodName(), contract.builderMethodName());
169            }
170        } else {
171            instantiator = new BuilderConstructorBasedInstantiator<>(contract.builderClass(),
172                    contract.builderMethodName());
173        }
174
175        return Optional.of(new BuilderContractImpl<>(instantiator, new RuntimeProperties(metadata)));
176    }
177}