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.generator.dynamic.impl; 017 018import java.lang.reflect.Constructor; 019import java.lang.reflect.InvocationTargetException; 020import java.lang.reflect.Modifier; 021import java.lang.reflect.Parameter; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Comparator; 025import java.util.List; 026import java.util.Optional; 027 028import de.cuioss.test.generator.TypedGenerator; 029import de.cuioss.test.valueobjects.generator.TypedGeneratorRegistry; 030import de.cuioss.test.valueobjects.generator.dynamic.GeneratorResolver; 031import de.cuioss.test.valueobjects.objects.impl.ExceptionHelper; 032import de.cuioss.tools.logging.CuiLogger; 033import de.cuioss.tools.string.Joiner; 034import lombok.AccessLevel; 035import lombok.NonNull; 036import lombok.RequiredArgsConstructor; 037import lombok.ToString; 038 039/** 040 * @author Oliver Wolff 041 * @param <T> identifying the type to be generated 042 */ 043@RequiredArgsConstructor(access = AccessLevel.PRIVATE) 044@ToString 045public class ConstructorBasedGenerator<T> implements TypedGenerator<T> { 046 047 private static final CuiLogger log = new CuiLogger(ConstructorBasedGenerator.class); 048 049 private static final String UNABLE_TO_CALL_CONSTRUCTOR_FOR_CLASS = "Unable to call constructor '%s' for class '%s' due to: '%s'"; 050 051 @NonNull 052 private final Class<T> type; 053 @NonNull 054 private final List<TypedGenerator<?>> constructorGenerators; 055 @NonNull 056 private final Constructor<T> constructor; 057 058 @Override 059 public T next() { 060 if (constructorGenerators.isEmpty()) { 061 try { 062 return constructor.newInstance(); 063 } catch (final InstantiationException | IllegalAccessException | IllegalArgumentException 064 | InvocationTargetException e) { 065 throw new IllegalStateException( 066 UNABLE_TO_CALL_CONSTRUCTOR_FOR_CLASS.formatted(constructor, type, e.getMessage()), e); 067 } 068 } 069 final var parameter = new ArrayList<>(); 070 constructorGenerators.forEach(gen -> parameter.add(gen.next())); 071 try { 072 logExtendedInformationAboutUsedConstructor(parameter); 073 return constructor.newInstance(parameter.toArray()); 074 } catch (final InstantiationException | IllegalAccessException | IllegalArgumentException 075 | InvocationTargetException e) { 076 throw new IllegalStateException(UNABLE_TO_CALL_CONSTRUCTOR_FOR_CLASS.formatted(constructor, type, 077 ExceptionHelper.extractCauseMessageFromThrowable(e)), e); 078 } 079 } 080 081 private void logExtendedInformationAboutUsedConstructor(final ArrayList<Object> parameterValues) { 082 final var constructorModifierValue = constructor.getModifiers(); 083 if (!Modifier.isPublic(constructorModifierValue)) { 084 085 log.warn(""" 086 !!! Attention :\s\ 087 A non public constructor will be used to create an instance for {}.\s\ 088 This is illegal and can cause unexpected behaviour.\s\ 089 Solution: provide a fitting generator instead!""", constructor.getName()); 090 091 final List<String> parameterInfo = new ArrayList<>(constructor.getParameters().length); 092 for (final Parameter parameter : constructor.getParameters()) { 093 parameterInfo.add(parameter.getType().getSimpleName() + " " + parameter.getName()); 094 } 095 096 final var modifier = Modifier.toString(constructorModifierValue); 097 final var constructorInfo = modifier + " " + constructor.getName() + "(" 098 + Joiner.on(", ").skipNulls().join(parameterInfo) + ")"; 099 log.info("Used constructor : {}", constructorInfo); 100 log.info("Used constructor parameter : {}", logUsedValuesForConstructor(parameterValues)); 101 } 102 } 103 104 private static String logUsedValuesForConstructor(final ArrayList<Object> parameter) { 105 final List<String> parameterInfo = new ArrayList<>(); 106 for (final Object object : parameter) { 107 parameterInfo.add("[" + object.getClass().getSimpleName() + " " + object.toString() + "]"); 108 } 109 return Joiner.on(", ").skipNulls().join(parameterInfo); 110 } 111 112 @Override 113 public Class<T> getType() { 114 return type; 115 } 116 117 /** 118 * Factory method for creating an instance of {@link ConstructorBasedGenerator}. 119 * It first tries to find a public, than protected, package privates, private 120 * constructor. It always uses the constructor with the fewest arguments 121 * 122 * @param type to be checked for constructors, must not be null, nor an 123 * interface, nor an annotation nor an abstract-class nor an enum 124 * @return an {@link Optional} on the corresponding {@link TypedGenerator} if a 125 * constructor can be found 126 */ 127 public static final <T> Optional<TypedGenerator<T>> getGeneratorForType(final Class<T> type) { 128 if (!isReponsibleForType(type)) { 129 return Optional.empty(); 130 } 131 final List<Constructor<?>> constructors = Arrays.asList(type.getDeclaredConstructors()); 132 if (constructors.isEmpty()) { 133 log.warn("Unable to determine constructor for class {} ", type); 134 return Optional.empty(); 135 } 136 // Order according to parameter-count 137 constructors.sort(Comparator.comparingInt(Constructor::getParameterCount)); 138 139 // Find fitting public constructor 140 var filteredConstructors = constructors.stream().filter(c -> Modifier.isPublic(c.getModifiers())).toList(); 141 if (!filteredConstructors.isEmpty()) { 142 return findFittingConstructor(type, filteredConstructors); 143 } 144 145 // Find fitting protected constructor 146 filteredConstructors = constructors.stream().filter(c -> Modifier.isProtected(c.getModifiers())).toList(); 147 if (!filteredConstructors.isEmpty()) { 148 return findFittingConstructor(type, filteredConstructors); 149 } 150 151 // Find fitting package private constructor 152 filteredConstructors = constructors.stream().filter(c -> 0 == c.getModifiers()).toList(); 153 if (!filteredConstructors.isEmpty()) { 154 return findFittingConstructor(type, filteredConstructors); 155 } 156 157 // Find fitting private constructor 158 filteredConstructors = constructors.stream().filter(c -> Modifier.isPrivate(c.getModifiers())).toList(); 159 if (!filteredConstructors.isEmpty()) { 160 return findFittingConstructor(type, filteredConstructors); 161 } 162 log.warn("Unable to determine constructor for class {} ", type); 163 return Optional.empty(); 164 } 165 166 private static boolean isReponsibleForType(final Class<?> type) { 167 if (null == type || type.isAnnotation()) { 168 return false; 169 } 170 return !type.isEnum() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers()); 171 } 172 173 private static <T> Optional<TypedGenerator<T>> findFittingConstructor(final Class<T> type, 174 final List<Constructor<?>> constructorList) { 175 log.debug("Searching constructor for class {}", type); 176 if (1 == constructorList.size()) { 177 log.debug("Only one constructor present, so choosing this one"); 178 return createForConstructor(type, constructorList.get(0)); 179 } 180 for (final Constructor<?> con : constructorList) { 181 // Ok, try to find a constructor where the parameter are already registered 182 // Generator 183 final List<Class<?>> parameter = Arrays.asList(con.getParameterTypes()); 184 var allGeneratorAvailable = true; 185 for (final Class<?> param : parameter) { 186 if (!TypedGeneratorRegistry.containsGenerator(param)) { 187 allGeneratorAvailable = false; 188 log.debug("Missing generator for {}", param); 189 break; 190 } 191 } 192 if (allGeneratorAvailable) { 193 return createForConstructor(type, con); 194 } 195 } 196 log.warn("No valid constructor found for class {}", type); 197 for (final Constructor<?> con : constructorList) { 198 if (1 == con.getParameterCount() && con.getParameterTypes()[0].equals(type)) { 199 log.debug("Skipping copy constructor..."); 200 continue; 201 } 202 return createForConstructor(type, con); 203 } 204 throw new IllegalStateException("No matching constructor found for class " + type); 205 } 206 207 @SuppressWarnings("java:S3011") // owolff: Setting accessible is ok for test-code 208 private static <T> Optional<TypedGenerator<T>> createForConstructor(final Class<T> type, final Constructor<?> con) { 209 @SuppressWarnings("unchecked") 210 final var constructor = (Constructor<T>) con; 211 constructor.setAccessible(true); 212 213 final List<TypedGenerator<?>> generators = new ArrayList<>(); 214 for (final Class<?> parameterType : constructor.getParameterTypes()) { 215 if (parameterType.equals(type)) { 216 // Special case her: eventually copy-constructor -> Play safe prevent infinite 217 // loop, type in this case is not an interface 218 log.warn( 219 "Unable to create a generator for copy-constuctor of same type for class {}, constructor parameter type = {}", 220 type, parameterType); 221 return Optional.empty(); 222 223 } 224 generators.add(GeneratorResolver.resolveGenerator(parameterType)); 225 } 226 return Optional.of(new ConstructorBasedGenerator<>(type, generators, constructor)); 227 } 228}