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}