MigrateDataSet.java

/**
 * 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.test;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

import javax.swing.JFileChooser;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlWriter;
import org.dbunit.operation.DatabaseOperation;

/**
 * This is a runnable java class that migrates/converts the current dbunit xml files to a new schema
 * structure. After running this class, you are able to pick either a folder to convert. Xmls files
 * are recursively found and converted. The
 * {@link BaseContextSensitiveTest#INITIAL_DATA_SET_XML_FILENAME} file is overwritten by values in
 * the database defined by the runtime properties
 */
public class MigrateDataSet {
	
	private static String OLD_SCHEMA_FILE = "/home/ben/workspace/openmrs-trunk/metadata/model/1.3.0-schema-only.sql";
	
	private static String OLD_UPDATE_FILE = "/home/ben/workspace/openmrs-trunk/metadata/model/update-to-latest-db.mysqldiff.sql";
	
	private static String NEW_UPDATE_FILE = "/home/ben/workspace/openmrs-concept-name-tag/metadata/model/update-to-latest-db.mysqldiff.sql";
	
	private static String[] credentials = BaseContextSensitiveTest
	        .askForUsernameAndPassword("Enter your MYSQL DATABASE username and password");
	
	private static String tempDatabaseName = "junitmigration";
	
	/**
	 * Do the stuff for this class (create the file)
	 * 
	 * @throws Exception
	 */
	public static void main(String args[]) throws Exception {
		System.out.println("Starting...");
		
		String wd = "./test";
		
		// choose the directory to open
		JFileChooser chooser = new JFileChooser();
		chooser.setCurrentDirectory(new File(wd));
		chooser.setMultiSelectionEnabled(true);
		chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		int result = chooser.showOpenDialog(null);
		
		if (result == JFileChooser.APPROVE_OPTION) {
			File[] dirs = chooser.getSelectedFiles();
			
			for (File directory : dirs) {
				System.out.println("Doing migration on directory: " + directory.getAbsolutePath());
				doMigration(directory);
			}
		}
	}
	
	/**
	 * Recurse over the given file and convert all xml files there in
	 * 
	 * @param fileOrDirectory
	 * @throws Exception
	 */
	private static void doMigration(File fileOrDirectory) throws Exception {
		
		String filename = fileOrDirectory.getName();
		
		if (filename.startsWith(".svn") || filename.endsWith("TestingApplicationContext.xml")) {
			// skip .svn files
		} else if (fileOrDirectory.isDirectory()) {
			for (File innerFile : fileOrDirectory.listFiles()) {
				doMigration(innerFile);
			}
		} else if (filename.endsWith(".xml")) {
			InputStream fileOrDirectoryStream = new FileInputStream(fileOrDirectory);
			
			System.out.println("Migrating " + fileOrDirectory.getAbsolutePath());
			
			System.out.println(execMysqlCmd("DROP DATABASE IF EXISTS " + tempDatabaseName, null, false));
			System.out.println(execMysqlCmd("CREATE DATABASE " + tempDatabaseName + " DEFAULT CHARACTER SET utf8", null,
			    false));
			System.out.println(execMysqlCmd(null, OLD_SCHEMA_FILE, true));
			System.out.println(execMysqlCmd(null, OLD_UPDATE_FILE, true));
			
			// the straight-up database connection
			String url = "jdbc:mysql://localhost/" + tempDatabaseName;
			Class.forName("com.mysql.jdbc.Driver");
			Connection con = DriverManager.getConnection(url, credentials[0], credentials[1]);
			
			// database connection for dbunit
			IDatabaseConnection dbunitConnection = new DatabaseConnection(con);
			
			try {
				PreparedStatement ps = con.prepareStatement("SET FOREIGN_KEY_CHECKS=0;");
				ps.execute();
				ps.close();
				
				IDataSet dataset = new FlatXmlDataSet(fileOrDirectoryStream);
				
				DatabaseOperation.REFRESH.execute(dbunitConnection, dataset);
				
				//turn off foreign key checks here too.
				System.out.println(execMysqlCmd("SET FOREIGN_KEY_CHECKS=0", NEW_UPDATE_FILE, true));
				
				System.out.println("Dumping new xml file");
				
				// get a new connection so dbunit knows the right column headers
				dbunitConnection = new DatabaseConnection(con);
				
				// full database export that will ignore empty tables
				FlatXmlWriter datasetWriter = new FlatXmlWriter(new FileOutputStream(fileOrDirectory));
				datasetWriter.write(dbunitConnection.createDataSet());
			}
			catch (Exception e) {
				System.err.println("Unable to convert: " + filename + " Error: " + e.getMessage());
			}
			finally {
				fileOrDirectoryStream.close();
				dbunitConnection = null;
			}
			
			System.out.println("Finished!");
		}
	}
	
	/**
	 * Execute the given sourceFile and the given command against the temporary mysql database.
	 * 
	 * @param sourceFile (nullable)
	 * @param cmd (nullable)
	 * @param includeDB
	 * @return
	 * @throws Exception
	 */
	private static String execMysqlCmd(String cmd, String sourceFile, boolean includeDB) throws Exception {
		
		if (sourceFile == null && cmd == null)
			throw new Exception("wha...?");
		
		String shellCommand = "";
		if (cmd != null)
			shellCommand = "echo " + cmd + "\\; | ";
		
		if (sourceFile != null) {
			shellCommand = shellCommand + "cat " + (cmd != null ? "-" : "") + " " + sourceFile + " | ";
		}
		
		shellCommand = shellCommand + "mysql -u" + credentials[0] + " -p" + credentials[1];
		
		if (includeDB)
			shellCommand = shellCommand + " -D" + tempDatabaseName;
		
		System.out.println("Executing: " + shellCommand);
		
		String[] cmds = new String[] { "/bin/sh", "-c", shellCommand };
		
		File wd = new File("/tmp");
		
		StringBuffer out = new StringBuffer();
		
		try {
			// Needed to add support for working directory because of a linux
			// file system permission issue.
			// Could not create lcab.tmp file in default working directory
			// (jmiranda).
			Process p = (wd != null) ? Runtime.getRuntime().exec(cmds, null, wd) : Runtime.getRuntime().exec(cmds);
			
			// get the stdout
			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();
			
			// get the errout
			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();
			
			// wait for the thread to finish and get the exit value
			int exitValue = p.waitFor();
			
		}
		catch (Exception e) {
			System.out.println("Error while executing command: '" + cmd + "': " + e.getMessage());
		}
		
		return out.toString();
	}
}