blob: 4d937ce4e3e45dd55f3530fd5862608e02bbbdb8 [file] [log] [blame]
/*
* Copyright 2004-2008 Malcolm A. Edgar
*
* Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sf.click;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import net.sf.click.util.ClickLogger;
import net.sf.click.util.ClickUtils;
import net.sf.click.util.ErrorPage;
import net.sf.click.util.ErrorReport;
import net.sf.click.util.Format;
import net.sf.click.util.HtmlStringBuffer;
import net.sf.click.util.PageImports;
import net.sf.click.util.PropertyUtils;
import net.sf.click.util.RequestTypeConverter;
import net.sf.click.util.SessionMap;
import ognl.Ognl;
import ognl.OgnlException;
import ognl.TypeConverter;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.io.VelocityWriter;
import org.apache.velocity.util.SimplePool;
/**
* Provides the Click application HttpServlet.
* <p/>
* Generally developers will simply configure the <tt>ClickServlet</tt> and
* will not use it directly in their code. For a Click web application to
* function the <tt>ClickServlet</tt> must be configured in the web
* application's <tt>/WEB-INF/web.xml</tt> file. A simple web application which
* maps all <tt>*.htm</tt> requests to a ClickServlet is provided below.
*
* <pre class="codeConfig">
* &lt;web-app&gt;
* &lt;servlet&gt;
* &lt;servlet-name&gt;<font color="blue">click-servlet</font>&lt;/servlet-name&gt;
* &lt;servlet-class&gt;<font color="red">net.sf.click.ClickServlet</font>&lt;/servlet-class&gt;
* &lt;load-on-startup&gt;<font color="red">0</font>&lt;/load-on-startup&gt;
* &lt;/servlet&gt;
* &lt;servlet-mapping&gt;
* &lt;servlet-name&gt;<font color="blue">click-servlet</font>&lt;/servlet-name&gt;
* &lt;url-pattern&gt;<font color="red">*.htm</font>&lt;/url-pattern&gt;
* &lt;/servlet-mapping&gt;
* &lt;/web-app&gt; </pre>
*
* By default the <tt>ClickServlet</tt> will attempt to load an application
* configuration file using the path: &nbsp; <tt>/WEB-INF/click.xml</tt>
*
* <h4>Servlet Mapping</h4>
* By convention all Click page templates should have a .htm extension, and
* the ClickServlet should be mapped to process all *.htm URL requests. With
* this convention you have all the static HTML pages use a .html extension
* and they will not be processed as Click pages.
*
* <h4>Load On Startup</h4>
* Note you should always set <tt>load-on-startup</tt> element to be 0 so the
* servlet is initialized when the server is started. This will prevent any
* delay for the first client which uses the application.
* <p/>
* The <tt>ClickServlet</tt> performs as much work as possible at startup to
* improve performance later on. The Click start up and caching strategy is
* configured with the Click application mode in the "<tt>click.xml</tt>" file.
* See the User Guide for information on how to configure the application mode.
*
* <a name="app-reloading"><h4>Application Reloading</h4></a>
* The <tt>ClickServlet</tt> supports the ability to reload the click
* application "<tt>click.xml</tt>" without having to restart the entire web
* application.
* <p/>
* To reload the application simply make the GET request
* <font color="blue">/click/reload-app.htm</font> while in the role
* <font color="red">click-admin</font>.
* <p/>
* To enable application reloading you need to configure the servlet
* init parameter <tt>app-reloadable</tt> as true, and secure
* the path <tt>/click/reload-app.htm</tt> with the role <tt>click-admin</tt>.
* If the user making a GET request to this path is not in this role the
* ClickServlet will return the page not found template.
*
* <pre class="codeConfig">
* &lt;web-app&gt;
* &lt;servlet&gt;
* &lt;servlet-name&gt;click-servlet&lt;/servlet-name&gt;
* &lt;servlet-class&gt;net.sf.click.ClickServlet&lt;/servlet-class&gt;
* &lt;init-param&gt;
* &lt;param-name&gt;<font color="blue">app-reloadable</font>&lt;/param-name&gt;
* &lt;param-value&gt;<font color="red">true</font>&lt;/param-value&gt;
* &lt;/init-param&gt;
* &lt;load-on-startup&gt;0&lt;/load-on-startup&gt;
* &lt;/servlet&gt;
*
* &lt;servlet-mapping&gt;
* &lt;servlet-name&gt;click-servlet&lt;/servlet-name&gt;
* &lt;url-pattern&gt;*.htm&lt;/url-pattern&gt;
* &lt;/servlet-mapping&gt;
*
* &lt;security-constraint&gt;
* &lt;web-resource-collection&gt;
* &lt;web-resource-name&gt;click-admin&lt;/web-resource-name&gt;
* &lt;url-pattern&gt;<font color="blue">/click/reload-app.htm</font>&lt;/url-pattern&gt;
* &lt;/web-resource-collection&gt;
* &lt;auth-constraint&gt;
* &lt;role-name&gt;<font color="red">click-admin</font>&lt;/role-name&gt;
* &lt;/auth-constraint&gt;
* &lt;/security-constraint&gt;
*
* &lt;login-config&gt;
* &lt;auth-method&gt;DIGEST&lt;/auth-method&gt;
* &lt;realm-name&gt;MyCorp&lt;/realm-name&gt;
* &lt;/login-config&gt;
*
* &lt;security-role&gt;
* &lt;role-name&gt;<font color="red">click-admin</font>&lt;/role-name&gt;
* &lt;/security-role&gt;
* &lt;/web-app&gt; </pre>
*
* @author Malcolm Edgar
*/
public class ClickServlet extends HttpServlet {
// -------------------------------------------------------------- Constants
private static final long serialVersionUID = 1L;
private static final String APPLICAION_RELOADED_MSG =
"<html><head>"
+ "<style type='text/css'>body{font-family:Arial;}</style></head>"
+ "<body><h2>Application Reloaded</h2></body></html>";
/** The Velocity writer buffer size. */
private static final int WRITER_BUFFER_SIZE = 32 * 1024;
/**
* The click application is reloadable flag servlet init parameter name:
* &nbsp; "<tt>app-reloadable</tt>".
*/
protected final static String APP_RELOADABLE = "app-reloadable";
/**
* The forwarded request marker attribute: &nbsp; "<tt>click-forward</tt>".
*/
protected final static String CLICK_FORWARD = "click-forward";
/**
* The Page to forward to request attribute: &nbsp; "<tt>click-page</tt>".
*/
protected final static String FORWARD_PAGE = "forward-page";
// ------------------------------------------------------ Instance Varables
/** The servlet logger. */
protected ClickLogger logger;
/** The click application is reloadable flag. */
protected boolean reloadable = false;
/** The request parameters OGNL type converter. */
protected TypeConverter typeConverter;
/** Cache of velocity writers. */
protected SimplePool writerPool;
/** The click application. */
ClickApp clickApp;
/** The click service proxy. */
ClickService clickService;
// --------------------------------------------------------- Public Methods
/**
* Initialize the Click servlet and the Velocity runtime.
*
* @see javax.servlet.GenericServlet#init()
*
* @throws ServletException if the click app could not be initialized
*/
public void init() throws ServletException {
try {
// Dereference any allocated objects
clickApp = null;
writerPool = null;
// Determine whether the click application is reloadable
reloadable =
"true".equalsIgnoreCase(getInitParameter(APP_RELOADABLE));
// Initialize the click application.
ClickApp newClickApp = new ClickApp();
newClickApp.setServletContext(getServletContext());
newClickApp.init(createClickLogger());
clickService = new ClickService(this);
logger = newClickApp.getLogger();
// Initialise the Cache of velocity writers.
SimplePool newWriterPool = new SimplePool(40);
// Set the new ClickApp and writer pool
clickApp = newClickApp;
// Create the Velocity writer pool
writerPool = newWriterPool;
if (logger.isInfoEnabled()) {
logger.info("initialized in " + clickApp.getModeValue()
+ " mode");
}
} catch (Throwable e) {
e.printStackTrace();
String msg = "error initializing throwing "
+ "javax.servlet.UnavailableException";
log(msg, e);
throw new UnavailableException(e.toString());
}
}
//---------------------------------------------- protected methods
/**
* Handle HTTP GET requests. This method will delegate the request to
* {@link #handleRequest(HttpServletRequest, HttpServletResponse, boolean)}.
*
* @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse)
*
* @param request the servlet request
* @param response the servlet response
* @throws ServletException if click app has not been initialized
* @throws IOException if an I/O error occurs
*/
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
ClickLogger.setInstance(logger);
ensureAppInitialized();
if (ifAuthorizedReloadRequest(request)) {
reloadClickApp(request, response);
} else {
handleRequest(request, response, false);
}
}
/**
* Handle HTTP POST requests. This method will delegate the request to
* {@link #handleRequest(HttpServletRequest, HttpServletResponse, boolean)}.
*
* @see HttpServlet#doPost(HttpServletRequest, HttpServletResponse)
*
* @param request the servlet request
* @param response the servlet response
* @throws ServletException if click app has not been initialized
* @throws IOException if an I/O error occurs
*/
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
ClickLogger.setInstance(logger);
ensureAppInitialized();
handleRequest(request, response, true);
}
/**
* Handle the given servlet request and render the results to the
* servlet response.
* <p/>
* If an exception occurs within this method the exception will be delegated
* to:
* <p/>
* {@link #handleException(HttpServletRequest, HttpServletResponse, boolean, Throwable, Class)}
*
* @param request the servlet request to process
* @param response the servlet response to render the results to
* @param isPost determines whether the request is a POST
*/
protected void handleRequest(HttpServletRequest request,
HttpServletResponse response, boolean isPost) {
long startTime = System.currentTimeMillis();
Context context = createContext(request, response, isPost);
// Bind context to current thread
Context.pushThreadLocalContext(context);
if (logger.isDebugEnabled()) {
HtmlStringBuffer buffer = new HtmlStringBuffer(200);
buffer.append(request.getMethod());
if (ServletFileUpload.isMultipartContent(request)) {
buffer.append(" (multipart) ");
} else {
buffer.append(" ");
}
buffer.append(request.getRequestURL());
logger.debug(buffer);
}
Page page = null;
try {
page = createPage(request);
if (page.isStateful()) {
synchronized (page) {
processPage(page);
processPageOnDestroy(page, startTime);
// Mark page as already destroyed for finally block
page = null;
}
} else {
processPage(page);
}
} catch (Exception e) {
Class pageClass =
clickApp.getPageClass(ClickUtils.getResourcePath(request));
handleException(request, response, isPost, e, pageClass);
} catch (ExceptionInInitializerError eiie) {
Throwable cause = eiie.getException();
cause = (cause != null) ? cause : eiie;
Class pageClass =
clickApp.getPageClass(ClickUtils.getResourcePath(request));
handleException(request, response, isPost, cause, pageClass);
} finally {
try {
if (page != null) {
if (page.isStateful()) {
synchronized (page) {
processPageOnDestroy(page, startTime);
}
} else {
processPageOnDestroy(page, startTime);
}
}
} finally {
Context.popThreadLocalContext();
ClickLogger.setInstance(null);
}
}
}
/**
* Provides the application exception handler. The application exception
* will be delegated to the configured error page. The default error page is
* {@link ErrorPage} and the page template is "click/error.htm" <p/>
* Applications which wish to provide their own customized error handling
* must subclass ErrorPage and specify their page in the
* "/WEB-INF/click.xml" application configuration file. For example:
*
* <pre class="codeConfig">
* &lt;page path=&quot;&lt;span class=&quot;navy&quot;&gt;click/error.htm&lt;/span&gt;&quot; classname=&quot;&lt;span class=&quot;maroon&quot;&gt;com.mycorp.util.ErrorPage&lt;/span&gt;&quot;/&gt;
* </pre>
*
* If the ErrorPage throws an exception, it will be logged as an error and
* then be rethrown nested inside a RuntimeException.
*
* @param request the servlet request with the associated error
* @param response the servlet response
* @param isPost boolean flag denoting the request method is "POST"
* @param exception the error causing exception
* @param pageClass the page class with the error
*/
protected void handleException(HttpServletRequest request,
HttpServletResponse response, boolean isPost, Throwable exception,
Class pageClass) {
if (exception instanceof ParseErrorException == false) {
logger.error("handleException: ", exception);
}
ErrorPage finalizeRef = null;
try {
final ErrorPage errorPage =
(ErrorPage) clickApp.getErrorPageClass().newInstance();
finalizeRef = errorPage;
errorPage.setError(exception);
if (errorPage.getFormat() == null) {
errorPage.setFormat(clickApp.getFormat());
}
errorPage.setHeaders(clickApp.getPageHeaders(ClickApp.ERROR_PATH));
errorPage.setMode(clickApp.getModeValue());
errorPage.setPageClass(pageClass);
errorPage.setPath(ClickApp.ERROR_PATH);
processPageFields(errorPage, new FieldCallback() {
public void processField(String fieldName, Object fieldValue) {
if (fieldValue instanceof Control) {
Control control = (Control) fieldValue;
if (control.getName() == null) {
control.setName(fieldName);
}
if (!errorPage.getModel().containsKey(control.getName())) {
errorPage.addControl(control);
}
}
}
});
if (errorPage.isStateful()) {
synchronized (errorPage) {
processPage(errorPage);
processPageOnDestroy(errorPage, 0);
// Mark page as already destroyed for finally block
finalizeRef = null;
}
} else {
processPage(errorPage);
}
} catch (Exception ex) {
String message =
"handleError: " + ex.getClass().getName()
+ " thrown while handling " + exception.getClass().getName()
+ ". Now throwing RuntimeException.";
logger.error(message, ex);
throw new RuntimeException(ex);
} finally {
if (finalizeRef != null) {
if (finalizeRef.isStateful()) {
synchronized (finalizeRef) {
processPageOnDestroy(finalizeRef, 0);
}
} else {
processPageOnDestroy(finalizeRef, 0);
}
}
}
}
/**
* Process the given page invoking its "on" event callback methods
* and directing the response. This method does not invoke the "onDestroy()"
* callback method.
*
* @param page the Page to process
* @throws Exception if an error occurs
*/
protected void processPage(Page page) throws Exception {
final HttpServletRequest request = page.getContext().getRequest();
final HttpServletResponse response = page.getContext().getResponse();
final boolean isPost = page.getContext().isPost();
// Support direct access of click-error.htm
if (page instanceof ErrorPage) {
ErrorPage errorPage = (ErrorPage) page;
errorPage.setMode(clickApp.getModeValue());
}
boolean continueProcessing = page.onSecurityCheck();
String tracePrefix = null;
if (logger.isTraceEnabled()) {
tracePrefix = page.getClass().getName();
tracePrefix = tracePrefix.substring(tracePrefix.lastIndexOf('.') + 1);
tracePrefix = " invoked: " + tracePrefix;
logger.trace(tracePrefix + ".onSecurityCheck() : " + continueProcessing);
}
if (continueProcessing) {
page.onInit();
if (logger.isTraceEnabled()) {
logger.trace(tracePrefix + ".onInit()");
}
if (page.hasControls()) {
List controls = page.getControls();
for (int i = 0, size = controls.size(); i < size; i++) {
Control control = (Control) controls.get(i);
control.onInit();
if (logger.isTraceEnabled()) {
String controlClassName = control.getClass().getName();
controlClassName = controlClassName.substring(controlClassName.lastIndexOf('.') + 1);
String msg = " invoked: '" + control.getName()
+ "' " + controlClassName + ".onInit()";
logger.trace(msg);
}
}
}
// Make sure dont process a forwarded request
if (page.hasControls() && !page.getContext().isForward()) {
List controls = page.getControls();
for (int i = 0, size = controls.size(); i < size; i++) {
Control control = (Control) controls.get(i);
continueProcessing = control.onProcess();
if (logger.isTraceEnabled()) {
String controlClassName = control.getClass().getName();
controlClassName = controlClassName.substring(controlClassName.lastIndexOf('.') + 1);
String msg = " invoked: '" + control.getName() + "' "
+ controlClassName + ".onProcess() : " + continueProcessing;
logger.trace(msg);
}
if (!continueProcessing) {
break;
}
}
}
if (continueProcessing) {
if (isPost) {
page.onPost();
if (logger.isTraceEnabled()) {
logger.trace(tracePrefix + ".onPost()");
}
} else {
page.onGet();
if (logger.isTraceEnabled()) {
logger.trace(tracePrefix + ".onGet()");
}
}
page.onRender();
if (logger.isTraceEnabled()) {
logger.trace(tracePrefix + ".onRender()");
}
if (page.hasControls()) {
List controls = page.getControls();
for (int i = 0, size = controls.size(); i < size; i++) {
Control control = (Control) controls.get(i);
control.onRender();
if (logger.isTraceEnabled()) {
String controlClassName = control.getClass().getName();
controlClassName = controlClassName.substring(controlClassName.lastIndexOf('.') + 1);
String msg = " invoked: '" + control.getName()
+ "' " + controlClassName + ".onRender()";
logger.trace(msg);
}
}
}
}
}
if (StringUtils.isNotBlank(page.getRedirect())) {
String url = page.getRedirect();
if (url.charAt(0) == '/') {
url = request.getContextPath() + url;
// Check for two scenarios, one without parameters and one with:
// #1. /context/my-page.jsp
// #2. /context/my-page.jsp?param1=value&param2=other-page.jsp
if (url.endsWith(".jsp")) {
url = StringUtils.replaceOnce(url, ".jsp", ".htm");
} else if (url.indexOf(".jsp?") >= 0) {
url = StringUtils.replaceOnce(url, ".jsp?", ".htm?");
}
}
url = response.encodeRedirectURL(url);
if (logger.isTraceEnabled()) {
logger.debug(" redirect: " + url);
} else if (logger.isDebugEnabled()) {
logger.debug("redirect: " + url);
}
response.sendRedirect(url);
} else if (StringUtils.isNotBlank(page.getForward())) {
request.setAttribute(CLICK_FORWARD, CLICK_FORWARD);
if (logger.isTraceEnabled()) {
logger.debug(" forward: " + page.getForward());
} else if (logger.isDebugEnabled()) {
logger.debug("forward: " + page.getForward());
}
if (page.getForward().endsWith(".jsp")) {
renderJSP(page);
} else {
RequestDispatcher dispatcher =
request.getRequestDispatcher(page.getForward());
dispatcher.forward(request, response);
}
} else if (page.getPath() != null) {
renderTemplate(page);
} else {
if (logger.isTraceEnabled()) {
logger.debug(" path not defined for " + page.getClass().getName());
} else if (logger.isDebugEnabled()) {
logger.debug("path not defined for " + page.getClass().getName());
}
}
}
/**
* Render the Velocity template defined by the page's path.
* <p/>
* This method creates a Velocity Context using the Page's model Map and
* then merges the template with the Context writing the result to the
* HTTP servlet response.
* <p/>
* This method was adapted from org.apache.velocity.servlet.VelocityServlet.
*
* @param page the page template to merge
* @throws Exception if an error occurs
*/
protected void renderTemplate(Page page) throws Exception {
long startTime = System.currentTimeMillis();
final VelocityContext context = createVelocityContext(page);
// May throw parsing error if template could not be obtained
Template template = clickApp.getTemplate(page.getTemplate());
HttpServletResponse response = page.getContext().getResponse();
response.setContentType(page.getContentType());
Writer writer = response.getWriter();
if (page.getHeaders() != null) {
setPageResponseHeaders(response, page.getHeaders());
}
VelocityWriter velocityWriter = null;
try {
velocityWriter = (VelocityWriter) writerPool.get();
if (velocityWriter == null) {
velocityWriter =
new VelocityWriter(writer, WRITER_BUFFER_SIZE, true);
} else {
velocityWriter.recycle(writer);
}
template.merge(context, velocityWriter);
} catch (Exception error) {
// Exception occured merging template and model. It is possible
// that some output has already been written, so we will append the
// error report to the previous output.
ErrorReport errorReport =
new ErrorReport(error,
page.getClass(),
clickApp.isProductionMode(),
page.getContext().getRequest(),
getServletContext());
if (velocityWriter == null) {
velocityWriter =
new VelocityWriter(writer, WRITER_BUFFER_SIZE, true);
}
velocityWriter.write(errorReport.toString());
throw error;
} finally {
if (velocityWriter != null) {
// flush and put back into the pool don't close to allow
// us to play nicely with others.
velocityWriter.flush();
// Clear the VelocityWriter's reference to its
// internal Writer to allow the latter
// to be GC'd while vw is pooled.
velocityWriter.recycle(null);
writerPool.put(velocityWriter);
}
writer.flush();
writer.close();
}
if (!clickApp.isProductionMode()) {
HtmlStringBuffer buffer = new HtmlStringBuffer(50);
if (logger.isTraceEnabled()) {
buffer.append(" ");
}
buffer.append("renderTemplate: ");
if (!page.getTemplate().equals(page.getPath())) {
buffer.append(page.getPath());
buffer.append(",");
}
buffer.append(page.getTemplate());
buffer.append(" - ");
buffer.append(System.currentTimeMillis() - startTime);
buffer.append(" ms");
logger.info(buffer);
}
}
/**
* Render the given page as a JSP to the response.
*
* @param page the page to render
* @throws Exception if an error occurs rendering the JSP
*/
protected void renderJSP(Page page) throws Exception {
long startTime = System.currentTimeMillis();
HttpServletRequest request = page.getContext().getRequest();
HttpServletResponse response = page.getContext().getResponse();
setRequestAttributes(page);
RequestDispatcher dispatcher = null;
// Since the "getTemplate" returns the page.getPath() by default, which is *.htm
// we need to change to *.jsp in order to compare to the page.getForward()
String jspTemplate = StringUtils.replace(page.getTemplate(), ".htm", ".jsp");
if (page.getForward().equals(jspTemplate)) {
dispatcher = request.getRequestDispatcher(page.getForward());
} else {
dispatcher = request.getRequestDispatcher(page.getTemplate());
}
dispatcher.forward(request, response);
if (!clickApp.isProductionMode()) {
HtmlStringBuffer buffer = new HtmlStringBuffer(50);
buffer.append("renderJSP: ");
if (!page.getTemplate().equals(page.getForward())) {
buffer.append(page.getTemplate());
buffer.append(",");
}
buffer.append(page.getForward());
buffer.append(" - ");
buffer.append(System.currentTimeMillis() - startTime);
buffer.append(" ms");
logger.info(buffer);
}
}
/**
* Return a new Page instance for the given request. This method will
* invoke {@link #initPage(String, Class, HttpServletRequest)} to create
* the Page instance and then set the properties on the page.
*
* @param request the servlet request
* @return a new Page instance for the given request
*/
protected Page createPage(HttpServletRequest request) {
// Log request parameters
if (logger.isTraceEnabled()) {
Map requestParams = new TreeMap();
Enumeration e = request.getParameterNames();
while (e.hasMoreElements()) {
String name = e.nextElement().toString();
String value = request.getParameter(name);
requestParams.put(name, value);
}
Iterator i = requestParams.entrySet().iterator();
while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next();
String name = entry.getKey().toString();
String value = entry.getValue().toString();
String msg = " request param: " + name + "="
+ ClickUtils.limitLength(value, 40);
logger.trace(msg);
}
}
String path = Context.getThreadLocalContext().getResourcePath();
if (request.getAttribute(FORWARD_PAGE) != null) {
Page forwardPage = (Page) request.getAttribute(FORWARD_PAGE);
if (forwardPage.getFormat() == null) {
forwardPage.setFormat(clickApp.getFormat());
}
request.removeAttribute(FORWARD_PAGE);
return forwardPage;
}
Class pageClass = clickApp.getPageClass(path);
if (pageClass == null) {
pageClass = clickApp.getNotFoundPageClass();
path = ClickApp.NOT_FOUND_PATH;
}
final Page page = initPage(path, pageClass, request);
if (page.getFormat() == null) {
page.setFormat(clickApp.getFormat());
}
return page;
}
/**
* Process the given pages controls <tt>onDestroy</tt> methods, reset the pages
* navigation state and process the pages <tt>onDestroy</tt> method.
*
* @param page the page to process
* @param startTime the start time to log if greater than 0 and not in
* production mode
*/
protected void processPageOnDestroy(Page page, long startTime) {
if (page.hasControls()) {
List controls = page.getControls();
for (int i = 0, size = controls.size(); i < size; i++) {
try {
Control control = (Control) controls.get(i);
control.onDestroy();
if (logger.isTraceEnabled()) {
String controlClassName = control.getClass().getName();
controlClassName = controlClassName.substring(controlClassName.lastIndexOf('.') + 1);
String msg = " invoked: '" + control.getName()
+ "' " + controlClassName + ".onDestroy()";
logger.trace(msg);
}
} catch (Throwable error) {
logger.error(error.toString(), error);
}
}
}
// Reset the page navigation state
try {
// Reset the path
String path = page.getContext().getResourcePath();
page.setPath(path);
// Reset the foward
if (clickApp.isJspPage(path)) {
page.setForward(StringUtils.replace(path, ".htm", ".jsp"));
} else {
page.setForward((String) null);
}
// Reset the redirect
page.setRedirect((String) null);
} catch (Throwable error) {
logger.error(error.toString(), error);
}
try {
page.onDestroy();
if (page.isStateful()) {
page.getContext().setSessionAttribute(page.getClass().getName(), page);
} else {
page.getContext().removeSessionAttribute(page.getClass().getName());
}
if (logger.isTraceEnabled()) {
String shortClassName = page.getClass().getName();
shortClassName =
shortClassName.substring(shortClassName.lastIndexOf('.') + 1);
logger.trace(" invoked: " + shortClassName + ".onDestroy()");
}
if (!clickApp.isProductionMode() && startTime > 0) {
logger.info("handleRequest: " + page.getPath() + " - "
+ (System.currentTimeMillis() - startTime)
+ " ms");
}
} catch (Throwable error) {
logger.error(error.toString(), error);
}
}
/**
* Initialize a new page instance using
* {@link #newPageInstance(String, Class, HttpServletRequest)} method and
* setting format, headers and the forward if a JSP.
* <p/>
* This method will also automatically register any public Page controls
* in the page's model. When the page is created any public visible
* page Control variables will be automatically added to the page using
* the method {@link Page#addControl(Control)} method. If the controls name
* is not defined it is set to the member variables name before it is added
* to the page.
* <p/>
* This feature saves you from having to manually add the controls yourself.
* If you don't want the controls automatically added, simply declare them
* as non public variables.
* <p/>
* An example auto control registration is provided below. In this example
* the Table control is automatically added to the model using the name
* <tt>"table"</tt>, and the ActionLink controls are added using the names
* <tt>"editDetailsLink"</tt> and <tt>"viewDetailsLink"</tt>.
*
* <pre class="javaCode">
* <span class="kw">public class</span> OrderDetailsPage <span class="kw">extends</span> Page {
*
* <span class="kw">public</span> Table table = <span class="kw">new</span> Table();
* <span class="kw">public</span> ActionLink editDetailsLink = <span class="kw">new</span> ActionLink();
* <span class="kw">public</span>ActionLink viewDetailsLink = <span class="kw">new</span> ActionLink();
*
* <span class="kw">public</span> OrderDetailsPage() {
* ..
* }
* } </pre>
*
* @param path the page path
* @param pageClass the page class
* @param request the page request
* @return initialized page
*/
protected Page initPage(String path, Class pageClass,
HttpServletRequest request) {
try {
Page newPage = null;
// Look up the page in the users session.
HttpSession session = request.getSession(false);
if (session != null) {
newPage = (Page) session.getAttribute(pageClass.getName());
}
if (newPage == null) {
newPage = newPageInstance(path, pageClass, request);
if (logger.isTraceEnabled()) {
String shortClassName = pageClass.getName();
shortClassName =
shortClassName.substring(shortClassName.lastIndexOf('.') + 1);
logger.trace(" invoked: " + shortClassName + ".<<init>>");
}
}
activatePageInstance(newPage);
if (newPage.getHeaders() == null) {
newPage.setHeaders(clickApp.getPageHeaders(path));
}
newPage.setPath(path);
if (clickApp.isJspPage(path)) {
newPage.setForward(StringUtils.replace(path, ".htm", ".jsp"));
}
// Bind to final variable to enable callback processing
final Page page = newPage;
if (clickApp.isPagesAutoBinding()) {
// Automatically add public controls to the page
processPageFields(newPage, new FieldCallback() {
public void processField(String fieldName, Object fieldValue) {
if (fieldValue instanceof Control) {
Control control = (Control) fieldValue;
if (control.getName() == null) {
control.setName(fieldName);
}
if (!page.getModel().containsKey(control.getName())) {
page.addControl(control);
}
}
}
});
processPageRequestParams(page);
}
return newPage;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Process the page binding any request parameters to any public Page
* fields with the same name which are "primitive" types. These types
* include string, numbers and booleans.
* <p/>
* Type conversion is performed using the <tt>TypeConverter</tt>
* returned by the {@link #getTypeConverter()} method.
*
* @param page the page whose fields are to be processed
* @throws OgnlException if an error occurs
*/
protected void processPageRequestParams(Page page) throws OgnlException {
if (clickApp.getPageFields(page.getClass()).isEmpty()) {
return;
}
Map ognlContext = null;
boolean customConverter =
! getTypeConverter().getClass().equals(RequestTypeConverter.class);
HttpServletRequest request = page.getContext().getRequest();
for (Enumeration e = request.getParameterNames(); e.hasMoreElements();) {
String name = e.nextElement().toString();
String value = request.getParameter(name);
if (StringUtils.isNotBlank(value)) {
Field field = clickApp.getPageField(page.getClass(), name);
if (field != null) {
Class type = field.getType();
if (customConverter
|| (type.isPrimitive()
|| String.class.isAssignableFrom(type)
|| Number.class.isAssignableFrom(type)
|| Boolean.class.isAssignableFrom(type))) {
if (ognlContext == null) {
ognlContext = Ognl.createDefaultContext(page, null, getTypeConverter());
}
PropertyUtils.setValueOgnl(page, name, value, ognlContext);
if (logger.isTraceEnabled()) {
logger.trace(" auto bound variable: " + name + "=" + value);
}
}
}
}
}
}
/**
* Return a new Page instance for the given page path, class and request.
* <p/>
* The default implementation of this method simply creates new page
* instances:
* <pre class="codeJava">
* <span class="kw">protected</span> Page newPageInstance(String path, Class pageClass,
* HttpServletRequest request) <span class="kw">throws</span> Exception {
*
* <span class="kw">return</span> (Page) pageClass.newInstance();
* } </pre>
*
* This method is designed to be overridden by applications providing their
* own page creation patterns.
* <p/>
* A typical example of this would be with Inversion of Control (IoC)
* frameworks such as Spring or HiveMind. For example a Spring application
* could override this method and use a <tt>ApplicationContext</tt> to instantiate
* new Page objects:
* <pre class="codeJava">
* <span class="kw">protected</span> Page newPageInstance(String path, Class pageClass,
* HttpServletRequest request) <span class="kw">throws</span> Exception {
*
* String beanName = path.substring(0, path.indexOf(<span class="st">"."</span>));
*
* <span class="kw">if</span> (applicationContext.containsBean(beanName)) {
* Page page = (Page) applicationContext.getBean(beanName);
*
* } <span class="kw">else</span> {
* page = (Page) pageClass.newIntance();
* }
*
* <span class="kw">return</span> page;
* } </pre>
*
* @param path the request page path
* @param pageClass the page Class the request is mapped to
* @param request the page request
* @return a new Page object
* @throws Exception if an error occurs creating the Page
*/
protected Page newPageInstance(String path, Class pageClass,
HttpServletRequest request) throws Exception {
return (Page) pageClass.newInstance();
}
/**
* Provides an extension point for ClickServlet sub classes to activate
* stateful page which may have been deserialized.
* <p/>
* This method does nothing and is designed for extension.
*
* @param page the page instance to activate
*/
protected void activatePageInstance(Page page) {
}
/**
* Return a new VelocityContext for the given pages model and Context.
* <p/>
* The following values automatically added to the VelocityContext:
* <ul>
* <li>any public Page fields using the fields name</li>
* <li>context - the Servlet context path, e.g. /mycorp</li>
* <li>format - the {@link Format} object for formatting the display of objects</li>
* <li>imports - the {@link PageImports} object</li>
* <li>messages - the page messages bundle</li>
* <li>path - the page of the page template to render</li>
* <li>request - the pages servlet request</li>
* <li>response - the pages servlet request</li>
* <li>session - the {@link SessionMap} adaptor for the users HttpSession</li>
* </ul>
*
* @param page the page to create a VelocityContext for
* @return a new VelocityContext
*/
protected VelocityContext createVelocityContext(final Page page) {
final VelocityContext context = new VelocityContext(page.getModel());
if (clickApp.isPagesAutoBinding()) {
processPageFields(page, new FieldCallback() {
public void processField(String fieldName, Object fieldValue) {
if (fieldValue instanceof Control == false) {
context.put(fieldName, fieldValue);
} else {
// Add any controls not already added to model
Control control = (Control) fieldValue;
if (!page.getModel().containsKey(control.getName())) {
page.addControl(control);
}
}
}
});
}
final HttpServletRequest request = page.getContext().getRequest();
Object pop = context.put("request", request);
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"request\". The page model object "
+ pop + " has been replaced with the request object";
logger.warn(msg);
}
pop = context.put("response", page.getContext().getResponse());
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"response\". The page model object "
+ pop + " has been replaced with the response object";
logger.warn(msg);
}
SessionMap sessionMap = new SessionMap(request.getSession(false));
pop = context.put("session", sessionMap);
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"session\". The page model object "
+ pop + " has been replaced with the request "
+ " session";
logger.warn(msg);
}
pop = context.put("context", request.getContextPath());
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"context\". The page model object "
+ pop + " has been replaced with the request "
+ " context path";
logger.warn(msg);
}
Format format = page.getFormat();
if (format != null && !page.isStateful()) {
pop = context.put("format", format);
if (pop != null) {
String msg = page.getClass().getName() + " on "
+ page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"format\". The page model object " + pop
+ " has been replaced with the format object";
logger.warn(msg);
}
}
String path = page.getPath();
if (path != null) {
pop = context.put("path", path);
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on "
+ page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"path\". The page model object " + pop
+ " has been replaced with the page path";
logger.warn(msg);
}
}
pop = context.put("messages", page.getMessages());
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"messages\". The page model object "
+ pop + " has been replaced with the request "
+ " messages";
logger.warn(msg);
}
PageImports pageImports = new PageImports(page);
pop = context.put("imports", pageImports.getAllIncludes());
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"imports\". The page model object "
+ pop + " has been replaced with a PageImports object";
logger.warn(msg);
}
pop = context.put("cssImports", pageImports.getCssImports());
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"cssImports\". The page model object "
+ pop + " has been replaced with a PageImports object";
logger.warn(msg);
}
pop = context.put("jsImports", pageImports.getJsImports());
if (pop != null && !page.isStateful()) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"jsImports\". The page model object "
+ pop + " has been replaced with a PageImports object";
logger.warn(msg);
}
return context;
}
/**
* Set the HTTP headers in the servlet response. The Page response headers
* are defined in {@link Page#getHeaders()}.
*
* @param response the response to set the headers in
* @param headers the map of HTTP headers to set in the response
*/
protected void setPageResponseHeaders(HttpServletResponse response,
Map headers) {
for (Iterator i = headers.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
String name = entry.getKey().toString();
Object value = entry.getValue();
if (value instanceof String) {
String strValue = (String) value;
if (!strValue.equalsIgnoreCase("Content-Encoding")) {
response.setHeader(name, strValue);
}
} else if (value instanceof Date) {
long time = ((Date) value).getTime();
response.setDateHeader(name, time);
} else {
int intValue = ((Integer) value).intValue();
response.setIntHeader(name, intValue);
}
}
}
/**
* Ensure the ClickApp and the Velocity WriterPool have been initialized
* otherwise throw a UnavailableException.
* <p/>
* If <tt>app-reloadable</tt> is true a temporarily UnavailableException
* is thrown, otherwise if not reloadable then a permanent
* UnavailableException is thrown.
*
* @throws UnavailableException if the application has not been initialized
*/
protected void ensureAppInitialized() throws UnavailableException {
if (clickApp == null || writerPool == null) {
if (reloadable) {
String msg = "The application is temporarily unavailable"
+ " - please try again in 1 minute";
throw new UnavailableException(msg, 60);
} else {
String msg = "The application is unavailable.";
throw new UnavailableException(msg);
}
}
}
/**
* Return true if the request is click application reload request GET
* <tt>"/click/reload-app.htm"</tt> and the user is in the role
* <tt>"click-admin"</tt>. To reload the click application the
* servlet init parameter <tt>app-reloadable</tt> must also be defined.
*
* @param request the servlet request
* @return if a reload request and servlet configured to enable reloading
* and the user is in "click-admin" role
*/
protected boolean ifAuthorizedReloadRequest(HttpServletRequest request) {
if (reloadable && request.isUserInRole("click-admin")) {
String path = ClickUtils.getResourcePath(request);
return "/click/reload-app.htm".equals(path);
} else {
return false;
}
}
/**
* Reload the ClickApp and send status message to the given response.
*
* @param request the servlet request
* @param response the response to write the status message to
* @throws ServletException if an error occurs reloading the application
* @throws IOException if an I/O error occurs
*/
protected void reloadClickApp(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
init();
final String msg =
"ClickApp reloaded by " + request.getRemoteUser() + " on "
+ (new Date()).toString();
logger.info(msg);
response.setContentType("text/html");
response.getWriter().print(APPLICAION_RELOADED_MSG);
response.getWriter().close();
}
/**
* Set the page model, context, format, messages and path as request
* attributes to support JSP rendering. These request attributes include:
* <ul>
* <li>any public Page fields using the fields name</li>
* <li>context - the Servlet context path, e.g. /mycorp</li>
* <li>format - the {@link Format} object for formatting the display of objects</li>
* <li>forward - the page forward path, if defined</li>
* <li>imports - the {@link PageImports} object</li>
* <li>messages - the page messages bundle</li>
* <li>path - the page of the page template to render</li>
* </ul>
*
* @param page the page to set the request attributes on
*/
protected void setRequestAttributes(final Page page) {
final HttpServletRequest request = page.getContext().getRequest();
processPageFields(page, new FieldCallback() {
public void processField(String fieldName, Object fieldValue) {
if (fieldValue instanceof Control == false) {
request.setAttribute(fieldName, fieldValue);
} else {
// Add any controls not already added to model
Control control = (Control) fieldValue;
if (!page.getModel().containsKey(control.getName())) {
page.addControl(control);
}
}
}
});
Map model = page.getModel();
for (Iterator i = model.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
String name = entry.getKey().toString();
Object value = entry.getValue();
request.setAttribute(name, value);
}
request.setAttribute("context", request.getContextPath());
if (model.containsKey("context")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"context\". The request attribute "
+ "has been replaced with the request "
+ "context path";
logger.warn(msg);
}
request.setAttribute("format", page.getFormat());
if (model.containsKey("format")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"format\". The request attribute "
+ "has been replaced with the format object";
logger.warn(msg);
}
request.setAttribute("forward", page.getForward());
if (model.containsKey("forward")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"forward\". The request attribute "
+ "has been replaced with the page path";
logger.warn(msg);
}
request.setAttribute("path", page.getPath());
if (model.containsKey("path")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"path\". The request attribute "
+ "has been replaced with the page path";
logger.warn(msg);
}
request.setAttribute("messages", page.getMessages());
if (model.containsKey("messages")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"messages\". The request attribute "
+ "has been replaced with the page messages";
logger.warn(msg);
}
PageImports pageImports = new PageImports(page);
request.setAttribute("imports", pageImports.getAllIncludes());
if (model.containsKey("imports")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"imports\". The request attribute "
+ "has been replaced with a PageImports object";
logger.warn(msg);
}
request.setAttribute("cssImports", pageImports.getCssImports());
if (model.containsKey("cssImports")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"cssImports\". The request attribute "
+ "has been replaced with a PageImports object";
logger.warn(msg);
}
request.setAttribute("jsImports", pageImports.getJsImports());
if (model.containsKey("jsImports")) {
String msg = page.getClass().getName() + " on " + page.getPath()
+ " model contains an object keyed with reserved "
+ "name \"jsImports\". The request attribute "
+ "has been replaced with a PageImports object";
logger.warn(msg);
}
}
/**
* Return the request parameters OGNL <tt>TypeConverter</tt>. By default
* this method returns a {@link RequestTypeConverter} instance.
*
* @return the request parameters OGNL <tt>TypeConverter</tt>
*/
protected TypeConverter getTypeConverter() {
if (typeConverter == null) {
typeConverter = new RequestTypeConverter();
}
return typeConverter;
}
/**
* Return a new ClickLogger instance to be used for logging output for the
* Click runtime. This logger should have the name <tt>"Click"</tt>.
* <p/>
* You can subclass this method to provide your own logging behaviour.
*
* @return a new ClickLogger instance to be used for Click runtime logging output
*/
protected ClickLogger createClickLogger() {
return new ClickLogger("Click");
}
/**
* Creates and returns a new Context instance for this path, class and
* request.
* <p/>
* The default implementation of this method simply creates a new Context
* instance.
* <p/>
* Subclasses can override this method to provide a custom Context.
*
* @param request the page request
* @param response the page response
* @param isPost true if this is a post request, false otherwise
* @return a Context instance
*/
protected Context createContext(HttpServletRequest request,
HttpServletResponse response, boolean isPost) {
Context context = new Context(getServletContext(),
getServletConfig(),
request,
response,
isPost,
this);
return context;
}
// ------------------------------------------------ Package Private Methods
/**
* Process all the Pages public fields using the given callback.
*
* @param page the page to obtain the fields from
* @param callback the fields iterator callback
*/
void processPageFields(Page page, FieldCallback callback) {
Field[] fields = clickApp.getPageFieldArray(page.getClass());
if (fields != null) {
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
try {
Object fieldValue = field.get(page);
if (fieldValue != null) {
callback.processField(field.getName(), fieldValue);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
/**
* Return the Click Application instance.
*
* @return the Click Application instance
*/
ClickApp getClickApp() {
return clickApp;
}
// ---------------------------------------------------------- Inner Classes
/**
* Field iterator callback.
*/
static interface FieldCallback {
public void processField(String fieldName, Object fieldValue);
}
}