All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdbi.v3.sqlobject.SqlObjectFactory Maven / Gradle / Ivy

Go to download

SqlObject is a declarative, annotation-driven API for database access. It complements the core API. Jdbi 3 is designed to provide convenient tabular data access in Java(tm) and other JVM based languages. It uses the Java collections framework for query results, provides a convenient means of externalizing SQL statements, and named parameter support for any database that supports JDBC.

There is a newer version: 3.45.4
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jdbi.v3.sqlobject;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.config.ConfigRegistry;
import org.jdbi.v3.core.config.JdbiCache;
import org.jdbi.v3.core.config.JdbiCaches;
import org.jdbi.v3.core.extension.ExtensionFactory;
import org.jdbi.v3.core.extension.Extensions;
import org.jdbi.v3.core.extension.HandleSupplier;
import org.jdbi.v3.core.internal.OnDemandExtensions;
import org.jdbi.v3.sqlobject.config.Configurer;
import org.jdbi.v3.sqlobject.config.ConfiguringAnnotation;
import org.jdbi.v3.sqlobject.internal.SqlObjectInitData;
import org.jdbi.v3.sqlobject.internal.SqlObjectInitData.InContextInvoker;

/**
 * Creates implementations for SqlObject interfaces.
 */
public class SqlObjectFactory implements ExtensionFactory, OnDemandExtensions.Factory {
    private final JdbiCache, SqlObjectInitData> sqlObjectCache =
            JdbiCaches.declare(SqlObjectFactory::initDataFor);

    SqlObjectFactory() {}

    @Override
    public boolean accepts(Class extensionType) {
        if (looksLikeSqlObject(extensionType)) {
            if (extensionType.getAnnotation(GenerateSqlObject.class) != null) {
                return true;
            }

            if (!extensionType.isInterface()) {
                throw new IllegalArgumentException("SQL Objects are only supported for interfaces.");
            }

            return true;
        }

        return false;
    }

    private boolean looksLikeSqlObject(Class extensionType) {
        if (SqlObject.class.isAssignableFrom(extensionType)) {
            return true;
        }

        return Stream.of(extensionType.getMethods())
                .flatMap(m -> Stream.of(m.getAnnotations()))
                .anyMatch(a -> a.annotationType().isAnnotationPresent(SqlOperation.class));
    }

    /**
     * Create a sql object of the specified type bound to this handle. Any state changes to the handle, or the sql
     * object, such as transaction status, closing it, etc, will apply to both the object and the handle.
     *
     * @param extensionType the type of sql object to create
     * @param handle the Handle instance to attach ths sql object to
     * @return the new sql object bound to this handle
     */
    @Override
    public  E attach(Class extensionType, HandleSupplier handle) {
        final SqlObjectInitData data = sqlObjectCache.get(extensionType, handle.getConfig());
        final ConfigRegistry instanceConfig = data.configureInstance(handle.getConfig().createCopy());

        if (data.isConcrete()) {
            return data.instantiate(extensionType, handle, instanceConfig);
        }
        instanceConfig.get(Extensions.class).onCreateProxy();

        Map> handlers = new HashMap<>();
        final Object proxy = Proxy.newProxyInstance(
                extensionType.getClassLoader(),
                new Class[] {extensionType},
                (p, m, a) -> handlers.get(m).get().invoke(a));

        data.forEachMethodHandler((m, h) ->
                handlers.put(m, data.lazyInvoker(proxy, m, handle, instanceConfig)));
        return extensionType.cast(proxy);
    }

    @Override
    public Optional onDemand(Jdbi db, Class extensionType, Class... extraTypes) {
        SqlObjectInitData data = sqlObjectCache.get(extensionType, db);
        if (!data.isConcrete()) {
            return Optional.empty();
        }
        try {
            return Optional.of(Class.forName(
                    extensionType.getPackage().getName() + "." + extensionType.getSimpleName() + "Impl$OnDemand")
                .getConstructor(Jdbi.class)
                .newInstance(db));
        } catch (ReflectiveOperationException | ExceptionInInitializerError e) {
            throw new UnableToCreateSqlObjectException(e);
        }
    }

