Newer
Older
simple-database / src / nl / astraeus / database / MetaData.java
package nl.astraeus.database;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import nl.astraeus.database.annotations.Cache;
import nl.astraeus.database.annotations.Id;
import nl.astraeus.database.annotations.Reference;
import nl.astraeus.database.annotations.Table;
import nl.astraeus.database.annotations.Transient;
import nl.astraeus.database.util.ReferenceGenerator;
import nl.astraeus.template.SimpleTemplate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Date: 11/13/13
 * Time: 9:41 PM
 */
public class MetaData<M> {
    private final static Logger logger = LoggerFactory.getLogger(MetaData.class);

    private Class<M> cls;
    private SimpleDatabase db;
    private DdlMapping ddlMapping;

    private String tableName;
    private FieldMetaData pk = null;
    private FieldMetaData [] fieldsMetaData;
    private String insertSql;
    private String selectSql;
    private String updateSql;
    private String deleteSql;

    private ThreadLocal<Map<Class<?>, Map<Long, Object>>> circularReferences = new ThreadLocal<>();

    public MetaData(Class<M> cls, SimpleDatabase database) {
        this.cls = cls;
        this.db = database;
        this.ddlMapping = database.getDdlMapping();

        processAnnotation(cls.getAnnotation(Table.class));

        if (tableName == null) {
            tableName = cls.getSimpleName();
        }

        if (ddlMapping.ddlNamesInUppercase()) {
            tableName = tableName.toUpperCase();
        }

        Cache cache = cls.getAnnotation(Cache.class);

        if (cache != null) {
            db.getCache().setMaxSize(cls, cache.maxSize());
        }

        Field [] fields = cls.getDeclaredFields();
        List<FieldMetaData> fieldMeta = new ArrayList<>();

        for (Field field : fields) {
            int modifiers = field.getModifiers();
            Transient trans = field.getAnnotation(Transient.class);
            if (!field.getName().contains("jacoco") && trans == null && !Modifier.isStatic(modifiers)) {
                FieldMetaData info = new FieldMetaData(database, field);

                fieldMeta.add(info);

                if (field.getAnnotation(Id.class) != null) {
                    if (!field.getType().equals(Long.class) &&
                        !field.getType().equals(long.class) &&
                        !field.getType().equals(Integer.class) &&
                        !field.getType().equals(int.class)) {

                        throw new IllegalStateException("PK Field must be long or integer! ("+field.getDeclaringClass().getSimpleName()+"."+field.getName()+")");
                    } else if (pk != null) {
                        throw new IllegalStateException("Compound primary keys not supported, multable id field defined in "+field.getDeclaringClass().getSimpleName());
                    }

                    pk = info;
                }
            }
        }

        this.fieldsMetaData = fieldMeta.toArray(new FieldMetaData[fieldMeta.size()]);

        Connection connection = null;
        try {
            // get metadata from database
            connection = db.getNewConnection();
            ResultSet result = connection.getMetaData().getTables(null, null, tableName, null);

            if (result.next()) {
                Map<String, ColumnMetaData> columnMetaData = getColumnMetaData(connection);

                for (FieldMetaData meta : fieldsMetaData) {
                    ColumnMetaData cmd = columnMetaData.get(meta.getColumnInfo().getName());
                    if (cmd != null) {
                        if (meta.isPrimaryKey()) {

                        }
                        // check type etc
/*                        if (cmd.getSqlType() != null && !cmd.getSqlType().equals(meta.getSqlType())) {
                            throw new IllegalStateException("Field " + cls.getSimpleName() + "." + meta.getFieldName() + " doesn't match type for column " + tableName + "." + cmd.getName());
                        }*/
                        // (re)create index
                        if (meta.hasIndex()) {
                            createIndexes(meta);
                        }
                    } else if (db.isExecuteDdlUpdates()) {
                        // create Column....
                        createColumn(meta);
                    } else {
                        throw new IllegalStateException("Column "+cls.getSimpleName()+"."+meta.getFieldName()+" not found in table "+tableName);
                    }
                }
            } else if (db.isExecuteDdlUpdates()) {
                createTable();
            } else {
                throw new IllegalStateException("Table "+tableName+" not found for class "+cls.getSimpleName());
            }

            SimpleTemplate insertTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.INSERT);
            SimpleTemplate selectTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.SELECT);
            SimpleTemplate updateTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.UPDATE);
            SimpleTemplate deleteTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.DELETE);

            Map<String, Object> model = new HashMap<>();

            List<String> columns = new LinkedList<>();

            for (FieldMetaData meta : fieldsMetaData) {
                if (!meta.isPrimaryKey()) {
                    columns.add(meta.getColumnInfo().getName());
                }
            }

            model.put("tableName", tableName);
            model.put("columns", columns);
            model.put("key", pk.getColumnInfo().getName());

            insertSql = insertTemplate.render(model);
            updateSql = updateTemplate.render(model);
            deleteSql = deleteTemplate.render(model);

            columns.clear();
            for (FieldMetaData meta : fieldsMetaData) {
                columns.add(meta.getColumnInfo().getName());
            }
            model.put("columns", columns);
            selectSql = selectTemplate.render(model);

            connection.commit();
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    private Map<String, ColumnMetaData> getColumnMetaData(Connection connection) throws SQLException {
        Map<String, ColumnMetaData> result = new HashMap<>();
        ResultSet columnsMetaData = connection.getMetaData().getColumns(null, null, tableName, null);

        while(columnsMetaData.next()) {
            ColumnMetaData cmd = new ColumnMetaData(
                    columnsMetaData.getString("COLUMN_NAME"),
                    columnsMetaData.getInt("DATA_TYPE"),
                    columnsMetaData.getInt("COLUMN_SIZE"),
                    columnsMetaData.getInt("DECIMAL_DIGITS")
            );

            result.put(cmd.getName(), cmd);
        }

        return result;
    }

    private void processAnnotation(Table table) {
        if (table != null && !table.name().isEmpty() ) {
            tableName = table.name();
        }
    }

    private void createTable() {
        Map<String, Object> model = new HashMap<>();

        List<ColumnInfo> columns = new ArrayList<>();

        for (FieldMetaData meta : fieldsMetaData) {
            if (!meta.isPrimaryKey()) {
                columns.add(meta.getColumnInfo());
            }
        }

        model.put("tableName", tableName);
        model.put("columns", columns);
        model.put("key", pk.getColumnInfo().getName());

        SimpleTemplate template = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.CREATE);

        execute(template, model);

        for (FieldMetaData meta : fieldsMetaData) {
            if (meta.hasIndex()) {
                createIndexes(meta);
            }
        }
    }

    private void createColumn(FieldMetaData meta) {
        Map<String, Object> model = new HashMap<>();

        model.put("tableName", tableName);
        model.put("column", meta.getColumnInfo());

        SimpleTemplate template = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.CREATE_COLUMN);

        execute(template, model);

        if (meta.hasIndex()) {
            createIndexes(meta);
        }
    }

    private void createIndexes(FieldMetaData meta) {
        Map<String, Object> model = new HashMap<>();

        model.put("tableName", tableName);
        model.put("column", meta.getColumnInfo());
        model.put("unique", meta.hasUniqueIndex());

        SimpleTemplate template = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.CREATE_INDEX);

        execute(template, model);
    }

    private void execute(SimpleTemplate createTemplate, Map<String, Object> model) {
        Connection connection = null;
        PreparedStatement statement = null;

        try {
            connection = db.getNewConnection();

            String sql = createTemplate.render(model);

            statement = connection.prepareStatement(sql);

            statement.execute();

            connection.commit();
        } catch (SQLException e) {
            if (connection != null) {
                try {
                    connection.rollback();
                } catch (SQLException ex) {
                    throw new IllegalStateException(ex);
                }
            }
            throw new IllegalStateException(e);
        } finally {
            try {
                if (statement != null) {
                    statement.close();
                }
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            } finally {
                try {
                    if (connection != null) {
                        connection.close();
                    }
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    protected M find(final Long id) {
        return execute(new ExecuteConnectionWithResult<M>() {
            @Override
            public M execute(Connection connection) throws SQLException {
                PreparedStatement statement = null;

                try {
                    M result = null;
                    statement = connection.prepareStatement(selectSql);

                    statement.setLong(1, id);

                    ResultSet rs = statement.executeQuery();

                    if (rs.next()) {
                        result = getFromResultSet(rs);
                    }

                    rs.close();

                    return result;
                } finally {
                    if (statement != null) {
                        statement.close();
                    }
                }
            }
        });
    }

    private M getFromResultSet(ResultSet rs) {
        M result;

        try {
            result = cls.newInstance();
            int index = 1;

            for (FieldMetaData meta : fieldsMetaData) {
                meta.set(rs, index++, result);
            }

            return result;
        } catch (InstantiationException | SQLException | IllegalAccessException e) {
            throw new IllegalStateException (e);
        }
    }

    protected void insert(M object) {
        PreparedStatement statement = null;

        try {
            statement = db.getConnection().prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS);
            int index = 1;

            for (FieldMetaData meta : fieldsMetaData) {
                Reference reference = meta.getReference();
                if (reference != null) {
                    meta.set(object, ReferenceGenerator.generateRandomReference(reference.length()));
                }

                if (!meta.isPrimaryKey()) {
                    meta.set(statement, index++, object);
                }
            }

            statement.execute();

            ResultSet rs = statement.getGeneratedKeys();

            if (rs != null && rs.next()) {
                pk.set(object, rs.getLong(1));
            }
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    protected void update(M object) {
        PreparedStatement statement = null;

        try {
            statement = db.getConnection().prepareStatement(updateSql);
            int index = 1;

            for (FieldMetaData meta : fieldsMetaData) {
                if (!meta.isPrimaryKey()) {
                    meta.set(statement, index++, object);
                }
            }

            pk.set(statement, index, object);

            statement.execute();
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    public void delete(Long id) {
        PreparedStatement statement = null;

        try {
            statement = db.getConnection().prepareStatement(deleteSql);

            statement.setLong(1, id);

            statement.execute();
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    public List<M> selectAll() {
        return selectFrom("order by "+pk.getColumnInfo().getName(), new Object [0]);
    }

    public List<M> selectFrom(String query, final Object[] params) {
        List<M> result;
        SimpleTemplate fromTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.SELECT_FROM);

        Map<String, Object> model = new HashMap<>();

        model.put("tableName", tableName);
        model.put("key", pk.getColumnInfo().getName());
        model.put("query", query);

        final String fromSql = fromTemplate.render(model);

        result = execute(new ExecuteSelect<>(this, fromSql, params));

        return result;
    }

    public List<M> selectWhere(String query, final Object[] params) {
        List<M> result;
        SimpleTemplate whereTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.SELECT_WHERE);

        Map<String, Object> model = new HashMap<>();

        model.put("tableName", tableName);
        model.put("key", pk.getColumnInfo().getName());
        model.put("query", query);

        final String whereSql = whereTemplate.render(model);

        result = execute(new ExecuteSelect<M>(this, whereSql, params));

        return result;
    }

    public List<M> selectWhere(int from, int max, String query, final Object[] params) {
        List<M> result;
        SimpleTemplate whereTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.SELECT_WHERE_PAGED);

        Map<String, Object> model = new HashMap<>();

        model.put("tableName", tableName);
        model.put("key", pk.getColumnInfo().getName());
        model.put("query", query);
        model.put("order", query);
        model.put("from", from);
        model.put("to", from+max);
        model.put("max", max);

        final String whereSql = whereTemplate.render(model);

        result = execute(new ExecuteSelect<M>(this, whereSql, params));

        return result;
    }

    public int selectCount(String query, final Object[] params) {
        Integer result;
        SimpleTemplate whereTemplate = ddlMapping.getQueryTemplate(DdlMapping.QueryTemplates.SELECT_WHERE);

        Map<String, Object> model = new HashMap<>();

        model.put("tableName", tableName);
        model.put("key", "COUNT("+pk.getColumnInfo().getName()+")");
        model.put("query", query);
        final String whereSql = whereTemplate.render(model);

        result = execute(new ExecuteConnectionWithResult<Integer>() {
            @Override
            public Integer execute(Connection connection) throws SQLException {
                Integer result = 0;
                PreparedStatement statement = null;

                try {
                    statement = connection.prepareStatement(whereSql);
                    int index = 1;

                    for (Object param : params) {
                        StatementHelper.setStatementParameter(statement, index++, param);
                    }

                    ResultSet rs = statement.executeQuery();

                    if (rs.next()) {
                        result = rs.getInt(1);
                    }

                    return result;
                } finally {
                    if (statement != null) {
                        statement.close();
                    }
                }
            }
        });

        return result;
    }

    public M findWhere(String query, final Object[] params) {
        List<M> results = selectWhere(query, params);
        M result = null;

        if (results.size() == 1) {
            result = results.get(0);
        } else if (results.size() > 1) {
            throw new IllegalStateException("Finder found more than one row! ["+query+"]");
        }

        return result;
    }

    private <T> T execute(ExecuteConnectionWithResult<T> es) {
        Connection connection = null;
        boolean close = false;

        try {
            if (db.transactionActive()) {
                connection = db.getConnection();
            } else {
                connection = db.getNewConnection();
                close = true;
            }

            return es.execute(connection);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (close && connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    public Long getId(Object object) {
        Object obj = pk.get(object);

        if (obj instanceof Integer) {
            obj = ((Integer) obj).longValue();
        }

        return (Long)obj;
    }

    public <T> void reloadReferences(T result) {
        for (FieldMetaData field : fieldsMetaData) {
            if (field.getType() == FieldMetaData.ColumnType.REFERENCE) {
                field.reloadReference(result);
            }
        }
    }

    public <T> T clone(T original) {
        try {
            T result = (T) cls.newInstance();

            for (FieldMetaData meta : fieldsMetaData) {
                meta.set(result, meta.get(original));
            }

            return result;
        } catch (InstantiationException e) {
            throw new IllegalStateException(e);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }
}