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.collect.CollectionLiterals.immutableSet; 019import static de.cuioss.tools.collect.CollectionLiterals.immutableSortedSet; 020import static java.util.Objects.requireNonNull; 021 022import java.lang.reflect.Field; 023import java.lang.reflect.Method; 024import java.lang.reflect.ParameterizedType; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Optional; 031import java.util.Set; 032import java.util.SortedSet; 033import java.util.TreeSet; 034 035import de.cuioss.test.valueobjects.api.property.PropertyConfig; 036import de.cuioss.test.valueobjects.api.property.PropertyReflectionConfig; 037import de.cuioss.test.valueobjects.generator.dynamic.GeneratorResolver; 038import de.cuioss.test.valueobjects.property.PropertyMetadata; 039import de.cuioss.test.valueobjects.property.impl.PropertyMetadataImpl; 040import de.cuioss.test.valueobjects.property.util.CollectionType; 041import de.cuioss.tools.collect.CollectionBuilder; 042import de.cuioss.tools.logging.CuiLogger; 043import de.cuioss.tools.property.PropertyHolder; 044import de.cuioss.tools.reflect.MoreReflection; 045import lombok.experimental.UtilityClass; 046 047/** 048 * @author Oliver Wolff 049 */ 050@UtilityClass 051public final class ReflectionHelper { 052 053 /** 054 * Identifies the properties to be ignored. Currently only consisting of 'class' 055 */ 056 @SuppressWarnings("squid:S2386") // owolff: False positive -> is immutable 057 public static final Set<String> PROPERTY_IGNORE_SET = immutableSet("class"); 058 059 private static final CuiLogger log = new CuiLogger(ReflectionHelper.class); 060 061 /** 062 * One stop method for the deriving of configured metadata 063 * 064 * @param annotated must not be null 065 * @param targetClass must not be null 066 * @return an immutable {@link List} of {@link PropertyMetadata} containing the 067 * result of the actual scanning with 068 * {@link #scanBeanTypeForProperties(Class, PropertyReflectionConfig)} 069 * and {@link #handlePostProcess(Class, SortedSet)} and 070 * {@link PropertyHelper#handlePropertyConfigAnnotations(Class)} and 071 * {@link PropertyHelper#handlePrimitiveAsDefaults(Collection)} 072 */ 073 public static <T> List<PropertyMetadata> handlePropertyMetadata(final Class<?> annotated, 074 final Class<T> targetClass) { 075 requireNonNull(annotated); 076 requireNonNull(targetClass); 077 078 final List<PropertyMetadata> builder = new ArrayList<>(); 079 if (shouldScanClass(annotated)) { 080 final SortedSet<PropertyMetadata> scanned = new TreeSet<>(scanBeanTypeForProperties(targetClass, 081 MoreReflection.extractAnnotation(annotated, PropertyReflectionConfig.class).orElse(null))); 082 083 builder.addAll(handlePostProcess(annotated, scanned)); 084 } 085 final var mapView = PropertyHelper.toMapView(builder); 086 PropertyHelper.handlePropertyConfigAnnotations(annotated).forEach(p -> mapView.put(p.getName(), p)); 087 final var handled = PropertyHelper.handlePrimitiveAsDefaults(mapView.values()); 088 PropertyHelper.logMessageForPropertyMetadata(handled); 089 return new ArrayList<>(handled); 090 } 091 092 /** 093 * One stop method for the deriving of configured metadata 094 * 095 * @param propertyReflectionConfig extracted from the target class, may be null. 096 * @param propertyConfig extracted from the target class, may be empty 097 * 098 * @param targetClass must not be null 099 * @return an immutable {@link List} of {@link PropertyMetadata} containing the 100 * result of the actual scanning with 101 * {@link #scanBeanTypeForProperties(Class, PropertyReflectionConfig)} 102 * and {@link #handlePostProcess(Class, SortedSet)} and 103 * {@link PropertyHelper#handlePropertyConfigAnnotations(Class)} and 104 * {@link PropertyHelper#handlePrimitiveAsDefaults(Collection)} 105 */ 106 public static <T> List<PropertyMetadata> handlePropertyMetadata(PropertyReflectionConfig propertyReflectionConfig, 107 final List<PropertyConfig> propertyConfig, final Class<T> targetClass) { 108 final List<PropertyMetadata> builder = new ArrayList<>(); 109 if (shouldScanClass(propertyReflectionConfig)) { 110 final SortedSet<PropertyMetadata> scanned = new TreeSet<>( 111 scanBeanTypeForProperties(targetClass, propertyReflectionConfig)); 112 113 builder.addAll(handlePostProcessConfig(propertyReflectionConfig, scanned)); 114 } 115 final var mapView = PropertyHelper.toMapView(builder); 116 PropertyHelper.handlePropertyConfigAnnotations(propertyConfig).forEach(p -> mapView.put(p.getName(), p)); 117 final var handled = PropertyHelper.handlePrimitiveAsDefaults(mapView.values()); 118 PropertyHelper.logMessageForPropertyMetadata(handled); 119 return new ArrayList<>(handled); 120 } 121 122 /** 123 * Uses {@link MoreReflection} to scan the concrete bean and describe the 124 * properties with fitting {@link PropertyMetadata}. Each property will contain 125 * the derived data for the attributes: 126 * <ul> 127 * <li>{@link PropertyMetadata#getName()}</li> 128 * <li>{@link PropertyMetadata#getGenerator()} with the generator being 129 * dynamically resolved using 130 * {@link GeneratorResolver#resolveGenerator(Class)}</li> 131 * <li>{@link PropertyMetadata#getPropertyReadWrite()}</li> 132 * <li>{@link PropertyMetadata#getCollectionType()}</li> 133 * <li>{@link PropertyMetadata#getPropertyMemberInfo()}</li> 134 * </ul> 135 * Other attributes use there defaults: 136 * <ul> 137 * <li>{@link PropertyMetadata#isRequired()} defaults to false</li> 138 * <li>{@link PropertyMetadata#isDefaultValue()} defaults to false</li> 139 * </ul> 140 * 141 * @param beanType to be checked by reflection 142 * @param config optional instance of {@link PropertyReflectionConfig} used 143 * for filtering the scanning 144 * @return a {@link SortedSet} containing the result of the inspection 145 */ 146 public static SortedSet<PropertyMetadata> scanBeanTypeForProperties(final Class<?> beanType, 147 final PropertyReflectionConfig config) { 148 final Set<String> filter = new HashSet<>(PROPERTY_IGNORE_SET); 149 if (null != config) { 150 filter.addAll(Arrays.asList(config.exclude())); 151 } 152 final var found = new CollectionBuilder<PropertyMetadata>(); 153 154 var builder = new CollectionBuilder<PropertyHolder>(); 155 for (Method method : MoreReflection.retrieveAccessMethods(beanType)) { 156 var attributeName = MoreReflection.computePropertyNameFromMethodName(method.getName()); 157 if (filter.contains(attributeName)) { 158 log.debug("Filtering attribute '%s' for type '%s' as configured", attributeName, beanType); 159 continue; 160 } 161 var holder = PropertyHolder.from(beanType, attributeName); 162 if (holder.isEmpty()) { 163 log.info("Unable to extract metadata for type '%s' and method '%s'", beanType, method.getName()); 164 } else { 165 builder.add(holder.get()); 166 } 167 } 168 169 for (PropertyHolder holder : builder) { 170 found.add(createPropertyMetadata(beanType, holder)); 171 } 172 return found.toImmutableNavigableSet(); 173 } 174 175 /** 176 * Creates a {@link PropertyMetadata} for a given field. 177 * 178 * @param beanType providing the property, must not be null 179 * @param propertyHolder identifying the property-metadata, must not be null 180 * @return an instance of {@link PropertyMetadata} describing the field. 181 * @throws IllegalArgumentException wrapping underlying reflection exceptions. 182 */ 183 public static PropertyMetadata createPropertyMetadata(final Class<?> beanType, PropertyHolder propertyHolder) { 184 requireNonNull(beanType); 185 requireNonNull(propertyHolder); 186 187 var collectionType = CollectionType.NO_ITERABLE; 188 Class<?> propertyType = propertyHolder.getType(); 189 190 final var field = MoreReflection.accessField(beanType, propertyHolder.getName()); 191 192 if (field.isPresent()) { 193 final var collectionTypeOption = CollectionType.findResponsibleCollectionType(field.get().getType()); 194 if (collectionTypeOption.isPresent()) { 195 collectionType = collectionTypeOption.get(); 196 if (CollectionType.ARRAY_MARKER.equals(collectionType)) { 197 propertyType = field.get().getType().getComponentType(); 198 } else { 199 propertyType = extractParameterizedType(field.get(), 200 (ParameterizedType) field.get().getGenericType()); 201 } 202 } 203 } 204 if (null == propertyType) { 205 throw new IllegalArgumentException("Unable to extract property '%s' on type '%s'" 206 .formatted(propertyHolder.getName(), beanType.getName())); 207 } 208 var defaultValued = propertyType.isPrimitive(); 209 if (defaultValued && CollectionType.ARRAY_MARKER.equals(collectionType)) { 210 defaultValued = false; 211 } 212 return PropertyMetadataImpl.builder().name(propertyHolder.getName()).defaultValue(defaultValued) 213 .collectionType(collectionType).propertyMemberInfo(propertyHolder.getMemberInfo()) 214 .propertyReadWrite(propertyHolder.getReadWrite()) 215 .generator(GeneratorResolver.resolveGenerator(propertyType)).build(); 216 } 217 218 private static Class<?> extractParameterizedType(final Field field, final ParameterizedType parameterizedType) { 219 try { 220 return (Class<?>) parameterizedType.getActualTypeArguments()[0]; 221 } catch (final ClassCastException e) { 222 throw new IllegalStateException(""" 223 Unable to determine generic-type for %s, ususally this is the case with nested generics.\s\ 224 225 You need to provide a custom @PropertyConfig for this field and exclude it from scanning\ 226 , by using PropertyReflectionConfig#exclude. 227 See package-javadoc of de.cuioss.test.valueobjects for samples.""".formatted(field.toString()), e); 228 } 229 } 230 231 /** 232 * Filters the given properties according to the annotated element given. It 233 * checks for the annotation {@link PropertyReflectionConfig} and filters the 234 * given {@link SortedSet} 235 * 236 * @param annotated must not be null 237 * @param metatdata must not be null 238 * @return the filtered {@link SortedSet} 239 */ 240 public static SortedSet<PropertyMetadata> handlePostProcess(final Class<?> annotated, 241 final SortedSet<PropertyMetadata> metatdata) { 242 requireNonNull(annotated); 243 244 final Optional<PropertyReflectionConfig> configOption = MoreReflection.extractAnnotation(annotated, 245 PropertyReflectionConfig.class); 246 247 return handlePostProcessConfig(configOption.orElse(null), metatdata); 248 } 249 250 /** 251 * Filters the given properties according to the annotated element given. It 252 * checks for the annotation {@link PropertyReflectionConfig} and filters the 253 * given {@link SortedSet} 254 * 255 * @param config must not be null 256 * @param metatdata must not be null 257 * @return the filtered {@link SortedSet} 258 */ 259 public static SortedSet<PropertyMetadata> handlePostProcessConfig(final PropertyReflectionConfig config, 260 final SortedSet<PropertyMetadata> metatdata) { 261 requireNonNull(metatdata); 262 263 if (metatdata.isEmpty() || null == config) { 264 return metatdata; 265 } 266 267 var map = PropertyHelper.handleWhiteAndBlacklist(config.of(), config.exclude(), metatdata); 268 269 map = AnnotationHelper.modifyPropertyMetadata(map, config.defaultValued(), config.readOnly(), config.required(), 270 config.transientProperties(), config.writeOnly(), config.assertUnorderedCollection()); 271 272 return immutableSortedSet(map.values()); 273 } 274 275 /** 276 * Checks the given type for the annotation {@link PropertyReflectionConfig} if 277 * it is there it checks on the value {@link PropertyReflectionConfig#skip()} 278 * 279 * @param annotated the class that may or may not provide the annotations, must 280 * not be null 281 * @return boolean indicating whether to skip property scanning. 282 */ 283 public static boolean shouldScanClass(final Class<?> annotated) { 284 requireNonNull(annotated); 285 286 return shouldScanClass( 287 MoreReflection.extractAnnotation(annotated, PropertyReflectionConfig.class).orElse(null)); 288 } 289 290 /** 291 * Checks the given type for the annotation {@link PropertyReflectionConfig} if 292 * it is there it checks on the value {@link PropertyReflectionConfig#skip()} 293 * 294 * @param config the optional annotation, may be null 295 * @return boolean indicating whether to skip property scanning. 296 */ 297 public static boolean shouldScanClass(final PropertyReflectionConfig config) { 298 if (null != config) { 299 return !config.skip(); 300 } 301 return true; 302 } 303 304 /** 305 * Helper method that determines the actual type of a given {@link Iterable} by 306 * peeking into it. <em>For testing only, should never be used in productive 307 * code</em> 308 * 309 * @param iterable must not be null nor empty, the iterator must be reentrant. 310 * @return The Class of the given {@link Iterable}. 311 */ 312 @SuppressWarnings("unchecked") 313 public static <T> Class<T> determineSupertypeFromIterable(final Iterable<T> iterable) { 314 requireNonNull(iterable, "iterable must not be null"); 315 final var iterator = iterable.iterator(); 316 if (iterator.hasNext()) { 317 return (Class<T>) iterator.next().getClass(); 318 } 319 throw new IllegalArgumentException("Must contain at least a single element"); 320 } 321}