package org.gomba;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import org.gomba.utils.servlet.ServletParameterUtils;
/**
* Base class for Servlets that render data accessed via JDBC.
* <p>
* Context params:
* <dl>
* <dt>org.gomba.dataSource</dt>
* <dd>The default JNDI data source. If not specified, the data source must be
* set in the <code>data-source</code> servlet init-param. (Optional)</dd>
* </dl>
* <dl>
* <dt>org.gomba.debug</dt>
* <dd>The default debug mode. To enable debug logging set the value to 'true'.
* (Optional)</dd>
* </dl>
* </p>
*
* <p>
* This servlet inherits the init-params of
* {@link org.gomba.TransactorAbstractServlet}
* </p>
*
* </p>
* Init params:
* <dl>
* <dt>data-source</dt>
* <dd>The JNDI data source. The default data source may be specified in the
* <code>org.gomba.dataSource</code> context-param. (Optional)</dd>
* <dt>response-headers</dt>
* <dd>The HTTP response headers in Java Properties format. May contain ${}
* parameters in the property values. It is highly recommended to set this
* init-param, especially for cache headers like "Last-modified", "Expires" etc.
* (Optional)</dd>
* <dt>http-status</dt>
* <dd>The HTTP status code to send when the request is successfully processed.
* Defaults to 200 (OK). (Optional)</dd>
* <dt>multipart-size-threshold</dt>
* <dd>The size threshold beyond which files are written directly to disk.
* Defaut: 64K (Optional)</dd>
* <dt>multipart-max-size</dt>
* <dd>The maximum allowed upload size. If negative, there is no maximum.
* Defaut: 128K (Optional)</dd>
* <dt>multipart-repository-path</dt>
* <dd>The location used to temporarily store files that are larger than the
* configured size threshold. Defaut: the system default temp directory, as
* returned by System.getProperty("java.io.tmpdir") (Optional)</dd>
* <dt>debug</dt>
* <dd>Log debug mode. To enable debug logging set the value to 'true'. The
* default debug mode may be specified in the <code>org.gomba.debug</code>
* context-param. (Optional)</dd>
* </dl>
* </p>
*
* @author Flavio Tordini
* @version $Id: AbstractServlet.java,v 1.2 2004/06/28 16:12:05 flaviotordini
* Exp $
*/
abstract class AbstractServlet extends TransactorAbstractServlet {
private final static String CONTEXT_PARAM_DATA_SOURCE = "org.gomba.dataSource";
protected final static String CONTEXT_PARAM_DEBUG = "org.gomba.debug";
protected final static String INIT_PARAM_DEBUGMODE = "debug";
private final static String INIT_PARAM_DATA_SOURCE = "data-source";
private final static String INIT_PARAM_RESPONSE_HEADERS = "response-headers";
private final static String INIT_PARAM_HTTP_STATUS = "http-status";
private final static String INIT_PARAM_MULTIPART_SIZE_THRESHOLD = "multipart-size-threshold";
private final static String INIT_PARAM_MULTIPART_MAX_SIZE = "multipart-max-size";
private final static String INIT_PARAM_MULTIPART_REPOSITORY_PATH = "multipart-repository-path";
private final static String APP_NAME = "Gomba";
static {
// get version
InputStream is = AbstractServlet.class.getClassLoader()
.getResourceAsStream("org/gomba/version.properties");
Properties version = new Properties();
try {
version.load(is);
is.close();
} catch (IOException ioe) {
System.err.println(AbstractServlet.class.getName()
+ ": Cannot load version information.");
ioe.printStackTrace();
}
String major = version.getProperty("major");
String minor = version.getProperty("minor");
String micro = version.getProperty("micro");
System.out.println(APP_NAME + ' ' + major + '.' + minor + '.' + micro
+ " http://gomba.sourceforge.net/");
}
/**
* <code>true</code> if debug logging is turned on.
*/
private boolean debugMode;
/**
* The data source to query.
*/
private DataSource dataSource;
/**
* Map HTTP response header name to an {@link Expression}. May be null.
*/
private Map responseHeaders;
/**
* The HTTP status code when the request is successfully processed.
*/
private int httpStatusCode = HttpServletResponse.SC_OK;
private int multipartSizeThreshold = 1024 * 64;
private int multipartMaxSize = 1024 * 128;
private String multipartRepositoryPath;
/**
* @return <code>true</code> if debug is enabled.
*/
protected static boolean getDebugMode(ServletConfig config) {
boolean debugMode;
String debugStr = config.getInitParameter(INIT_PARAM_DEBUGMODE);
if (debugStr != null) {
debugMode = Boolean.valueOf(debugStr).booleanValue();
} else {
debugMode = Boolean.valueOf(
config.getServletContext().getInitParameter(
CONTEXT_PARAM_DEBUG)).booleanValue();
}
return debugMode;
}
/**
* @return The JDBC DataSource for this servlet instance.
*/
protected static DataSource getDataSource(ServletConfig config)
throws ServletException {
DataSource dataSource;
String dataSourceName = config.getInitParameter(INIT_PARAM_DATA_SOURCE);
if (dataSourceName == null) {
dataSourceName = config.getServletContext().getInitParameter(
CONTEXT_PARAM_DATA_SOURCE);
}
if (dataSourceName == null) {
throw new ServletException(
"JDBC data source not specified. Please set the '"
+ CONTEXT_PARAM_DATA_SOURCE
+ "' context-param or the '"
+ INIT_PARAM_DATA_SOURCE + "' servlet init-param.");
}
try {
Context initContext = new InitialContext();
Context envContext = (Context) initContext.lookup("java:/comp/env");
dataSource = (DataSource) envContext.lookup(dataSourceName);
} catch (NamingException ne) {
throw new ServletException(
"Error getting reference to data source.", ne);
}
return dataSource;
}
/**
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// debug mode
this.debugMode = getDebugMode(config);
// get the JNDI data source
this.dataSource = getDataSource(config);
// parse the response header settings
try {
this.responseHeaders = parseResponseHeaderSettings(config
.getInitParameter(INIT_PARAM_RESPONSE_HEADERS));
} catch (Exception e) {
throw new ServletException(
"Error parsing response header settings.", e);
}
// the HTTP status code to send when everything's alright.
try {
String httpStatusCodeString = config
.getInitParameter(INIT_PARAM_HTTP_STATUS);
if (httpStatusCodeString != null) {
this.httpStatusCode = Integer.parseInt(httpStatusCodeString);
}
} catch (NumberFormatException e) {
throw new ServletException("Error parsing "
+ INIT_PARAM_HTTP_STATUS, e);
}
// the multipart size threshold between memory and disk based storage
try {
String multipartSizeThresholdString = config
.getInitParameter(INIT_PARAM_MULTIPART_SIZE_THRESHOLD);
if (multipartSizeThresholdString != null) {
this.multipartSizeThreshold = Integer
.parseInt(multipartSizeThresholdString);
}
} catch (NumberFormatException e) {
throw new ServletException("Error parsing "
+ INIT_PARAM_MULTIPART_SIZE_THRESHOLD, e);
}
// the multipart max size
try {
String multipartMaxSizeString = config
.getInitParameter(INIT_PARAM_MULTIPART_MAX_SIZE);
if (multipartMaxSizeString != null) {
this.multipartMaxSize = Integer
.parseInt(multipartMaxSizeString);
}
} catch (NumberFormatException e) {
throw new ServletException("Error parsing "
+ INIT_PARAM_MULTIPART_MAX_SIZE, e);
}
// The multipart repository path
this.multipartRepositoryPath = config
.getInitParameter(INIT_PARAM_MULTIPART_REPOSITORY_PATH);
}
/**
* Get a Query instance for our QueryDefinition.
*/
protected Query getQuery(QueryDefinition requestQueryDefinition,
ParameterResolver parameterResolver) throws ServletException {
// build the Query
final Query query;
try {
query = new Query(requestQueryDefinition, parameterResolver);
// debug queries
if (this.debugMode) {
log("Query: " + query);
}
} catch (Exception e) {
throw new ServletException("Error building query.", e);
}
return query;
}
/**
* Ensure the resultset cursor is positioned on a row.
*
* @return true if the ResultSet is positioned on a row. if the ResultSet
* contains no rows return false.
*/
protected final static boolean maybeMoveCursor(ResultSet resultSet)
throws SQLException {
if (resultSet.isBeforeFirst()) {
// we're before the first row
if (!resultSet.next()) {
// 0 rows in this result set
return false;
}
} else if (resultSet.getRow() == 0) {
// no current row (we should be after the last row)
return false;
}
return true;
}
/**
* Parse the response header settings.
*
* @param settings
* Response header in Java Properties format.
* @return Map the response header name to its dynamic value. Mapping is
* String to {@link Expression}
* @throws Exception
*/
private final static Map parseResponseHeaderSettings(String settings)
throws Exception {
if (settings == null) {
return null;
}
Properties headers = new Properties();
InputStream inputStream = new ByteArrayInputStream(settings.getBytes());
try {
headers.load(inputStream);
} finally {
inputStream.close();
}
final Map responseHeaders = new HashMap(headers.size(), 1);
Enumeration headerNames = headers.propertyNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
String headerValue = headers.getProperty(headerName);
responseHeaders.put(headerName, new Expression(headerValue));
}
// make responseHeaders immutable
return Collections.unmodifiableMap(responseHeaders);
}
protected final static void setResponseHeader(HttpServletResponse response,
String headerName, Object headerValue) {
if (headerValue instanceof java.util.Date) {
response.setDateHeader(headerName, ((Date) headerValue).getTime());
} else if (headerValue instanceof java.lang.Integer) {
response.setIntHeader(headerName, ((Integer) headerValue)
.intValue());
} else {
response.setHeader(headerName, headerValue.toString());
}
}
protected final void setResponseHeaders(HttpServletResponse response,
ParameterResolver parameterResolver) throws ServletException {
if (this.responseHeaders != null) {
try {
for (Iterator i = this.responseHeaders.entrySet().iterator(); i
.hasNext();) {
Map.Entry mapEntry = (Map.Entry) i.next();
String headerName = (String) mapEntry.getKey();
Object headerValue = ((Expression) mapEntry.getValue())
.replaceParameters(parameterResolver);
setResponseHeader(response, headerName, headerValue);
}
} catch (Exception e) {
throw new ServletException("Error setting response headers.", e);
}
}
}
/**
* Include a resource (a JSP) and capture its response body.
*
* @param queryResource
* The resource to include
* @param request
* the HTTP request
* @param response
* the HTTP response
* @return The captured resource response body.
*/
protected final String getDynamicQuery(String queryResource,
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// get the dispatcher
RequestDispatcher dispatcher = getServletContext()
.getRequestDispatcher(queryResource);
if (dispatcher == null) {
throw new ServletException(
"Cannot get a RequestDispatcher for path: " + queryResource);
}
// set some useful information in the request
request.setAttribute("path", ServletParameterUtils
.getPathElements(request));
// wrap the request in order to supply extra functionality (multiparm
// form handling)
CustomHttpServletRequestWrapper requestWrapper = new CustomHttpServletRequestWrapper(
request, this.multipartSizeThreshold, this.multipartMaxSize,
this.multipartRepositoryPath);
// wrap our response, so we can capture the body (and the included
// resource cannot mess with it)
CustomHttpServletResponseWrapper responseWrapper = new CustomHttpServletResponseWrapper(
response);
// include the resource
dispatcher.include(requestWrapper, responseWrapper);
// and get the body
String sql = responseWrapper.getBody();
if (sql == null) {
throw new ServletException("Can't happen! Dynamic query is null. "
+ queryResource);
}
// trim SQL to minimize in order to make it more readable
sql = sql.trim();
return sql;
}
/**
* @return Returns the dataSource.
*/
protected DataSource getDataSource() {
return this.dataSource;
}
/**
* @return Returns the responseHeaders.
*/
protected final Map getResponseHeaders() {
return this.responseHeaders;
}
/**
* @return Returns the debugMode.
*/
protected final boolean isDebugMode() {
return this.debugMode;
}
/**
* @return Returns the httpStatusCode.
*/
protected final int getHttpStatusCode() {
return this.httpStatusCode;
}
/**
* Utility method that returns profiling/debugging info for the current
* request.
*
* @param request The current HTTP request
* @param startTime The time when the processing has begun.
*/
protected static String getProfilingMessage(HttpServletRequest request,
long startTime) {
final long processingTime = System.currentTimeMillis() - startTime;
StringBuffer msg = new StringBuffer(request.getMethod());
msg.append(' ');
msg.append(request.getRequestURI());
String qs = request.getQueryString();
if (qs != null) {
msg.append('?');
msg.append(qs);
}
msg.append(" processed in ");
msg.append(processingTime);
msg.append("ms");
return msg.toString();
}
}