/* * 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 static java.util.Collections.synchronizedMap; import org.jdbi.v3.core.config.ConfigRegistry; import org.jdbi.v3.core.extension.ExtensionFactory; import org.jdbi.v3.core.extension.ExtensionMethod; import org.jdbi.v3.core.extension.HandleSupplier; import org.jdbi.v3.sqlobject.config.Configurer; import org.jdbi.v3.sqlobject.config.ConfiguringAnnotation; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.InvocationHandler; 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.Map; import java.util.WeakHashMap; import java.util.function.BiConsumer; import java.util.stream.Stream; public class SqlObjectFactory implements ExtensionFactory { private static final Object[] NO_ARGS = new Object[0]; private final Map<Class<?>, Map<Method, Handler>> handlersCache = synchronizedMap(new WeakHashMap<>()); private final Map<Class<? extends Configurer>, Configurer> configurers = synchronizedMap(new WeakHashMap<>()); SqlObjectFactory() { } @Override public boolean accepts(Class<?> extensionType) { if (looksLikeSqlObject(extensionType)) { if (!extensionType.isInterface()) { throw new IllegalArgumentException("SQL Objects are only supported for interfaces."); } if (!Modifier.isPublic(extensionType.getModifiers())) { throw new IllegalArgumentException("SQL Object types must be public."); } 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(SqlMethodAnnotation.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> E attach(Class<E> extensionType, HandleSupplier handle) { Map<Method, Handler> handlers = methodHandlersFor( extensionType, handle.getConfig(Handlers.class), handle.getConfig(HandlerDecorators.class)); ConfigRegistry instanceConfig = handle.getConfig().createCopy(); forEachConfigurer(extensionType, (configurer, annotation) -> configurer.configureForType(instanceConfig, annotation, extensionType)); InvocationHandler invocationHandler = createInvocationHandler(extensionType, instanceConfig, handlers, handle); return extensionType.cast( Proxy.newProxyInstance( extensionType.getClassLoader(), new Class[]{extensionType}, invocationHandler)); } private Map<Method, Handler> methodHandlersFor(Class<?> sqlObjectType, Handlers registry, HandlerDecorators decorators) { return handlersCache.computeIfAbsent(sqlObjectType, type -> { final Map<Method, Handler> 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 for (Method method : sqlObjectType.getMethods()) { handlers.computeIfAbsent(method, m -> buildMethodHandler(sqlObjectType, m, registry, decorators)); } return handlers; }); } private Handler buildMethodHandler(Class<?> sqlObjectType, Method method, Handlers handlers, HandlerDecorators decorators) { Handler handler = handlers.findFor(sqlObjectType, method) .orElseThrow(() -> new IllegalStateException(String.format( "Method %s.%s must be default or be annotated with a SQL method annotation.", sqlObjectType.getSimpleName(), method.getName()))); return decorators.applyDecorators(handler, sqlObjectType, method); } private static Map<Method, Handler> handlerEntry(Handler handler, Class<?> klass, String methodName, Class<?>... parameterTypes) { try { return Collections.singletonMap(klass.getMethod(methodName, parameterTypes), handler); } catch (NoSuchMethodException | SecurityException e) { throw new IllegalStateException( String.format("can't find %s#%s%s", klass.getName(), methodName, Arrays.asList(parameterTypes)), e); } } private InvocationHandler createInvocationHandler(Class<?> sqlObjectType, ConfigRegistry instanceConfig, Map<Method, Handler> handlers, HandleSupplier handle) { return (proxy, method, args) -> { Handler handler = handlers.get(method); ConfigRegistry methodConfig = instanceConfig.createCopy(); forEachConfigurer(method, (configurer, annotation) -> configurer.configureForMethod(methodConfig, annotation, sqlObjectType, method)); return handle.invokeInContext(new ExtensionMethod(sqlObjectType, method), methodConfig, () -> handler.invoke(proxy, args == null ? NO_ARGS : args, handle)); }; } private void forEachConfigurer(AnnotatedElement element, BiConsumer<Configurer, Annotation> consumer) { Stream.of(element.getAnnotations()) .filter(a -> a.annotationType().isAnnotationPresent(ConfiguringAnnotation.class)) .forEach(a -> { ConfiguringAnnotation meta = a.annotationType() .getAnnotation(ConfiguringAnnotation.class); consumer.accept(getConfigurer(meta.value()), a); }); } private Configurer getConfigurer(Class<? extends Configurer> factoryClass) { return configurers.computeIfAbsent(factoryClass, c -> { try { return c.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalStateException("Unable to instantiate configurer factory class " + factoryClass, e); } }); } }