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.objects.impl;
017
018import static de.cuioss.test.valueobjects.objects.impl.ExceptionHelper.extractCauseMessageFromThrowable;
019import static de.cuioss.tools.base.Preconditions.checkArgument;
020import static de.cuioss.tools.string.MoreStrings.isEmpty;
021import static java.util.Objects.requireNonNull;
022import static org.junit.jupiter.api.Assertions.assertNotNull;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.util.ArrayList;
028import java.util.List;
029
030import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator;
031import de.cuioss.test.valueobjects.objects.RuntimeProperties;
032import de.cuioss.tools.logging.CuiLogger;
033
034/**
035 * This {@link ParameterizedInstantiator} uses a factory method derived by the
036 * given {@link RuntimeProperties#getAllProperties()} in order to instantiate
037 * {@link Object}s
038 *
039 * @author Oliver Wolff
040 * @param <T> identifying the type of objects to be created
041 */
042public class FactoryBasedInstantiator<T> extends AbstractOrderedArgsInstantiator<T> {
043
044    private static final CuiLogger log = new CuiLogger(FactoryBasedInstantiator.class);
045
046    private final Method factoryMethod;
047
048    /**
049     * Constructor.
050     *
051     * @param type              the type of the object to be created
052     * @param runtimeProperties must not be null. defines the attributes in the
053     *                          exact order to be used for the constructor:
054     *                          {@link RuntimeProperties#getAllProperties()}
055     * @param enclosingType     the type providing the factory method, usually the
056     *                          target type to be created, must not be null
057     * @param factoryMethodName the name of the factory method, must not be null,
058     *                          nor empty.
059     */
060    public FactoryBasedInstantiator(final Class<T> type, final RuntimeProperties runtimeProperties,
061            final Class<?> enclosingType, final String factoryMethodName) {
062
063        super(runtimeProperties);
064        requireNonNull(type, "type must not be null");
065        requireNonNull(enclosingType, "enclosingType must not be null");
066        requireNonNull(factoryMethodName);
067        checkArgument(!isEmpty(factoryMethodName), "factoryMethodName msut not be null nor empty");
068
069        final List<Class<?>> parameter = new ArrayList<>();
070        runtimeProperties.getAllProperties().forEach(meta -> parameter.add(meta.resolveActualClass()));
071        try {
072            if (parameter.isEmpty()) {
073                factoryMethod = enclosingType.getDeclaredMethod(factoryMethodName);
074            } else {
075                factoryMethod = enclosingType.getDeclaredMethod(factoryMethodName, toClassArray(parameter));
076            }
077            assertNotNull(
078
079                    factoryMethod,
080                    "Unable to find a factory method with signature " + parameter + " and name " + factoryMethodName);
081            assertTrue(type.isAssignableFrom(factoryMethod.getReturnType()),
082                    "Invalid type found on factory method: " + factoryMethod.getReturnType());
083        } catch (NoSuchMethodException | SecurityException e) {
084            final var message = new StringBuilder("Unable to find a constructor with signature ").append(parameter)
085                    .toString();
086            log.error(message, e);
087            throw new AssertionError(message);
088        }
089    }
090
091    @SuppressWarnings("unchecked")
092    @Override
093    protected T doInstantiate(final Object... args) {
094        try {
095            return (T) factoryMethod.invoke(null, args);
096        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
097            final var message = new StringBuilder("Unable to invoke constructor ").append(", due to ")
098                    .append(extractCauseMessageFromThrowable(e)).toString();
099            throw new AssertionError(message, e);
100        }
101    }
102
103    @Override
104    public String toString() {
105        final var builder = new StringBuilder(getClass().getName());
106        builder.append("\nFactory Method: ").append(factoryMethod);
107        builder.append("\nProperty Configuration: ").append(getRuntimeProperties().toString());
108        return builder.toString();
109    }
110}