package org.joget.apps.app.web;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.lang.StringEscapeUtils;
import org.joget.apps.app.model.AppDefinition;
import org.joget.apps.app.service.AppService;
import org.joget.apps.app.service.AppUtil;
import org.joget.commons.spring.web.CustomContextLoaderListener;
import org.joget.commons.spring.web.CustomDispatcherServlet;
import org.joget.commons.util.DynamicDataSourceManager;
import org.joget.commons.util.HostManager;
import org.joget.commons.util.LogUtil;
import org.joget.commons.util.ResourceBundleUtil;
import org.joget.commons.util.SecurityUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Servlet to handle first-time database setup and initialization.
*/
public class SetupServlet extends HttpServlet {
/**
* Processes requests for both HTTP <code>GET</code> and <code>POST</code>
* methods.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// check for existing setup, return 403 forbidden if already configured (currentProfile key exists in app_datasource.properties) or virtual hosting is enabled
Properties properties = DynamicDataSourceManager.getProfileProperties();
if (HostManager.isVirtualHostEnabled() || properties == null || properties.containsKey("currentProfile")) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
// handle request
String path = request.getPathInfo();
if (path == null || "/".equals(path)) {
// forward to the setup JSP
request.getRequestDispatcher("/WEB-INF/jsp/setup/setup.jsp").forward(request, response);
} else if ("/init".equals(path)) {
// only allow POST for initialization request
if (!"post".equalsIgnoreCase(request.getMethod())) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return;
}
// get parameters
String jdbcDriver = request.getParameter("jdbcDriver");
String jdbcUrl = request.getParameter("jdbcUrl");
String jdbcFullUrl = request.getParameter("jdbcFullUrl");
String jdbcUser = request.getParameter("jdbcUser");
String jdbcPassword = request.getParameter("jdbcPassword");
String dbType = request.getParameter("dbType");
String dbName = request.getParameter("dbName");
String sampleApps = request.getParameter("sampleApps");
String sampleUsers = request.getParameter("sampleUsers");
if ("custom".equals(dbType)) {
dbName = null;
}
// validate dbName
if (dbName != null) {
dbName = SecurityUtil.validateStringInput(dbName);
}
// create datasource
LogUtil.info(getClass().getName(), "===== Starting Database Setup =====");
boolean success = false;
String message = "";
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
InputStream in = null;
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName(jdbcDriver);
ds.setUrl(jdbcUrl);
ds.setUsername(jdbcUser);
ds.setPassword(jdbcPassword);
try {
// test connection
con = ds.getConnection();
con.setAutoCommit(false);
success = true;
message = ResourceBundleUtil.getMessage("setup.datasource.label.success");
// switch database
if (dbName != null) {
try {
LogUtil.info(getClass().getName(), "Use database " + dbName);
con.setCatalog(dbName);
} catch (SQLException ex) {
// ignore
}
}
// check for existing tables
boolean exists = false;
try {
stmt = con.createStatement();
rs = stmt.executeQuery("SELECT * FROM dir_role");
if (rs.next()) {
exists = true;
LogUtil.info(getClass().getName(), "Database already initialized " + jdbcUrl);
}
} catch (SQLException ex) {
LogUtil.info(getClass().getName(), "Database not yet initialized " + jdbcUrl);
}
if (!exists) {
// get schema file
String schemaFile = null;
if ("oracle".equals(dbType) || jdbcUrl.contains("oracle")) {
schemaFile = "/setup/sql/jwdb-oracle.sql";
} else if ("sqlserver".equals(dbType) || jdbcUrl.contains("sqlserver")) {
schemaFile = "/setup/sql/jwdb-mssql.sql";
} else if ("mysql".equals(dbType) || jdbcUrl.contains("mysql")) {
schemaFile = "/setup/sql/jwdb-mysql.sql";
} else {
throw new SQLException("Unrecognized database type, please setup the datasource manually");
}
if (dbName != null && stmt != null) {
// create database
try {
LogUtil.info(getClass().getName(), "Create database " + dbName);
stmt.executeUpdate("CREATE DATABASE " + dbName);
} catch (SQLException ex) {
// ignore
}
// switch database
LogUtil.info(getClass().getName(), "Use database " + dbName);
con.setCatalog(dbName);
}
// execute schema file
LogUtil.info(getClass().getName(), "Execute schema " + schemaFile);
ScriptRunner runner = new ScriptRunner(con, false, true);
in = getClass().getResourceAsStream(schemaFile);
runner.runScript(new BufferedReader(new InputStreamReader(in)));
}
if ("true".equals(sampleUsers)) {
// create users
String schemaFile = "/setup/sql/jwdb-users.sql";
LogUtil.info(getClass().getName(), "Create users using schema " + schemaFile);
ScriptRunner runner = new ScriptRunner(con, false, true);
in = getClass().getResourceAsStream(schemaFile);
runner.runScript(new BufferedReader(new InputStreamReader(in)));
}
con.commit();
LogUtil.info(getClass().getName(), "Datasource init complete: " + success);
// save profile
String profileName = (dbName != null && !dbName.trim().isEmpty()) ? dbName : "custom";
String jdbcUrlToSave = (jdbcFullUrl != null && !jdbcFullUrl.trim().isEmpty()) ? jdbcFullUrl : jdbcUrl;
LogUtil.info(getClass().getName(), "Save profile " + profileName);
DynamicDataSourceManager.createProfile(profileName);
DynamicDataSourceManager.changeProfile(profileName);
DynamicDataSourceManager.writeProperty("workflowDriver", jdbcDriver);
DynamicDataSourceManager.writeProperty("workflowUrl", jdbcUrlToSave);
DynamicDataSourceManager.writeProperty("workflowUser", jdbcUser);
DynamicDataSourceManager.writeProperty("workflowPassword", jdbcPassword);
// initialize spring application context
ServletContext sc = request.getServletContext();
ServletContextEvent sce = new ServletContextEvent(sc);
CustomContextLoaderListener cll = new CustomContextLoaderListener();
cll.contextInitialized(sce);
DispatcherServlet servlet = CustomDispatcherServlet.getCustomDispatcherServlet();
servlet.init();
if (sampleApps != null) {
// import sample apps
ApplicationContext context = AppUtil.getApplicationContext();
InputStream setupInput = getClass().getResourceAsStream("/setup/setup.properties");
Properties setupProps = new Properties();
try {
setupProps.load(setupInput);
String sampleDelimitedApps = setupProps.getProperty("sample.apps");
StringTokenizer appTokenizer = new StringTokenizer(sampleDelimitedApps, ",");
while (appTokenizer.hasMoreTokens()) {
String appPath = appTokenizer.nextToken();
importApp(context, appPath);
}
} finally {
if (setupInput != null) {
setupInput.close();
}
}
}
LogUtil.info(getClass().getName(), "Profile init complete: " + profileName);
LogUtil.info(getClass().getName(), "===== Database Setup Complete =====");
} catch (Exception ex) {
LogUtil.error(getClass().getName(), null, ex.toString());
success = false;
message = ex.getMessage().replace("'", " ");
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException ex) {
// ignore
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException ex) {
// ignore
}
}
if (con != null) {
try {
con.close();
} catch (SQLException ex) {
// ignore
}
}
try {
ds.close();
} catch (SQLException ex) {
// ignore
}
if (in != null) {
try {
in.close();
} catch (IOException ex) {
// ignore
}
}
}
// send result
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("{\"action\":\"init\",\"result\":\"" + success + "\",\"message\":\"" + StringEscapeUtils.escapeJavaScript(message) + "\"}");
} finally {
out.close();
}
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* Import an app from a specified path.
* @param context
* @param path
*/
protected void importApp(ApplicationContext context, String path) {
LogUtil.info(getClass().getName(), "Import app " + path);
InputStream in = null;
try {
final AppService appService = (AppService)context.getBean("appService");
in = getClass().getResourceAsStream(path);
byte[] fileContent = readInputStream(in);
final AppDefinition appDef = appService.importApp(fileContent);
if (appDef != null) {
TransactionTemplate transactionTemplate = (TransactionTemplate) AppUtil.getApplicationContext().getBean("transactionTemplate");
transactionTemplate.execute(new TransactionCallback<Object>() {
public Object doInTransaction(TransactionStatus ts) {
appService.publishApp(appDef.getId(), null);
return null;
}
});
}
} catch(Exception ex) {
LogUtil.error(getClass().getName(), ex, "Failed to import app " + path);
} finally {
try {
if (in != null) {
in.close();
}
} catch(IOException e) {
}
}
}
/**
* Reads a specified InputStream, returning its contents in a byte array
* @param in
* @return
* @throws IOException
*/
protected byte[] readInputStream(InputStream in) throws IOException {
byte[] fileContent;
ByteArrayOutputStream out = null;
try {
out = new ByteArrayOutputStream();
BufferedInputStream bin = new BufferedInputStream(in);
int len;
byte[] buffer = new byte[4096];
while ((len = bin.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.flush();
fileContent = out.toByteArray();
return fileContent;
} finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
LogUtil.error(getClass().getName(), ex, ex.getMessage());
}
}
}
/**
* Handles the HTTP <code>GET</code> method.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
/**
* Handles the HTTP <code>POST</code> method.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
/**
* Returns a short description of the servlet.
*
* @return a String containing servlet description
*/
@Override
public String getServletInfo() {
return "Servlet to handle first-time database setup and initialization";
}
}