/* * Copyright (c) 2017 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.obiba.magma.datasource.jdbc; import com.google.common.base.Joiner; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import liquibase.change.Change; import liquibase.change.ColumnConfig; import liquibase.change.core.AddColumnChange; import liquibase.change.core.DropColumnChange; import liquibase.change.core.ModifyDataTypeChange; import liquibase.change.core.UpdateDataChange; import liquibase.structure.core.Column; import liquibase.structure.core.Table; import org.obiba.magma.*; import org.obiba.magma.datasource.jdbc.JdbcDatasource.ChangeDatabaseCallback; import org.obiba.magma.datasource.jdbc.support.AddColumnChangeBuilder; import org.obiba.magma.datasource.jdbc.support.InsertDataChangeBuilder; import org.obiba.magma.datasource.jdbc.support.TableUtils; import org.obiba.magma.datasource.jdbc.support.UpdateDataChangeBuilder; import org.obiba.magma.type.TextType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCallback; import org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter; import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import javax.validation.constraints.NotNull; import java.io.ByteArrayInputStream; import java.security.InvalidParameterException; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; class JdbcValueTableWriter implements ValueTableWriter { @SuppressWarnings("unused") private static final Logger log = LoggerFactory.getLogger(JdbcValueTableWriter.class); private final SimpleDateFormat timestampDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); static final String VALUE_TABLES_TABLE = "value_tables"; static final String VARIABLES_TABLE = "variables"; static final String VARIABLE_ATTRIBUTES_TABLE = "variable_attributes"; static final String CATEGORIES_TABLE = "categories"; static final String CATEGORY_ATTRIBUTES_TABLE = "category_attributes"; static final String DATASOURCE_COLUMN = "datasource"; static final String VALUE_TABLE_COLUMN = "value_table"; static final String VARIABLE_COLUMN = "variable"; static final String CATEGORY_COLUMN = "category"; static final String VALUE_TYPE_COLUMN = "value_type"; static final String LOCALE_COLUMN = "locale"; static final String NAMESPACE_COLUMN = "namespace"; static final String VALUE_COLUMN = "value"; static final String NAME_COLUMN = "name"; static final String SQL_NAME_COLUMN = "sql_name"; static final String MISSING_COLUMN = "missing"; static final String ENTITY_TYPE_COLUMN = "entity_type"; static final String CREATED_COLUMN = "created"; static final String UPDATED_COLUMN = "updated"; private final JdbcValueTable valueTable; private final List<JdbcOperation> batch = Lists.newArrayList(); private final String ESC_CATEGORY_ATTRIBUTES_TABLE, ESC_DATASOURCE_COLUMN, ESC_VALUE_TABLE_COLUMN, ESC_VARIABLE_COLUMN, ESC_NAME_COLUMN, ESC_CATEGORIES_TABLE, ESC_VARIABLES_TABLE, ESC_VARIABLE_ATTRIBUTES_TABLE; private int batchSize; JdbcValueTableWriter(JdbcValueTable valueTable) { if (valueTable.isSQLView()) throw new MagmaRuntimeException("A SQL view cannot be written"); this.valueTable = valueTable; ESC_CATEGORY_ATTRIBUTES_TABLE = valueTable.getDatasource().escapeTableName(CATEGORY_ATTRIBUTES_TABLE); ESC_CATEGORIES_TABLE = valueTable.getDatasource().escapeTableName(CATEGORIES_TABLE); ESC_VARIABLES_TABLE = valueTable.getDatasource().escapeTableName(VARIABLES_TABLE); ESC_VARIABLE_ATTRIBUTES_TABLE = valueTable.getDatasource().escapeTableName(VARIABLE_ATTRIBUTES_TABLE); ESC_DATASOURCE_COLUMN = valueTable.getDatasource().escapeColumnName(DATASOURCE_COLUMN); ESC_VALUE_TABLE_COLUMN = valueTable.getDatasource().escapeColumnName(VALUE_TABLE_COLUMN); ESC_VARIABLE_COLUMN = valueTable.getDatasource().escapeColumnName(VARIABLE_COLUMN); ESC_NAME_COLUMN = valueTable.getDatasource().escapeColumnName(NAME_COLUMN); batchSize = valueTable.getDatasource().getSettings().getBatchSize(); } @NotNull @Override public ValueSetWriter writeValueSet(@NotNull VariableEntity entity) { return new JdbcValueSetWriter(entity); } @Override public VariableWriter writeVariables() { return valueTable.getDatasource().getSettings().isUseMetadataTables() ? new JdbcMetadataVariableWriter() : new JdbcVariableWriter(); } @Override public void close() { List<JdbcOperation> toSave = null; synchronized (valueTable) { if (!batch.isEmpty()) { toSave = Lists.newArrayList(batch); batch.clear(); } } if (toSave != null) batchUpdate(toSave); getDatasource().databaseChanged(); valueTable.tableChanged(); } private JdbcTemplate getJdbcTemplate() { return this.valueTable.getDatasource().getJdbcTemplate(); } private JdbcDatasource getDatasource() { return this.valueTable.getDatasource(); } private TransactionTemplate getTransactionTemplate() { return this.valueTable.getDatasource().getTransactionTemplate(); } private void batchUpdate(final List<JdbcOperation> operations) { getTransactionTemplate().execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { batchUpdateInternal(operations); } }); } @SuppressWarnings({"OverlyLongMethod", "PMD.NcssMethodCount"}) private void batchUpdateInternal(List<JdbcOperation> operations) { final DefaultLobHandler lobHandler = new DefaultLobHandler(); List<String> sqls = operations.stream().map(JdbcOperation::getSql).distinct().collect(Collectors.toList()); for (String sql : sqls) { final List<List<Object>> batchValues = operations.stream() .filter(op -> sql.equals(op.getSql())) .map(JdbcOperation::getParameters).collect(Collectors.toList()); int[] res = getJdbcTemplate().batchUpdate(sql, new AbstractInterruptibleBatchPreparedStatementSetter() { @Override protected boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException { int index = 1; if (batchValues.size() <= i) { return false; } for (Object value : batchValues.get(i)) { if (value instanceof byte[]) { lobHandler.getLobCreator().setBlobAsBinaryStream(ps, index++, new ByteArrayInputStream((byte[]) value), ((byte[]) value).length); } else if (value instanceof java.util.Date) { ps.setDate(index++, new Date(((java.util.Date) value).getTime())); } else if (value instanceof MagmaDate) { ps.setDate(index++, new Date(((MagmaDate) value).asDate().getTime())); } else { ps.setObject(index++, value); } } return true; } }); log.debug("batchUpdate modified {} rows", res.length); } } private String formattedDate(java.util.Date date) { return timestampDateFormat.format(date); } private String getVariableSqlName(String variableName) { return valueTable.getVariableSqlName(variableName); } private class JdbcVariableWriter implements ValueTableWriter.VariableWriter { List<Change> changes = new ArrayList<>(); @Override public void writeVariable(@NotNull Variable variable) { if (!valueTable.isForEntityType(variable.getEntityType())) { throw new InvalidParameterException( "Wrong entity type for variable '" + variable.getName() + "': " + valueTable.getEntityType() + " expected, " + variable.getEntityType() + " received."); } doWriteVariable(variable); valueTable.writeVariableValueSource(variable); } @Override public void removeVariable(@NotNull Variable variable) { Variable existingVariable = valueTable.getVariable(variable.getName()); DropColumnChange dcc = new DropColumnChange(); dcc.setTableName(valueTable.getSqlName()); dcc.setColumnName(getVariableSqlName(existingVariable.getName())); changes.add(dcc); if (valueTable.hasUpdatedTimestampColumn()) { UpdateDataChange udc = new UpdateDataChange(); udc.setTableName(valueTable.getSqlName()); ColumnConfig col = new ColumnConfig(); col.setName(valueTable.getUpdatedTimestampColumnName()); col.setValueDate(formattedDate(new java.util.Date())); udc.addColumn(col); changes.add(udc); } } @Override public void close() { getDatasource().doWithDatabase(new ChangeDatabaseCallback(changes)); valueTable.refreshTable(); valueTable.refreshVariablesMap(); } protected void doWriteVariable(Variable variable) { String columnName = getVariableSqlName(variable.getName()); String dataType = variable.isRepeatable() && !valueTable.isMultilines() ? SqlTypes.sqlTypeFor(TextType.get(), SqlTypes.TEXT_TYPE_HINT_LARGE) : SqlTypes.sqlTypeFor(variable.getValueType(), variable.getValueType().equals(TextType.get()) ? SqlTypes.TEXT_TYPE_HINT_MEDIUM : null); if (variableExists(variable)) { modifyColumn(columnName, dataType); } else { addNewColumn(variable.getName(), dataType); } } private void modifyColumn(String columnName, String dataType) { ModifyDataTypeChange modifyDataTypeChange = new ModifyDataTypeChange(); modifyDataTypeChange.setTableName(valueTable.getSqlName()); modifyDataTypeChange.setColumnName(columnName); modifyDataTypeChange.setNewDataType(dataType); changes.add(modifyDataTypeChange); } private void addNewColumn(String variableName, String dataType) { String columnName = generateColumnName(variableName); AddColumnChange addColumnChange = AddColumnChangeBuilder.newBuilder()// .table(valueTable.getSqlName())// .column(columnName, dataType).build(); changes.add(addColumnChange); } String generateColumnName(String variableName) { return String.format("%s", TableUtils.normalize(variableName, 64)); } boolean variableExists(Variable variable) { String columnName = getVariableSqlName(variable.getName()); return getDatasource().getDatabaseSnapshot() .get(new Column(Table.class, null, null, valueTable.getSqlName(), columnName)) != null; } } private class JdbcMetadataVariableWriter extends JdbcVariableWriter { // // JdbcVariableWriter Methods // @Override protected void doWriteVariable(Variable variable) { boolean variableExists = variableExists(variable); if (variableExists) { deleteVariableMetadata(variable.getName()); } else { addTableTimestampChange(); } InsertDataChangeBuilder builder = new InsertDataChangeBuilder() // .tableName(VARIABLES_TABLE); if (getDatasource().getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, valueTable.getDatasource().getName()); builder.withColumn(VALUE_TABLE_COLUMN, valueTable.getName()) // .withColumn(NAME_COLUMN, variable.getName()) // .withColumn(VALUE_TYPE_COLUMN, variable.getValueType().getName()) // .withColumn("ref_entity_type", variable.getReferencedEntityType())// .withColumn("mime_type", variable.getMimeType())// .withColumn("units", variable.getUnit()) // .withColumn("is_repeatable", variable.isRepeatable()) // .withColumn("occurrence_group", variable.getOccurrenceGroup()) // .withColumn("index", Integer.toString(variable.getIndex())) // .withColumn(SQL_NAME_COLUMN, generateColumnName(variable.getName())); changes.add(builder.build()); writeAttributes(variable); writeCategories(variable); super.doWriteVariable(variable); } @Override public void removeVariable(@NotNull Variable variable) { super.removeVariable(variable); deleteVariableMetadata(variable.getName()); } private void addTableTimestampChange() { String whereClause = getDatasource().getSettings().isMultipleDatasources() ? String.format("%s = '%s' AND %s = '%s'", ESC_DATASOURCE_COLUMN, getDatasource().getName(), ESC_NAME_COLUMN, valueTable.getName()) : String.format("%s = '%s'", ESC_NAME_COLUMN, valueTable.getName()); changes.add(UpdateDataChangeBuilder.newBuilder().tableName(VALUE_TABLES_TABLE) // .withColumn(UPDATED_COLUMN, new java.util.Date()) // .where(whereClause).build()); } // // Methods // private void deleteVariableMetadata(String variableName) { JdbcTemplate jdbcTemplate = getJdbcTemplate(); JdbcDatasourceSettings settings = getDatasource().getSettings(); Object[] params = settings.isMultipleDatasources() ? new Object[]{getDatasource().getName(), valueTable.getName(), variableName} : new Object[]{valueTable.getName(), variableName}; // Delete category attributes String sql = settings.isMultipleDatasources() ? String .format("DELETE FROM %s WHERE %s = ? AND %s = ? AND %s = ?", ESC_CATEGORY_ATTRIBUTES_TABLE, ESC_DATASOURCE_COLUMN, ESC_VALUE_TABLE_COLUMN, ESC_VARIABLE_COLUMN) : String.format("DELETE FROM %s WHERE %s = ? AND %s = ?", ESC_CATEGORY_ATTRIBUTES_TABLE, ESC_VALUE_TABLE_COLUMN, ESC_VARIABLE_COLUMN); jdbcTemplate.update(sql, params); // Delete categories sql = settings.isMultipleDatasources() ? String.format("DELETE FROM %s WHERE %s = ? AND %s = ? AND %s = ?", ESC_CATEGORIES_TABLE, ESC_DATASOURCE_COLUMN, ESC_VALUE_TABLE_COLUMN, ESC_VARIABLE_COLUMN) : String.format("DELETE FROM %s WHERE %s = ? AND %s = ?", ESC_CATEGORIES_TABLE, ESC_VALUE_TABLE_COLUMN, ESC_VARIABLE_COLUMN); jdbcTemplate.update(sql, params); // Delete variable attributes sql = settings.isMultipleDatasources() ? String .format("DELETE FROM %s WHERE %s = ? AND %s = ? AND %s = ?", ESC_VARIABLE_ATTRIBUTES_TABLE, ESC_DATASOURCE_COLUMN, ESC_VALUE_TABLE_COLUMN, ESC_VARIABLE_COLUMN) : String.format("DELETE FROM %s WHERE %s = ? AND %s = ?", ESC_VARIABLE_ATTRIBUTES_TABLE, ESC_VALUE_TABLE_COLUMN, ESC_VARIABLE_COLUMN); jdbcTemplate.update(sql, params); // Delete variable sql = settings.isMultipleDatasources() ? String.format("DELETE FROM %s WHERE %s = ? AND %s = ? AND %s = ?", ESC_VARIABLES_TABLE, ESC_DATASOURCE_COLUMN, ESC_VALUE_TABLE_COLUMN, ESC_NAME_COLUMN) : String.format("DELETE FROM %s WHERE %s = ? AND %s = ?", ESC_VARIABLES_TABLE, ESC_VALUE_TABLE_COLUMN, ESC_NAME_COLUMN); jdbcTemplate.update(sql, params); addTableTimestampChange(); } private void writeAttributes(Variable variable) { if (!variable.hasAttributes()) return; for (Attribute attribute : variable.getAttributes()) { InsertDataChangeBuilder builder = new InsertDataChangeBuilder() // .tableName(VARIABLE_ATTRIBUTES_TABLE); if (getDatasource().getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, valueTable.getDatasource().getName()); builder.withColumn(VALUE_TABLE_COLUMN, valueTable.getName()) // .withColumn(VARIABLE_COLUMN, variable.getName()) // .withColumn(NAME_COLUMN, attribute.getName()) // .withColumn(LOCALE_COLUMN, attribute.isLocalised() ? attribute.getLocale().toString() : "") // .withColumn(NAMESPACE_COLUMN, attribute.hasNamespace() ? attribute.getNamespace() : "") // .withColumn(VALUE_COLUMN, attribute.getValue().toString()); changes.add(builder.build()); } } private void writeCategories(Variable variable) { if (!variable.hasCategories()) return; for (Category category : variable.getCategories()) { InsertDataChangeBuilder builder = new InsertDataChangeBuilder() // .tableName(CATEGORIES_TABLE); if (getDatasource().getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, valueTable.getDatasource().getName()); builder.withColumn(VALUE_TABLE_COLUMN, valueTable.getName()) // .withColumn(VARIABLE_COLUMN, variable.getName()) // .withColumn(NAME_COLUMN, category.getName()) // .withColumn(MISSING_COLUMN, category.isMissing()); changes.add(builder.build()); writeCategoryAttributes(variable, category); } } private void writeCategoryAttributes(Variable variable, Category category) { if (!category.hasAttributes()) return; for (Attribute attribute : category.getAttributes()) { InsertDataChangeBuilder builder = new InsertDataChangeBuilder() // .tableName(CATEGORY_ATTRIBUTES_TABLE); if (getDatasource().getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, valueTable.getDatasource().getName()); builder.withColumn(VALUE_TABLE_COLUMN, valueTable.getName()) // .withColumn(VARIABLE_COLUMN, variable.getName()) // .withColumn(CATEGORY_COLUMN, category.getName()) // .withColumn(NAME_COLUMN, attribute.getName()) // .withColumn(LOCALE_COLUMN, attribute.isLocalised() ? attribute.getLocale().toString() : "") // .withColumn(NAMESPACE_COLUMN, attribute.hasNamespace() ? attribute.getNamespace() : "") // .withColumn(VALUE_COLUMN, attribute.getValue().toString()); changes.add(builder.build()); } } } private class JdbcValueSetWriter implements ValueTableWriter.ValueSetWriter { private final VariableEntity entity; private final JdbcLine jdbcLine; private String insertSql; private String updateSql; private String whereClause; private boolean remove; private JdbcValueSetWriter(VariableEntity entity) { this.entity = entity; this.jdbcLine = new JdbcLine(entity, valueTable); } @Override public void writeValue(@NotNull Variable variable, Value value) { jdbcLine.setValue(variable, value); } @Override public void remove() { remove = true; } @Override public void close() { if (remove) doRemove(); else doInsertOrUpdate(); } private void doRemove() { getTransactionTemplate().execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { getJdbcTemplate() .execute(getDeleteSql(), new PreparedStatementCallback<Integer>() { @Override public Integer doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { ps.setString(1, entity.getIdentifier()); return ps.executeUpdate(); } }); } }); } private void doInsertOrUpdate() { final String sql = valueTable.hasValueSet(entity) ? getUpdateSql() : getInsertSql(); List<JdbcOperation> toSave = null; synchronized (valueTable) { jdbcLine.getLines().forEach(values -> { values.add(entity.getIdentifier()); batch.add(new JdbcOperation(sql, values)); }); if (batch.size() >= batchSize) { toSave = Lists.newArrayList(batch); batch.clear(); } } if (toSave != null) { batchUpdate(toSave); } } private String getInsertSql() { if (insertSql == null) { String colNames = Joiner.on(", ").join(getEscapedColumnNames()); colNames = colNames.isEmpty() ? getEscapedEntityIdentifierColumnName() : colNames + ", " + getEscapedEntityIdentifierColumnName(); String values = Joiner.on(", ").join(Collections.nCopies(jdbcLine.size() + 1, "?")); insertSql = String.format("INSERT INTO %s (%s) VALUES (%s)", getEscapedTableName(), colNames, values); } return insertSql; } private String getUpdateSql() { if (updateSql == null) { String colNames = Joiner.on(", ") .join(getEscapedColumnNames().stream().map(c -> String.format("%s = ?", c)).collect(Collectors.toList())); updateSql = String.format("UPDATE %s SET %s %s", getEscapedTableName(), colNames, getWhereClause()); } return updateSql; } private String getEscapedTableName() { return getDatasource().escapeTableName(valueTable.getSqlName()); } private List<String> getEscapedColumnNames() { return jdbcLine.getColumnNames().stream().map(getDatasource()::escapeColumnName).collect(Collectors.toList()); } private String getEscapedEntityIdentifierColumnName() { return getDatasource().escapeColumnName(valueTable.getSettings().getEntityIdentifierColumn()); } private String getDeleteSql() { return String.format("DELETE FROM %s %s", getDatasource().escapeTableName(valueTable.getSqlName()), getWhereClause()); } private String getWhereClause() { if (whereClause == null) { whereClause = String.format("WHERE %s = ?", getEscapedEntityIdentifierColumnName()); } return whereClause; } } private class JdbcOperation { private final String sql; private final List<Object> parameters; private JdbcOperation(String sql, List<Object> parameters) { this.sql = sql; this.parameters = parameters; } public String getSql() { return sql; } public List<Object> getParameters() { return parameters; } } }