/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.util.databasechange; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Map; import java.util.UUID; import liquibase.FileOpener; import liquibase.change.custom.CustomTaskChange; import liquibase.database.Database; import liquibase.database.DatabaseConnection; import liquibase.exception.CustomChangeException; import liquibase.exception.InvalidChangeDefinitionException; import liquibase.exception.SetupException; import liquibase.exception.UnsupportedChangeException; import org.apache.commons.lang.StringUtils; import org.openmrs.api.context.Context; import org.openmrs.util.OpenmrsUtil; /** * Uses Java's {@link UUID} class to generate UUIDs for all rows in all tables in the tableNames * parameter. <br/> * <br/> * This class should only be used if you are not using MySQL, Oracle, MsSql, or some other dbms that * has a UUID-like function. <br/> * <br/> * Expects parameter: "tableNames" : whitespace delimited list of table names to add <br/> * Expects parameter: "columnName" : name of the column to change. Default: "uuid" <br/> * Expects parameter: "idExceptions" : list of id columns that don't follow the standard naming * convention. Should be a pipe-separated list of key=value, where key is the name an id column * would have by convention, and value is the name it actually has. In this example the id of the * field_answer table is 'field_id' rather than 'field_answer_id', etc: * "field_answer_id=field_id|role_id=role|privilege_id=privilege" */ public class GenerateUuid implements CustomTaskChange { /** * The "tableNames" parameter defined in the liquibase xml changeSet element that is calling * this class (whitespace separated). */ private String tableNames = null; /** * The "columnName" parameter defined in the liquibase xml changeSet element that is calling * this class */ private String columnName = "uuid"; /** * Key-value pairs of table name ids that don't follow the convention. The key is what the * convention would be and the value is what it actually is: <br/> * e.g. "field_answer_id=field_id|role_id=role|privilege_id=privilege" */ private String idExceptions = ""; /** * Set by the {@link #setUp()} method from the value of the {@link #idExceptions} parameter */ private Map<String, String> idExceptionsMap = null; /** * Set by the {@link #setUp()} method from the value of the {@link #tableNames} parameter */ private String[] tableNamesArray = null; /** * The sql statement to select out the ids. Generated in the {@link #setUp()} method. */ private String genericIdSql; /** * The sql statement to update the rows with the uuids. Generated in the {@link #setUp()} * method. */ private String genericUpdateSql; /** * Does the work of adding UUIDs to all rows. * * @see liquibase.change.custom.CustomTaskChange#execute(liquibase.database.Database) */ public void execute(Database database) throws CustomChangeException, UnsupportedChangeException { // if we're in a "generate sql file" mode, quit early if (Context.getRuntimeProperties().size() == 0) return; if (tableNamesArray == null || tableNamesArray.length == 0) throw new CustomChangeException("At least one table name in the 'tableNames' parameter is required", null); DatabaseConnection connection = database.getConnection(); // loop over all tables for (String tableName : tableNamesArray) { try { Statement idStatement = null; PreparedStatement updateStatement = null; try { String idSql = genericIdSql.replaceAll("tablename", tableName); String updateSql = genericUpdateSql.replaceAll("tablename", tableName); // hacky way to deal with tables that don't follow the tableNam_id convention for (Map.Entry<String, String> idException : idExceptionsMap.entrySet()) { idSql = idSql.replaceFirst(idException.getKey(), idException.getValue()); updateSql = updateSql.replaceFirst(idException.getKey(), idException.getValue()); } idStatement = connection.createStatement(); updateStatement = connection.prepareStatement(updateSql); // Map<Integer, UUID> uuids = new HashMap<Integer, UUID>(); ResultSet ids = idStatement.executeQuery(idSql); while (ids.next()) { updateStatement.setInt(2, ids.getInt(1)); // set the primary key number updateStatement.setString(1, UUID.randomUUID().toString()); // set the uuid for this row updateStatement.executeUpdate(); } } finally { if (idStatement != null) idStatement.close(); if (updateStatement != null) updateStatement.close(); } } catch (SQLException e) { throw new CustomChangeException("Unable to set uuid on table: " + tableName, e); } } } /** * @see liquibase.change.custom.CustomChange#getConfirmationMessage() */ public String getConfirmationMessage() { return "Finished adding uuids to all rows in all tables"; } /** * @see liquibase.change.custom.CustomChange#setFileOpener(liquibase.FileOpener) */ public void setFileOpener(FileOpener fileOpener) { } /** * Get the values of the parameters passed in and set them to the local variables on this class. * * @see liquibase.change.custom.CustomChange#setUp() */ public void setUp() throws SetupException { tableNamesArray = StringUtils.split(tableNames); idExceptionsMap = OpenmrsUtil.parseParameterList(idExceptions); genericIdSql = "select tablename_id from tablename where columnName is null"; genericIdSql = genericIdSql.replace("columnName", columnName); genericUpdateSql = "update tablename set columnName = ? where tablename_id = ?"; genericUpdateSql = genericUpdateSql.replace("columnName", columnName); } /** * @see liquibase.change.custom.CustomChange#validate(liquibase.database.Database) */ public void validate(Database database) throws InvalidChangeDefinitionException { } /** * This is called by liquibase to set the parameter "tableNames" onto this change. * * @param tableNames the tableNames to set */ public void setTableNames(String tableNames) { this.tableNames = tableNames; } /** * This is called by liquibase to set the parameter "columnName" onto this change. * * @param columnName the columnName to set */ public void setColumnName(String columnName) { this.columnName = columnName; } /** * Way to specify the table id columns that don't follow the table_name.table_name_id pattern * * @param idExceptions */ public void setIdExceptions(String idExceptions) { this.idExceptions = idExceptions; } }