    private static Map buildMethodHandlers(
            Class sqlObjectType,
            Handlers registry,
            HandlerDecorators decorators) {
        final Map handlers = new HashMap<>();

        handlers.putAll(handlerEntry((t, a, h) ->
                sqlObjectType.getName() + '@' + Integer.toHexString(t.hashCode()),
            Object.class, "toString"));
        handlers.putAll(handlerEntry((t, a, h) -> t == a[0], Object.class, "equals", Object.class));
        handlers.putAll(handlerEntry((t, a, h) -> System.identityHashCode(t), Object.class, "hashCode"));
        handlers.putAll(handlerEntry((t, a, h) -> h.getHandle(), SqlObject.class, "getHandle"));
        try {
            handlers.putAll(handlerEntry((t, a, h) -> null, sqlObjectType, "finalize"));
        } catch (IllegalStateException expected) {
            // optional implementation
        }

        final Set methods = new LinkedHashSet<>();
        methods.addAll(Arrays.asList(sqlObjectType.getMethods()));
        methods.addAll(Arrays.asList(sqlObjectType.getDeclaredMethods()));

        final Set seen = handlers.keySet().stream()
                .collect(Collectors.toCollection(HashSet::new));
        for (Method method : methods) {
            if (Modifier.isStatic(method.getModifiers()) || !seen.add(method)) {
                continue;
            }
            handlers.put(method, decorators.applyDecorators(
                        registry.findFor(sqlObjectType, method)
                            .orElseGet(() -> {
                                Supplier x = () -> new IllegalStateException(String.format(
                                        "Method %s.%s must have an implementation or be annotated with a SQL method annotation.",
                                        method.getDeclaringClass().getSimpleName(),
                                        method.getName()));
                                if (!SqlObjectInitData.isConcrete(sqlObjectType) && !method.isSynthetic() && !Modifier.isPrivate(method.getModifiers())) {
                                    throw x.get();
                                }
                                return (t, a, h) -> {
                                    throw x.get();
                                };
                            }),
                        sqlObjectType,
                        method));
        }

        methods.stream()
            .filter(m -> !m.isSynthetic())
            .collect(Collectors.groupingBy(m -> Arrays.asList(m.getName(), Arrays.asList(m.getParameterTypes()))))
            .values()
            .stream()
            .filter(l -> l.size() > 1)
            .findAny()
            .ifPresent(ms -> {
                throw new UnableToCreateSqlObjectException(sqlObjectType + " has ambiguous methods " + ms + ", please resolve with an explicit override");
            });

        return handlers;
    }

    private static Map handlerEntry(Handler handler, Class klass, String methodName, Class... parameterTypes) {
        return Collections.singletonMap(Handlers.methodLookup(klass, methodName, parameterTypes), handler);
    }

    private static UnaryOperator buildConfigurers(Stream elements, ConfigurerMethod consumer) {
        List> myConfigurers = elements
                .flatMap(ae -> Arrays.stream(ae.getAnnotations()))
                .filter(a -> a.annotationType().isAnnotationPresent(ConfiguringAnnotation.class))
                .map(a -> {
                    ConfiguringAnnotation meta = a.annotationType()
                            .getAnnotation(ConfiguringAnnotation.class);

                    Configurer configurer = getConfigurer(meta.value());
                    return (Consumer) config -> consumer.configure(configurer, config, a);
                })
                .collect(Collectors.toList());
        return config -> {
            myConfigurers.forEach(configurer -> configurer.accept(config));
            return config;
        };
    }

    private static Configurer getConfigurer(Class factoryClass) {
        try {
            return factoryClass.getDeclaredConstructor().newInstance();
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException("Unable to instantiate configurer factory class " + factoryClass, e);
        }
    }

    static SqlObjectInitData initDataFor(ConfigRegistry handlersConfig, Class sqlObjectType) {
        Map methodHandlers = buildMethodHandlers(
                sqlObjectType,
                handlersConfig.get(Handlers.class),
                handlersConfig.get(HandlerDecorators.class));

        UnaryOperator instanceConfigurer = buildConfigurers(
                Stream.concat(
                    superTypes(sqlObjectType),
                    Stream.of(sqlObjectType)),
                (configurer, config, annotation) ->
                    configurer.configureForType(config, annotation, sqlObjectType));

        Map> methodConfigurers =
            methodHandlers.keySet().stream().collect(
                Collectors.toMap(
                        Function.identity(),
                        method -> buildConfigurers(
                            Stream.of(method),
                            (configurer, config, annotation) ->
                                configurer.configureForMethod(config, annotation, sqlObjectType, method))));

        return new SqlObjectInitData(
                sqlObjectType,
                instanceConfigurer,
                methodConfigurers,
                methodHandlers);
    }

    // duplicate implementation in CustomizingStatementHandler
    private static Stream> superTypes(Class type) {
        Class[] interfaces = type.getInterfaces();
        return Stream.concat(
            Arrays.stream(interfaces).flatMap(SqlObjectFactory::superTypes),
            Arrays.stream(interfaces));
    }

    interface ConfigurerMethod {
        void configure(Configurer configurer, ConfigRegistry config, Annotation annotation);
    }
}