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}