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}