/** * This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. * * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS * graphic logo is a trademark of OpenMRS Inc. */ package org.openmrs.util.databasechange; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import org.openmrs.api.context.Context; import org.openmrs.util.OpenmrsConstants; import org.openmrs.util.OpenmrsUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import liquibase.change.custom.CustomTaskChange; import liquibase.database.Database; import liquibase.database.DatabaseConnection; import liquibase.exception.CustomChangeException; import liquibase.exception.DatabaseException; import liquibase.exception.SetupException; import liquibase.exception.ValidationErrors; import liquibase.resource.ResourceAccessor; /** * Executes (aka "source"s) the given file on the current database. <br> * <br> * Expects parameter: "sqlFile" : name of file on classpath to source on mysql */ public class SourceMySqldiffFile implements CustomTaskChange { public static final String CONNECTION_USERNAME = "connection.username"; public static final String CONNECTION_PASSWORD = "connection.password"; private static Logger log = LoggerFactory.getLogger(SourceMySqldiffFile.class); /** * Absolute path and name of file to source */ private String sqlFile = null; private ResourceAccessor fileOpener = null; /** * Does the work of executing the file on mysql * * @see liquibase.change.custom.CustomTaskChange#execute(liquibase.database.Database) */ @Override public void execute(Database database) throws CustomChangeException { Properties runtimeProperties = Context.getRuntimeProperties(); String username = runtimeProperties.getProperty(CONNECTION_USERNAME); String password = runtimeProperties.getProperty(CONNECTION_PASSWORD); if (username == null) { username = System.getProperty(CONNECTION_USERNAME); } if (password == null) { password = System.getProperty(CONNECTION_PASSWORD); } // if we're in a "generate sql file" mode, quit early if (username == null || password == null) { return; } DatabaseConnection connection = database.getConnection(); // copy the file from the classpath to a real file File tmpOutputFile = null; try { tmpOutputFile = File.createTempFile(sqlFile, "tmp"); InputStream sqlFileInputStream = fileOpener.getResourceAsStream(sqlFile); OutputStream outputStream = new FileOutputStream(tmpOutputFile); OpenmrsUtil.copyFile(sqlFileInputStream, outputStream); } catch (IOException e) { if (tmpOutputFile != null) { throw new CustomChangeException( "Unable to copy " + sqlFile + " to file: " + tmpOutputFile.getAbsolutePath(), e); } else { throw new CustomChangeException("Unable to copy " + sqlFile, e); } } // build the mysql command line string List<String> commands = new ArrayList<String>(); String databaseName; try { commands.add("mysql"); commands.add("-u" + username); commands.add("-p" + password); String path = tmpOutputFile.getAbsolutePath(); if (!OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM) { // windows hacks path = fixWindowsPathHack(path); } commands.add("-esource " + path); databaseName = connection.getCatalog(); commands.add(databaseName); } catch (DatabaseException e) { throw new CustomChangeException("Unable to generate command string for file: " + sqlFile, e); } // to be used in error messages if this fails String errorCommand = "\"mysql -u" + username + " -p -e\"source " + tmpOutputFile.getAbsolutePath() + "\"" + databaseName; // run the command line string StringBuilder output = new StringBuilder(); Integer exitValue = -1; // default to a non-zero exit value in case of exceptions try { exitValue = execCmd(tmpOutputFile.getParentFile(), commands.toArray(new String[] {}), output); } catch (IOException io) { if (io.getMessage().endsWith("not found")) { throw new CustomChangeException("Unable to run command: " + commands.get(0) + ". Make sure that it is on the PATH and then restart your server and try again. " + " Or run " + errorCommand + " at the command line with the appropriate full mysql path", io); } } catch (Exception e) { throw new CustomChangeException("Error while executing command: '" + commands.get(0) + "'", e); } log.debug("Exec called: " + Arrays.asList(commands)); if (exitValue != 0) { log.error("There was an error while running the " + commands.get(0) + " command. Command output: " + output.toString()); throw new CustomChangeException( "There was an error while running the " + commands.get(0) + " command. See your server's error log for the full error output. As an alternative, you" + " can run this command manually on your database to skip over this error. Run this at the command line " + errorCommand + " "); } else { // a normal exit value log.debug("Output of exec: " + output); } } /** * A hacky way to get rid of the spaces in the java exec call because mysql and java are not * communicating well * * @param path * @return */ private String fixWindowsPathHack(String path) { StringBuilder returnedPath = new StringBuilder(); path = path.replace("\\", "/"); // so java doesn't freak out with windows backslashes for (String pathPart : path.split("/")) { if (pathPart.contains(" ")) { // shorten to the first 6 characters uppercased pathPart = pathPart.substring(0, 6).toUpperCase(); // add in the tilda and assume the first one (very hacky part) pathPart = pathPart + "~1"; } returnedPath.append(pathPart).append("/"); } returnedPath.deleteCharAt(returnedPath.length() - 1); return returnedPath.toString(); } /** * @param cmdWithArguments * @param wd * @param the string * @return process exit value */ private Integer execCmd(File wd, String[] cmdWithArguments, StringBuilder out) throws Exception { log.debug("executing command: " + Arrays.toString(cmdWithArguments)); Integer exitValue = -1; // Needed to add support for working directory because of a linux // file system permission issue. if (!OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM) { wd = null; } Process p = (wd != null) ? Runtime.getRuntime().exec(cmdWithArguments, null, wd) : Runtime.getRuntime().exec( cmdWithArguments); out.append("Normal cmd output:\n"); Reader reader = new InputStreamReader(p.getInputStream()); BufferedReader input = new BufferedReader(reader); int readChar = 0; while ((readChar = input.read()) != -1) { out.append((char) readChar); } input.close(); reader.close(); out.append("ErrorStream cmd output:\n"); reader = new InputStreamReader(p.getErrorStream()); input = new BufferedReader(reader); readChar = 0; while ((readChar = input.read()) != -1) { out.append((char) readChar); } input.close(); reader.close(); exitValue = p.waitFor(); log.debug("Process exit value: " + exitValue); log.debug("execCmd output: \n" + out.toString()); return exitValue; } /** * @see liquibase.change.custom.CustomChange#getConfirmationMessage() */ @Override public String getConfirmationMessage() { return "Finished executing " + sqlFile + " on database"; } /** * @see liquibase.change.custom.CustomChange#setFileOpener(ResourceAccessor) */ @Override public void setFileOpener(ResourceAccessor fileOpener) { this.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() */ @Override public void setUp() throws SetupException { } /** * @see liquibase.change.custom.CustomChange#validate(liquibase.database.Database) */ @Override public ValidationErrors validate(Database database) { return new ValidationErrors(); } /** * @param sqlFile the sqlFile to set */ public void setSqlFile(String sqlFile) { this.sqlFile = sqlFile; } }