/**
* 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.web.filter.initialization;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import liquibase.ChangeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Appender;
import org.apache.log4j.Logger;
import org.apache.xerces.impl.dv.util.Base64;
import org.openmrs.ImplementationId;
import org.openmrs.api.PasswordException;
import org.openmrs.api.context.Context;
import org.openmrs.module.MandatoryModuleException;
import org.openmrs.module.OpenmrsCoreModuleException;
import org.openmrs.module.web.WebModuleUtil;
import org.openmrs.scheduler.SchedulerUtil;
import org.openmrs.util.DatabaseUpdateException;
import org.openmrs.util.DatabaseUpdater;
import org.openmrs.util.DatabaseUpdater.ChangeSetExecutorCallback;
import org.openmrs.util.DatabaseUtil;
import org.openmrs.util.InputRequiredException;
import org.openmrs.util.MemoryAppender;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.OpenmrsUtil;
import org.openmrs.util.PrivilegeConstants;
import org.openmrs.util.Security;
import org.openmrs.web.Listener;
import org.openmrs.web.WebConstants;
import org.openmrs.web.filter.StartupFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ContextLoader;
/**
* This is the first filter that is processed. It is only active when starting OpenMRS for the very
* first time. It will redirect all requests to the {@link WebConstants#SETUP_PAGE_URL} if the
* {@link Listener} wasn't able to find any runtime properties
*/
public class InitializationFilter extends StartupFilter {
protected final Log log = LogFactory.getLog(getClass());
private static final String LIQUIBASE_SCHEMA_DATA = "liquibase-schema-only.xml";
private static final String LIQUIBASE_CORE_DATA = "liquibase-core-data.xml";
private static final String LIQUIBASE_DEMO_DATA = "liquibase-demo-data.xml";
/**
* The first page of the wizard that asks for simple or advanced installation.
*/
private final String INSTALL_METHOD = "installmethod.vm";
/**
* The simple installation setup page.
*/
private final String SIMPLE_SETUP = "simplesetup.vm";
/**
* The first page of the advanced installation of the wizard that asks for a current or past
* database
*/
private final String DATABASE_SETUP = "databasesetup.vm";
/**
* The velocity macro page to redirect to if an error occurs or on initial startup
*/
private final String DEFAULT_PAGE = INSTALL_METHOD;
/**
* This page asks whether database tables/demo data should be inserted and what the
* username/password that will be put into the runtime properties is
*/
private final String DATABASE_TABLES_AND_USER = "databasetablesanduser.vm";
/**
* This page lets the user define the admin user
*/
private static final String ADMIN_USER_SETUP = "adminusersetup.vm";
/**
* This page lets the user pick an implementation id
*/
private static final String IMPLEMENTATION_ID_SETUP = "implementationidsetup.vm";
/**
* This page asks for settings that will be put into the runtime properties files
*/
private final String OTHER_RUNTIME_PROPS = "otherruntimeproperties.vm";
/**
* A page that tells the user that everything is collected and will now be processed
*/
private static final String WIZARD_COMPLETE = "wizardcomplete.vm";
/**
* A page that lists off what is happening while it is going on. This page has ajax that callst
* he {@value #PROGRESS_VM_AJAXREQUEST} page
*/
private static final String PROGRESS_VM = "progress.vm";
/**
* This url is called by javascript to get the status of the install
*/
private static final String PROGRESS_VM_AJAXREQUEST = "progress.vm.ajaxRequest";
/**
* The model object that holds all the properties that the rendered templates use. All
* attributes on this object are made available to all templates via reflection in the
* {@link #renderTemplate(String, Map, httpResponse)} method.
*/
private InitializationWizardModel wizardModel = null;
private InitializationCompletion initJob;
// the actual driver loaded by the DatabaseUpdater class
private String loadedDriverString;
/**
* Variable set at the end of the wizard when spring is being restarted
*/
private static boolean initializationComplete = false;
synchronized protected void setInitializationComplete(boolean initializationComplete) {
InitializationFilter.initializationComplete = initializationComplete;
}
/**
* Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on GET requests
*
* @param httpRequest
* @param httpResponse
*/
@Override
protected void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException,
ServletException {
String page = httpRequest.getParameter("page");
Map<String, Object> referenceMap = new HashMap<String, Object>();
if (page == null) {
// get props and render the first page
File runtimeProperties = getRuntimePropertiesFile();
if (!runtimeProperties.exists()) {
try {
runtimeProperties.createNewFile();
// reset the error objects in case of refresh
wizardModel.canCreate = true;
wizardModel.cannotCreateErrorMessage = "";
}
catch (IOException io) {
wizardModel.canCreate = false;
wizardModel.cannotCreateErrorMessage = io.getMessage();
}
// check this before deleting the file again
wizardModel.canWrite = runtimeProperties.canWrite();
// delete the file again after testing the create/write
// so that if the user stops the webapp before finishing
// this wizard, they can still get back into it
runtimeProperties.delete();
} else {
wizardModel.canWrite = runtimeProperties.canWrite();
}
wizardModel.runtimePropertiesPath = runtimeProperties.getAbsolutePath();
// do step one of the wizard
httpResponse.setContentType("text/html");
renderTemplate(DEFAULT_PAGE, referenceMap, httpResponse);
} else if (PROGRESS_VM_AJAXREQUEST.equals(page)) {
httpResponse.setContentType("text/json");
httpResponse.setHeader("Cache-Control", "no-cache");
Map<String, Object> result = new HashMap<String, Object>();
if (initJob != null) {
result.put("hasErrors", initJob.hasErrors());
if (initJob.hasErrors()) {
result.put("errorPage", initJob.getErrorPage());
errors.addAll(initJob.getErrors());
}
result.put("initializationComplete", isInitializationComplete());
result.put("message", initJob.getMessage());
result.put("actionCounter", initJob.getStepsComplete());
if (!isInitializationComplete()) {
result.put("executingTask", initJob.getExecutingTask());
result.put("executedTasks", initJob.getExecutedTasks());
result.put("completedPercentage", initJob.getCompletedPercentage());
}
Appender appender = Logger.getRootLogger().getAppender("MEMORY_APPENDER");
if (appender instanceof MemoryAppender) {
MemoryAppender memoryAppender = (MemoryAppender) appender;
List<String> logLines = memoryAppender.getLogLines();
// truncate the list to the last 5 so we don't overwhelm jquery
if (logLines.size() > 5)
logLines = logLines.subList(logLines.size() - 5, logLines.size());
result.put("logLines", logLines);
} else {
result.put("logLines", new ArrayList<String>());
}
}
httpResponse.getWriter().write(toJSONString(result, true));
}
}
/**
* Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on POST requests
*
* @param httpRequest
* @param httpResponse
*/
@Override
protected void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException,
ServletException {
String page = httpRequest.getParameter("page");
Map<String, Object> referenceMap = new HashMap<String, Object>();
// TODO make these page names variables.
// / SIMPLE_SETUP \
// 0.INSTALL_METHOD < > WIZARD_COMPLETE
// \ 1.DATABASE_SETUP ... 5.IMPLEMENTATION_ID_SETUP /
// step zero
if (INSTALL_METHOD.equals(page)) {
wizardModel.installMethod = httpRequest.getParameter("install_method");
if (InitializationWizardModel.INSTALL_METHOD_SIMPLE.equals(wizardModel.installMethod)) {
page = SIMPLE_SETUP;
} else {
page = DATABASE_SETUP;
}
renderTemplate(page, referenceMap, httpResponse);
} // simple method
else if (SIMPLE_SETUP.equals(page)) {
if ("Back".equals(httpRequest.getParameter("back"))) {
renderTemplate(INSTALL_METHOD, referenceMap, httpResponse);
return;
}
wizardModel.databaseRootPassword = httpRequest.getParameter("database_root_password");
checkForEmptyValue(wizardModel.databaseRootPassword, errors, "Database root password");
wizardModel.hasCurrentOpenmrsDatabase = false;
wizardModel.createTables = true;
// default wizardModel.databaseName is openmrs
// default wizardModel.createDatabaseUsername is root
wizardModel.createDatabasePassword = wizardModel.databaseRootPassword;
wizardModel.addDemoData = false;
wizardModel.hasCurrentDatabaseUser = false;
wizardModel.createDatabaseUser = true;
// default wizardModel.createUserUsername is root
wizardModel.createUserPassword = wizardModel.databaseRootPassword;
wizardModel.moduleWebAdmin = true;
wizardModel.autoUpdateDatabase = false;
wizardModel.adminUserPassword = InitializationWizardModel.ADMIN_DEFAULT_PASSWORD;
try {
loadedDriverString = DatabaseUtil.loadDatabaseDriver(wizardModel.databaseConnection,
wizardModel.databaseDriver);
}
catch (ClassNotFoundException e) {
errors.add("The given database driver class was not found. "
+ "Please ensure that the database driver jar file is on the class path "
+ "(like in the webapp's lib folder)");
renderTemplate(page, referenceMap, httpResponse);
return;
}
if (errors.isEmpty()) {
page = WIZARD_COMPLETE;
}
renderTemplate(page, referenceMap, httpResponse);
} // step one
else if (DATABASE_SETUP.equals(page)) {
if ("Back".equals(httpRequest.getParameter("back"))) {
renderTemplate(INSTALL_METHOD, referenceMap, httpResponse);
return;
}
wizardModel.databaseConnection = httpRequest.getParameter("database_connection");
checkForEmptyValue(wizardModel.databaseConnection, errors, "Database connection string");
wizardModel.databaseDriver = httpRequest.getParameter("database_driver");
checkForEmptyValue(wizardModel.databaseConnection, errors, "Database connection string");
try {
loadedDriverString = DatabaseUtil.loadDatabaseDriver(wizardModel.databaseConnection,
wizardModel.databaseDriver);
log.info("using database driver :" + loadedDriverString);
}
catch (ClassNotFoundException e) {
errors.add("The given database driver class was not found. "
+ "Please ensure that the database driver jar file is on the class path "
+ "(like in the webapp's lib folder)");
renderTemplate(page, referenceMap, httpResponse);
return;
}
//TODO make each bit of page logic a (unit testable) method
// asked the user for their desired database name
if ("yes".equals(httpRequest.getParameter("current_openmrs_database"))) {
wizardModel.databaseName = httpRequest.getParameter("openmrs_current_database_name");
checkForEmptyValue(wizardModel.databaseName, errors, "Current database name");
wizardModel.hasCurrentOpenmrsDatabase = true;
// TODO check to see if this is an active database
} else {
// mark this wizard as a "to create database" (done at the end)
wizardModel.hasCurrentOpenmrsDatabase = false;
wizardModel.createTables = true;
wizardModel.databaseName = httpRequest.getParameter("openmrs_new_database_name");
checkForEmptyValue(wizardModel.databaseName, errors, "New database name");
// TODO create database now to check if its possible?
wizardModel.createDatabaseUsername = httpRequest.getParameter("create_database_username");
checkForEmptyValue(wizardModel.createDatabaseUsername, errors,
"A user that has 'CREATE DATABASE' privileges");
wizardModel.createDatabasePassword = httpRequest.getParameter("create_database_password");
checkForEmptyValue(wizardModel.createDatabasePassword, errors,
"Password for user with 'CREATE DATABASE' privileges");
}
if (errors.isEmpty()) {
page = DATABASE_TABLES_AND_USER;
}
renderTemplate(page, referenceMap, httpResponse);
} // step two
else if (DATABASE_TABLES_AND_USER.equals(page)) {
if ("Back".equals(httpRequest.getParameter("back"))) {
renderTemplate(DATABASE_SETUP, referenceMap, httpResponse);
return;
}
if (wizardModel.hasCurrentOpenmrsDatabase) {
wizardModel.createTables = "yes".equals(httpRequest.getParameter("create_tables"));
}
wizardModel.addDemoData = "yes".equals(httpRequest.getParameter("add_demo_data"));
if ("yes".equals(httpRequest.getParameter("current_database_user"))) {
wizardModel.currentDatabaseUsername = httpRequest.getParameter("current_database_username");
checkForEmptyValue(wizardModel.currentDatabaseUsername, errors, "Curent user account");
wizardModel.currentDatabasePassword = httpRequest.getParameter("current_database_password");
checkForEmptyValue(wizardModel.currentDatabasePassword, errors, "Current user account password");
wizardModel.hasCurrentDatabaseUser = true;
wizardModel.createDatabaseUser = false;
} else {
wizardModel.hasCurrentDatabaseUser = false;
wizardModel.createDatabaseUser = true;
// asked for the root mysql username/password
wizardModel.createUserUsername = httpRequest.getParameter("create_user_username");
checkForEmptyValue(wizardModel.createUserUsername, errors, "A user that has 'CREATE USER' privileges");
wizardModel.createUserPassword = httpRequest.getParameter("create_user_password");
checkForEmptyValue(wizardModel.createUserPassword, errors,
"Password for user that has 'CREATE USER' privileges");
}
if (errors.isEmpty()) { // go to next page
page = OTHER_RUNTIME_PROPS;
}
renderTemplate(page, referenceMap, httpResponse);
} // step three
else if (OTHER_RUNTIME_PROPS.equals(page)) {
if ("Back".equals(httpRequest.getParameter("back"))) {
renderTemplate(DATABASE_TABLES_AND_USER, referenceMap, httpResponse);
return;
}
wizardModel.moduleWebAdmin = "yes".equals(httpRequest.getParameter("module_web_admin"));
wizardModel.autoUpdateDatabase = "yes".equals(httpRequest.getParameter("auto_update_database"));
if (wizardModel.createTables) { // go to next page if they are creating tables
page = ADMIN_USER_SETUP;
} else { // skip a page
page = IMPLEMENTATION_ID_SETUP;
}
renderTemplate(page, referenceMap, httpResponse);
} // optional step four
else if (ADMIN_USER_SETUP.equals(page)) {
if ("Back".equals(httpRequest.getParameter("back"))) {
renderTemplate(OTHER_RUNTIME_PROPS, referenceMap, httpResponse);
return;
}
wizardModel.adminUserPassword = httpRequest.getParameter("new_admin_password");
String adminUserConfirm = httpRequest.getParameter("new_admin_password_confirm");
// throw back to admin user if passwords don't match
if (!wizardModel.adminUserPassword.equals(adminUserConfirm)) {
errors.add("Admin passwords don't match");
renderTemplate(ADMIN_USER_SETUP, referenceMap, httpResponse);
return;
}
// throw back if the user didn't put in a password
if (wizardModel.adminUserPassword.equals("")) {
errors.add("An admin password is required");
renderTemplate(ADMIN_USER_SETUP, referenceMap, httpResponse);
return;
}
try {
OpenmrsUtil.validatePassword("admin", wizardModel.adminUserPassword, "admin");
}
catch (PasswordException p) {
errors
.add("The password is not long enough, does not contain both uppercase characters and a number, or matches the username.");
renderTemplate(ADMIN_USER_SETUP, referenceMap, httpResponse);
return;
}
if (errors.isEmpty()) { // go to next page
page = IMPLEMENTATION_ID_SETUP;
}
renderTemplate(page, referenceMap, httpResponse);
} // optional step five
else if (IMPLEMENTATION_ID_SETUP.equals(page)) {
if ("Back".equals(httpRequest.getParameter("back"))) {
if (wizardModel.createTables)
renderTemplate(ADMIN_USER_SETUP, referenceMap, httpResponse);
else
renderTemplate(OTHER_RUNTIME_PROPS, referenceMap, httpResponse);
return;
}
wizardModel.implementationIdName = httpRequest.getParameter("implementation_name");
wizardModel.implementationId = httpRequest.getParameter("implementation_id");
wizardModel.implementationIdPassPhrase = httpRequest.getParameter("pass_phrase");
wizardModel.implementationIdDescription = httpRequest.getParameter("description");
// throw back if the user-specified ID is invalid (contains ^ or |).
if (wizardModel.implementationId.indexOf('^') != -1 || wizardModel.implementationId.indexOf('|') != -1) {
errors.add("Implementation ID cannot contain '^' or '|'");
renderTemplate(IMPLEMENTATION_ID_SETUP, referenceMap, httpResponse);
return;
}
if (errors.isEmpty()) { // go to next page
page = WIZARD_COMPLETE;
}
renderTemplate(page, referenceMap, httpResponse);
} else if (WIZARD_COMPLETE.equals(page)) {
if ("Back".equals(httpRequest.getParameter("back"))) {
if (InitializationWizardModel.INSTALL_METHOD_SIMPLE.equals(wizardModel.installMethod)) {
page = SIMPLE_SETUP;
} else {
page = IMPLEMENTATION_ID_SETUP;
}
renderTemplate(page, referenceMap, httpResponse);
return;
}
initJob = new InitializationCompletion();
//get the tasks the user selected and show them in the page while the initilization wizard runs
wizardModel.tasksToExecute = new ArrayList<WizardTask>();
if (!wizardModel.hasCurrentOpenmrsDatabase)
wizardModel.tasksToExecute.add(WizardTask.CREATE_SCHEMA);
if (wizardModel.createDatabaseUser)
wizardModel.tasksToExecute.add(WizardTask.CREATE_DB_USER);
if (wizardModel.createTables) {
wizardModel.tasksToExecute.add(WizardTask.CREATE_TABLES);
wizardModel.tasksToExecute.add(WizardTask.ADD_CORE_DATA);
}
if (wizardModel.addDemoData)
wizardModel.tasksToExecute.add(WizardTask.ADD_DEMO_DATA);
wizardModel.tasksToExecute.add(WizardTask.UPDATE_TO_LATEST);
referenceMap.put("tasksToExecute", wizardModel.tasksToExecute);
initJob.start();
renderTemplate(PROGRESS_VM, referenceMap, httpResponse);
}
}
/**
* Verify the database connection works.
*
* @param connectionUsername
* @param connectionPassword
* @param databaseConnectionFinalUrl
* @return true/false whether it was verified or not
*/
private boolean verifyConnection(String connectionUsername, String connectionPassword, String databaseConnectionFinalUrl) {
try {
// verify connection
//Set Database Driver using driver String
Class.forName(loadedDriverString).newInstance();
DriverManager.getConnection(databaseConnectionFinalUrl, connectionUsername, connectionPassword);
return true;
}
catch (Exception e) {
errors.add("User account " + connectionUsername + " does not work. " + e.getMessage()
+ " See the error log for more details"); // TODO internationalize this
log.warn("Error while checking the connection user account", e);
return false;
}
}
/**
* Convenience method to load the runtime properties in the application data directory
*
* @return
*/
private File getRuntimePropertiesFile() {
String filename = WebConstants.WEBAPP_NAME + "-runtime.properties";
File file = new File(OpenmrsUtil.getApplicationDataDirectory(), filename);
log.debug("Using file: " + file.getAbsolutePath());
return file;
}
/**
* @see org.openmrs.web.filter.StartupFilter#getTemplatePrefix()
*/
@Override
protected String getTemplatePrefix() {
return "org/openmrs/web/filter/initialization/";
}
/**
* @see org.openmrs.web.filter.StartupFilter#getModel()
*/
@Override
protected Object getModel() {
return wizardModel;
}
/**
* @see org.openmrs.web.filter.StartupFilter#skipFilter()
*/
@Override
public boolean skipFilter(HttpServletRequest httpRequest) {
// If progress.vm makes an ajax request even immediately after initialization has completed
// let the request pass in order to let progress.vm load the start page of OpenMRS
// (otherwise progress.vm is displayed "forever")
return !PROGRESS_VM_AJAXREQUEST.equals(httpRequest.getParameter("page")) && !initializationRequired();
}
/**
* Public method that returns true if database+runtime properties initialization is required
*
* @return true if this initialization wizard needs to run
*/
public static boolean initializationRequired() {
return !isInitializationComplete();
}
/**
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
wizardModel = new InitializationWizardModel();
//set whether need to do initialization work
if (isDatabaseEmpty(OpenmrsUtil.getRuntimeProperties(WebConstants.WEBAPP_NAME))) {
//if runtime-properties file doesn't exist, have to do initialization work
setInitializationComplete(false);
} else {
//if database is not empty, then let UpdaterFilter to judge whether need database update
setInitializationComplete(true);
}
}
/**
* @param silent if this statement fails do not display stack trace or record an error in the
* wizard object.
* @param user username to connect with
* @param pw password to connect with
* @param sql String containing sql and question marks
* @param args the strings to fill into the question marks in the given sql
* @return result of executeUpdate or -1 for error
*/
private int executeStatement(boolean silent, String user, String pw, String sql, String... args) {
Connection connection = null;
try {
String replacedSql = sql;
// TODO how to get the driver for the other dbs...
if (wizardModel.databaseConnection.contains("mysql")) {
Class.forName("com.mysql.jdbc.Driver").newInstance();
} else {
replacedSql = replacedSql.replaceAll("`", "\"");
}
String tempDatabaseConnection = "";
if (sql.contains("create database")) {
tempDatabaseConnection = wizardModel.databaseConnection.replace("@DBNAME@", ""); // make this dbname agnostic so we can create the db
} else {
tempDatabaseConnection = wizardModel.databaseConnection.replace("@DBNAME@", wizardModel.databaseName);
}
connection = DriverManager.getConnection(tempDatabaseConnection, user, pw);
for (String arg : args) {
arg = arg.replace(";", "^"); // to prevent any sql injection
replacedSql = replacedSql.replaceFirst("\\?", arg);
}
// run the sql statement
Statement statement = connection.createStatement();
return statement.executeUpdate(replacedSql);
}
catch (SQLException sqlex) {
if (!silent) {
// log and add error
log.warn("error executing sql: " + sql, sqlex);
errors.add("Error executing sql: " + sql + " - " + sqlex.getMessage());
}
}
catch (InstantiationException e) {
log.error("Error generated", e);
}
catch (IllegalAccessException e) {
log.error("Error generated", e);
}
catch (ClassNotFoundException e) {
log.error("Error generated", e);
}
finally {
try {
if (connection != null) {
connection.close();
}
}
catch (Throwable t) {
log.warn("Error while closing connection", t);
}
}
return -1;
}
/**
* Convenience variable to know if this wizard has completed successfully and that this wizard
* does not need to be executed again
*
* @return true if this has been run already
*/
synchronized private static boolean isInitializationComplete() {
return initializationComplete;
}
/**
* Check if the given value is null or a zero-length String
*
* @param value the string to check
* @param errors the list of errors to append the errorMessage to if value is empty
* @param errorMessage the string error message to append if value is empty
* @return true if the value is non-empty
*/
private boolean checkForEmptyValue(String value, List<String> errors, String errorMessage) {
if (value != null && !value.equals("")) {
return true;
}
errors.add(errorMessage + " required.");
return false;
}
/**
* Separate thread that will run through all tasks to complete the initialization. The database
* is created, user's created, etc here
*/
private class InitializationCompletion {
private Thread thread;
private int steps = 0;
private String message = "";
private List<String> errors = new ArrayList<String>();
private String errorPage = null;
private boolean erroneous = false;
private int completedPercentage = 0;
private WizardTask executingTask;
private List<WizardTask> executedTasks = new ArrayList<WizardTask>();
synchronized public void reportError(String error, String errorPage) {
errors.add(error);
this.errorPage = errorPage;
erroneous = true;
}
synchronized public boolean hasErrors() {
return erroneous;
}
synchronized public String getErrorPage() {
return errorPage;
}
synchronized public List<String> getErrors() {
return errors;
}
/**
* Start the completion stage. This fires up the thread to do all the work.
*/
public void start() {
setStepsComplete(0);
setInitializationComplete(false);
thread.start();
}
public void waitForCompletion() {
try {
thread.join();
}
catch (InterruptedException e) {
// TODO Auto-generated catch block
log.error("Error generated", e);
}
}
synchronized protected void setStepsComplete(int steps) {
this.steps = steps;
}
synchronized protected int getStepsComplete() {
return steps;
}
synchronized public String getMessage() {
return message;
}
synchronized public void setMessage(String message) {
this.message = message;
setStepsComplete(getStepsComplete() + 1);
}
/**
* @return the executingTask
*/
synchronized protected WizardTask getExecutingTask() {
return executingTask;
}
/**
* @return the completedPercentage
*/
protected synchronized int getCompletedPercentage() {
return completedPercentage;
}
/**
* @param completedPercentage the completedPercentage to set
*/
protected synchronized void setCompletedPercentage(int completedPercentage) {
this.completedPercentage = completedPercentage;
}
/**
* Adds a task that has been completed to the list of executed tasks
*
* @param task
*/
synchronized protected void addExecutedTask(WizardTask task) {
this.executedTasks.add(task);
}
/**
* @param executingTask the executingTask to set
*/
synchronized protected void setExecutingTask(WizardTask executingTask) {
this.executingTask = executingTask;
}
/**
* @return the executedTasks
*/
synchronized protected List<WizardTask> getExecutedTasks() {
return this.executedTasks;
}
/**
* This class does all the work of creating the desired database, user, updates, etc
*/
public InitializationCompletion() {
Runnable r = new Runnable() {
/**
* TODO split this up into multiple testable methods
*
* @see java.lang.Runnable#run()
*/
public void run() {
try {
String connectionUsername;
String connectionPassword;
if (!wizardModel.hasCurrentOpenmrsDatabase) {
setMessage("Create database");
setExecutingTask(WizardTask.CREATE_SCHEMA);
// connect via jdbc and create a database
String sql = "create database if not exists `?` default character set utf8";
int result = executeStatement(false, wizardModel.createDatabaseUsername,
wizardModel.createDatabasePassword, sql, wizardModel.databaseName);
// throw the user back to the main screen if this error occurs
if (result < 0) {
reportError(
"Unable to create the database. The password might be incorrect or the database is not started.",
DEFAULT_PAGE);
return;
} else {
wizardModel.workLog.add("Created database " + wizardModel.databaseName);
}
addExecutedTask(WizardTask.CREATE_SCHEMA);
}
if (wizardModel.createDatabaseUser) {
setMessage("Create database user");
setExecutingTask(WizardTask.CREATE_DB_USER);
connectionUsername = wizardModel.databaseName + "_user";
if (connectionUsername.length() > 16)
connectionUsername = wizardModel.databaseName.substring(0, 11) + "_user"; // trim off enough to leave space for _user at the end
connectionPassword = "";
// generate random password from this subset of alphabet
// intentionally left out these characters: ufsb$() to prevent certain words forming randomly
String chars = "acdeghijklmnopqrtvwxyzACDEGHIJKLMNOPQRTVWXYZ0123456789.|~@#^&";
Random r = new Random();
for (int x = 0; x < 12; x++) {
connectionPassword += chars.charAt(r.nextInt(chars.length()));
}
// connect via jdbc with root user and create an openmrs user
String sql = "drop user '?'@'localhost'";
executeStatement(true, wizardModel.createUserUsername, wizardModel.createUserPassword, sql,
connectionUsername);
sql = "create user '?'@'localhost' identified by '?'";
if (-1 != executeStatement(false, wizardModel.createUserUsername,
wizardModel.createUserPassword, sql, connectionUsername, connectionPassword)) {
wizardModel.workLog.add("Created user " + connectionUsername);
} else {
// if error occurs stop
reportError("Unable to create a database user", DEFAULT_PAGE);
return;
}
// grant the roles
sql = "GRANT ALL ON `?`.* TO '?'@'localhost'";
int result = executeStatement(false, wizardModel.createUserUsername,
wizardModel.createUserPassword, sql, wizardModel.databaseName, connectionUsername);
// throw the user back to the main screen if this error occurs
if (result < 0) {
reportError("Unable to grant privileges on openmrs database to user", DEFAULT_PAGE);
return;
} else {
wizardModel.workLog.add("Granted user " + connectionUsername
+ " all privileges to database " + wizardModel.databaseName);
}
addExecutedTask(WizardTask.CREATE_DB_USER);
} else {
connectionUsername = wizardModel.currentDatabaseUsername;
connectionPassword = wizardModel.currentDatabasePassword;
}
String finalDatabaseConnectionString = wizardModel.databaseConnection.replace("@DBNAME@",
wizardModel.databaseName);
// verify that the database connection works
if (!verifyConnection(connectionUsername, connectionPassword, finalDatabaseConnectionString)) {
setMessage("Verify that the database connection works");
// redirect to setup page if we got an error
reportError("Unable to connect to database", DEFAULT_PAGE);
return;
}
// save the properties for startup purposes
Properties runtimeProperties = new Properties();
runtimeProperties.put("connection.url", finalDatabaseConnectionString);
runtimeProperties.put("connection.username", connectionUsername);
runtimeProperties.put("connection.password", connectionPassword);
if (StringUtils.hasText(wizardModel.databaseDriver))
runtimeProperties.put("connection.driver_class", wizardModel.databaseDriver);
runtimeProperties.put("module.allow_web_admin", wizardModel.moduleWebAdmin.toString());
runtimeProperties.put("auto_update_database", wizardModel.autoUpdateDatabase.toString());
runtimeProperties.put(OpenmrsConstants.ENCRYPTION_VECTOR_RUNTIME_PROPERTY, Base64.encode(Security
.generateNewInitVector()));
runtimeProperties.put(OpenmrsConstants.ENCRYPTION_KEY_RUNTIME_PROPERTY, Base64.encode(Security
.generateNewSecretKey()));
Context.setRuntimeProperties(runtimeProperties);
/**
* A callback class that prints out info about liquibase changesets
*/
class PrintingChangeSetExecutorCallback implements ChangeSetExecutorCallback {
private int i = 1;
private String message;
public PrintingChangeSetExecutorCallback(String message) {
this.message = message;
}
/**
* @see org.openmrs.util.DatabaseUpdater.ChangeSetExecutorCallback#executing(liquibase.ChangeSet,
* int)
*/
public void executing(ChangeSet changeSet, int numChangeSetsToRun) {
setMessage(message + " (" + i++ + "/" + numChangeSetsToRun + "): Author: "
+ changeSet.getAuthor() + " Comments: " + changeSet.getComments() + " Description: "
+ changeSet.getDescription());
setCompletedPercentage(Math.round(i * 100 / numChangeSetsToRun));
}
}
if (wizardModel.createTables) {
// use liquibase to create core data + tables
try {
setMessage("Executing " + LIQUIBASE_SCHEMA_DATA);
setExecutingTask(WizardTask.CREATE_TABLES);
DatabaseUpdater.executeChangelog(LIQUIBASE_SCHEMA_DATA, null,
new PrintingChangeSetExecutorCallback("OpenMRS schema file"));
addExecutedTask(WizardTask.CREATE_TABLES);
//reset for this task
setCompletedPercentage(0);
setExecutingTask(WizardTask.ADD_CORE_DATA);
DatabaseUpdater.executeChangelog(LIQUIBASE_CORE_DATA, null,
new PrintingChangeSetExecutorCallback("OpenMRS core data file"));
wizardModel.workLog.add("Created database tables and added core data");
addExecutedTask(WizardTask.ADD_CORE_DATA);
}
catch (Exception e) {
reportError(e.getMessage() + " See the error log for more details", null);
log.warn("Error while trying to create tables and demo data", e);
}
}
// add demo data only if creating tables fresh and user selected the option add demo data
if (wizardModel.createTables && wizardModel.addDemoData) {
try {
setMessage("Adding demo data");
setCompletedPercentage(0);
setExecutingTask(WizardTask.ADD_DEMO_DATA);
DatabaseUpdater.executeChangelog(LIQUIBASE_DEMO_DATA, null,
new PrintingChangeSetExecutorCallback("OpenMRS demo patients, users, and forms"));
wizardModel.workLog.add("Added demo data");
addExecutedTask(WizardTask.ADD_DEMO_DATA);
}
catch (Exception e) {
reportError(e.getMessage() + " See the error log for more details", null);
log.warn("Error while trying to add demo data", e);
}
}
// update the database to the latest version
try {
setMessage("Updating the database to the latest version");
setCompletedPercentage(0);
setExecutingTask(WizardTask.UPDATE_TO_LATEST);
DatabaseUpdater.executeChangelog(null, null, new PrintingChangeSetExecutorCallback(
"Updating database tables to latest version "));
addExecutedTask(WizardTask.UPDATE_TO_LATEST);
}
catch (Exception e) {
reportError(e.getMessage() + " Error while trying to update to the latest database version",
DEFAULT_PAGE);
log.warn("Error while trying to update to the latest database version", e);
return;
}
setExecutingTask(null);
setMessage("Starting OpenMRS");
// start spring
// after this point, all errors need to also call: contextLoader.closeWebApplicationContext(event.getServletContext())
// logic copied from org.springframework.web.context.ContextLoaderListener
ContextLoader contextLoader = new ContextLoader();
contextLoader.initWebApplicationContext(filterConfig.getServletContext());
// start openmrs
try {
Context.openSession();
// load core modules so that required modules are known at openmrs startup
Listener.loadBundledModules(filterConfig.getServletContext());
Context.startup(runtimeProperties);
}
catch (DatabaseUpdateException updateEx) {
log.warn("Error while running the database update file", updateEx);
reportError(
updateEx.getMessage() + " There was an error while running the database update file: "
+ updateEx.getMessage(), DEFAULT_PAGE);
return;
}
catch (InputRequiredException inputRequiredEx) {
// TODO display a page looping over the required input and ask the user for each.
// When done and the user and put in their say, call DatabaseUpdater.update(Map);
// with the user's question/answer pairs
log
.warn("Unable to continue because user input is required for the db updates and we cannot do anything about that right now");
reportError(
"Unable to continue because user input is required for the db updates and we cannot do anything about that right now",
DEFAULT_PAGE);
return;
}
catch (MandatoryModuleException mandatoryModEx) {
log.warn(
"A mandatory module failed to start. Fix the error or unmark it as mandatory to continue.",
mandatoryModEx);
reportError(mandatoryModEx.getMessage(), DEFAULT_PAGE);
return;
}
catch (OpenmrsCoreModuleException coreModEx) {
log
.warn(
"A core module failed to start. Make sure that all core modules (with the required minimum versions) are installed and starting properly.",
coreModEx);
reportError(coreModEx.getMessage(), DEFAULT_PAGE);
return;
}
// TODO catch openmrs errors here and drop the user back out to the setup screen
if (!wizardModel.implementationId.equals("")) {
try {
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_CONCEPT_SOURCES);
Context.addProxyPrivilege(PrivilegeConstants.VIEW_CONCEPT_SOURCES);
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_IMPLEMENTATION_ID);
ImplementationId implId = new ImplementationId();
implId.setName(wizardModel.implementationIdName);
implId.setImplementationId(wizardModel.implementationId);
implId.setPassphrase(wizardModel.implementationIdPassPhrase);
implId.setDescription(wizardModel.implementationIdDescription);
Context.getAdministrationService().setImplementationId(implId);
}
catch (Throwable t) {
reportError(t.getMessage() + " Implementation ID could not be set.", DEFAULT_PAGE);
log.warn("Implementation ID could not be set.", t);
Context.shutdown();
WebModuleUtil.shutdownModules(filterConfig.getServletContext());
contextLoader.closeWebApplicationContext(filterConfig.getServletContext());
return;
}
finally {
Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_CONCEPT_SOURCES);
Context.removeProxyPrivilege(PrivilegeConstants.VIEW_CONCEPT_SOURCES);
Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_IMPLEMENTATION_ID);
}
}
try {
// change the admin user password from "test" to what they input above
if (wizardModel.createTables) {
Context.authenticate("admin", "test");
Context.getUserService().changePassword("test", wizardModel.adminUserPassword);
Context.logout();
}
// web load modules
Listener.performWebStartOfModules(filterConfig.getServletContext());
// start the scheduled tasks
SchedulerUtil.startup(runtimeProperties);
}
catch (Throwable t) {
Context.shutdown();
WebModuleUtil.shutdownModules(filterConfig.getServletContext());
contextLoader.closeWebApplicationContext(filterConfig.getServletContext());
reportError(t.getMessage() + " Unable to complete the startup.", DEFAULT_PAGE);
log.warn("Unable to complete the startup.", t);
return;
}
// output properties to the openmrs runtime properties file so that this wizard is not run again
FileOutputStream fos = null;
try {
fos = new FileOutputStream(getRuntimePropertiesFile());
OpenmrsUtil.storeProperties(runtimeProperties, fos,
"Auto generated by OpenMRS initialization wizard");
wizardModel.workLog.add("Saved runtime properties file " + getRuntimePropertiesFile());
// don't need to catch errors here because we tested it at the beginning of the wizard
}
finally {
if (fos != null) {
fos.close();
}
}
// set this so that the wizard isn't run again on next page load
Context.closeSession();
}
catch (IOException e) {
reportError(e.getMessage() + " Unable to complete the startup.", DEFAULT_PAGE);
}
finally {
if (!hasErrors()) {
// set this so that the wizard isn't run again on next page load
setInitializationComplete(true);
}
}
}
};
thread = new Thread(r);
}
}
/**
* Check whether openmrs database is empty. Having just one non-liquibase table in the given
* database qualifies this as a non-empty database.
*
* @param props the runtime properties
* @return true/false whether openmrs database is empty or doesn't exist yet
*/
private static boolean isDatabaseEmpty(Properties props) {
if (props != null) {
String databaseConnectionFinalUrl = props.getProperty("connection.url");
if (databaseConnectionFinalUrl == null)
return true;
String connectionUsername = props.getProperty("connection.username");
if (connectionUsername == null)
return true;
String connectionPassword = props.getProperty("connection.password");
if (connectionPassword == null)
return true;
Connection connection = null;
try {
DatabaseUtil.loadDatabaseDriver(databaseConnectionFinalUrl);
connection = DriverManager.getConnection(databaseConnectionFinalUrl, connectionUsername, connectionPassword);
DatabaseMetaData dbMetaData = (DatabaseMetaData) connection.getMetaData();
String[] types = { "TABLE" };
//get all tables
ResultSet tbls = dbMetaData.getTables(null, null, null, types);
while (tbls.next()) {
String tableName = tbls.getString("TABLE_NAME");
//if any table exist besides "liquibasechangelog" or "liquibasechangeloglock", return false
if (!("liquibasechangelog".equals(tableName)) && !("liquibasechangeloglock".equals(tableName)))
return false;
}
return true;
}
catch (Exception e) {
//pass
}
finally {
try {
if (connection != null) {
connection.close();
}
}
catch (Throwable t) {
//pass
}
}
//if catch an exception while query database, then consider as database is empty.
return true;
} else
return true;
}
}