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.util;
017
018import static de.cuioss.tools.base.Preconditions.checkArgument;
019import static de.cuioss.tools.collect.CollectionLiterals.mutableList;
020import static java.util.Objects.requireNonNull;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Optional;
030import java.util.Set;
031import java.util.SortedSet;
032
033import de.cuioss.test.valueobjects.api.VerifyMapperConfiguration;
034import de.cuioss.test.valueobjects.api.contracts.VerifyBeanProperty;
035import de.cuioss.test.valueobjects.api.contracts.VerifyBuilder;
036import de.cuioss.test.valueobjects.api.contracts.VerifyConstructor;
037import de.cuioss.test.valueobjects.api.contracts.VerifyConstructors;
038import de.cuioss.test.valueobjects.api.contracts.VerifyFactoryMethod;
039import de.cuioss.test.valueobjects.api.contracts.VerifyFactoryMethods;
040import de.cuioss.test.valueobjects.api.property.PropertyReflectionConfig;
041import de.cuioss.test.valueobjects.property.PropertyMetadata;
042import de.cuioss.test.valueobjects.property.impl.BuilderMetadata;
043import de.cuioss.test.valueobjects.property.impl.PropertyMetadataImpl;
044import de.cuioss.test.valueobjects.property.util.AssertionStrategy;
045import de.cuioss.test.valueobjects.property.util.PropertyAccessStrategy;
046import de.cuioss.tools.collect.CollectionBuilder;
047import de.cuioss.tools.logging.CuiLogger;
048import de.cuioss.tools.property.PropertyMemberInfo;
049import de.cuioss.tools.property.PropertyReadWrite;
050import de.cuioss.tools.reflect.MoreReflection;
051import lombok.experimental.UtilityClass;
052
053/**
054 * Simple helper class dealing with annotation on the test-base classes.
055 *
056 * @author Oliver Wolff
057 */
058@UtilityClass
059public final class AnnotationHelper {
060
061    private static final String NO_PROPERTIES_GIVEN_IS_THIS_INTENTIONAL = "No properties given: Is this intentional?";
062    static final String UNABLE_TO_INSTANTIATE_GENERATOR = "Unable to instantiate generator, You must provide a no-arg public constructor: ";
063
064    private static final CuiLogger log = new CuiLogger(AnnotationHelper.class);
065
066    /**
067     * Creates a {@link List} of {@link PropertyMetadata} according to the given
068     * parameter
069     *
070     * @param config        identifying the concrete configuration
071     * @param givenMetadata used fore deriving the concrete metadata. Must not be
072     *                      null
073     * @return a {@link List} of {@link PropertyMetadata} according to the given
074     *         parameter
075     */
076    public static List<PropertyMetadata> constructorConfigToPropertyMetadata(final VerifyConstructor config,
077            final Collection<PropertyMetadata> givenMetadata) {
078
079        requireNonNull(config);
080        requireNonNull(givenMetadata);
081
082        final Map<String, PropertyMetadata> map = new HashMap<>();
083        final List<String> ofAsList = Arrays.asList(config.of());
084
085        givenMetadata.stream().filter(p -> ofAsList.contains(p.getName()))
086                .forEach(meta -> map.put(meta.getName(), meta));
087
088        for (final String name : config.of()) {
089            PropertyHelper.assertPropertyExists(name, map);
090            modifyPropertyMetadata(map, config.defaultValued(), config.readOnly(), config.required(),
091                    config.transientProperties(), config.writeOnly(), config.assertUnorderedCollection());
092        }
093
094        if (config.allRequired()) {
095            map.replaceAll((k, v) -> PropertyMetadataImpl.builder(v).required(true).build());
096        }
097
098        return orderPropertyMetadata(config.of(), handleWritableAttributes(map));
099    }
100
101    private static List<PropertyMetadata> handleWritableAttributes(final Map<String, PropertyMetadata> map) {
102        final List<PropertyMetadata> result = new ArrayList<>();
103        for (Entry<String, PropertyMetadata> entry : map.entrySet()) {
104            result.add(PropertyMetadataImpl.builder(entry.getValue())
105                    .propertyReadWrite(determinePropertyReadWrite(entry.getValue())).build());
106        }
107        return result;
108    }
109
110    private static PropertyReadWrite determinePropertyReadWrite(final PropertyMetadata propertyMetadata) {
111        if (PropertyReadWrite.WRITE_ONLY.equals(propertyMetadata.getPropertyReadWrite())) {
112            return PropertyReadWrite.WRITE_ONLY;
113        }
114        return PropertyReadWrite.READ_WRITE;
115    }
116
117    /**
118     * Creates a {@link List} of {@link PropertyMetadata} according to the given
119     * parameter
120     *
121     * @param config        identifying the concrete configuration
122     * @param givenMetadata used fore deriving the concrete metadata. Must not be
123     *                      null
124     * @return a {@link List} of {@link PropertyMetadata} according to the given
125     *         parameter
126     */
127    public static List<PropertyMetadata> factoryConfigToPropertyMetadata(final VerifyFactoryMethod config,
128            final Collection<PropertyMetadata> givenMetadata) {
129
130        requireNonNull(config);
131        requireNonNull(givenMetadata);
132
133        final Map<String, PropertyMetadata> map = new HashMap<>();
134        final List<String> ofAsList = Arrays.asList(config.of());
135
136        givenMetadata.stream().filter(p -> ofAsList.contains(p.getName()))
137                .forEach(meta -> map.put(meta.getName(), meta));
138
139        for (final String name : config.of()) {
140            PropertyHelper.assertPropertyExists(name, map);
141            modifyPropertyMetadata(map, config.defaultValued(), config.readOnly(), config.required(),
142                    config.transientProperties(), config.writeOnly(), config.assertUnorderedCollection());
143        }
144
145        return orderPropertyMetadata(config.of(), handleWritableAttributes(map));
146    }
147
148    /**
149     * Checks the given type for the annotation {@link VerifyConstructor} and
150     * {@link VerifyConstructors} and puts all found in the returned {@link Set}
151     *
152     * @param annotated the class that may or may not provide the annotations, must
153     *                  not be null
154     * @return a {@link Set} of {@link VerifyConstructor} extracted from the
155     *         annotations of the given type. May be empty but never null
156     */
157    public static Set<VerifyConstructor> extractConfiguredConstructorContracts(final Class<?> annotated) {
158        requireNonNull(annotated);
159        final var builder = new CollectionBuilder<VerifyConstructor>();
160
161        MoreReflection.extractAllAnnotations(annotated, VerifyConstructors.class)
162                .forEach(contract -> builder.add(Arrays.asList(contract.value())));
163        MoreReflection.extractAllAnnotations(annotated, VerifyConstructor.class).forEach(builder::add);
164
165        return builder.toImmutableSet();
166    }
167
168    /**
169     * Checks the given type for the annotation {@link VerifyFactoryMethod} and
170     * {@link VerifyFactoryMethods} and puts all found in the returned list
171     *
172     * @param annotated the class that may or may not provide the annotations, must
173     *                  not be null
174     * @return a Set of {@link VerifyFactoryMethod} extracted from the annotations
175     *         of the given type. May be empty but never null
176     */
177    public static Set<VerifyFactoryMethod> extractConfiguredFactoryContracts(final Class<?> annotated) {
178        requireNonNull(annotated);
179        final var builder = new CollectionBuilder<VerifyFactoryMethod>();
180
181        MoreReflection.extractAllAnnotations(annotated, VerifyFactoryMethods.class)
182                .forEach(contract -> builder.add(Arrays.asList(contract.value())));
183        MoreReflection.extractAllAnnotations(annotated, VerifyFactoryMethod.class).forEach(builder::add);
184
185        return builder.toImmutableSet();
186    }
187
188    /**
189     * @param annotated     must not be null and must be annotated with
190     *                      {@link VerifyBeanProperty}
191     * @param givenMetadata must not be null
192     * @return a {@link SortedSet} providing the property configuration derived by
193     *         the given properties and the annotated {@link VerifyBeanProperty}
194     */
195    public static List<PropertyMetadata> handleMetadataForPropertyTest(final Class<?> annotated,
196            final List<PropertyMetadata> givenMetadata) {
197        requireNonNull(annotated);
198        requireNonNull(givenMetadata);
199
200        if (givenMetadata.isEmpty()) {
201            log.warn(NO_PROPERTIES_GIVEN_IS_THIS_INTENTIONAL);
202            return givenMetadata;
203        }
204
205        final Optional<VerifyBeanProperty> contractOption = MoreReflection.extractAnnotation(annotated,
206                VerifyBeanProperty.class);
207
208        final var contract = contractOption.orElseThrow(() -> new IllegalArgumentException(
209                "Given type does not provide the expected annotation BeanPropertyTestContract, type=" + annotated));
210
211        var map = PropertyHelper.handleWhiteAndBlacklist(contract.of(), contract.exclude(), givenMetadata);
212
213        modifyPropertyMetadata(map, contract.defaultValued(), contract.readOnly(), contract.required(),
214                contract.transientProperties(), contract.writeOnly(), contract.assertUnorderedCollection());
215
216        return orderPropertyMetadata(contract.of(), map.values());
217    }
218
219    private static List<PropertyMetadata> orderPropertyMetadata(final String[] of,
220            final Collection<PropertyMetadata> givenMetadata) {
221        final var builder = new CollectionBuilder<PropertyMetadata>();
222        if (0 == of.length) {
223            builder.add(givenMetadata);
224        } else {
225            final Map<String, PropertyMetadata> map = new HashMap<>();
226            for (final PropertyMetadata metadata : givenMetadata) {
227                map.put(metadata.getName(), metadata);
228            }
229            for (final String name : of) {
230                builder.add(map.get(name));
231            }
232        }
233        return builder.toImmutableList();
234
235    }
236
237    /**
238     * @param annotated     must not be null and must be annotated with
239     *                      {@link VerifyBeanProperty}
240     * @param givenMetadata must not be null
241     * @return a {@link SortedSet} providing the property configuration derived by
242     *         the given properties and the annotated {@link VerifyBuilder}
243     */
244    public static List<PropertyMetadata> handleMetadataForBuilderTest(final Class<?> annotated,
245            final List<PropertyMetadata> givenMetadata) {
246
247        requireNonNull(annotated);
248        requireNonNull(givenMetadata);
249
250        if (givenMetadata.isEmpty()) {
251            log.warn(NO_PROPERTIES_GIVEN_IS_THIS_INTENTIONAL);
252            return givenMetadata;
253        }
254
255        final Optional<VerifyBuilder> contractOption = MoreReflection.extractAnnotation(annotated, VerifyBuilder.class);
256
257        final var contract = contractOption.orElseThrow(() -> new IllegalArgumentException(
258                "Given type does not provide the expected annotation BuilderTestContract, type=" + annotated));
259
260        var map = PropertyHelper.handleWhiteAndBlacklist(contract.of(), contract.exclude(), givenMetadata);
261
262        modifyPropertyMetadata(map, contract.defaultValued(), contract.readOnly(), contract.required(),
263                contract.transientProperties(), contract.writeOnly(), contract.assertUnorderedCollection());
264
265        final Map<String, PropertyMetadata> builderPropertyMap = new HashMap<>();
266        for (final PropertyMetadata metadata : BuilderPropertyHelper.handleBuilderPropertyConfigAnnotations(annotated,
267                mutableList(map.values()))) {
268            checkArgument(map.containsKey(metadata.getName()),
269                    "Invalid Configuration found: BuilderPropertyConfig and BuilderTestContract do not agree on configuration. offending property: "
270                            + metadata);
271            builderPropertyMap.put(metadata.getName(), metadata);
272        }
273
274        for (final Entry<String, PropertyMetadata> entry : map.entrySet()) {
275            if (!builderPropertyMap.containsKey(entry.getKey())) {
276                var delegate = entry.getValue();
277                if (PropertyAccessStrategy.BEAN_PROPERTY.equals(delegate.getPropertyAccessStrategy())) {
278                    var propertyReadWrite = PropertyReadWrite.READ_WRITE;
279                    if (PropertyReadWrite.WRITE_ONLY.equals(delegate.getPropertyReadWrite())) {
280                        propertyReadWrite = PropertyReadWrite.WRITE_ONLY;
281                    }
282
283                    delegate = PropertyMetadataImpl.builder(delegate)
284                            .propertyAccessStrategy(PropertyAccessStrategy.BUILDER_DIRECT)
285                            .propertyReadWrite(propertyReadWrite).build();
286                }
287                builderPropertyMap.put(entry.getKey(), BuilderMetadata.builder().delegateMetadata(delegate)
288                        .builderMethodPrefix(contract.methodPrefix()).build());
289            }
290        }
291
292        return orderPropertyMetadata(contract.of(), builderPropertyMap.values());
293    }
294
295    /**
296     * @param verifyMapper  must not be null and must be annotated with
297     *                      {@link VerifyMapperConfiguration}
298     * @param givenMetadata must not be null
299     * @return a {@link SortedSet} providing the property configuration derived by
300     *         the given properties and the annotated
301     *         {@link VerifyMapperConfiguration}
302     */
303    public static List<PropertyMetadata> handleMetadataForMapperTest(final VerifyMapperConfiguration verifyMapper,
304            final List<PropertyMetadata> givenMetadata) {
305
306        requireNonNull(verifyMapper);
307        requireNonNull(givenMetadata);
308
309        if (givenMetadata.isEmpty()) {
310            log.warn(NO_PROPERTIES_GIVEN_IS_THIS_INTENTIONAL);
311            return givenMetadata;
312        }
313
314        var map = PropertyHelper.handleWhiteAndBlacklist(verifyMapper.of(), verifyMapper.exclude(), givenMetadata);
315
316        modifyPropertyMetadata(map, verifyMapper.defaultValued(), verifyMapper.readOnly(), verifyMapper.required(),
317                new String[0], verifyMapper.writeOnly(), verifyMapper.assertUnorderedCollection());
318
319        return orderPropertyMetadata(verifyMapper.of(), map.values());
320    }
321
322    /**
323     * Checks the individual contracts and changes / modifies the corresponding
324     * {@link PropertyMetadata} accordingly
325     *
326     * @param map                 must not be null
327     * @param defaultValued       must not be null
328     * @param readOnly            must not be null
329     * @param required            must not be null
330     * @param transientProperties must not be null
331     * @param writeOnly           must not be null
332     * @param unorderedCollection must not be null, see
333     *                            {@link PropertyReflectionConfig#assertUnorderedCollection()}
334     * @return the filtered map
335     */
336    public static Map<String, PropertyMetadata> modifyPropertyMetadata(final Map<String, PropertyMetadata> map,
337            final String[] defaultValued, final String[] readOnly, final String[] required,
338            final String[] transientProperties, final String[] writeOnly, final String[] unorderedCollection) {
339
340        for (final String name : defaultValued) {
341            PropertyHelper.assertPropertyExists(name, map);
342            map.put(name, PropertyMetadataImpl.builder(map.get(name)).defaultValue(true).build());
343        }
344        for (final String name : readOnly) {
345            PropertyHelper.assertPropertyExists(name, map);
346            map.put(name,
347                    PropertyMetadataImpl.builder(map.get(name)).propertyReadWrite(PropertyReadWrite.READ_ONLY).build());
348        }
349        for (final String name : writeOnly) {
350            PropertyHelper.assertPropertyExists(name, map);
351            map.put(name, PropertyMetadataImpl.builder(map.get(name)).propertyReadWrite(PropertyReadWrite.WRITE_ONLY)
352                    .build());
353        }
354        for (final String name : required) {
355            PropertyHelper.assertPropertyExists(name, map);
356            map.put(name, PropertyMetadataImpl.builder(map.get(name)).required(true).build());
357        }
358        for (final String name : transientProperties) {
359            PropertyHelper.assertPropertyExists(name, map);
360            map.put(name, PropertyMetadataImpl.builder(map.get(name)).propertyMemberInfo(PropertyMemberInfo.TRANSIENT)
361                    .build());
362        }
363        for (final String name : unorderedCollection) {
364            PropertyHelper.assertPropertyExists(name, map);
365            map.put(name, PropertyMetadataImpl.builder(map.get(name))
366                    .assertionStrategy(AssertionStrategy.COLLECTION_IGNORE_ORDER).build());
367        }
368        return map;
369    }
370
371    /**
372     * Checks the individual contracts and changes / modifies the corresponding
373     * {@link PropertyMetadata} accordingly
374     *
375     * @param map                 must not be null
376     * @param defaultValued       must not be null
377     * @param readOnly            must not be null
378     * @param required            must not be null
379     * @param transientProperties must not be null
380     * @param writeOnly           must not be null
381     * @param unorderedCollection must not be null, see
382     *                            {@link PropertyReflectionConfig#assertUnorderedCollection()}
383     * @return the filtered map
384     */
385    public static Map<String, PropertyMetadata> modifyPropertyMetadata(final Map<String, PropertyMetadata> map,
386            final List<String> defaultValued, final List<String> readOnly, final List<String> required,
387            final List<String> transientProperties, final List<String> writeOnly,
388            final List<String> unorderedCollection) {
389
390        return modifyPropertyMetadata(map, defaultValued.toArray(new String[defaultValued.size()]),
391                readOnly.toArray(new String[readOnly.size()]), required.toArray(new String[required.size()]),
392                transientProperties.toArray(new String[transientProperties.size()]),
393                writeOnly.toArray(new String[writeOnly.size()]),
394                unorderedCollection.toArray(new String[unorderedCollection.size()]));
395    }
396
397}