/*
* InstallRequestProcessor.java
*
* This work is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation; either version 2 of the License,
* or (at your option) any later version.
*
* This work is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*
* Copyright (c) 2004-2006 Per Cederberg. All rights reserved.
*/
package org.liquidsite.app.install;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import org.liquidsite.app.servlet.Application;
import org.liquidsite.app.servlet.Configuration;
import org.liquidsite.app.servlet.ConfigurationException;
import org.liquidsite.app.servlet.RequestException;
import org.liquidsite.app.servlet.RequestProcessor;
import org.liquidsite.app.template.TemplateException;
import org.liquidsite.core.content.ContentException;
import org.liquidsite.core.content.ContentManager;
import org.liquidsite.core.content.ContentSecurityException;
import org.liquidsite.core.content.ContentSite;
import org.liquidsite.core.content.Domain;
import org.liquidsite.core.content.User;
import org.liquidsite.core.web.Request;
import org.liquidsite.util.db.DatabaseConnection;
import org.liquidsite.util.db.DatabaseConnectionException;
import org.liquidsite.util.db.DatabaseDataException;
import org.liquidsite.util.db.DatabaseException;
import org.liquidsite.util.db.DatabaseQuery;
import org.liquidsite.util.db.DatabaseResults;
import org.liquidsite.util.db.MySQLDatabaseConnector;
import org.liquidsite.util.log.Log;
/**
* The installation request processor. This processor differs from
* the other processors in that it uses instance variables to keep
* session information. This makes this processor impossible to use
* in a multi-user scenario, but the installation process is supposed
* to be run by a single user.
*
* @author Marielle Fois, <marielle at kth dot se>
* @author Per Cederberg, <per at percederberg dot net>
* @version 1.0
*/
public class InstallRequestProcessor extends RequestProcessor {
/**
* The class logger.
*/
private static final Log LOG = new Log(InstallRequestProcessor.class);
/**
* The application context.
*/
private Application application;
/**
* The installer to use. This variable is initially set to null if
* no valid database connection has been made.
*/
private Installer installer = null;
/**
* The MySQL database connector. This variable is set to null if
* no valid database connection has been made.
*/
private MySQLDatabaseConnector connector = null;
/**
* The description of the last error encountered. If this
* variable is set to null, no error has ocurred.
*/
private String lastError = null;
/**
* The database host name.
*/
private String host = "localhost";
/**
* The database name.
*/
private String database = "liquidsite";
/**
* The data directory.
*/
private String dataDir = "/var/lib/liquidsite";
/**
* The database user used in the installation.
*/
private String installUser = "";
/**
* The database user password used in the installation.
*/
private String installPassword = "";
/**
* The database user name for Liquid Site.
*/
private String databaseUser = "liquidsite";
/**
* The database user password for Liquid Site.
*/
private String databasePassword = "";
/**
* The administrator user name for Liquid Site.
*/
private String adminUser = "root";
/**
* The administrator user password for Liquid Site.
*/
private String adminPassword = "";
/**
* The create database flag.
*/
private boolean createDatabase = false;
/**
* The create database user flag.
*/
private boolean createDatabaseUser = false;
/**
* The update version number. This is set to null if no update
* should be attempted.
*/
private String updateVersion = null;
/**
* Creates a new install request processor.
*
* @param app the application context
*/
public InstallRequestProcessor(Application app) {
super(app.getContentManager(), app.getBaseDir());
this.application = app;
}
/**
* Destroys this request processor. This method frees all
* internal resources used by this processor.
*/
public void destroy() {
closeConnector();
}
/**
* Processes a request.
*
* @param request the request object to process
*
* @throws RequestException if the request couldn't be processed
*/
public void process(Request request) throws RequestException {
String path = request.getPath();
String step = request.getParameter("step", "");
Domain domain;
ContentSite site;
// Fake request environment
path = path.substring(request.getServletPath().length());
domain = new Domain(getContentManager(), "ROOT");
request.getEnvironment().setDomain(domain);
site = new ContentSite(getContentManager(), domain);
site.setDirectory(request.getServletPath());
request.getEnvironment().setSite(site);
// Process request
lastError = null;
if (path.startsWith("/liquidsite/")) {
processLiquidSite(request, path.substring(12));
} else if (!path.equals("/") && !path.equals("/install.html")) {
throw RequestException.RESOURCE_NOT_FOUND;
} else if (request.getParameter("prev", "").equals("true")) {
processPrevious(request, step);
} else if (step.equals("1")) {
processStep1(request);
} else if (step.equals("2")) {
processStep2(request);
} else if (step.equals("3")) {
processStep3(request);
} else if (step.equals("4")) {
processStep4(request);
} else if (step.equals("5")) {
processStep5(request);
} else {
displayStep1(request);
}
}
/**
* Processes a request for the previous page.
*
* @param request the request object to process
* @param step the request step
*/
private void processPrevious(Request request, String step) {
if (step.equals("5")) {
displayStep4(request);
} else if (step.equals("4")) {
displayStep3(request);
} else if (step.equals("3")) {
displayStep2(request);
} else {
displayStep1(request);
}
}
/**
* Processes a request originating from step 1.
*
* @param request the request object to process
*/
private void processStep1(Request request) {
host = request.getParameter("host", "").trim();
installUser = request.getParameter("user", "").trim();
installPassword = request.getParameter("password", "");
createConnector();
if (lastError == null && !isAdministrator()) {
databaseUser = installUser;
databasePassword = installPassword;
}
if (lastError != null) {
displayStep1(request);
} else {
displayStep2(request);
}
}
/**
* Processes a request originating from step 2.
*
* @param request the request object to process
*/
private void processStep2(Request request) {
createDatabase = false;
updateVersion = null;
database = request.getParameter("database1", "");
if (database.equals("")) {
createDatabase = true;
database = request.getParameter("database2", "").trim();
} else {
updateVersion = getVersion(database);
}
if (database.equals("")) {
lastError = "No database selected";
} else if (createDatabase && listDatabases().contains(database)) {
lastError = "Cannot create a database that already exists";
}
if (lastError != null) {
displayStep2(request);
} else {
displayStep3(request);
}
}
/**
* Processes a request originating from step 3.
*
* @param request the request object to process
*/
private void processStep3(Request request) {
MySQLDatabaseConnector test;
String str;
// Extract form data
createDatabaseUser = false;
databaseUser = request.getParameter("user1", "");
if (databaseUser.equals("")) {
createDatabaseUser = true;
databaseUser = request.getParameter("user2", "").trim();
}
databasePassword = request.getParameter("password1", "");
str = request.getParameter("password2", "");
// Validate form data
if (databaseUser.equals("")) {
lastError = "No user name selected";
} else if (createDatabaseUser && !databasePassword.equals(str)) {
lastError = "The two passwords must be identical";
databasePassword = "";
} else if (createDatabaseUser && listUsers().contains(databaseUser)) {
lastError = "Cannot create a user that already exists";
} else if (!createDatabaseUser) {
if (createDatabase) {
test = new MySQLDatabaseConnector(host,
databaseUser,
databasePassword);
} else {
test = new MySQLDatabaseConnector(host,
database,
databaseUser,
databasePassword);
}
try {
test.returnConnection(test.getConnection());
} catch (DatabaseConnectionException e) {
lastError = "Couldn't connect to database with specified " +
"user name and password";
}
}
// Print results
if (lastError != null) {
displayStep3(request);
} else {
displayStep4(request);
}
}
/**
* Processes a request originating from step 4.
*
* @param request the request object to process
*/
private void processStep4(Request request) {
File file;
String str;
// Validate form data
if (updateVersion == null) {
dataDir = request.getParameter("dir", "").trim();
adminUser = request.getParameter("user", "").trim();
adminPassword = request.getParameter("password1", "");
str = request.getParameter("password2", "");
if (dataDir.equals("")) {
lastError = "No data directory specified";
} else if (adminUser.equals("")) {
lastError = "No administrator user name specified";
} else if (adminPassword.equals("")) {
lastError = "No administrator password specified";
} else if (!adminPassword.equals(str)) {
lastError = "The two passwords must be identical";
adminPassword = "";
} else {
file = new File(dataDir);
if (!file.exists()) {
lastError = "Data directory does not exist";
} else if (!file.canWrite()) {
lastError = "Cannot write to data directory, " +
"check permissions";
}
}
}
// Print results
if (lastError != null) {
displayStep4(request);
} else {
displayStep5(request);
}
}
/**
* Processes a request originating from step 5.
*
* @param request the request object to process
*/
private void processStep5(Request request) {
// Write database and configuration
try {
if (createDatabase) {
connector.createDatabase(database);
}
if (createDatabaseUser) {
connector.createUser(databaseUser, databasePassword);
}
if (isAdministrator()) {
connector.addAccessPrivileges(database, databaseUser);
}
if (updateVersion == null) {
installer.createTables(database);
} else {
installer.updateTables(database, updateVersion);
}
writeConfiguration();
createDirs();
application.restart();
if (updateVersion == null) {
writeDefaultData(request.getProtocol(),
request.getServletPath());
}
} catch (InstallException e) {
LOG.error("couldn't finish installation", e);
lastError = e.getMessage();
} catch (DatabaseConnectionException e) {
LOG.error("couldn't finish installation", e);
lastError = e.getMessage();
} catch (DatabaseException e) {
LOG.error("couldn't finish installation", e);
lastError = e.getMessage();
} catch (FileNotFoundException e) {
LOG.error("couldn't finish installation", e);
lastError = e.getMessage();
} catch (IOException e) {
LOG.error("couldn't finish installation", e);
lastError = e.getMessage();
} catch (ConfigurationException e) {
LOG.error("couldn't finish installation", e);
lastError = e.getMessage();
}
// Display errors or start application
if (lastError != null) {
displayStep5(request);
} else {
request.sendRedirect("index.html");
}
}
/**
* Displays the step 1 page.
*
* @param request the request object
*/
private void displayStep1(Request request) {
request.setAttribute("error", lastError);
request.setAttribute("host", host);
request.setAttribute("user", installUser);
request.setAttribute("password", installPassword);
displayTemplate(request, "install/install1.ftl");
}
/**
* Displays the step 2 page.
*
* @param request the request object
*/
private void displayStep2(Request request) {
String originalError = lastError;
ArrayList databaseInfo = new ArrayList();
boolean enableNext = false;
ArrayList databases;
ArrayList tables;
String version;
HashMap info;
String str;
// Find database information
databases = listDatabases();
for (int i = 0; i < databases.size(); i++) {
info = new HashMap();
databaseInfo.add(info);
lastError = null;
tables = listTables(databases.get(i).toString());
if (tables.contains("LS_CONFIGURATION")) {
version = getVersion(databases.get(i).toString());
} else {
version = null;
}
info.put("name", databases.get(i));
info.put("tables", new Integer(tables.size()));
if (lastError != null) {
info.put("status", new Integer(0));
info.put("info", "Couldn't read database");
} else if (databases.get(i).equals("mysql")) {
info.put("status", new Integer(0));
info.put("info", "MySQL administration database");
} else if (databases.get(i).equals("information_schema")) {
info.put("status", new Integer(0));
info.put("info", "MySQL information schema");
} else if (version != null) {
str = "Liquid Site version " + version;
if (installer.canUpdate(version)) {
info.put("status", new Integer(1));
} else {
str += " (Unsupported)";
info.put("status", new Integer(0));
}
info.put("info", str);
} else if (getTableConflicts(tables) > 0) {
str = getTableConflicts(tables) +
" conflicting tables found";
info.put("status", new Integer(0));
info.put("info", str);
} else {
info.put("status", new Integer(1));
info.put("info", "");
enableNext = true;
}
}
lastError = originalError;
if (!enableNext) {
if (isAdministrator()) {
enableNext = true;
} else {
lastError = "No databases available for selection";
}
}
// Display database list
request.setAttribute("error", lastError);
request.setAttribute("database", database);
request.setAttribute("databaseInfo", databaseInfo);
request.setAttribute("enableCreate", isAdministrator());
request.setAttribute("enableNext", enableNext);
displayTemplate(request, "install/install2.ftl");
}
/**
* Displays the step 3 page.
*
* @param request the request object
*/
private void displayStep3(Request request) {
request.setAttribute("error", lastError);
request.setAttribute("user", databaseUser);
request.setAttribute("password", databasePassword);
request.setAttribute("userNames", listUsers());
request.setAttribute("enableCreate", isAdministrator());
displayTemplate(request, "install/install3.ftl");
}
/**
* Displays the step 4 page.
*
* @param request the request object
*/
private void displayStep4(Request request) {
request.setAttribute("error", lastError);
request.setAttribute("updateVersion", updateVersion);
if (updateVersion == null) {
request.setAttribute("dir", dataDir);
request.setAttribute("user", adminUser);
request.setAttribute("password", adminPassword);
}
displayTemplate(request, "install/install4.ftl");
}
/**
* Displays the step 5 page.
*
* @param request the request object
*/
private void displayStep5(Request request) {
request.setAttribute("error", lastError);
request.setAttribute("host", host);
request.setAttribute("database", database);
request.setAttribute("databaseUser", databaseUser);
request.setAttribute("createDatabase", createDatabase);
request.setAttribute("createDatabaseUser", createDatabaseUser);
request.setAttribute("updateVersion", updateVersion);
if (updateVersion == null) {
request.setAttribute("dataDir", dataDir);
request.setAttribute("adminUser", adminUser);
}
displayTemplate(request, "install/install5.ftl");
}
/**
* Processes a request to a template file. The template file name
* is relative to the web context directory, and the output MIME
* type will always be set to "text/html".
*
* @param request the request object
* @param templateName the template file name
*/
private void displayTemplate(Request request, String templateName) {
try {
sendTemplate(request, templateName);
} catch (TemplateException e) {
request.sendData("text/plain", "Error: " + e.getMessage());
}
}
/**
* Creates a new database connector and tests it. If an old
* database connector exists, it will be closed. The instance
* variables are used for passing the connection details. As a
* side-effect, this method will also log any error encountered,
* and set the lastError variable.
*/
private void createConnector() {
if (connector != null) {
closeConnector();
}
connector = new MySQLDatabaseConnector(host,
installUser,
installPassword);
connector.setPoolSize(3);
try {
connector.loadFunctions(getFile("WEB-INF/database.properties"));
connector.returnConnection(connector.getConnection());
int[] ver = connector.getVersion();
if (ver[0] < 5) {
lastError = "MySQL server version 5.0.0 or higher is " +
"required, found version " + ver[0] + "." +
ver[1] + "." + ver[2];
LOG.error(lastError);
connector = null;
} else {
installer = new Installer(application.getBuildVersion(),
connector,
getFile("WEB-INF/sql"));
}
} catch (FileNotFoundException e) {
LOG.error("couldn't read database functions", e);
lastError = "Couldn't find 'database.properties' file";
connector = null;
} catch (IOException e) {
LOG.error("couldn't read database functions", e);
lastError = "Couldn't read 'database.properties' file";
connector = null;
} catch (DatabaseConnectionException e) {
LOG.error("couldn't connect to database", e);
lastError = e.getMessage();
connector = null;
} catch (DatabaseException e) {
LOG.error("couldn't read database version", e);
lastError = e.getMessage();
connector = null;
}
}
/**
* Closes the connector if it was created.
*/
private void closeConnector() {
if (connector != null) {
connector.setPoolSize(0);
try {
connector.update();
} catch (DatabaseConnectionException ignore) {
// Do ignore
}
connector = null;
installer = null;
}
}
/**
* Checks if the current connector is has administrator
* privileges. As a side-effect, this method will log any error
* encountered, and set the lastError variable.
*
* @return true if the connector user is administrator, or
* false otherwise
*/
private boolean isAdministrator() {
try {
return connector.isAdministrator();
} catch (DatabaseConnectionException e) {
LOG.error("couldn't connect to database", e);
lastError = e.getMessage();
} catch (DatabaseException e) {
LOG.error("couldn't get user admin status", e);
lastError = e.getMessage();
}
return false;
}
/**
* Returns a list of databases found with the current connector.
* As a side-effect, this method will log any error encountered,
* and set the lastError variable.
*
* @return the list of database names found
*/
private ArrayList listDatabases() {
try {
return connector.listDatabases();
} catch (DatabaseConnectionException e) {
LOG.error("couldn't connect to database", e);
lastError = e.getMessage();
} catch (DatabaseException e) {
LOG.error("couldn't list databases", e);
lastError = e.getMessage();
}
return new ArrayList();
}
/**
* Returns a list of tables found in a specified database with
* the current connector. As a side-effect, this method will log
* any error encountered, and set the lastError variable.
*
* @param database the database to check
*
* @return the list of database names found
*/
private ArrayList listTables(String database) {
try {
return connector.listTables(database);
} catch (DatabaseConnectionException e) {
LOG.error("couldn't connect to database", e);
lastError = e.getMessage();
} catch (DatabaseException e) {
LOG.error("couldn't list tables", e);
lastError = e.getMessage();
}
return new ArrayList();
}
/**
* Returns a list of all users found with the current connector.
* This method will ignore any error encountered.
*
* @return the list with user names, or
* a list with the install user if no users could be found
*/
private ArrayList listUsers() {
ArrayList users;
try {
return connector.listUsers();
} catch (DatabaseConnectionException ignore) {
// Do nothing
} catch (DatabaseException ignore) {
// Do noting
}
users = new ArrayList();
users.add(installUser);
return users;
}
/**
* Returns the Liquid Site version of the specified database. This
* method will ignore any error encountered.
*
* @param database the database to check
*
* @return the version number found in the database, or
* null if the database wasn't a Liquid Site database
*/
private String getVersion(String database) {
DatabaseConnection con = null;
DatabaseQuery query = new DatabaseQuery("config.select.name");
DatabaseResults res;
try {
con = connector.getConnection();
con.setCatalog(database);
query.addParameter(Configuration.VERSION);
res = con.execute(query);
return res.getRow(0).getString(0);
} catch (DatabaseConnectionException ignore) {
// Do nothing
} catch (DatabaseException ignore) {
// Do nothing
} catch (DatabaseDataException ignore) {
// Do nothing
} finally {
if (con != null) {
connector.returnConnection(con);
}
}
return null;
}
/**
* Returns the number of tables in a list that may cause
* conflicts. A conflicting table is one that has a name starting
* with "LS_".
*
* @param tables the list of table names to check
*
* @return the number of conflicting table names
*/
private int getTableConflicts(ArrayList tables) {
int conflicts = 0;
String name;
for (int i = 0; i < tables.size(); i++) {
name = ((String) tables.get(i)).toUpperCase();
if (name.startsWith("LS_")) {
conflicts++;
}
}
return conflicts;
}
/**
* Writes the Liquid Site configuration file and database table.
*
* @throws FileNotFoundException if the database functions file
* couldn't be found
* @throws IOException if the database functions file couldn't be
* read
* @throws ConfigurationException if the configuration couldn't
* be written
*/
private void writeConfiguration()
throws FileNotFoundException, IOException, ConfigurationException {
MySQLDatabaseConnector con = null;
Configuration config;
Configuration oldConfig;
// Create database connector
con = new MySQLDatabaseConnector(host,
database,
databaseUser,
databasePassword);
con.loadFunctions(getFile("WEB-INF/database.properties"));
// Write configuration
config = application.getConfig();
if (updateVersion != null) {
oldConfig = new Configuration();
oldConfig.read(con);
config.setAll(oldConfig);
}
config.set(Configuration.VERSION, application.getBuildVersion());
config.set(Configuration.DATABASE_HOSTNAME, host);
config.set(Configuration.DATABASE_NAME, database);
config.set(Configuration.DATABASE_USER, databaseUser);
config.set(Configuration.DATABASE_PASSWORD, databasePassword);
config.set(Configuration.DATABASE_POOL_SIZE, 50);
if (updateVersion == null) {
config.set(Configuration.FILE_DIRECTORY, dataDir);
config.set(Configuration.UPLOAD_DIRECTORY,
application.getBaseDir() + "/tmp");
config.set(Configuration.UPLOAD_MAX_SIZE, 10000000);
}
config.write(con);
}
/**
* Creates various required application directories.
*/
private void createDirs()
{
Configuration config;
File dir;
// Create temporary upload directory
config = application.getConfig();
dir = new File(config.get(Configuration.UPLOAD_DIRECTORY,
application.getBaseDir() + "/tmp"));
dir.mkdirs();
// Create plugin directory
dir = new File(application.getBaseDir(), "plugins");
dir.mkdirs();
}
/**
* Writes the Liquid Site database default data. The servlet path
* will be used to place the admin site in the correct base
* directory.
*
* @param protocol the servlet protocol
* @param path the servlet path
*/
private void writeDefaultData(String protocol, String path) {
ContentManager manager = application.getContentManager();
Domain domain = new Domain(manager, "ROOT");
User user = new User(manager, null, adminUser);
ContentSite site = new ContentSite(manager, domain);
try {
domain.setDescription("Root Domain");
domain.save(user);
user.setRealName("Administrator");
user.setPassword(adminPassword);
user.save(user);
site.setName("Admin");
site.setRevisionNumber(1);
site.setProtocol(protocol);
site.setHost("*");
site.setPort(0);
site.setDirectory(path);
site.setAdmin(true);
site.setOnlineDate(new Date());
site.setOfflineDate(null);
site.setComment("Created");
site.save(user);
} catch (ContentException e) {
LOG.error(e.getMessage());
} catch (ContentSecurityException e) {
LOG.error(e.getMessage());
}
}
}