001/*
002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC.
003 *
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 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
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 */
016
017package com.pyranid;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.ThreadSafe;
023import java.lang.reflect.Constructor;
024import java.lang.reflect.InvocationTargetException;
025import java.util.Arrays;
026
027import static java.lang.String.format;
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Contract for a factory that creates instances given a type.
032 * <p>
033 * Useful for resultset mapping, where each row in the resultset might require a new instance.
034 * <p>
035 * Implementors are suggested to employ application-specific strategies, such as having a DI container handle instance
036 * creation.
037 * <p>
038 * Implementations should be threadsafe.
039 *
040 * @author <a href="https://www.revetkn.com">Mark Allen</a>
041 * @since 1.0.0
042 */
043@ThreadSafe
044public interface InstanceProvider {
045        /**
046         * Provides an instance of the given {@code instanceType}.
047         * <p>
048         * Whether the instance is new every time or shared/reused is implementation-dependent.
049         *
050         * @param <T>          instance type token
051         * @param instanceType the type of instance to create
052         * @return an instance of the given {@code instanceType}
053         */
054        @NonNull
055        default <T> T provide(@NonNull StatementContext<T> statementContext,
056                                                                                                @NonNull Class<T> instanceType) {
057                requireNonNull(statementContext);
058                requireNonNull(instanceType);
059
060                try {
061                        return instanceType.getDeclaredConstructor().newInstance();
062                } catch (Exception e) {
063                        throw new DatabaseException(format(
064                                        "Unable to create an instance of %s. Please verify that %s has a public no-argument constructor",
065                                        instanceType, instanceType.getSimpleName()), e);
066                }
067        }
068
069        /**
070         * Provides an instance of the given {@code recordType}.
071         * <p>
072         * Whether the instance is new every time or shared/reused is implementation-dependent.
073         *
074         * @param <T>        instance type token
075         * @param recordType the type of instance to create (must be a record)
076         * @param initargs   values used to construct the record instance
077         * @return an instance of the given {@code recordType}
078         * @since 2.0.0
079         */
080        @NonNull
081        default <T extends Record> T provideRecord(@NonNull StatementContext<T> statementContext,
082                                                                                                                                                                                 @NonNull Class<T> recordType,
083                                                                                                                                                                                 Object @Nullable ... initargs) {
084                requireNonNull(statementContext);
085                requireNonNull(recordType);
086
087                try {
088                        // Find the canonical constructor for the record.
089                        // Hat tip to https://stackoverflow.com/a/67127067
090                        Class<?>[] componentTypes = Arrays.stream(recordType.getRecordComponents())
091                                        .map(rc -> rc.getType())
092                                        .toArray(Class<?>[]::new);
093
094                        Constructor<T> constructor = recordType.getDeclaredConstructor(componentTypes);
095                        return constructor.newInstance(initargs);
096                } catch (NoSuchMethodException | InstantiationException | IllegalAccessException |
097                                                 IllegalArgumentException | InvocationTargetException e) {
098                        throw new DatabaseException(String.format("Unable to instantiate Record type %s with args %s", recordType,
099                                        initargs == null ? "[none]" : Arrays.asList(initargs)), e);
100                }
101        }
102}