io.lavagna.common.ConstructorAnnotationRowMapper.java Source code

Java tutorial

Introduction

Here is the source code for io.lavagna.common.ConstructorAnnotationRowMapper.java

Source

/**
 * This file is part of lavagna.
 *
 * lavagna 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.
 *
 * lavagna 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 lavagna.  If not, see <http://www.gnu.org/licenses/>.
 */
package io.lavagna.common;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.sql.Clob;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.util.Assert;

public class ConstructorAnnotationRowMapper<T> implements RowMapper<T> {

    private final Constructor<T> con;
    private final List<ColumnMapper> mappedColumn;

    /**
     * Check if the given class has the correct form.
     * <p/>
     * <ul>
     * <li>must have exactly one public constructor.</li>
     * <li>must at least have one parameter.</li>
     * <li>all the parameters must be annotated with @Column annotation.</li>
     * </ul>
     *
     * @param clazz
     * @return
     */
    public static boolean hasConstructorInTheCorrectForm(Class<?> clazz) {

        if (clazz.getConstructors().length != 1) {
            return false;
        }

        Constructor<?> con = clazz.getConstructors()[0];

        if (con.getParameterTypes().length == 0) {
            return false;
        }

        Annotation[][] parameterAnnotations = con.getParameterAnnotations();
        for (Annotation[] as : parameterAnnotations) {
            if (!hasColumnAnnotation(as)) {
                return false;
            }
        }

        return true;
    }

    private static boolean hasColumnAnnotation(Annotation[] as) {
        if (as == null || as.length == 0) {
            return false;
        }
        for (Annotation a : as) {
            if (a.annotationType().isAssignableFrom(Column.class)) {
                return true;
            }
        }

        return false;
    }

    @SuppressWarnings("unchecked")
    public ConstructorAnnotationRowMapper(Class<T> clazz) {
        int constructorCount = clazz.getConstructors().length;
        Assert.isTrue(constructorCount == 1, "The class " + clazz.getName()
                + " must have exactly one public constructor, " + constructorCount + " are present");

        con = (Constructor<T>) clazz.getConstructors()[0];
        mappedColumn = from(clazz, con.getParameterAnnotations(), con.getParameterTypes());
    }

    @Override
    public T mapRow(ResultSet rs, int rowNum) throws SQLException {
        List<Object> vals = new ArrayList<>(mappedColumn.size());

        for (ColumnMapper colMapper : mappedColumn) {
            vals.add(colMapper.getObject(rs));
        }

        try {
            return con.newInstance(vals.toArray(new Object[vals.size()]));
        } catch (ReflectiveOperationException e) {
            throw new SQLException(e);
        } catch (IllegalArgumentException e) {
            throw new SQLException("type mismatch between the expected one from the construct and the one passed,"
                    + " check 1: some values are null and passed to primitive types 2: incompatible numeric types",
                    e);
        }
    }

    private static List<ColumnMapper> from(Class<?> clazz, Annotation[][] annotations, Class<?>[] paramTypes) {
        List<ColumnMapper> res = new ArrayList<>();
        for (int i = 0; i < annotations.length; i++) {
            res.add(findColumnAnnotationValue(clazz, i, annotations[i], paramTypes[i]));
        }
        return res;
    }

    private static ColumnMapper findColumnAnnotationValue(Class<?> clazz, int position, Annotation[] annotations,
            Class<?> paramType) {

        for (Annotation a : annotations) {
            if (Column.class.isAssignableFrom(a.annotationType())) {

                String name = ((Column) a).value();

                if (paramType.isEnum()) {
                    return new EnumColumnMapper(name, paramType);
                } else if (boolean.class == paramType || Boolean.class == paramType) {
                    return new BooleanColumnMapper(name);
                } else {
                    return new ColumnMapper(name);
                }
            }
        }

        throw new IllegalStateException("No annotation @Column found for class: " + clazz.getName()
                + " in constructor at position " + position);
    }

    static class ColumnMapper {
        protected final String name;

        ColumnMapper(String name) {
            this.name = name;
        }

        public Object getObject(ResultSet rs) throws SQLException {
            Object res = rs.getObject(name);
            if (res != null && Clob.class.isAssignableFrom(res.getClass())) {
                try (ClobAutoCloseable clob = new ClobAutoCloseable((Clob) res)) {
                    return clob.clob.getSubString(1, (int) clob.clob.length());
                }
            } else if (res != null && BigDecimal.class.isAssignableFrom(res.getClass())) {
                return ((BigDecimal) res).longValue();
            } else {
                return res;
            }
        }
    }

    private static class ClobAutoCloseable implements AutoCloseable {

        private final Clob clob;

        public ClobAutoCloseable(Clob clob) {
            this.clob = clob;
        }

        @Override
        public void close() throws SQLException {
            clob.free();
        }
    }

    static class BooleanColumnMapper extends ColumnMapper {

        BooleanColumnMapper(String name) {
            super(name);
        }

        public Object getObject(ResultSet rs) throws SQLException {
            Object res = rs.getObject(name);
            Class<?> resClass = res == null ? null : res.getClass();
            if (res == null || Boolean.class.isAssignableFrom(resClass)) {
                return res;
            } else if (Number.class.isAssignableFrom(resClass)) {
                return 1 == ((Number) res).intValue();
            } else if (String.class.isAssignableFrom(resClass)) {
                return "true".equalsIgnoreCase(res.toString());
            } else {
                throw new IllegalArgumentException("was not able to extract a boolean value");
            }
        }
    }

    static class EnumColumnMapper extends ColumnMapper {

        @SuppressWarnings("rawtypes")
        private final Class<? extends Enum> enumType;

        @SuppressWarnings("unchecked")
        EnumColumnMapper(String name, Class<?> enumType) {
            super(name);
            this.enumType = (Class<? extends Enum<?>>) enumType;
        }

        @SuppressWarnings("unchecked")
        @Override
        public Object getObject(ResultSet rs) throws SQLException {
            String res = rs.getString(name);
            return res == null ? null : Enum.valueOf(enumType, res);
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface Column {
        /**
         * Column name
         *
         * @return
         */
        String value();
    }

}