Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.click; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; import java.lang.reflect.Field; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; 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 ognl.DefaultMemberAccess; import ognl.MemberAccess; import ognl.Ognl; import ognl.OgnlException; import ognl.TypeConverter; import org.apache.click.service.ConfigService; import org.apache.click.service.ConfigService.AutoBinding; import org.apache.click.service.LogService; import org.apache.click.service.ResourceService; import org.apache.click.service.TemplateException; import org.apache.click.service.XmlConfigService; import org.apache.click.util.ClickUtils; import org.apache.click.util.ErrorPage; import org.apache.click.util.HtmlStringBuffer; import org.apache.click.util.PageImports; import org.apache.click.util.PropertyUtils; import org.apache.click.util.RequestTypeConverter; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.StringUtils; import com.ga.click.exception.BizException; import com.ga.click.mvc.UnitPage; import com.ga.click.page.GaPage; import com.ga.click.util.ClickUtil; /** * 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">org.apache.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. * * <h4>ConfigService</h4> * * A single application {@link ConfigService} instance is created by the ClickServlet at * startup. Once the ConfigService has been initialized it is stored in the * ServletContext using the key {@value org.apache.click.service.ConfigService#CONTEXT_NAME}. */ public class ClickServlet extends HttpServlet { // -------------------------------------------------------------- Constants private static final long serialVersionUID = 1L; /** * The <tt>mock page reference</tt> request attribute: key: * <tt>mock_page_reference</tt>. * <p/> * This attribute stores the each Page instance as a request attribute. * <p/> * <b>Note:</b> a page is <tt>only</tt> stored as a request attribute * if the {@link #MOCK_MODE_ENABLED} attribute is set. */ static final String MOCK_PAGE_REFERENCE = "mock_page_reference"; /** * The <tt>mock mode</tt> request attribute: key: * <tt>mock_mode_enabled</tt>. * <p/> * If this attribute is set (the value does not matter) certain features * will be enabled which is needed for running Click in a mock environment. */ static final String MOCK_MODE_ENABLED = "mock_mode_enabled"; /** * The click application configuration service classname init parameter name: * "<tt>config-service-class</tt>". */ protected final static String CONFIG_SERVICE_CLASS = "config-service-class"; /** * The custom TypeConverter classname as an init parameter name: * &nbps; "<tt>type-converter-class</tt>". */ protected final static String TYPE_CONVERTER_CLASS = "type-converter-class"; /** * 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 Variables /** The click application configuration service. */ protected ConfigService configService; /** The application log service. */ protected LogService logger; /** The OGNL member access handler. */ protected MemberAccess memberAccess; /** The application resource service. */ protected ResourceService resourceService; /** The request parameters OGNL type converter. */ protected TypeConverter typeConverter; /** The thread local page listeners. */ private static final ThreadLocal<List<PageInterceptor>> THREAD_LOCAL_INTERCEPTORS = new ThreadLocal<List<PageInterceptor>>(); // --------------------------------------------------------- Public Methods /** * Initialize the Click servlet and the Velocity runtime. * * @see javax.servlet.GenericServlet#init() * * @throws ServletException if the application configuration service could * not be initialized */ @Override public void init() throws ServletException { try { // Create and initialize the application config service configService = createConfigService(getServletContext()); initConfigService(getServletContext()); logger = configService.getLogService(); if (logger.isInfoEnabled()) { logger.info("Click " + ClickUtils.getClickVersion() + " initialized in " + configService.getApplicationMode() + " mode"); } resourceService = configService.getResourceService(); } catch (Throwable e) { // In mock mode this exception can occur if click.xml is not // available. if (getServletContext().getAttribute(MOCK_MODE_ENABLED) != null) { return; } e.printStackTrace(); String msg = "error while initializing Click servlet; throwing " + "javax.servlet.UnavailableException"; log(msg, e); throw new UnavailableException(e.toString()); } } /** * @see javax.servlet.GenericServlet#destroy() */ @Override public void destroy() { try { // Destroy the application config service destroyConfigService(getServletContext()); } catch (Throwable e) { // In mock mode this exception can occur if click.xml is not // available. if (getServletContext().getAttribute(MOCK_MODE_ENABLED) != null) { return; } e.printStackTrace(); String msg = "error while destroying Click servlet, throwing " + "javax.servlet.UnavailableException"; log(msg, e); } finally { // Dereference the application config service configService = null; } super.destroy(); } // ------------------------------------------------------ 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 */ @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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 */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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 * @throws IOException if resource request could not be served */ protected void handleRequest(HttpServletRequest request, HttpServletResponse response, boolean isPost) throws IOException { // Handle requests for click resources, i.e. CSS, JS and image files if (resourceService.isResourceRequest(request)) { resourceService.renderResource(request, response); return; } long startTime = System.currentTimeMillis(); 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); } // Handle click page requests Page page = null; try { ActionEventDispatcher eventDispatcher = createActionEventDispatcher(); // Bind ActionEventDispatcher to current thread ActionEventDispatcher.pushThreadLocalDispatcher(eventDispatcher); ControlRegistry controlRegistry = createControlRegistry(); // Bind ControlRegistry to current thread ControlRegistry.pushThreadLocalRegistry(controlRegistry); Context context = createContext(request, response, isPost); // Bind context to current thread Context.pushThreadLocalContext(context); // Check for fatal error that occurred while creating Context Throwable error = (Throwable) request.getAttribute(Context.CONTEXT_FATAL_ERROR); if (error != null) { // Process exception through Click's exception handler. if (error instanceof Exception) { throw (Exception) error; } // Errors are not handled by Click, let the server handle it if (error instanceof Error) { throw (Error) error; } else { // Throwables are not handled by Click, let the server handle it throw new RuntimeException(error); } } page = createPage(context); // If no page created, then an PageInterceptor has aborted processing if (page == null) { return; } 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<? extends Page> pageClass = configService.getPageClass(ClickUtils.getResourcePath(request)); handleException(request, response, isPost, e, pageClass); } catch (ExceptionInInitializerError eiie) { Throwable cause = eiie.getException(); cause = (cause != null) ? cause : eiie; Class<? extends Page> pageClass = configService.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); } } for (PageInterceptor interceptor : getThreadLocalInterceptors()) { interceptor.postDestroy(page); } setThreadLocalInterceptors(null); } finally { // Only clear the context when running in normal mode. if (request.getAttribute(MOCK_MODE_ENABLED) == null) { Context.popThreadLocalContext(); } ControlRegistry.popThreadLocalRegistry(); ActionEventDispatcher.popThreadLocalDispatcher(); } } } /** * 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<? extends Page> pageClass) { if (isAjaxRequest(request)) { handleAjaxException(request, response, isPost, exception, pageClass); // Exit after handling ajax exception return; } if (exception instanceof TemplateException) { TemplateException te = (TemplateException) exception; if (!te.isParseError()) { logger.error("handleException: ", exception); } } else { logger.error("handleException: ", exception); } ErrorPage finalizeRef = null; try { final ErrorPage errorPage = createErrorPage(pageClass, exception); finalizeRef = errorPage; errorPage.setError(exception); if (errorPage.getFormat() == null) { errorPage.setFormat(configService.createFormat()); } errorPage.setHeaders(configService.getPageHeaders(ConfigService.ERROR_PATH)); errorPage.setMode(configService.getApplicationMode()); errorPage.setPageClass(pageClass); errorPage.setPath(ConfigService.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. * <p/> * This method does not invoke the "onDestroy()" callback method. * * @see #processPageEvents(org.apache.click.Page, org.apache.click.Context) * * @param page the Page to process * @throws Exception if an error occurs */ @SuppressWarnings("deprecation") protected void processPage(Page page) throws Exception { final Context context = page.getContext(); PageImports pageImports = createPageImports(page); page.setPageImports(pageImports); if (context.isAjaxRequest()) { processAjaxPageEvents(page, context); } else { processPageEvents(page, context); } } /** * Process the given page events, invoking the "on" event callback methods * and directing the response. * <p/> * This method does not invoke the "onDestroy()" callback method. * * @param page the Page which events to process * @param context the request context * @throws Exception if an error occurs */ protected void processPageEvents(Page page, Context context) throws Exception { ActionEventDispatcher eventDispatcher = ActionEventDispatcher.getThreadLocalDispatcher(); ControlRegistry controlRegistry = ControlRegistry.getThreadLocalRegistry(); boolean errorOccurred = page instanceof ErrorPage; // Support direct access of click-error.htm if (errorOccurred) { ErrorPage errorPage = (ErrorPage) page; errorPage.setMode(configService.getApplicationMode()); // Notify the eventDispatcher and controlRegistry of the error eventDispatcher.errorOccurred(errorPage.getError()); controlRegistry.errorOccurred(errorPage.getError()); } boolean continueProcessing = performOnSecurityCheck(page, context); ActionResult actionResult = null; if (continueProcessing && !errorOccurred) { // Handle page method String pageAction = context.getRequestParameter(Page.PAGE_ACTION); if (pageAction != null) { // Returned actionResult could be null actionResult = performPageAction(page, pageAction, context); continueProcessing = false; } } if (continueProcessing) { performOnInit(page, context); continueProcessing = performOnProcess(page, context, eventDispatcher); if (continueProcessing) { performOnPostOrGet(page, context, context.isPost()); performOnRender(page, context); } } controlRegistry.processPreResponse(context); controlRegistry.processPreRenderHeadElements(context); performRender(page, context, actionResult); } /** * Perform the onSecurityCheck event callback for the specified page, * returning true if processing should continue, false otherwise. * * @param page the page to perform the security check on * @param context the request context * @return true if processing should continue, false otherwise */ protected boolean performOnSecurityCheck(Page page, Context context) { boolean continueProcessing = page.onSecurityCheck(); if (logger.isTraceEnabled()) { logger.trace(" invoked: " + ClassUtils.getShortClassName(page.getClass()) + ".onSecurityCheck() : " + continueProcessing); } return continueProcessing; } /** * Perform the page action for the given page and return the action result. * * @param page the page which action to perform * @param pageAction the name of the page action * @param context the request context * @return the page action ActionResult instance */ protected ActionResult performPageAction(Page page, String pageAction, Context context) { ActionResult actionResult = ClickUtils.invokeAction(page, pageAction); if (logger.isTraceEnabled()) { HtmlStringBuffer buffer = new HtmlStringBuffer(); String pageClassName = ClassUtils.getShortClassName(page.getClass()); buffer.append(" invoked: "); buffer.append(pageClassName); buffer.append(".").append(pageAction).append("() : "); if (actionResult == null) { buffer.append("null (*no* ActionResult was returned by PageAction)"); } else { buffer.append(ClassUtils.getShortClassName(actionResult.getClass())); } logger.trace(buffer.toString()); } return actionResult; } /** * Perform the onInit event callback for the specified page. * * @param page the page to initialize * @param context the request context */ protected void performOnInit(Page page, Context context) { page.onInit(); if (logger.isTraceEnabled()) { logger.trace(" invoked: " + ClassUtils.getShortClassName(page.getClass()) + ".onInit()"); } if (page.hasControls()) { List<Control> controls = page.getControls(); for (int i = 0, size = controls.size(); i < size; i++) { 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); } } } } /** * Perform onProcess event callback for the specified page, returning true * if processing should continue, false otherwise. * * @param page the page to process * @param context the request context * @param eventDispatcher the action event dispatcher * @return true if processing should continue, false otherwise */ protected boolean performOnProcess(Page page, Context context, ActionEventDispatcher eventDispatcher) { boolean continueProcessing = true; // Make sure don't process a forwarded request if (page.hasControls() && !context.isForward()) { List<Control> controls = page.getControls(); for (int i = 0, size = controls.size(); i < size; i++) { Control control = controls.get(i); int initialListenerCount = 0; if (logger.isTraceEnabled()) { initialListenerCount = eventDispatcher.getEventSourceList().size(); } boolean onProcessResult = control.onProcess(); if (!onProcessResult) { continueProcessing = false; } if (logger.isTraceEnabled()) { String controlClassName = ClassUtils.getShortClassName(control.getClass()); String msg = " invoked: '" + control.getName() + "' " + controlClassName + ".onProcess() : " + onProcessResult; logger.trace(msg); if (initialListenerCount != eventDispatcher.getEventSourceList().size()) { logger.trace(" listener was registered while processing control"); } } } if (continueProcessing) { // Fire registered action events continueProcessing = eventDispatcher.fireActionEvents(context); if (logger.isTraceEnabled()) { String msg = " invoked: Control listeners : " + continueProcessing; logger.trace(msg); } } } return continueProcessing; } /** * Perform onPost or onGet event callback for the specified page. * * @param page the page for which the onGet or onPost is performed * @param context the request context * @param isPost specifies whether the request is a post or a get */ protected void performOnPostOrGet(Page page, Context context, boolean isPost) { if (isPost) { page.onPost(); if (logger.isTraceEnabled()) { logger.trace(" invoked: " + ClassUtils.getShortClassName(page.getClass()) + ".onPost()"); } } else { page.onGet(); if (logger.isTraceEnabled()) { logger.trace(" invoked: " + ClassUtils.getShortClassName(page.getClass()) + ".onGet()"); } } } /** * Perform onRender event callback for the specified page. * * @param page page to render * @param context the request context */ protected void performOnRender(Page page, Context context) { page.onRender(); if (logger.isTraceEnabled()) { logger.trace(" invoked: " + ClassUtils.getShortClassName(page.getClass()) + ".onRender()"); } if (page.hasControls()) { List<Control> controls = page.getControls(); for (int i = 0, size = controls.size(); i < size; i++) { 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); } } } } /** * Performs rendering of the specified page. * * @param page page to render * @param context the request context * @throws java.lang.Exception if error occurs */ protected void performRender(Page page, Context context) throws Exception { performRender(page, context, null); } /** * Performs rendering of the specified page. * * @param page page to render * @param context the request context * @param actionResult the action result * @throws java.lang.Exception if error occurs */ protected void performRender(Page page, Context context, ActionResult actionResult) throws Exception { // Process page interceptors, and abort rendering if specified for (PageInterceptor interceptor : getThreadLocalInterceptors()) { if (!interceptor.preResponse(page)) { return; } } final HttpServletRequest request = context.getRequest(); final HttpServletResponse response = context.getResponse(); if (StringUtils.isNotBlank(page.getRedirect())) { String url = page.getRedirect(); 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())) { // Indicates the request is forwarded 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 (actionResult != null) { renderActionResult(actionResult, page, context); } else if (page.getPath() != null) { // Render template unless the request was a page action. This check // guards against the scenario where the page action returns null // instead of a action result if (context.getRequestParameter(Page.PAGE_ACTION) == null) { String pagePath = page.getPath(); // Check if request is a JSP page if (pagePath.endsWith(".jsp") || configService.isJspPage(pagePath)) { // CLK-141. Set pagePath as the forward value. page.setForward(StringUtils.replace(pagePath, ".htm", ".jsp")); // Indicates the request is forwarded request.setAttribute(CLICK_FORWARD, CLICK_FORWARD); renderJSP(page); } else { 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 Map<String, Object> model = createTemplateModel(page); Context context = page.getContext(); HttpServletResponse response = context.getResponse(); response.setContentType(page.getContentType()); Writer writer = getWriter(response); if (page.hasHeaders()) { setPageResponseHeaders(response, page.getHeaders()); } if (page instanceof GaPage && ((GaPage) page).getLayout() != null) { // Map<String, Object> rtnInfo = new HashMap<String, Object>(); rtnInfo.put("page", ((GaPage) page).getLayout()); configService.getTemplateService().renderTemplate("/clicktemplate/page.htm", rtnInfo, writer); } else if (page instanceof UnitPage && ((UnitPage) page).getLayout() != null) { // Map<String, Object> rtnInfo = new HashMap<String, Object>(); rtnInfo.put("page", ((UnitPage) page).getLayout()); configService.getTemplateService().renderTemplate("/clicktemplate/page.htm", rtnInfo, writer); } else { configService.getTemplateService().renderTemplate(page, model, writer); } if (!configService.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; String forward = page.getForward(); // As "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 (forward.equals(jspTemplate)) { dispatcher = request.getRequestDispatcher(forward); } else { dispatcher = request.getRequestDispatcher(page.getTemplate()); } dispatcher.forward(request, response); if (!configService.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); } } /** * Render the given ActionResult. If the action result is null, nothing is * rendered. * * @param actionResult the action result to render * @param page the requested page * @param context the request context */ protected void renderActionResult(ActionResult actionResult, Page page, Context context) { if (actionResult == null) { return; } long startTime = System.currentTimeMillis(); actionResult.render(context); if (!configService.isProductionMode()) { HtmlStringBuffer buffer = new HtmlStringBuffer(50); if (logger.isTraceEnabled()) { buffer.append(" "); } buffer.append("renderActionResult ("); buffer.append(actionResult.getContentType()); buffer.append(")"); String template = actionResult.getTemplate(); if (template != null) { buffer.append(": "); buffer.append(template); } buffer.append(" - "); buffer.append(System.currentTimeMillis() - startTime); buffer.append(" ms"); logger.info(buffer); } } /** * Return a new Page instance for the given request context. This method will * invoke {@link #initPage(String, Class, HttpServletRequest)} to create * the Page instance and then set the properties on the page. * * @param context the page request context * @return a new Page instance for the given request, or null if an * PageInterceptor has aborted page creation */ protected Page createPage(Context context) { HttpServletRequest request = context.getRequest(); // Log request parameters if (logger.isTraceEnabled()) { logger.trace(" is Ajax request: " + context.isAjaxRequest()); logRequestParameters(request); } String path = context.getResourcePath(); if (request.getAttribute(FORWARD_PAGE) != null) { Page forwardPage = (Page) request.getAttribute(FORWARD_PAGE); if (forwardPage.getFormat() == null) { forwardPage.setFormat(configService.createFormat()); } request.removeAttribute(FORWARD_PAGE); return forwardPage; } Class<? extends Page> pageClass = configService.getPageClass(path); if (pageClass == null) { pageClass = configService.getNotFoundPageClass(); path = ConfigService.NOT_FOUND_PATH; } // Set thread local app page listeners List<PageInterceptor> interceptors = configService.getPageInterceptors(); setThreadLocalInterceptors(interceptors); for (PageInterceptor listener : interceptors) { if (!listener.preCreate(pageClass, context)) { return null; } } final Page page = initPage(path, pageClass, request); if (page.getFormat() == null) { page.setFormat(configService.createFormat()); } for (PageInterceptor listener : interceptors) { if (!listener.postCreate(page)) { return null; } } 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 */ @SuppressWarnings("deprecation") protected void processPageOnDestroy(Page page, long startTime) { Context context = page.getContext(); if (page.hasControls()) { // notify callbacks of destroy event // TODO check that exceptions don't unnecessarily trigger preDestroy ControlRegistry.getThreadLocalRegistry().processPreDestroy(context); List<Control> controls = page.getControls(); for (int i = 0, size = controls.size(); i < size; i++) { try { 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 = context.getResourcePath(); page.setPath(path); // Reset the forward if (configService.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()) { context.setSessionAttribute(page.getClass().getName(), page); } else { context.removeSessionAttribute(page.getClass().getName()); } if (logger.isTraceEnabled()) { String shortClassName = page.getClass().getName(); shortClassName = shortClassName.substring(shortClassName.lastIndexOf('.') + 1); logger.trace(" invoked: " + shortClassName + ".onDestroy()"); } if (!configService.isProductionMode() && startTime > 0) { logger.info("handleRequest: " + page.getPath() + " - " + (System.currentTimeMillis() - startTime) + " ms"); } } catch (Throwable error) { logger.error(error.toString(), error); } finally { // Nullify PageImports page.setPageImports(null); } } /** * 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="codeJava"> * <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<? extends Page> 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); Map<String, Object> defaultHeaders = configService.getPageHeaders(path); if (newPage.hasHeaders()) { // Don't override existing headers Map pageHeaders = newPage.getHeaders(); for (Map.Entry entry : defaultHeaders.entrySet()) { if (!pageHeaders.containsKey(entry.getKey())) { pageHeaders.put(entry.getKey(), entry.getValue()); } } } else { newPage.getHeaders().putAll(defaultHeaders); } newPage.setPath(path); // Bind to final variable to enable callback processing final Page page = newPage; if (configService.getAutoBindingMode() != AutoBinding.NONE) { 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); } // In mock mode add the Page instance as a request attribute. if (request.getAttribute(MOCK_MODE_ENABLED) != null) { request.setAttribute(MOCK_PAGE_REFERENCE, 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 (configService.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 = configService.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(), getMemberAccess()); } 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.newInstance(); * } * * <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<? extends Page> pageClass, HttpServletRequest request) throws Exception { return 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 org.apache.click.util.Format} object for formatting * the display of objects</li> * <li>imports - the {@link org.apache.click.util.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 org.apache.click.util.SessionMap} adaptor for the * users HttpSession</li> * </ul> * * @see org.apache.click.util.ClickUtils#createTemplateModel(org.apache.click.Page, org.apache.click.Context) * * @param page the page to create a VelocityContext for * @return a new VelocityContext */ @SuppressWarnings("deprecation") protected Map<String, Object> createTemplateModel(final Page page) { if (configService.getAutoBindingMode() != AutoBinding.NONE) { processPageFields(page, new FieldCallback() { public void processField(String fieldName, Object fieldValue) { if (fieldValue instanceof Control == false) { page.addModel(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 Context context = page.getContext(); final Map<String, Object> model = ClickUtils.createTemplateModel(page, context); PageImports pageImports = page.getPageImports(); pageImports.populateTemplateModel(model); return model; } /** * 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<String, Object> headers) { for (Map.Entry<String, Object> entry : headers.entrySet()) { String name = entry.getKey(); 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 if (value instanceof Integer) { int intValue = (Integer) value; response.setIntHeader(name, intValue); } else if (value != null) { throw new IllegalStateException("Invalid Page header value type: " + value.getClass() + ". Header value must of type String, Date or Integer."); } } } /** * 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 org.apache.click.util.Format} object for * formatting the display of objects</li> * <li>forward - the page forward path, if defined</li> * <li>imports - the {@link org.apache.click.util.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 */ @SuppressWarnings("deprecation") 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<String, Object> model = page.getModel(); for (Map.Entry<String, Object> entry : model.entrySet()) { String name = entry.getKey(); 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 = page.getPageImports(); pageImports.populateRequest(request, model); } /** * Return the request parameters OGNL <tt>TypeConverter</tt>. This method * performs a lazy load of the TypeConverter object, using the classname * defined in the Servlet init parameter <tt>type-converter-class</tt>, * if this parameter is not defined this method will return a * {@link RequestTypeConverter} instance. * * @return the request parameters OGNL <tt>TypeConverter</tt> * @throws RuntimeException if the TypeConverter instance could not be created */ @SuppressWarnings("unchecked") protected TypeConverter getTypeConverter() throws RuntimeException { if (typeConverter == null) { Class<? extends TypeConverter> converter = RequestTypeConverter.class; try { String classname = getInitParameter(TYPE_CONVERTER_CLASS); if (StringUtils.isNotBlank(classname)) { converter = ClickUtils.classForName(classname); } typeConverter = converter.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } return typeConverter; } /** * 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; } /** * Creates and returns a new ActionEventDispatcher instance. * * @return the new ActionEventDispatcher instance */ protected ActionEventDispatcher createActionEventDispatcher() { return new ActionEventDispatcher(configService); } /** * Creates and returns a new ControlRegistry instance. * * @return the new ControlRegistry instance */ protected ControlRegistry createControlRegistry() { return new ControlRegistry(configService); } /** * Creates and returns a new ErrorPage instance. * <p/> * This method creates the custom page as specified in <tt>click.xml</tt>, * otherwise the default ErrorPage instance. * <p/> * Subclasses can override this method to provide custom ErrorPages tailored * for specific exceptions. * <p/> * <b>Note</b> you can safely use {@link org.apache.click.Context} in this * method. * * @param pageClass the page class with the error * @param exception the error causing exception * @return a new ErrorPage instance */ protected ErrorPage createErrorPage(Class<? extends Page> pageClass, Throwable exception) { try { return (ErrorPage) configService.getErrorPageClass().newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } /** * Return the application configuration service instance. * * @return the application configuration service instance */ protected ConfigService getConfigService() { return configService; } /** * Return a new Page instance for the given path. The path must start with * a <tt>"/"</tt>. * * @param path the path which maps to a Page class * @param request the Page request * @return a new Page object * @throws IllegalArgumentException if the Page is not found */ @SuppressWarnings("unchecked") protected <T extends Page> T createPage(String path, HttpServletRequest request) { Class<? extends Page> pageClass = getConfigService().getPageClass(path); if (pageClass == null) { String msg = "No Page class configured for path: " + path; throw new IllegalArgumentException(msg); } return (T) initPage(path, pageClass, request); } /** * Return a new Page instance for the page Class. * * @param pageClass the class of the Page to create * @param request the Page request * @return a new Page object * @throws IllegalArgumentException if the Page Class is not configured * with a unique path */ @SuppressWarnings("unchecked") protected <T extends Page> T createPage(Class<T> pageClass, HttpServletRequest request) { String path = getConfigService().getPagePath(pageClass); if (path == null) { String msg = "No path configured for Page class: " + pageClass.getName(); throw new IllegalArgumentException(msg); } return (T) initPage(path, pageClass, request); } /** * Creates and returns a new PageImports instance for the specified page. * * @param page the page to create a new PageImports instance for * @return the new PageImports instance */ protected PageImports createPageImports(Page page) { return new PageImports(page); } // TODO refactor Page events into its a separate Livecycle class. This will // take some of the responsibility off ClickServlet and remove code duplication /** * Process the given page events, invoking the "on" event callback methods * and directing the response. * * @param page the page which events to process * @param context the request context * @throws Exception if an error occurs */ protected void processAjaxPageEvents(Page page, Context context) throws Exception { ActionEventDispatcher eventDispatcher = ActionEventDispatcher.getThreadLocalDispatcher(); ControlRegistry controlRegistry = ControlRegistry.getThreadLocalRegistry(); // TODO Ajax requests shouldn't reach this code path since errors // are rendered directly if (page instanceof ErrorPage) { ErrorPage errorPage = (ErrorPage) page; errorPage.setMode(configService.getApplicationMode()); // Notify the dispatcher and registry of the error eventDispatcher.errorOccurred(errorPage.getError()); controlRegistry.errorOccurred(errorPage.getError()); } boolean continueProcessing = performOnSecurityCheck(page, context); ActionResult actionResult = null; if (continueProcessing) { // Handle page method String pageAction = context.getRequestParameter(Page.PAGE_ACTION); if (pageAction != null) { continueProcessing = false; // Returned action result could be null actionResult = performPageAction(page, pageAction, context); controlRegistry.processPreResponse(context); controlRegistry.processPreRenderHeadElements(context); renderActionResult(actionResult, page, context); } } if (continueProcessing) { performOnInit(page, context); // TODO: Ajax doesn't support forward. Is it still necessary to // check isForward? if (controlRegistry.hasAjaxTargetControls() && !context.isForward()) { // Perform onProcess for regsitered Ajax target controls processAjaxTargetControls(context, eventDispatcher, controlRegistry); // Fire AjaxBehaviors registered during the onProcess event // The target AjaxBehavior will set the eventDispatcher action // result instance to render eventDispatcher.fireAjaxBehaviors(context); // Ensure we execute the beforeResponse and beforeGetHeadElements // for Ajax requests controlRegistry.processPreResponse(context); controlRegistry.processPreRenderHeadElements(context); actionResult = eventDispatcher.getActionResult(); // Render the actionResult renderActionResult(actionResult, page, context); } else { // If no Ajax target controls have been registered fallback to // the old behavior of processing and rendering the page template if (logger.isTraceEnabled()) { String msg = " *no* Ajax target controls have been registered." + " Will process the page as a normal non Ajax request."; logger.trace(msg); } continueProcessing = performOnProcess(page, context, eventDispatcher); if (continueProcessing) { performOnPostOrGet(page, context, context.isPost()); performOnRender(page, context); } // If Ajax request does not target a valid page, return a 404 // repsonse status, allowing JavaScript to display a proper message if (ConfigService.NOT_FOUND_PATH.equals(page.getPath())) { HttpServletResponse response = context.getResponse(); response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } controlRegistry.processPreResponse(context); controlRegistry.processPreRenderHeadElements(context); performRender(page, context); } } else { // If security check fails for an Ajax request, Click returns without // any rendering. It is up to the user to render an ActionResult // in the onSecurityCheck event // Note: this code path is also followed if a pageAction is invoked } } /** * Process all Ajax target controls and return true if the page should continue * processing, false otherwise. * * @param context the request context * @param eventDispatcher the event dispatcher * @param controlRegistry the control registry * @return true if the page should continue processing, false otherwise */ protected boolean processAjaxTargetControls(Context context, ActionEventDispatcher eventDispatcher, ControlRegistry controlRegistry) { boolean continueProcessing = true; // Resolve the Ajax target control for this request Control ajaxTarget = resolveAjaxTargetControl(context, controlRegistry); if (ajaxTarget != null) { // Process the control if (!ajaxTarget.onProcess()) { continueProcessing = false; } // Log a trace if no behavior was registered after processing the control if (logger.isTraceEnabled()) { HtmlStringBuffer buffer = new HtmlStringBuffer(); String controlClassName = ClassUtils.getShortClassName(ajaxTarget.getClass()); buffer.append(" invoked: '"); buffer.append(ajaxTarget.getName()); buffer.append("' ").append(controlClassName); buffer.append(".onProcess() : ").append(continueProcessing); logger.trace(buffer.toString()); if (!eventDispatcher.hasAjaxBehaviorSourceSet()) { logger.trace(" *no* AjaxBehavior was registered while processing the control"); } } } return continueProcessing; } /** * Provides an Ajax exception handler. Exceptions are wrapped inside a * <tt>div</tt> element and streamed back to the browser. The response status * is set to an {@link javax.servlet.http.HttpServletResponse#SC_INTERNAL_SERVER_ERROR HTTP 500 error} * which allows the JavaScript that initiated the Ajax request to handle * the error as appropriate. * <p/> * If Click is running in <tt>development</tt> modes the exception stackTrace * will be rendered, in <tt>production</tt> modes an error message is * rendered. * <p/> * Below is an example error response: * * <pre class="prettyprint"> * <div id='errorReport' class='errorReport'> * The application encountered an unexpected error. * </div> * </pre> * * @param request the servlet request * @param response the servlet response * @param isPost determines whether the request is a POST * @param exception the error causing exception * @param pageClass the page class with the error */ protected void handleAjaxException(HttpServletRequest request, HttpServletResponse response, boolean isPost, Throwable exception, Class<? extends Page> pageClass) { // If an exception occurs during an Ajax request, stream // the exception instead of creating an ErrorPage PrintWriter writer = null; try { String returnJson = ""; BizException bizException = null; if (exception instanceof BizException) { bizException = (BizException) exception; } else if (exception.getCause() instanceof BizException) { bizException = (BizException) exception.getCause(); } else { bizException = null; } if (bizException != null) { if (!bizException.isAjaxRequest()) { //ajax response.setContentType("text/html;charset=GBK"); writer = getPrintWriter(response); response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); returnJson = ClickUtil.creaReturnJSON(bizException); } else { //ajax response.setContentType("text/json;charset=GBK"); writer = getPrintWriter(response); response.setStatus(HttpServletResponse.SC_OK); returnJson = ClickUtil.creaReturnJSON(bizException); } } else { // response.setContentType("text/html;charset=GBK"); writer = getPrintWriter(response); response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); returnJson = "{\"statusCode\":\"300\", \"message\":\"-" + exception.getMessage() + "\"}"; } writer.write(returnJson); } catch (Throwable error) { logger.error(error.getMessage(), error); throw new RuntimeException(error); } finally { if (writer != null) { writer.flush(); } } if (exception instanceof BizException && exception != null && ((BizException) exception).getCode() == BizException.AJAXEND) { logger.debug("ajax request success end"); } else { logger.error("handleException: ", exception); } } // ------------------------------------------------ Package Private Methods /** * Return the OGNL <tt>MemberAccess</tt>. This method performs a lazy load * of the MemberAccess object, using a {@link DefaultMemberAccess} instance. * * @return the OGNL <tt>MemberAccess</tt> */ MemberAccess getMemberAccess() { if (memberAccess == null) { memberAccess = new DefaultMemberAccess(true); } return memberAccess; } /** * Create a Click application ConfigService instance. * * @param servletContext the Servlet Context * @return a new application ConfigService instance * @throws Exception if an initialization error occurs */ @SuppressWarnings("unchecked") ConfigService createConfigService(ServletContext servletContext) throws Exception { Class<? extends ConfigService> serviceClass = XmlConfigService.class; String classname = servletContext.getInitParameter(CONFIG_SERVICE_CLASS); if (StringUtils.isNotBlank(classname)) { serviceClass = ClickUtils.classForName(classname); } return serviceClass.newInstance(); } /** * Initialize the Click application <tt>ConfigService</tt> instance and bind * it as a ServletContext attribute using the key * "<tt>org.apache.click.service.ConfigService</tt>". * <p/> * This method will use the configuration service class specified by the * {@link #CONFIG_SERVICE_CLASS} parameter, otherwise it will create a * {@link org.apache.click.service.XmlConfigService} instance. * * @param servletContext the servlet context to retrieve the * {@link #CONFIG_SERVICE_CLASS} from * @throws RuntimeException if the configuration service cannot be * initialized */ void initConfigService(ServletContext servletContext) { if (configService != null) { try { // Note this order is very important as components need to lookup // the configService out of the ServletContext while the service // is being initialized. servletContext.setAttribute(ConfigService.CONTEXT_NAME, configService); // Initialize the ConfigService instance configService.onInit(servletContext); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new RuntimeException(e); } } } } /** * Destroy the application configuration service instance and remove * it from the ServletContext attribute. * * @param servletContext the servlet context * @throws RuntimeException if the configuration service cannot be * destroyed */ void destroyConfigService(ServletContext servletContext) { if (configService != null) { try { configService.onDestroy(); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new RuntimeException(e); } } finally { servletContext.setAttribute(ConfigService.CONTEXT_NAME, null); } } } /** * 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 = configService.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); } } } } List<PageInterceptor> getThreadLocalInterceptors() { List<PageInterceptor> listeners = THREAD_LOCAL_INTERCEPTORS.get(); if (listeners != null) { return listeners; } else { return Collections.emptyList(); } } void setThreadLocalInterceptors(List<PageInterceptor> listeners) { THREAD_LOCAL_INTERCEPTORS.set(listeners); } /** * Retrieve a writer instance for the given context. * * @param response the servlet response * @return a writer instance * @throws IOException if an input or output exception occurred */ Writer getWriter(HttpServletResponse response) throws IOException { try { return response.getWriter(); } catch (IllegalStateException ignore) { // If writer cannot be retrieved fallback to OutputStream. CLK-644 return new OutputStreamWriter(response.getOutputStream(), response.getCharacterEncoding()); } } /** * Return a PrintWriter instance for the given response. * * @param response the servlet response * @return a PrintWriter instance */ PrintWriter getPrintWriter(HttpServletResponse response) throws IOException { Writer writer = getWriter(response); if (writer instanceof PrintWriter) { return (PrintWriter) writer; } return new PrintWriter(writer); } /** * Return true if this is an ajax request, false otherwise. * * @param request the servlet request * @return true if this is an ajax request, false otherwise */ boolean isAjaxRequest(HttpServletRequest request) { boolean isAjaxRequest = false; if (Context.hasThreadLocalContext()) { Context context = Context.getThreadLocalContext(); if (context.isAjaxRequest()) { isAjaxRequest = true; } } else { isAjaxRequest = ClickUtils.isAjaxRequest(request); } return isAjaxRequest; } // ---------------------------------------------------------- Inner Classes /** * Field iterator callback. */ static interface FieldCallback { /** * Callback method invoked for each field. * * @param fieldName the field name * @param fieldValue the field value */ public void processField(String fieldName, Object fieldValue); } // Private methods -------------------------------------------------------- /** * Resolve and return the Ajax target control for this request or null if no * Ajax target was found. * * @param context the request context * @param controlRegistry the control registry * @return the target Ajax target control or null if no Ajax target was found */ private Control resolveAjaxTargetControl(Context context, ControlRegistry controlRegistry) { Control ajaxTarget = null; if (logger.isTraceEnabled()) { logger.trace(" the following controls have been registered as potential Ajax targets:"); if (controlRegistry.hasAjaxTargetControls()) { for (Control control : controlRegistry.getAjaxTargetControls()) { HtmlStringBuffer buffer = new HtmlStringBuffer(); String controlClassName = ClassUtils.getShortClassName(control.getClass()); buffer.append(" ").append(controlClassName); buffer.append(": name='").append(control.getName()).append("'"); logger.trace(buffer.toString()); } } else { logger.trace(" *no* control has been registered"); } } for (Control control : controlRegistry.getAjaxTargetControls()) { if (control.isAjaxTarget(context)) { ajaxTarget = control; // The first matching control will be processed. Multiple matching // controls are not supported break; } } if (logger.isTraceEnabled()) { if (ajaxTarget == null) { String msg = " *no* target control was found for the Ajax request"; logger.trace(msg); } else { HtmlStringBuffer buffer = new HtmlStringBuffer(); buffer.append(" invoked: '"); buffer.append(ajaxTarget.getName()).append("' "); String className = ClassUtils.getShortClassName(ajaxTarget.getClass()); buffer.append(className); buffer.append(".isAjaxTarget() : true (Ajax target control found)"); logger.trace(buffer.toString()); } } return ajaxTarget; } /** * Log the request parameter names and values. * * @param request the HTTP servlet request */ private void logRequestParameters(HttpServletRequest request) { Map<String, String[]> requestParams = new TreeMap<String, String[]>(); Enumeration<?> e = request.getParameterNames(); while (e.hasMoreElements()) { String name = e.nextElement().toString(); String[] values = request.getParameterValues(name); requestParams.put(name, values); } for (Map.Entry<String, String[]> entry : requestParams.entrySet()) { String name = entry.getKey(); String[] values = entry.getValue(); HtmlStringBuffer buffer = new HtmlStringBuffer(40); buffer.append(" request param: " + name + "="); if (values == null) { // ignore } else if (values.length == 1) { buffer.append(ClickUtils.limitLength(values[0], 40)); } else { for (int i = 0; i < values.length; i++) { if (i == 0) { buffer.append('['); } else { buffer.append(", "); } buffer.append(ClickUtils.limitLength(values[i], 40)); } buffer.append("]"); } logger.trace(buffer.toString()); } } }