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 java.util.Objects.requireNonNull;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.SortedSet;
028
029import de.cuioss.test.generator.TypedGenerator;
030import de.cuioss.test.valueobjects.api.property.PropertyConfig;
031import de.cuioss.test.valueobjects.api.property.PropertyConfigs;
032import de.cuioss.test.valueobjects.generator.dynamic.DynamicTypedGenerator;
033import de.cuioss.test.valueobjects.objects.impl.DefaultInstantiator;
034import de.cuioss.test.valueobjects.property.PropertyMetadata;
035import de.cuioss.test.valueobjects.property.impl.PropertyMetadataImpl;
036import de.cuioss.test.valueobjects.property.util.CollectionType;
037import de.cuioss.tools.collect.CollectionBuilder;
038import de.cuioss.tools.logging.CuiLogger;
039import de.cuioss.tools.reflect.MoreReflection;
040import de.cuioss.tools.string.Joiner;
041import lombok.AccessLevel;
042import lombok.RequiredArgsConstructor;
043
044/**
045 * Provides utility methods for dealing with {@link PropertyMetadata}
046 *
047 * @author Oliver Wolff
048 */
049@SuppressWarnings("squid:S1118") // owolff: lombok generated
050@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
051public class PropertyHelper {
052
053    private static final CuiLogger log = new CuiLogger(PropertyHelper.class);
054
055    /**
056     * Ensure that the result of property scanning is logged only once.
057     */
058    private static boolean propertyInformationLogged = false;
059
060    /**
061     * Ensure that the result of property scanning is logged only once.
062     */
063    private static boolean propertyTargetInformationLogged = false;
064
065    /**
066     * Simple helper method that creates a sensible information message for logging
067     * purpose. It uses a static field in order to ensure that the logging will only
068     * be done once per Test-unit *
069     *
070     * @param handled to be logged
071     */
072    public static void logMessageForPropertyMetadata(final Collection<? extends PropertyMetadata> handled) {
073        if (!propertyInformationLogged) {
074            final var messageBuilder = new StringBuilder(
075                    "Properties detected by using reflection and PropertyConfig-annotation: ").append("\n");
076            final List<String> elements = new ArrayList<>();
077            handled.forEach(data -> elements.add("-" + data.toString()));
078            Collections.sort(elements);
079            messageBuilder.append(Joiner.on("\n").join(elements));
080
081            log.info(messageBuilder.toString());
082            synchronized (PropertyHelper.class) {
083                propertyInformationLogged = true;
084            }
085        }
086    }
087
088    /**
089     * Simple helper method that creates a sensible information message for logging
090     * purpose. It uses a static field in order to ensure that the logging will only
091     * be done once per Test-unit *
092     *
093     * @param handled to be logged
094     */
095    public static void logMessageForTargetPropertyMetadata(final Collection<? extends PropertyMetadata> handled) {
096        if (!propertyTargetInformationLogged) {
097            final var messageBuilder = new StringBuilder("Properties detected for targetType: ").append("\n");
098            final List<String> elements = new ArrayList<>();
099            handled.forEach(data -> elements.add("-" + data.toString()));
100            Collections.sort(elements);
101            messageBuilder.append(Joiner.on("\n").join(elements));
102
103            log.info(messageBuilder.toString());
104            synchronized (PropertyHelper.class) {
105                propertyTargetInformationLogged = true;
106            }
107        }
108    }
109
110    /**
111     * Sets all primitives of the given list to
112     * {@link PropertyMetadata#isDefaultValue()} being {@code true} in case
113     * {@link PropertyMetadata#getCollectionType()} being
114     * {@link CollectionType#NO_ITERABLE} .
115     *
116     * @param metadata must not be null
117     * @return an immutable list with {@link PropertyMetadata}
118     */
119    public static final Collection<PropertyMetadata> handlePrimitiveAsDefaults(
120            final Collection<PropertyMetadata> metadata) {
121        requireNonNull(metadata);
122        if (metadata.isEmpty()) {
123            return metadata;
124        }
125        final var builder = new CollectionBuilder<PropertyMetadata>();
126        for (final PropertyMetadata propertyMetadata : metadata) {
127            if (propertyMetadata.getPropertyClass().isPrimitive()
128                    && CollectionType.NO_ITERABLE.equals(propertyMetadata.getCollectionType())) {
129                builder.add(PropertyMetadataImpl.builder(propertyMetadata).defaultValue(true).build());
130            } else {
131                builder.add(propertyMetadata);
132            }
133        }
134        return builder.toImmutableList();
135    }
136
137    /**
138     * Checks the given type for the annotation {@link PropertyConfig} and
139     * {@link PropertyConfigs} and puts all found in the immutable set to be
140     * returned
141     *
142     * @param annotated the class that may or may not provide the annotations, must
143     *                  not be null
144     * @return immutable set of found {@link PropertyMetadata} elements derived by
145     *         the annotations.
146     */
147    public static final Set<PropertyMetadata> handlePropertyConfigAnnotations(final Class<?> annotated) {
148        requireNonNull(annotated);
149        return handlePropertyConfigAnnotations(extractConfiguredPropertyConfigs(annotated));
150    }
151
152    /**
153     * Checks the given type for the annotation {@link PropertyConfig} and
154     * {@link PropertyConfigs} and puts all found in the immutable set to be
155     * returned
156     *
157     * @param config the PropertyConfig-annotations, must not be null
158     * @return immutable set of found {@link PropertyMetadata} elements derived by
159     *         the annotations.
160     */
161    public static final Set<PropertyMetadata> handlePropertyConfigAnnotations(final Collection<PropertyConfig> config) {
162        final var builder = new CollectionBuilder<PropertyMetadata>();
163
164        config.forEach(conf -> builder.add(propertyConfigToPropertyMetadata(conf)));
165
166        return builder.toImmutableSet();
167    }
168
169    /**
170     * Checks the given type for the annotation {@link PropertyConfig} and
171     * {@link PropertyConfigs} and puts all found in the returned list
172     *
173     * @param annotated the class that may or may not provide the annotations, must
174     *                  not be null
175     * @return a {@link Set} of {@link PropertyConfig} extract from the annotations
176     *         of the given type. May be empty but never null
177     */
178    public static final Set<PropertyConfig> extractConfiguredPropertyConfigs(final Class<?> annotated) {
179        requireNonNull(annotated);
180        final var builder = new CollectionBuilder<PropertyConfig>();
181
182        MoreReflection.extractAllAnnotations(annotated, PropertyConfigs.class)
183                .forEach(contract -> builder.add(contract.value()));
184        MoreReflection.extractAllAnnotations(annotated, PropertyConfig.class).forEach(builder::add);
185
186        return builder.toImmutableSet();
187    }
188
189    private static PropertyMetadata propertyConfigToPropertyMetadata(final PropertyConfig config) {
190        @SuppressWarnings("rawtypes")
191        final Class<? extends TypedGenerator> generatorClass = config.generator();
192        final TypedGenerator<?> generator;
193        if (!DynamicTypedGenerator.class.equals(generatorClass)) {
194            generator = new DefaultInstantiator<>(generatorClass).newInstance();
195        } else {
196            generator = new DynamicTypedGenerator<>(config.propertyClass());
197        }
198
199        return PropertyMetadataImpl.builder().name(config.name()).defaultValue(config.defaultValue())
200                .propertyAccessStrategy(config.propertyAccessStrategy()).generator(generator)
201                .propertyClass(config.propertyClass()).propertyMemberInfo(config.propertyMemberInfo())
202                .collectionType(config.collectionType()).assertionStrategy(config.assertionStrategy())
203                .propertyReadWrite(config.propertyReadWrite()).required(config.required()).build();
204    }
205
206    /**
207     * Simple helper, that create a map with with name as key for the given
208     * {@link PropertyMetadata}
209     *
210     * @param metadata if it is null or empty an empty {@link Map} will be returned.
211     * @return a map with with name as key for the given {@link PropertyMetadata}
212     */
213    public static final Map<String, PropertyMetadata> toMapView(final Collection<PropertyMetadata> metadata) {
214        final Map<String, PropertyMetadata> map = new HashMap<>();
215        if (null == metadata) {
216            return map;
217        }
218        metadata.forEach(m -> map.put(m.getName(), m));
219        return map;
220    }
221
222    /**
223     * Handles the white- / black-list for the given parameter {@link Collection}
224     *
225     * @param of            must not be null
226     * @param exclude       must not be null
227     * @param givenMetadata must not be null
228     * @return the filtered property map
229     */
230    public static Map<String, PropertyMetadata> handleWhiteAndBlacklist(final String[] of, final String[] exclude,
231            final Collection<PropertyMetadata> givenMetadata) {
232        var map = PropertyHelper.toMapView(givenMetadata);
233        if (of.length != 0) {
234            // Whitelist takes precedence
235            final Map<String, PropertyMetadata> copyMap = new HashMap<>();
236            for (final String name : of) {
237                assertPropertyExists(name, map);
238                copyMap.put(name, map.get(name));
239            }
240            map = copyMap;
241        } else {
242            // Remove all excluded properties
243            for (final String name : exclude) {
244                map.remove(name);
245            }
246        }
247        return map;
248    }
249
250    /**
251     * Handles the white- / black-list for the given parameter map. This variant
252     * returns a list with the exact order of the given list
253     *
254     * @param of            must not be null
255     * @param exclude       must not be null
256     * @param givenMetadata is it is null or empty an empty list will be returned.
257     * @return the filtered properties
258     */
259    public static List<PropertyMetadata> handleWhiteAndBlacklistAsList(final String[] of, final String[] exclude,
260            final List<PropertyMetadata> givenMetadata) {
261        if (null == givenMetadata || givenMetadata.isEmpty()) {
262            return Collections.emptyList();
263        }
264        final var map = handleWhiteAndBlacklist(of, exclude, givenMetadata);
265        final var builder = new CollectionBuilder<PropertyMetadata>();
266        for (final PropertyMetadata meta : givenMetadata) {
267            if (map.containsKey(meta.getName())) {
268                builder.add(meta);
269            }
270        }
271        return builder.toImmutableList();
272    }
273
274    /**
275     * Simple assertions indicating that the property identified by the name exists
276     * in the given map
277     *
278     * @param name must no be null
279     * @param map  must no be null
280     */
281    public static void assertPropertyExists(final String name, final Map<String, PropertyMetadata> map) {
282        if (!map.containsKey(name)) {
283            throw new IllegalArgumentException("'" + name + "'"
284                    + " is not a configured property within the given properties, check your configuration");
285        }
286    }
287
288    /**
289     * Simple assertions indicating that the property identified by the name exists
290     * in the given Set
291     *
292     * @param name          must no be null
293     * @param givenMetadata must no be null
294     */
295    public static void assertPropertyExists(final String name, final SortedSet<PropertyMetadata> givenMetadata) {
296        final Map<String, PropertyMetadata> map = new HashMap<>();
297        givenMetadata.forEach(meta -> map.put(meta.getName(), meta));
298        assertPropertyExists(name, map);
299    }
300
301}