| /* |
| * 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"> |
| * <web-app> |
| * <servlet> |
| * <servlet-name><font color="blue">click-servlet</font></servlet-name> |
| * <servlet-class><font color="red">net.sf.click.ClickServlet</font></servlet-class> |
| * <load-on-startup><font color="red">0</font></load-on-startup> |
| * </servlet> |
| * <servlet-mapping> |
| * <servlet-name><font color="blue">click-servlet</font></servlet-name> |
| * <url-pattern><font color="red">*.htm</font></url-pattern> |
| * </servlet-mapping> |
| * </web-app> </pre> |
| * |
| * By default the <tt>ClickServlet</tt> will attempt to load an application |
| * configuration file using the path: <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"> |
| * <web-app> |
| * <servlet> |
| * <servlet-name>click-servlet</servlet-name> |
| * <servlet-class>net.sf.click.ClickServlet</servlet-class> |
| * <init-param> |
| * <param-name><font color="blue">app-reloadable</font></param-name> |
| * <param-value><font color="red">true</font></param-value> |
| * </init-param> |
| * <load-on-startup>0</load-on-startup> |
| * </servlet> |
| * |
| * <servlet-mapping> |
| * <servlet-name>click-servlet</servlet-name> |
| * <url-pattern>*.htm</url-pattern> |
| * </servlet-mapping> |
| * |
| * <security-constraint> |
| * <web-resource-collection> |
| * <web-resource-name>click-admin</web-resource-name> |
| * <url-pattern><font color="blue">/click/reload-app.htm</font></url-pattern> |
| * </web-resource-collection> |
| * <auth-constraint> |
| * <role-name><font color="red">click-admin</font></role-name> |
| * </auth-constraint> |
| * </security-constraint> |
| * |
| * <login-config> |
| * <auth-method>DIGEST</auth-method> |
| * <realm-name>MyCorp</realm-name> |
| * </login-config> |
| * |
| * <security-role> |
| * <role-name><font color="red">click-admin</font></role-name> |
| * </security-role> |
| * </web-app> </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: |
| * "<tt>app-reloadable</tt>". |
| */ |
| protected final static String APP_RELOADABLE = "app-reloadable"; |
| |
| /** |
| * The forwarded request marker attribute: "<tt>click-forward</tt>". |
| */ |
| protected final static String CLICK_FORWARD = "click-forward"; |
| |
| /** |
| * The Page to forward to request attribute: "<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"> |
| * <page path="<span class="navy">click/error.htm</span>" classname="<span class="maroon">com.mycorp.util.ErrorPage</span>"/> |
| * </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¶m2=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); |
| |
| } |
| |
| } |