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 java.util.Objects.requireNonNull;
019
020import java.util.ArrayList;
021import java.util.List;
022
023import de.cuioss.test.valueobjects.api.TestContract;
024import de.cuioss.test.valueobjects.api.contracts.VerifyConstructor;
025import de.cuioss.test.valueobjects.api.contracts.VerifyConstructors;
026import de.cuioss.test.valueobjects.api.contracts.VerifyFactoryMethod;
027import de.cuioss.test.valueobjects.api.contracts.VerifyFactoryMethods;
028import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator;
029import de.cuioss.test.valueobjects.objects.RuntimeProperties;
030import de.cuioss.test.valueobjects.objects.impl.ConstructorBasedInstantiator;
031import de.cuioss.test.valueobjects.objects.impl.FactoryBasedInstantiator;
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.collect.CollectionBuilder;
036import de.cuioss.tools.logging.CuiLogger;
037import lombok.Getter;
038import lombok.NonNull;
039import lombok.RequiredArgsConstructor;
040
041/**
042 * TestContract for dealing Constructor and factories, {@link VerifyConstructor}
043 * and {@link VerifyFactoryMethod} respectively
044 *
045 * @author Oliver Wolff
046 * @param <T> identifying the objects to be tested.
047 */
048@RequiredArgsConstructor
049public class ObjectCreatorContractImpl<T> implements TestContract<T> {
050
051    private static final CuiLogger log = new CuiLogger(ObjectCreatorContractImpl.class);
052
053    @Getter
054    @NonNull
055    private final ParameterizedInstantiator<T> instantiator;
056
057    @Override
058    public void assertContract() {
059        final var builder = new StringBuilder("Verifying ");
060        builder.append(getClass().getName()).append("\nWith configuration: ").append(instantiator.toString());
061        log.info(builder.toString());
062
063        shouldPersistAllParameter();
064        shouldHandleRequiredAndDefaults();
065        shouldFailOnMissingRequiredAttributes();
066    }
067
068    private void shouldFailOnMissingRequiredAttributes() {
069        final var information = getInstantiator().getRuntimeProperties();
070        final var required = information.getRequiredAsPropertySupport(true);
071
072        for (final PropertySupport support : required) {
073            if (!support.isPrimitive()) {
074                final List<PropertySupport> iterating = new ArrayList<>(required);
075                iterating.remove(support);
076                iterating.add(support.createCopy(false));
077                var failed = false;
078                try {
079                    getInstantiator().newInstance(iterating, false);
080                    failed = true;
081                } catch (final AssertionError e) {
082                    // expected
083                }
084                if (failed) {
085                    throw new AssertionError("Object Should not build due to missing required attribute " + support);
086                }
087            }
088        }
089    }
090
091    private void shouldHandleRequiredAndDefaults() {
092        final var information = getInstantiator().getRuntimeProperties();
093
094        final var required = information.getRequiredAsPropertySupport(true);
095        final var instance = getInstantiator().newInstance(required, false);
096
097        for (final PropertySupport support : required) {
098            if (support.isReadable()) {
099                support.assertValueSet(instance);
100            }
101        }
102
103        for (final PropertySupport support : information.getDefaultAsPropertySupport(false)) {
104            if (support.isReadable()) {
105                support.assertDefaultValue(instance);
106            }
107        }
108
109        for (final PropertySupport support : information.getAdditionalAsPropertySupport(false)) {
110            if (support.isReadable() && !support.isDefaultValue()) {
111                support.assertValueSet(instance);
112            }
113        }
114
115    }
116
117    private void shouldPersistAllParameter() {
118        final var properties = instantiator.getRuntimeProperties().getAllAsPropertySupport(true);
119
120        final var instance = instantiator.newInstance(properties, false);
121        for (final PropertySupport support : properties) {
122            if (support.isReadable()) {
123                support.assertValueSet(instance);
124            }
125        }
126    }
127
128    /**
129     * Factory method for creating a {@link List} of instances of
130     * {@link ObjectCreatorContractImpl} depending on the given parameter
131     *
132     * @param beanType                identifying the type to be tested. Must not be
133     *                                null
134     * @param annotated               the annotated unit-test-class. It is expected
135     *                                to be annotated with {@link VerifyConstructor}
136     *                                and / or {@link VerifyConstructors},
137     *                                {@link VerifyFactoryMethod} and / or
138     *                                {@link VerifyFactoryMethods} otherwise the
139     *                                method will return empty list
140     * @param initialPropertyMetadata identifying the complete set of
141     *                                {@link PropertyMetadata}, where the actual
142     *                                {@link PropertyMetadata} for the test will be
143     *                                filtered by using the attributes defined
144     *                                within {@link VerifyConstructor} and / or
145     *                                {@link VerifyFactoryMethod}. Must not be null.
146     * @return a {@link List} of instances of {@link ObjectCreatorContractImpl} in
147     *         case all requirements for the parameters are correct, otherwise it
148     *         will return an empty list
149     */
150    public static final <T> List<ObjectCreatorContractImpl<T>> createTestContracts(final Class<T> beanType,
151            final Class<?> annotated, final List<PropertyMetadata> initialPropertyMetadata) {
152
153        requireNonNull(beanType, "beantype must not be null");
154        requireNonNull(initialPropertyMetadata, "initialPropertyMetadata must not be null");
155
156        final var builder = new CollectionBuilder<ObjectCreatorContractImpl<T>>();
157        // VerifyConstructor
158        for (final VerifyConstructor contract : AnnotationHelper.extractConfiguredConstructorContracts(annotated)) {
159            final var properties = AnnotationHelper.constructorConfigToPropertyMetadata(contract,
160                    initialPropertyMetadata);
161            final ParameterizedInstantiator<T> instantiator = new ConstructorBasedInstantiator<>(beanType,
162                    new RuntimeProperties(properties));
163            builder.add(new ObjectCreatorContractImpl<>(instantiator));
164        }
165        // Verify Factory method
166        for (final VerifyFactoryMethod contract : AnnotationHelper.extractConfiguredFactoryContracts(annotated)) {
167            final var properties = AnnotationHelper.factoryConfigToPropertyMetadata(contract, initialPropertyMetadata);
168            Class<?> enclosingType = beanType;
169            if (!VerifyFactoryMethod.class.equals(contract.enclosingType())) {
170                enclosingType = contract.enclosingType();
171            }
172            final ParameterizedInstantiator<T> instantiator = new FactoryBasedInstantiator<>(beanType,
173                    new RuntimeProperties(properties), enclosingType, contract.factoryMethodName());
174            builder.add(new ObjectCreatorContractImpl<>(instantiator));
175        }
176        return builder.toImmutableList();
177    }
178
179}