alfio.datamapper.QueryType.java Source code

Java tutorial

Introduction

Here is the source code for alfio.datamapper.QueryType.java

Source

/**
 * This file is part of alf.io.
 *
 * alf.io is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * alf.io is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with alf.io.  If not, see <http://www.gnu.org/licenses/>.
 */
package alfio.datamapper;

import org.apache.commons.lang3.tuple.Pair;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.StatementCreatorUtils;
import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.sql.Types;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Query Type:
 *
 * <ul>
 * <li>TEMPLATE : we receive the string defined in @Query/@QueryOverride annotation.
 * <li>EXECUTE : the query will be executed. If it's a select, the result will be mapped with a
 * ConstructorAnnotationRowMapper if it has the correct form.
 * </ul>
 *
 */
public enum QueryType {
    /**
     * Receive the string defined in @Query/@QueryOverride annotation.
     */
    TEMPLATE {
        @Override
        String apply(String template, NamedParameterJdbcTemplate jdbc, Method method, Object[] args) {
            return template;
        }
    },

    /**
     */
    EXECUTE {

        /**
         * Keep a mapping between a given class and a possible RowMapper.
         *
         * If the Class has the correct form, a ConstructorAnnotationRowMapper will be built and the boolean set to true
         * in the pair. If the class has not the correct form, the boolean will be false and the class will be used as
         * it is in the jdbc template.
         */
        private final Map<Class<Object>, HasRowmapper> cachedClassToMapper = new ConcurrentHashMap<Class<Object>, HasRowmapper>();

        @Override
        Object apply(String template, NamedParameterJdbcTemplate jdbc, Method method, Object[] args) {
            JdbcAction action = actionFromContext(method, template);
            SqlParameterSource parameters = extractParameters(method, args);
            switch (action) {
            case QUERY:
                return doQuery(template, jdbc, method, parameters);
            case UPDATE:
                return jdbc.update(template, parameters);
            case INSERT_W_AUTO_GENERATED_KEY:
                AutoGeneratedKey spec = method.getDeclaredAnnotation(AutoGeneratedKey.class);
                return executeUpdateAndKeepKeys(template, spec.value(), spec.keyClass(), jdbc, parameters);
            default:
                throw new IllegalArgumentException("unknown value for action: " + action);
            }
        }

        private <T> Pair<Integer, T> executeUpdateAndKeepKeys(String template, String keyName, Class<T> keyClass,
                NamedParameterJdbcTemplate jdbc, SqlParameterSource parameters) {
            KeyHolder keyHolder = new GeneratedKeyHolder();
            int result = jdbc.update(template, parameters, keyHolder);
            final Map<String, Object> keys = keyHolder.getKeys();
            Object key;
            if (keys.size() > 1) {
                key = keys.get(keyName);
            } else {
                key = keys.values().stream().findFirst().orElseThrow(IllegalStateException::new);
            }
            return Pair.of(result, keyClass.cast(key));
        }

        @SuppressWarnings("unchecked")
        private Object doQuery(String template, NamedParameterJdbcTemplate jdbc, Method method,
                SqlParameterSource parameters) {
            if (method.getReturnType().isAssignableFrom(List.class)) {
                Class<Object> c = (Class<Object>) ((ParameterizedType) method.getGenericReturnType())
                        .getActualTypeArguments()[0];
                HasRowmapper r = ensurePresence(c);
                if (r.present) {
                    return jdbc.query(template, parameters, r.rowMapper);
                } else {
                    return jdbc.queryForList(template, parameters, c);
                }
            } else {
                Class<Object> c = (Class<Object>) method.getReturnType();
                HasRowmapper r = ensurePresence(c);
                if (r.present) {
                    return jdbc.queryForObject(template, parameters, r.rowMapper);
                } else {
                    return jdbc.queryForObject(template, parameters, c);
                }
            }
        }

        private HasRowmapper ensurePresence(Class<Object> c) {
            if (!cachedClassToMapper.containsKey(c)) {
                cachedClassToMapper.put(c, handleClass(c));
            }
            return cachedClassToMapper.get(c);
        }
    };

    private static JdbcAction actionFromContext(Method method, String template) {
        if (method.getDeclaredAnnotation(AutoGeneratedKey.class) == null) {
            return actionFromTemplate(template);
        }
        return JdbcAction.INSERT_W_AUTO_GENERATED_KEY;
    }

    private static JdbcAction actionFromTemplate(String template) {
        String tmpl = StringUtils.deleteAny(template.toLowerCase(Locale.ENGLISH), "() ").trim();
        return tmpl.indexOf("select") == 0 ? JdbcAction.QUERY : JdbcAction.UPDATE;
    }

    private enum JdbcAction {
        QUERY, UPDATE, INSERT_W_AUTO_GENERATED_KEY
    }

    abstract Object apply(String template, NamedParameterJdbcTemplate jdbc, Method method, Object[] args);

    private static HasRowmapper handleClass(Class<Object> c) {
        if (ConstructorAnnotationRowMapper.hasConstructorInTheCorrectForm(c)) {
            return new HasRowmapper(true, new ConstructorAnnotationRowMapper<>(c));
        } else {
            return new HasRowmapper(false, null);
        }
    }

    private static SqlParameterSource extractParameters(Method m, Object[] args) {

        Annotation[][] parameterAnnotations = m.getParameterAnnotations();
        if (parameterAnnotations == null || parameterAnnotations.length == 0) {
            return new EmptySqlParameterSource();
        }

        MapSqlParameterSource ps = new MapSqlParameterSource();
        Class<?>[] parameterTypes = m.getParameterTypes();
        for (int i = 0; i < args.length; i++) {
            String name = parameterName(parameterAnnotations[i]);
            if (name != null) {
                if (args[i] != null && ZonedDateTime.class.isAssignableFrom(parameterTypes[i])) {
                    ZonedDateTime dateTime = ZonedDateTime.class.cast(args[i]);
                    final ZonedDateTime utc = dateTime.withZoneSameInstant(ZoneId.of("UTC"));
                    Calendar c = Calendar.getInstance();
                    c.setTimeZone(TimeZone.getTimeZone("UTC"));
                    c.setTimeInMillis(utc.toInstant().toEpochMilli());
                    ps.addValue(name, c, Types.TIMESTAMP);
                } else {
                    ps.addValue(name, args[i], StatementCreatorUtils.javaTypeToSqlParameterType(parameterTypes[i]));
                }
            }
        }

        return ps;
    }

    private static String parameterName(Annotation[] annotation) {

        if (annotation == null) {
            return null;
        }

        for (Annotation a : annotation) {
            if (a instanceof Bind) {
                return ((Bind) a).value();
            }
        }
        return null;
    }

    private static class HasRowmapper {
        private final boolean present;
        private final RowMapper<Object> rowMapper;

        HasRowmapper(boolean present, RowMapper<Object> rowMapper) {
            this.present = present;
            this.rowMapper = rowMapper;
        }
    }
}