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 java.util.Objects.requireNonNull;
020
021import java.lang.reflect.InvocationTargetException;
022import java.lang.reflect.Method;
023
024import de.cuioss.test.valueobjects.contract.BuilderContractImpl;
025import de.cuioss.test.valueobjects.objects.BuilderInstantiator;
026import de.cuioss.tools.logging.CuiLogger;
027import lombok.Getter;
028import lombok.ToString;
029
030/**
031 * Used for creating instances of a builder. This variant relies on a factory
032 * method on the target type usually with the name "builder". See
033 * {@link BuilderFactoryBasedInstantiator#BuilderFactoryBasedInstantiator(Class, String, String)}
034 * for details
035 *
036 * @author Oliver Wolff
037 * @param <T> identifying the type of the {@link Object} created by the builder
038 */
039@ToString
040public class BuilderFactoryBasedInstantiator<T> implements BuilderInstantiator<T> {
041
042    private static final String UNABLE_TO_ACCESS_METHOD = "Unable to access method %s on type %s, due to %s";
043
044    private static final CuiLogger log = new CuiLogger(BuilderFactoryBasedInstantiator.class);
045
046    private final Method builderFactoryMethod;
047    private final Method builderMethod;
048
049    @Getter
050    private final Class<T> targetClass;
051
052    @Getter
053    private final Class<?> builderClass;
054
055    /**
056     * Constructor. shortcut for calling
057     * {@link #BuilderFactoryBasedInstantiator(Class, String, String)} with
058     * {@link BuilderContractImpl#DEFAULT_BUILDER_FACTORY_METHOD_NAME} and
059     * {@value BuilderContractImpl#DEFAULT_BUILD_METHOD_NAME}
060     *
061     * @param enclosingType identifying the type where the method is located in,
062     *                      must not be null.
063     */
064    public BuilderFactoryBasedInstantiator(final Class<?> enclosingType) {
065        this(enclosingType, BuilderContractImpl.DEFAULT_BUILDER_FACTORY_METHOD_NAME,
066                BuilderContractImpl.DEFAULT_BUILD_METHOD_NAME);
067    }
068
069    /**
070     * Constructor.
071     *
072     * @param enclosingType            identifying the type where the method is
073     *                                 located in, must not be null.
074     * @param builderFactoryMethodName The name of the factory method on the
075     *                                 enclosing type. It is assumed that it is
076     *                                 parameter-free static method
077     * @param builderMethodName        the actual name or the builder-method
078     */
079    @SuppressWarnings("unchecked")
080    public BuilderFactoryBasedInstantiator(final Class<?> enclosingType, final String builderFactoryMethodName,
081            final String builderMethodName) {
082
083        requireNonNull(enclosingType, "enclosingType must not be null");
084        requireNonNull(builderMethodName, "builderMethodName must not be null");
085        requireNonNull(builderFactoryMethodName, "builderFactoryMethodName must not be null");
086
087        try {
088            builderFactoryMethod = enclosingType.getDeclaredMethod(builderFactoryMethodName);
089            builderClass = builderFactoryMethod.getReturnType();
090        } catch (NoSuchMethodException | SecurityException e) {
091            final var message = UNABLE_TO_ACCESS_METHOD.formatted(builderFactoryMethodName, enclosingType.getName(),
092                    extractCauseMessageFromThrowable(e));
093            log.error(message, e);
094            throw new AssertionError(message, e);
095        }
096
097        try {
098            builderMethod = builderClass.getDeclaredMethod(builderMethodName);
099            targetClass = (Class<T>) builderMethod.getReturnType();
100        } catch (NoSuchMethodException | SecurityException e) {
101            final var message = UNABLE_TO_ACCESS_METHOD.formatted(builderMethodName, builderClass,
102                    extractCauseMessageFromThrowable(e));
103            log.error(message, e);
104            throw new AssertionError(message, e);
105        }
106
107    }
108
109    @Override
110    public Object newBuilderInstance() {
111        try {
112            return builderFactoryMethod.invoke(null);
113        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
114            final var message = UNABLE_TO_ACCESS_METHOD.formatted(builderFactoryMethod.getName(), targetClass,
115                    extractCauseMessageFromThrowable(e));
116            log.error(message, e);
117            throw new AssertionError(message, e);
118        }
119    }
120
121    @SuppressWarnings("unchecked")
122    @Override
123    public T build(final Object builder) {
124        try {
125            return (T) builderMethod.invoke(builder);
126        } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) {
127            final var message = UNABLE_TO_ACCESS_METHOD.formatted(builderMethod.getName(), builderClass.getName(),
128                    extractCauseMessageFromThrowable(e));
129            log.debug(message, e);
130            throw new AssertionError(message, e);
131        }
132    }
133}