/*
* LiquidSiteServlet.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) 2003-2006 Per Cederberg. All rights reserved.
*/
package org.liquidsite.app.servlet;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.liquidsite.app.install.InstallRequestProcessor;
import org.liquidsite.app.plugin.PluginException;
import org.liquidsite.app.plugin.PluginLoader;
import org.liquidsite.app.template.TemplateException;
import org.liquidsite.app.template.TemplateManager;
import org.liquidsite.core.content.ContentManager;
import org.liquidsite.core.data.DataObjectException;
import org.liquidsite.core.data.DataSource;
import org.liquidsite.core.data.LockPeer;
import org.liquidsite.core.web.MultiPartRequest;
import org.liquidsite.core.web.Request;
import org.liquidsite.util.db.DatabaseConnectionException;
import org.liquidsite.util.db.DatabaseConnector;
import org.liquidsite.util.db.MySQLDatabaseConnector;
import org.liquidsite.util.log.Log;
import org.liquidsite.util.mail.MailTransportException;
import org.liquidsite.util.mail.MailQueue;
/**
* A front controller servlet. This class handles all incoming HTTP
* requests.
*
* @author Per Cederberg, <per at percederberg dot net>
* @version 1.0
*/
public class LiquidSiteServlet extends HttpServlet
implements Application {
/**
* The class logger.
*/
protected static final Log LOG = new Log(LiquidSiteServlet.class);
/**
* The application monitor thread.
*/
private ApplicationMonitor monitor = null;
/**
* The application build and version properties.
*/
private Properties build = new Properties();
/**
* The application configuration.
*/
private Configuration config = null;
/**
* The application plugin loader.
*/
private PluginLoader pluginLoader = null;
/**
* The application database connector.
*/
private DatabaseConnector database = null;
/**
* The application content manager.
*/
private ContentManager contentManager = null;
/**
* The main request processor.
*/
private RequestProcessor processor = null;
/**
* The application online flag.
*/
private boolean online = false;
/**
* Checks if the application is running correctly. This method
* will return true if no major errors have been encountered,
* such as database connections are not working or similar. Note
* that this method may return true even if the application is
* not installed, assuming that the installed has been properly
* launched.
*
* @return true if the application is online and working, or
* false otherwise
*/
public boolean isOnline() {
return online;
}
/**
* Initializes this servlet.
*/
public void init() {
startup();
monitor = new ApplicationMonitor();
}
/**
* Uninitializes this servlet.
*/
public void destroy() {
monitor.stop();
shutdown();
super.destroy();
}
/**
* Starts up the application. This will initialize the
* configuration, the database, and the relevant request
* processor.
*/
public void startup() {
int errors = 0;
File dir;
URL url;
String host;
String name;
String user;
String password;
String str;
int size;
// Initialize configuration
dir = new File(getBaseDir(), "WEB-INF");
config = new Configuration(new File(dir, "config.properties"));
try {
Log.initialize(new File(dir, "logging.properties"));
} catch (IOException e) {
errors++;
LOG.error("couldn't read logging configuration: " +
e.getMessage());
}
// Initialize build information
str = "org/liquidsite/build.properties";
url = getClass().getClassLoader().getResource(str);
try {
build.load(url.openStream());
} catch (IOException e) {
errors++;
LOG.error(e.getMessage());
}
// Initialize database
try {
MySQLDatabaseConnector.loadDriver();
} catch (DatabaseConnectionException e) {
errors++;
LOG.error(e.getMessage());
}
host = config.get(Configuration.DATABASE_HOSTNAME, "");
name = config.get(Configuration.DATABASE_NAME, "");
user = config.get(Configuration.DATABASE_USER, "");
password = config.get(Configuration.DATABASE_PASSWORD, "");
size = config.getInt(Configuration.DATABASE_POOL_SIZE, 0);
database = new MySQLDatabaseConnector(host, name, user, password);
database.setPoolSize(size);
try {
database.loadFunctions(new File(dir, "database.properties"));
} catch (IOException e) {
errors++;
LOG.error("couldn't read database configuration: " +
e.getMessage());
}
// Read configuration table
try {
if (config.isInitialized()) {
config.read(database);
}
} catch (ConfigurationException e) {
errors++;
LOG.error(e.getMessage());
}
// Initialize mail queue
host = config.get(Configuration.MAIL_HOST, "localhost");
user = config.get(Configuration.MAIL_USER, null);
str = config.get(Configuration.MAIL_FROM, null);
MailQueue.getInstance().initialize(host, user, str);
str = config.get(Configuration.MAIL_HEADER, null);
MailQueue.getInstance().setHeader(str);
str = config.get(Configuration.MAIL_FOOTER, null);
MailQueue.getInstance().setFooter(str);
// Initialize content and template managers
str = config.get(Configuration.FILE_DIRECTORY, null);
dir = (str == null) ? null : new File(str);
contentManager = new ContentManager(database, dir, false);
try {
TemplateManager.initialize(getBaseDir(),
getBuildVersion(),
getBuildDate());
} catch (TemplateException e) {
errors++;
LOG.error(e.getMessage());
}
// Initialize plugin loader
pluginLoader = new PluginLoader();
try {
pluginLoader.startup(new File(getBaseDir(), "plugins"));
} catch (PluginException e) {
errors++;
LOG.error(e.getMessage());
}
// Initialize request processor
if (!config.isInitialized()) {
processor = new InstallRequestProcessor(this);
} else {
processor = new DefaultRequestProcessor(this);
}
// Set the online status
online = (errors == 0);
}
/**
* Shuts down the application. This will deinitialize the request
* processor and the database connector.
*/
public void shutdown() {
processor.destroy();
pluginLoader.shutdown();
contentManager.reset();
database.setPoolSize(0);
try {
database.update();
} catch (DatabaseConnectionException ignore) {
// Do nothing
}
}
/**
* Restarts the application. This will perform a partial shutdown
* followed by a startup, flushing and rereading all
* datastructures, such as configuration, database connections,
* and similar.
*/
public void restart() {
shutdown();
startup();
}
/**
* Handles an incoming HTTP GET request.
*
* @param request the HTTP request object
* @param response the HTTP response object
*
* @throws ServletException if the request couldn't be handled by
* this servlet
* @throws IOException if an IO error occured while attempting to
* service this request
*/
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
process(request, response, true);
}
/**
* Handles an incoming HTTP HEAD request.
*
* @param request the HTTP request object
* @param response the HTTP response object
*
* @throws ServletException if the request couldn't be handled by
* this servlet
* @throws IOException if an IO error occured while attempting to
* service this request
*/
protected void doHead(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
process(request, response, false);
}
/**
* Handles an incoming HTTP POST request.
*
* @param request the HTTP request object
* @param response the HTTP response object
*
* @throws ServletException if the request couldn't be handled by
* this servlet
* @throws IOException if an IO error occured while attempting to
* service this request
*/
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
process(request, response, true);
}
/**
* Handles an incoming HTTP request. The response can be sent
* either completely or solely with the response headers.
*
* @param request the HTTP request object
* @param response the HTTP response object
* @param content the complete content response flag
*
* @throws ServletException if the request couldn't be handled by
* this servlet
* @throws IOException if an IO error occured while attempting to
* service this request
*/
private void process(HttpServletRequest request,
HttpServletResponse response,
boolean content)
throws ServletException, IOException {
Request r;
int size;
String str;
// Create request object
str = request.getContentType();
if (str != null && str.startsWith("multipart")) {
try {
str = config.get(Configuration.UPLOAD_DIRECTORY, "/tmp");
size = config.getInt(Configuration.UPLOAD_MAX_SIZE, 10000000);
r = new MultiPartRequest(getServletContext(),
request,
response,
str,
size);
} catch (ServletException e) {
LOG.warning("parse error on multi-part request", e);
response.sendError(
HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE,
e.getMessage());
return;
}
} else {
r = new Request(getServletContext(), request, response);
}
// TODO: handle offline state gracefully
// Process request
LOG.info("Incoming request: " + r);
try {
processor.process(r);
} catch (RequestException e) {
LOG.info("Erroneous request: " + r + ", Message: " +
e.getMessage());
processError(r, e);
}
try {
if (r.hasResponse()) {
r.commit(getServletContext(), content);
} else {
LOG.info("Unhandled request: " + r);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
} catch (IOException e) {
LOG.info("IO error when processing request: " + r +
", Message: " + e.getMessage());
}
r.dispose();
}
/**
* Processes a request error.
*
* @param request the request object
* @param error the request error
*/
private void processError(Request request, RequestException error) {
String text;
switch (error.getCode()) {
case 401:
request.setAttribute("heading", "Authentication Required (401)");
text = "You must be logged in to access the document " +
"or resource located at this URL.";
break;
case 403:
request.setAttribute("heading", "Access Forbidden (403)");
text = "You don't have permission to access the document " +
"or resource located at this URL.";
break;
case 404:
request.setAttribute("heading", "Document Not Found (404)");
text = "The document or resource pointed to by this URL " +
"doesn't exist. This may be caused by the document " +
"having been moved.";
break;
default:
text = "Internal Error (" + error.getCode() + ")";
request.setAttribute("heading", text);
text = "An internal error has ocurred. " +
"Please try again later.";
}
request.setAttribute("text", text);
try {
processor.sendError(request, error.getCode());
} catch (TemplateException e) {
text = "Error: " + e.getMessage();
request.sendError(error.getCode(), "text/plain", text);
}
}
/**
* Returns the application build version number.
*
* @return the application build version number
*/
public String getBuildVersion() {
return build.getProperty("build.version", "<unknown>");
}
/**
* Returns the application build date.
*
* @return the application build date
*/
public String getBuildDate() {
return build.getProperty("build.date", "<unknown>");
}
/**
* Returns the base application directory. This is the directory
* containing all the application files (i.e. the corresponding
* webapps directory).
*
* @return the base application directory
*/
public File getBaseDir() {
return new File(getServletContext().getRealPath("/"));
}
/**
* Returns the application configuration. The object returned by
* this method will not change, unless a reset is made, but the
* parameter values in the configuration may be modified.
*
* @return the application configuration
*/
public Configuration getConfig() {
return config;
}
/**
* Returns the application database connector. The object
* returned by this method will not change, unless a reset is
* made.
*
* @return the application database connector
*/
public DatabaseConnector getDatabase() {
return database;
}
/**
* Returns the application content manager. The object returned
* by this method will not change, unless a reset is made.
*
* @return the application content manager
*/
public ContentManager getContentManager() {
return contentManager;
}
/**
* An application monitor thread. This thread call the database
* update method regularly, and performs other monitoring tasks.
*
* @author Per Cederberg, <per at percederberg dot net>
* @version 1.0
*/
private class ApplicationMonitor implements Runnable {
/**
* The loop delay in milliseconds. The thread will wait for
* this period of time in each pass of the monitor loop.
*/
private static final int LOOP_DELAY = 1000;
/**
* The stop timeout in milliseconds.
*/
private static final int STOP_TIMEOUT = 3000;
/**
* The database update threshold. This is the number of loop
* iterations to skip in between calls to update() in the
* database connector.
*/
private static final int DATABASE_UPDATE_THRESHOLD = 60;
/**
* The mail processing threshold. This is the number of loop
* iterations to skip in between calls to process() in the
* mail queue.
*/
private static final int MAIL_PROCESS_THRESHOLD = 5;
/**
* The mail error threshold. This is the number of loop
* iterations to skip mail processing upon a mail transport
* error.
*/
private static final int MAIL_ERROR_THRESHOLD = 60;
/**
* The lock removal threshold. This is the number of loop
* iterations to skip in between calls to delete the outdated
* locks in the database.
*/
private static final int LOCK_REMOVE_THRESHOLD = 3600;
/**
* The alive flag. If this flag is set to false, the thread
* is supposed to die.
*/
private boolean alive = true;
/**
* The database update counter. This counter is increased
* every once in a while and is used to determine when the
* database update method should be called.
*/
private int databaseCounter = 0;
/**
* The mail process counter. This counter is increased every
* once in a while and is used to determine when the mail
* process method should be called.
*/
private int mailCounter = 0;
/**
* The lock removal counter. This counter is increased every
* once in a while and is used to determine when the lock
* remove method should be called.
*/
private int lockCounter = 0;
/**
* Creates a new application monitor. This will also create
* and start the actual thread running this monitor.
*/
public ApplicationMonitor() {
Thread thread;
thread = new Thread(this);
thread.start();
}
/**
* Runs the thread. This method is not supposed to be called
* directly, but rather by the monitor thread.
*/
public synchronized void run() {
while (alive) {
if (getConfig() == null || !getConfig().isInitialized()) {
// Do nothing
} else if (!isOnline()) {
restart();
} else {
runMonitorPass();
}
try {
wait(LOOP_DELAY);
} catch (InterruptedException ignore) {
// Do nothing
}
}
notifyAll();
}
/**
* Runs a single pass in the monitoring loop. Most of the
* time, this will only result in updated counters. Once the
* respective thresholds have been exceeded however, the
* corresponding update method will be called. This method
* will not be called if the system hasn't been configured and
* properly initialized.
*/
private void runMonitorPass() {
// Update database
databaseCounter++;
if (databaseCounter >= DATABASE_UPDATE_THRESHOLD) {
databaseCounter = 0;
try {
getDatabase().update();
} catch (DatabaseConnectionException e) {
LOG.error(e.getMessage());
}
}
// Process mail
mailCounter++;
if (mailCounter >= MAIL_PROCESS_THRESHOLD) {
mailCounter = 0;
try {
MailQueue.getInstance().process();
} catch (MailTransportException e) {
LOG.error(e.getMessage());
mailCounter = MAIL_PROCESS_THRESHOLD -
MAIL_ERROR_THRESHOLD;
}
}
// Remove outdated locks
lockCounter++;
if (lockCounter >= LOCK_REMOVE_THRESHOLD) {
lockCounter = 0;
try {
LockPeer.doDeleteOutdated(new DataSource(getDatabase()));
} catch (DataObjectException e) {
LOG.error(e.getMessage());
}
}
}
/**
* Stops the monitor thread. Once the thread is stopped, it
* cannot be started again. This method will not return until
* the monitor thread has stopped, or a timeout has passed.
*/
public synchronized void stop() {
alive = false;
try {
notifyAll();
wait(STOP_TIMEOUT);
} catch (InterruptedException ignore) {
// Do nothing
}
}
}
}