package org.molgenis.datatable.model; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.collections.CollectionUtils; import org.molgenis.MolgenisFieldTypes; import org.molgenis.MolgenisFieldTypes.FieldTypeEnum; import org.molgenis.framework.db.Database; import org.molgenis.model.elements.Field; import com.mysema.query.sql.RelationalPath; import com.mysema.query.sql.SQLQueryImpl; import com.mysema.query.sql.SQLTemplates; import com.mysema.query.types.expr.SimpleExpression; import com.mysema.query.types.path.BooleanPath; import com.mysema.query.types.path.DatePath; import com.mysema.query.types.path.NumberPath; import com.mysema.query.types.path.PathBuilder; import com.mysema.query.types.path.StringPath; /** * Implementation of the QueryCreator interface that allows simple joins. */ public class JoinQueryCreator implements QueryCreator { /** * Utility class to encapsulate which two tables are joined on which columns */ public static class Join { private final String leftColumnName; private final String leftTableName; private final String rightColumnName; private final String rightTableName; public Join(String leftTableName, String leftColumnName, String rightTableName, String rightColumnName) { this.leftColumnName = leftColumnName; this.leftTableName = leftTableName; this.rightColumnName = rightColumnName; this.rightTableName = rightTableName; } } private final Database db; private final List<String> tableNames; private final List<String> columnNames; private final List<String> hiddenFieldNames; private final List<Join> joins; private LinkedHashMap<String, SimpleExpression<? extends Object>> attributeExpressions; private final Map<String, List<Field>> tableColumns; /** * Create a {@link JoinQueryCreator}, specifying whence to get the data and * which columns to display. * * @param db * The database * @param tableNames * The names of the tables * @param columnNames * The names of the columns (excluding hidden ones) * @param hiddenFieldNames * The names of the columns that should not be displayed. Used * for the WHERE condition. * @param joins * The parametrisations of the joins between the tables. */ public JoinQueryCreator(final Database db, final List<String> tableNames, final List<String> columnNames, final List<String> hiddenFieldNames, final List<Join> joins) { this.db = db; this.tableNames = tableNames; this.columnNames = columnNames; this.hiddenFieldNames = hiddenFieldNames == null ? Collections.<String> emptyList() : hiddenFieldNames; this.joins = joins; tableColumns = loadColumnData(); } @SuppressWarnings("rawtypes") @Override public SQLQueryImpl createQuery(Connection connection, SQLTemplates dialect) { final SQLQueryImpl query = new SQLQueryImpl(connection, dialect); attributeExpressions = new LinkedHashMap<String, SimpleExpression<? extends Object>>(); // Fill attribute map with table and column expressions for (final String tableName : tableNames) { final PathBuilder<RelationalPath> table = new PathBuilder<RelationalPath>(RelationalPath.class, tableName); query.from(table); for (final Field f : tableColumns.get(tableName.toLowerCase())) { final SimpleExpression<?> path = createPath(f, table); attributeExpressions.put(f.getSqlName(), path); } } // Add joins for (final Join join : joins) { final PathBuilder<RelationalPath> leftTable = new PathBuilder<RelationalPath>(RelationalPath.class, join.leftTableName); final PathBuilder<RelationalPath> rightTable = new PathBuilder<RelationalPath>(RelationalPath.class, join.rightTableName); query.where(leftTable.get(join.leftColumnName).eq(rightTable.get(join.rightColumnName))); } return query; } @Override public List<String> getHiddenFieldNames() { return hiddenFieldNames; } @Override public LinkedHashMap<String, SimpleExpression<? extends Object>> getAttributeExpressions() { return attributeExpressions; } @Override public List<Field> getFields() { final List<Field> columns = new ArrayList<Field>(); final Map<String, List<Field>> columnsByTable = loadColumnData(); for (final String table : columnsByTable.keySet()) { for (final Field field : columnsByTable.get(table)) { columns.add(field); } } return columns; } /** * Retrieve column information from the database * * @return A map with table names as keys and a lists of the fields in that * table as values. */ private Map<String, List<Field>> loadColumnData() { final Map<String, List<Field>> tableColumns = new LinkedHashMap<String, List<Field>>(); try { final DatabaseMetaData metaData = db.getConnection().getMetaData(); for (final String tableName : tableNames) { tableColumns.put(tableName.toLowerCase(), new ArrayList<Field>()); final ResultSet columns = metaData.getColumns(null, "%", tableName, "%"); while (columns.next()) { final String columnName = columns.getString("COLUMN_NAME"); final int sqlType = columns.getInt("DATA_TYPE"); final String sqlColumnName = String.format("%s.%s", tableName, columnName); if (CollectionUtils.isNotEmpty(columnNames) || CollectionUtils.isNotEmpty(hiddenFieldNames)) { if (!columnNames.contains(sqlColumnName) && !hiddenFieldNames.contains(sqlColumnName)) { continue; } } final Field field = new Field(columnName); field.setType(MolgenisFieldTypes.getTypeBySqlTypesCode(sqlType)); field.setTableName(tableName); tableColumns.get(tableName.toLowerCase()).add(field); } } } catch (final Exception ex) { throw new IllegalStateException(ex); } return tableColumns; } /** * Create a {@link SimpleExpression} of the path of a field (i.e. column) in * a table. */ private static SimpleExpression<?> createPath(Field f, PathBuilder<RelationalPath> table) { final FieldTypeEnum type = f.getType().getEnumType(); final String name = f.getName().toLowerCase(); switch (type) { case STRING: return table.get(new StringPath(name)); case INT: return table.get(new NumberPath<Integer>(Integer.class, name)); case DECIMAL: return table.get(new NumberPath<Double>(Double.class, name)); case LONG: return table.get(new NumberPath<Long>(Long.class, name)); case BOOL: return table.get(new BooleanPath(name)); case DATE: case DATE_TIME: return table.get(new DatePath<Date>(Date.class, name)); default: throw new UnsupportedOperationException("create path not implemented for " + type.toString()); } } }