/** * 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.HashMap; 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.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.util.OpenmrsConstants; /** * Adds yes/no boolean concepts and changes all boolean obs values to match these concepts */ public class BooleanConceptChangeSet implements CustomTaskChange { private static Log log = LogFactory.getLog(BooleanConceptChangeSet.class); private Integer trueConceptId; private Integer falseConceptId; //string values for boolean concepts private static Map<String, String[]> trueNames = new HashMap<String, String[]>(); private static Map<String, String[]> falseNames = new HashMap<String, String[]>(); // how to say True and Yes in OpenMRS core languages static { // names may not include spaces, or else the logic to create concept words will break trueNames.put("en", new String[] { "True", "Yes" }); trueNames.put("fr", new String[] { "Vrai", "Oui" }); trueNames.put("es", new String[] { "Verdadero", "Sí" }); trueNames.put("it", new String[] { "Vero", "Sì" }); trueNames.put("pt", new String[] { "Verdadeiro", "Sim" }); falseNames.put("en", new String[] { "False", "No" }); falseNames.put("fr", new String[] { "Faux", "Non" }); falseNames.put("es", new String[] { "Falso", "No" }); falseNames.put("it", new String[] { "Falso", "No" }); falseNames.put("pt", new String[] { "Falso", "Não" }); } /** * @see liquibase.change.custom.CustomTaskChange#execute(liquibase.database.Database) */ public void execute(Database database) throws CustomChangeException, UnsupportedChangeException { DatabaseConnection connection = database.getConnection(); // try to find existing concepts with the right names trueConceptId = findConceptByName(connection, trueNames); falseConceptId = findConceptByName(connection, falseNames); // if they don't exist, create them if (trueConceptId == null) trueConceptId = createConcept(connection, trueNames); if (falseConceptId == null) falseConceptId = createConcept(connection, falseNames); // create the global properties final boolean trueFalseGlobalPropertiesPresent = getInt(connection, "SELECT COUNT(*) FROM global_property WHERE property IN ('" + OpenmrsConstants.GLOBAL_PROPERTY_TRUE_CONCEPT + "', '" + OpenmrsConstants.GLOBAL_PROPERTY_FALSE_CONCEPT + "')") == 2; if (!trueFalseGlobalPropertiesPresent) createGlobalProperties(connection, trueConceptId, falseConceptId); // now change all the existing obs changeObs(connection); } /** * Finds a concept that has any of the the given names in the given locale. If you have a * concept named 'True' in 'en_US' and you search for 'True' in 'en' this will be returned. * * @param connection * @param names a Map from (2-letter) locale to all possible names in that locale * @return a concept id. * @throws CustomChangeException */ private Integer findConceptByName(DatabaseConnection connection, Map<String, String[]> names) throws CustomChangeException { for (Map.Entry<String, String[]> e : names.entrySet()) { String locale = e.getKey(); for (String name : e.getValue()) { Integer ret = getInt(connection, "select concept_id from concept_name where name = '" + name + "' and locale like '" + locale + "%'"); if (ret != null) return ret; } } return null; } /** * creates a concept * * @param connection a DatabaseConnection * @param names a Map from locale to names in that locale, which will be added to the new * concept * @throws CustomChangeException */ private Integer createConcept(DatabaseConnection connection, Map<String, String[]> names) throws CustomChangeException { PreparedStatement updateStatement = null; try { int conceptId = getInt(connection, "SELECT MAX(concept_id) FROM concept"); conceptId++; updateStatement = connection .prepareStatement("INSERT INTO concept (concept_id, short_name, description, datatype_id, class_id, is_set, creator, date_created, uuid) VALUES (?, '', '', 4, 11, 0, 1, NOW(), ?)"); updateStatement.setInt(1, conceptId); updateStatement.setString(2, UUID.randomUUID().toString()); updateStatement.executeUpdate(); boolean preferredDoneAlready = false; // only tag one name as preferred int conceptNameId = getInt(connection, "SELECT MAX(concept_name_id) FROM concept_name"); for (Map.Entry<String, String[]> e : names.entrySet()) { String locale = e.getKey(); for (String name : e.getValue()) { conceptNameId++; updateStatement = connection .prepareStatement("INSERT INTO concept_name (concept_name_id, concept_id, locale, name, creator, date_created, uuid) VALUES (?, ?, ?, ?, 1, NOW(), ?)"); updateStatement.setInt(1, conceptNameId); updateStatement.setInt(2, conceptId); updateStatement.setString(3, locale); updateStatement.setString(4, name); updateStatement.setString(5, UUID.randomUUID().toString()); updateStatement.executeUpdate(); // Tag the first english name as preferred. This is ugly, but it's not feasible to // fix this before refactoring concept_name_tags. if (!preferredDoneAlready && "en".equals(locale)) { updateStatement = connection .prepareStatement("INSERT INTO concept_name_tag_map (concept_name_id, concept_name_tag_id) VALUES (?, 4)"); updateStatement.setInt(1, conceptNameId); updateStatement.executeUpdate(); preferredDoneAlready = true; } updateStatement = connection .prepareStatement("INSERT INTO concept_word (concept_id, word, locale, concept_name_id) VALUES (?, ?, ?, ?)"); updateStatement.setInt(1, conceptId); updateStatement.setString(2, name); updateStatement.setString(3, locale); updateStatement.setInt(4, conceptNameId); updateStatement.executeUpdate(); } } return conceptId; } catch (SQLException e) { throw new CustomChangeException("Unable to create concept with names " + names, e); } finally { if (updateStatement != null) { try { updateStatement.close(); } catch (SQLException e) {} } } } /** * changes all obs which have boolean values to the new (coded) representation of boolean * values. * * @param connection a DatabaseConnection * @param trueConceptName the concept name for boolean true values * @param falseConceptName the concept name for boolean false values * @throws CustomChangeException */ private void changeObs(DatabaseConnection connection) throws CustomChangeException { PreparedStatement updateStatement = null; try { /* replace value_numerical boolean values by coded boolean values */ updateStatement = connection .prepareStatement("UPDATE obs SET value_coded = ?, value_numeric = NULL WHERE value_numeric != 0 AND concept_id IN (SELECT concept_id FROM concept WHERE datatype_id = 10)"); updateStatement.setInt(1, trueConceptId); updateStatement.executeUpdate(); updateStatement = connection .prepareStatement("UPDATE obs SET value_coded = ?, value_numeric = NULL WHERE value_numeric = 0 AND concept_id IN (SELECT concept_id FROM concept WHERE datatype_id = 10)"); updateStatement.setInt(1, falseConceptId); updateStatement.executeUpdate(); } catch (SQLException e) { throw new CustomChangeException("Unable to change obs", e); } finally { if (updateStatement != null) { try { updateStatement.close(); } catch (SQLException e) {} } } } /** * Inserts global properties 'Concept.true' and 'Concept.false' into the global_property table * * @param connection a DatabaseConnection * @param trueConceptId the concept id for true boolean concept * @param falseConceptId the concept id for false boolean concept * @throws CustomChangeException */ private void createGlobalProperties(DatabaseConnection connection, Integer trueConceptId, Integer falseConceptId) throws CustomChangeException { if (trueConceptId == null || trueConceptId < 1 || falseConceptId == null || falseConceptId < 1) throw new CustomChangeException("Can't create global properties for true/false concepts with invalid conceptIds"); PreparedStatement updateStatement = null; try { updateStatement = connection .prepareStatement("INSERT INTO global_property (property, property_value, description, uuid) VALUES (?, ?, ?, ?)"); updateStatement.setString(1, OpenmrsConstants.GLOBAL_PROPERTY_TRUE_CONCEPT); updateStatement.setInt(2, trueConceptId); updateStatement.setString(3, "Concept id of the concept defining the TRUE boolean concept"); updateStatement.setString(4, UUID.randomUUID().toString()); updateStatement.executeUpdate(); updateStatement.setString(1, OpenmrsConstants.GLOBAL_PROPERTY_FALSE_CONCEPT); updateStatement.setInt(2, falseConceptId); updateStatement.setString(3, "Concept id of the concept defining the FALSE boolean concept"); updateStatement.setString(4, UUID.randomUUID().toString()); updateStatement.executeUpdate(); } catch (SQLException e) { throw new CustomChangeException("Unable to create global properties for concept ids defining boolean concepts", e); } finally { if (updateStatement != null) { try { updateStatement.close(); } catch (SQLException e) {} } } } /** * returns an integer resulting from the execution of an sql statement * * @param connection a DatabaseConnection * @param sql the sql statement to execute * @return integer resulting from the execution of the sql statement * @throws CustomChangeException */ private Integer getInt(DatabaseConnection connection, String sql) throws CustomChangeException { Statement stmt = null; try { stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql); Integer result = null; if (rs.next()) { result = rs.getInt(1); } else { // this is okay, we just return null in this case log.debug("Query returned no results: " + sql); } if (rs.next()) { log.warn("Query returned multiple results when we expected just one: " + sql); } return result; } catch (SQLException e) { throw new CustomChangeException("Unable to get int", e); } finally { if (stmt != null) { try { stmt.close(); } catch (SQLException e) {} } } } /** * @see liquibase.change.custom.CustomChange#getConfirmationMessage() */ public String getConfirmationMessage() { return "Finished creating boolean concepts"; } /** * @see liquibase.change.custom.CustomChange#setFileOpener(liquibase.FileOpener) */ public void setFileOpener(FileOpener fileOpener) { } /** * @see liquibase.change.custom.CustomChange#setUp() */ public void setUp() throws SetupException { } /** * @see liquibase.change.custom.CustomChange#validate(liquibase.database.Database) */ public void validate(Database database) throws InvalidChangeDefinitionException { } }