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.List;
021import java.util.Optional;
022
023import de.cuioss.test.valueobjects.api.TestContract;
024import de.cuioss.test.valueobjects.api.contracts.VerifyBeanProperty;
025import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator;
026import de.cuioss.test.valueobjects.objects.RuntimeProperties;
027import de.cuioss.test.valueobjects.objects.impl.BeanInstantiator;
028import de.cuioss.test.valueobjects.objects.impl.DefaultInstantiator;
029import de.cuioss.test.valueobjects.property.PropertyMetadata;
030import de.cuioss.test.valueobjects.property.PropertySupport;
031import de.cuioss.test.valueobjects.util.AnnotationHelper;
032import de.cuioss.tools.logging.CuiLogger;
033import de.cuioss.tools.property.PropertyReadWrite;
034import lombok.Getter;
035import lombok.NonNull;
036import lombok.RequiredArgsConstructor;
037
038/**
039 * Tests all given properties according to the given List of
040 * {@link PropertyMetadata}
041 *
042 * @author Oliver Wolff
043 * @param <T> Rule does not apply to annotations: There is no inheritance
044 */
045@RequiredArgsConstructor
046public class BeanPropertyContractImpl<T> implements TestContract<T> {
047
048    private static final CuiLogger log = new CuiLogger(BeanPropertyContractImpl.class);
049
050    @Getter
051    @NonNull
052    private final ParameterizedInstantiator<T> instantiator;
053
054    @Override
055    public void assertContract() {
056        final var builder = new StringBuilder("Verifying ");
057        builder.append(getClass().getName()).append("\nWith configuration: ").append(getInstantiator().toString());
058        log.info(builder.toString());
059
060        checkGetterAndSetterContract();
061        checkDefaultContract();
062    }
063
064    private void checkGetterAndSetterContract() {
065        final var readWriteProperties = getInstantiator().getRuntimeProperties().getAllProperties().stream()
066                .filter(p -> PropertyReadWrite.READ_WRITE.equals(p.getPropertyReadWrite())).toList();
067
068        if (readWriteProperties.isEmpty()) {
069            log.warn(
070                    "There are no properties defined that are readable and writable. Consider your configuration and/or the base class for your test.");
071        } else {
072            log.info("Verifying properties that are Read and Write: "
073                    + RuntimeProperties.extractNames(readWriteProperties));
074
075            final var supportList = readWriteProperties.stream().map(PropertySupport::new).toList();
076            final Object target = getInstantiator().newInstanceMinimal();
077            for (final PropertySupport support : supportList) {
078
079                support.generateTestValue();
080
081                support.apply(target);
082
083                support.assertValueSet(target);
084            }
085        }
086    }
087
088    private void checkDefaultContract() {
089        final var defaultProperties = getInstantiator().getRuntimeProperties().getDefaultProperties();
090        if (defaultProperties.isEmpty()) {
091            log.debug("No default properties configured");
092        } else {
093            final var defaultPropertySupport = defaultProperties.stream().map(PropertySupport::new).toList();
094            final Object target = getInstantiator().newInstanceMinimal();
095            for (final PropertySupport support : defaultPropertySupport) {
096                support.assertDefaultValue(target);
097            }
098        }
099    }
100
101    /**
102     * Factory method for creating an instance of {@link BeanPropertyContractImpl}
103     * depending on the given parameter
104     *
105     * @param beanType                identifying the type to be tested. Must not be
106     *                                null and must provide a no args public
107     *                                constructor
108     * @param annotated               the annotated unit-test-class. It is expected
109     *                                to be annotated with
110     *                                {@link BeanPropertyContractImpl}, otherwise
111     *                                the method will return
112     *                                {@link Optional#empty()}
113     * @param initialPropertyMetadata identifying the complete set of
114     *                                {@link PropertyMetadata}, where the actual
115     *                                {@link PropertyMetadata} for the bean tests
116     *                                will be filtered by using the attributes
117     *                                defined within
118     *                                {@link BeanPropertyContractImpl}. Must not be
119     *                                null. If it is empty the method will return
120     *                                {@link Optional#empty()}
121     * @return an instance Of {@link BeanPropertyContractImpl} in case all
122     *         requirements for the parameters are correct, otherwise it will return
123     *         {@link Optional#empty()}
124     */
125    public static final <T> Optional<TestContract<T>> createBeanPropertyTestContract(final Class<T> beanType,
126            final Class<?> annotated, final List<PropertyMetadata> initialPropertyMetadata) {
127
128        requireNonNull(beanType, "beantype must not be null");
129        requireNonNull(annotated, "annotated must not be null");
130        requireNonNull(initialPropertyMetadata, "initialPropertyMetadata must not be null");
131
132        if (!annotated.isAnnotationPresent(VerifyBeanProperty.class)) {
133            log.debug("No annotation of type BeanPropertyTestContract available on class: " + annotated);
134            return Optional.empty();
135        }
136        if (initialPropertyMetadata.isEmpty()) {
137            log.warn("No configured properties found to be tested, offending class: " + annotated);
138            return Optional.empty();
139        }
140        final var instantiator = new DefaultInstantiator<>(beanType);
141        final var metadata = AnnotationHelper.handleMetadataForPropertyTest(annotated, initialPropertyMetadata);
142
143        return Optional.of(
144                new BeanPropertyContractImpl<>(new BeanInstantiator<>(instantiator, new RuntimeProperties(metadata))));
145    }
146}