Java tutorial
/** * Copyright (C) 2015 digitalfondue (info@digitalfondue.ch) * * 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 ch.digitalfondue.npjt; import java.lang.invoke.MethodHandles; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.TreeSet; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import ch.digitalfondue.npjt.mapper.ColumnMapperFactory; import ch.digitalfondue.npjt.mapper.DefaultMapper; import ch.digitalfondue.npjt.mapper.EnumMapper; import ch.digitalfondue.npjt.mapper.InstantMapper; import ch.digitalfondue.npjt.mapper.LocalDateMapper; import ch.digitalfondue.npjt.mapper.LocalDateTimeMapper; import ch.digitalfondue.npjt.mapper.ParameterConverter; public class QueryFactory { private static final boolean localDateTimeAvailable = ClassUtils.isPresent("java.time.LocalDateTime", QueryFactory.class.getClassLoader()); private final String activeDb; private final NamedParameterJdbcTemplate jdbc; private final ClassReferencedSortedSet<ColumnMapperFactory> columnMapperFactories = new ClassReferencedSortedSet<>( new Comparator<ColumnMapperFactory>() { @Override public int compare(ColumnMapperFactory o1, ColumnMapperFactory o2) { return Integer.compare(o1.order(), o2.order()); } }); private final ClassReferencedSortedSet<ParameterConverter> parameterConverters = new ClassReferencedSortedSet<>( new Comparator<ParameterConverter>() { @Override public int compare(ParameterConverter o1, ParameterConverter o2) { return Integer.compare(o1.order(), o2.order()); } }); //default mappers and converters { columnMapperFactories.add(new EnumMapper.Factory()); parameterConverters.add(new EnumMapper.Converter()); columnMapperFactories.add(new DefaultMapper.Factory()); parameterConverters.add(new DefaultMapper.Converter()); // add support for LocalDateTime, LocalDate and Instant if (localDateTimeAvailable) { columnMapperFactories.add(new LocalDateMapper.Factory()); parameterConverters.add(new LocalDateMapper.Converter()); columnMapperFactories.add(new LocalDateTimeMapper.Factory()); parameterConverters.add(new LocalDateTimeMapper.Converter()); columnMapperFactories.add(new InstantMapper.Factory()); parameterConverters.add(new InstantMapper.Converter()); } } /* ugly solution, TODO: find a better one for handling the removal */ private static class ClassReferencedSortedSet<T> { final TreeSet<T> set; final Map<Class<?>, T> mapping = new HashMap<>(); ClassReferencedSortedSet(Comparator<T> comparator) { this.set = new TreeSet<>(comparator); } void add(T o) { set.add(o); mapping.put(o.getClass(), o); } void clear() { set.clear(); mapping.clear(); } void remove(Class<?> clazz) { T o = mapping.get(clazz); mapping.remove(clazz); if (o != null) { set.remove(o); } } } public QueryFactory(String activeDB, NamedParameterJdbcTemplate jdbc) { this.activeDb = activeDB; this.jdbc = jdbc; } public QueryFactory(String activeDB, DataSource dataSource) { this.activeDb = activeDB; this.jdbc = new NamedParameterJdbcTemplate(dataSource); } public QueryFactory(String activeDB, JdbcTemplate jdbcTemplate) { this.activeDb = activeDB; this.jdbc = new NamedParameterJdbcTemplate(jdbcTemplate); } private static class QueryTypeAndQuery { private final QueryType type; private final String query; private final Class<?> rowMapperClass; QueryTypeAndQuery(QueryType type, String query, Class<?> rowMapperClass) { this.type = type; this.query = query; this.rowMapperClass = rowMapperClass; } } // ----- public QueryFactory addColumnMapperFactory(ColumnMapperFactory columnMapperFactory) { columnMapperFactories.add(columnMapperFactory); return this; } public QueryFactory emptyColumnMapperFactories() { columnMapperFactories.clear(); return this; } public QueryFactory removeColumnMapperFactory(Class<? extends ColumnMapperFactory> clazz) { columnMapperFactories.remove(clazz); return this; } //----- public QueryFactory addParameterConverters(ParameterConverter parameterConverter) { parameterConverters.add(parameterConverter); return this; } public QueryFactory emptyParameterConverters() { parameterConverters.clear(); return this; } public QueryFactory removeParameterConverter(Class<? extends ParameterConverter> clazz) { parameterConverters.remove(clazz); return this; } // ----- private QueryTypeAndQuery extractQueryAnnotation(Class<?> clazz, Method method) { Query q = method.getAnnotation(Query.class); QueriesOverride qs = method.getAnnotation(QueriesOverride.class); // only one @Query annotation, thus we return the value without checking the database if (qs == null) { return new QueryTypeAndQuery(q.type(), q.value(), q.mapper()); } for (QueryOverride query : qs.value()) { if (query.db().equals(activeDb)) { return new QueryTypeAndQuery(q.type(), query.value(), query.mapper()); } } return new QueryTypeAndQuery(q.type(), q.value(), q.mapper()); } //from https://rmannibucau.wordpress.com/2014/03/27/java-8-default-interface-methods-and-jdk-dynamic-proxies/ private static final Method IS_DEFAULT_METHOD = ReflectionUtils.findMethod(Method.class, "isDefault"); private static final Constructor<MethodHandles.Lookup> LOOKUP_CONSTRUCTOR; static { try { LOOKUP_CONSTRUCTOR = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); if (!LOOKUP_CONSTRUCTOR.isAccessible()) { LOOKUP_CONSTRUCTOR.setAccessible(true); } } catch (NoSuchMethodException | SecurityException e) { throw new IllegalStateException(e); } } @SuppressWarnings("unchecked") public <T> T from(final Class<T> clazz) { return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { boolean hasAnnotation = method.getAnnotation(Query.class) != null; if (hasAnnotation) { QueryTypeAndQuery qs = extractQueryAnnotation(clazz, method); return qs.type.apply(qs.query, qs.rowMapperClass, jdbc, method, args, columnMapperFactories.set, parameterConverters.set); } else if (method.getReturnType().equals(NamedParameterJdbcTemplate.class) && args == null) { return jdbc; } else if (IS_DEFAULT_METHOD != null && (boolean) IS_DEFAULT_METHOD.invoke(method)) { final Class<?> declaringClass = method.getDeclaringClass(); return LOOKUP_CONSTRUCTOR.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE) .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args); } else { throw new IllegalArgumentException( String.format("missing @Query annotation for method %s in interface %s", method.getName(), clazz.getSimpleName())); } } } ); } }