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.extras.spring; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServletRequest; import org.apache.click.ClickServlet; import org.apache.click.Page; import org.apache.click.util.HtmlStringBuffer; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; /** * Provides a Spring framework integration <tt>SpringClickServlet</tt>. * <p/> * This Spring integration servlet provides a number of integration options * using Spring with Click pages. These options are detailed below. * <p/> * <b>Stateful pages caveat:</b> please note that stateful pages do not work * with all options. * * <h3><a name="option1"></a>1. Spring instantiated Pages with @Component * configuration</h3> * * With this option Page classes are configured with Spring using the * @Component annotation. When the SpringClickServlet receives a page * request it converts the auto-mapped page class to the equivalent Spring * bean name and gets a new instance from the Spring ApplicationContext. * * <pre class="codeConfig"> * customer-list.htm -> com.mycorp.page.CustomerListPage -> customerListPage * HTML Request Click Page Class Spring Bean Name </pre> * * When using this strategy use the PageScopeResolver class to ensure new Page * instances are created with each request, rather than Spring's default * "singleton" creation policy. Please see the {@link PageScopeResolver} Javadoc * for more information on configuring this option. * <p/> * An example Page class is provided below which uses the Spring @Component annotation. * Note in this example page the customerService with the @Resource * annotation is injected by Spring after the page instance has been instantiated. * * <pre class="prettyprint"> * package com.mycorp.page; * * import javax.annotation.Resource; * import org.apache.click.Page; * import org.springframework.stereotype.Component; * * import com.mycorp.service.CustomerService; * * @Component * public class CustomerListPage extends Page { * * @Resource(name="customerService") * private CustomerService customerService; * * .. * } </pre> * * This is the most powerful and convenient Spring integration option, but does * require Spring 2.5.x or later. * * <p/> * <b><a name="stateful-page-caveat"></a>Stateful page caveat:</b> Spring beans * injected on stateful pages will be serialized along with the page, meaning * those beans must implement the Serializable interface. If you do * not want the beans to be serialized, they need to be marked as * <tt>transient</tt>. Transient beans won't be serialized but when the page * is deserialized, the transient beans won't be re-injected, causing a * NullPointerException when invoked. If you want to use transient beans on * stateful pages, see <a href="#option3">option 3</a> below. * * <h3><a name="option2"></a>2. Spring instantiated Pages with Spring XML * configuration</h3> * * With this option Page classes are configured using Spring XML configuration. * When the SpringClickServlet receives a page request it converts the auto-mapped * page class to the equivalent Spring bean name and gets a new instance from the * Spring ApplicationContext. * * <pre class="codeConfig"> * customer-list.htm -> com.mycorp.page.CustomerListPage -> customerListPage * HTML Request Click Page Class Spring Bean Name </pre> * * If the page bean is not found in the ApplicationContxt then the full Page * class name is used. * * <pre class="codeConfig"> * customer-list.htm -> com.mycorp.page.CustomerListPage -> com.mycorp.page.CustomerListPage * HTML Request Click Page Class Spring Bean Name </pre> * * This integration option requires you to configure all your Spring Page beans * in your Spring XML configuration. While this may be quite laborious, it does * support Spring 1.x or later. An example page bean configuration is * provided below: * * <pre class="prettyprint"> * <?xml version="1.0" encoding="UTF-8"?> * <beans> * * <bean id="customerListPage" class="com.mycorp.page.CustomerListPage" scope="prototype"/> * * </beans> </pre> * * <b>Please Note</b> ensure the page beans scope is set to "prototype" so a new * page instance will be created with every HTTP request. Otherwise Spring will * default to using singletons and your code will not be thread safe. * <p/> * <b>Stateful page caveat:</b> option 2 has the same caveat as * <a href="#stateful-page-caveat">option 1</a>. * * <h3><a name="option3"></a>3. Click instantiated Pages with injected Spring * beans and/or ApplicationContext</h3> * * With this integration option Click will instantiate page instances and * automatically inject any page properties which match Spring beans defined in * the ApplicationContext. In order to enable bean injection, you need to * configure the SpringClickServlet init parameter: * <a href="#inject-page-beans">inject-page-beans</a>. * <p/> * While this option is not as powerful as @Component configured pages it is * much more convenient than Spring XML configured pages and supports Spring 1.x. * You can also use annotation based injection which requires Spring 2.5.x * or later. * <p/> * An example Page class is provided below which has the customerService property * automatically injected by the SpringClickServlet. Note the customerService * property will need to be defined in a Spring XML configuration. * * <pre class="prettyprint"> * package com.mycorp.page; * * import org.apache.click.Page; * * import com.mycorp.service.CustomerService; * * public class CustomerListPage extends Page { * * private CustomerService customerService; * * public void setCustomerService(CustomerService customerService) { * this.customerService = customerService; * } * * .. * } </pre> * * Page property bean name must match the bean name defined in the Spring XML * configuration. Continuing our example the Spring XML configuration is provided * below: * * <pre class="prettyprint"> * <?xml version="1.0" encoding="UTF-8"?> * <beans> * * <bean id="customerService" class="com.mycorp.service.CustomerService"/> * * </beans> </pre> * * This option will also automatically inject the ApplicationContext into * page instances which implement the {@link org.springframework.context.ApplicationContextAware} * interface. Using the applicationContext you can lookup Spring beans manually * in your pages. For example: * * <pre class="prettyprint"> * public class CustomerListPage extends Page implements ApplicationContextAware { * * protected ApplicationContext applicationContext; * * public void setApplicationContext(ApplicationContext applicationContext) { * this.applicationContext = applicationContext; * } * * public CustomerService getCustomerService() { * return (CustomerService) applicationContext.getBean("customerService"); * } * } </pre> * * This last strategy is probably the least convenient integration option. * * <h4><a name="option31"></a>3.1 Spring beans and Stateful pages</h4> * * Stateful pages are stored in the HttpSession and Spring beans referenced * by a stateful page must implement the Serializable interface. If you do not * want beans to be serialized they can be marked as <tt>transient</tt>. * Transient beans won't be serialized to disk. However once the page is * deserialized the transient beans will need to be injected again. * <p/> * <a href="#option3">Option 3</a> will re-inject Spring beans and the * ApplicationContext after every request. This allows beans to be marked as * <tt>transient</tt> and still function properly when used with stateful pages. * * <pre class="prettyprint"> * package com.mycorp.page; * * import org.apache.click.Page; * * import com.mycorp.service.CustomerService; * * public class CustomerListPage extends Page implements ApplicationContextAware { * * // Note the transient keyword * private transient CustomerService customerService; * * protected transient ApplicationContext applicationContext; * * public CustomerListPage { * // Page is marked as stateful * setStateful(true); * } * * // Inject the customer service * public void setCustomerService(CustomerService customerService) { * this.customerService = customerService; * } * * public CustomerService getCustomerService() { * return (CustomerService) applicationContext.getBean("customerService"); * } * * // Inject Spring's ApplicationContext * public void setApplicationContext(ApplicationContext applicationContext) { * this.applicationContext = applicationContext; * } * * .. * } </pre> * * <h3>Servlet Configuration</h3> * * The SpringClickServlet can obtain the ApplicationContext either from * {@link org.springframework.web.context.support.WebApplicationContextUtils} which is configured with a * {@link org.springframework.web.context.ContextLoaderListener}. For example: * * <pre class="codeConfig"> * <?xml version="1.0" encoding="UTF-8"?> * <web-app> * * <listener> * <listener-class> * <span class="blue">org.springframework.web.context.ContextLoaderListener</span> * </listener-class> * </listener> * * <servlet> * <servlet-name>SpringClickServlet</servlet-name> * <servlet-class>org.apache.click.extras.spring.SpringClickServlet</servlet-class> * <load-on-startup>0</load-on-startup> * </servlet> * * .. * * </web-app> </pre> * * Alternatively you can specify the path to the ApplicationContext as a * servlet init parameter. For example: * * <pre class="codeConfig"> * <?xml version="1.0" encoding="UTF-8"?> * <web-app> * * <servlet> * <servlet-name>SpringClickServlet</servlet-name> * <servlet-class>org.apache.click.extras.spring.SpringClickServlet</servlet-class> * <init-param> * <param-name><span class="blue">spring-path</span></param-name> * <param-value><span class="red">/applicationContext.xml</span></param-value> * </init-param> * <load-on-startup>0</load-on-startup> * </servlet> * * .. * * </web-app> </pre> * * <a name="inject-page-beans"></a>To configure page Spring bean injection * (<a href="#option3">option 3</a> above), you need to configure the * <span class="blue">inject-page-beans</span> servlet init parameter. For example: * * <pre class="codeConfig"> * <?xml version="1.0" encoding="UTF-8"?> * <web-app> * * .. * * <servlet> * <servlet-name>SpringClickServlet</servlet-name> * <servlet-class>org.apache.click.extras.spring.SpringClickServlet</servlet-class> * <init-param> * <param-name><span class="blue">inject-page-beans</span></param-name> * <param-value><span class="red">true</span></param-value> * </init-param> * <load-on-startup>0</load-on-startup> * </servlet> * * .. * * </web-app> </pre> * * @see PageScopeResolver */ public class SpringClickServlet extends ClickServlet { private static final long serialVersionUID = 1L; /** * The Servlet initialization parameter name for the option to have the * SpringClickServlet inject Spring beans into page instances: * <tt>"inject-page-beans"</tt>. */ public static final String INJECT_PAGE_BEANS = "inject-page-beans"; /** * The Servlet initialization parameter name for the path to the Spring XML * application context definition file: <tt>"spring-path"</tt>. */ public static final String SPRING_PATH = "spring-path"; /** The set of setter methods to ignore. */ static final Set<String> SETTER_METHODS_IGNORE_SET = new HashSet<String>(); // Initialize the setter method ignore set static { SETTER_METHODS_IGNORE_SET.add("setApplicationContext"); SETTER_METHODS_IGNORE_SET.add("setFormat"); SETTER_METHODS_IGNORE_SET.add("setForward"); SETTER_METHODS_IGNORE_SET.add("setHeader"); SETTER_METHODS_IGNORE_SET.add("setHeaders"); SETTER_METHODS_IGNORE_SET.add("setPageImports"); SETTER_METHODS_IGNORE_SET.add("setPath"); SETTER_METHODS_IGNORE_SET.add("setStateful"); SETTER_METHODS_IGNORE_SET.add("setRedirect"); SETTER_METHODS_IGNORE_SET.add("setTemplate"); } /** Spring application context bean factory. */ protected ApplicationContext applicationContext; /** The list of page injectable Spring beans, keyed on page class name. */ protected Map<Class<? extends Page>, List<BeanNameAndMethod>> pageSetterBeansMap = new HashMap<Class<? extends Page>, List<BeanNameAndMethod>>(); // Public Methods ---------------------------------------------------------- /** * Initialize the SpringClickServlet and the Spring application context * bean factory. An Spring <tt>ClassPathXmlApplicationContext</tt> bean * factory is used and initialize with the servlet <tt>init-param</tt> * named <tt>"spring-path"</tt>. * * @see ClickServlet#init() * * @throws ServletException if the click app could not be initialized */ @Override public void init() throws ServletException { super.init(); ServletContext servletContext = getServletContext(); applicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext); if (applicationContext == null) { String springPath = getInitParameter(SPRING_PATH); if (springPath == null) { String msg = SPRING_PATH + " servlet init parameter not defined"; throw new UnavailableException(msg); } applicationContext = new ClassPathXmlApplicationContext(springPath); } String injectPageBeans = getInitParameter(INJECT_PAGE_BEANS); if ("true".equalsIgnoreCase(injectPageBeans)) { // Process page classes looking for setter methods which match beans // available in the applicationContext List<Class<? extends Page>> pageClassList = getConfigService().getPageClassList(); for (Class<? extends Page> pageClass : pageClassList) { loadSpringBeanSetterMethods(pageClass); } } } // Protected Methods ------------------------------------------------------ /** * Create a new Spring Page bean if defined in the application context, or * a new Page instance otherwise. * <p/> * If the "inject-page-beans" option is enabled this method will inject * any Spring beans matching the Page's properties. * * @see ClickServlet#newPageInstance(String, Class, HttpServletRequest) * * @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 */ @Override protected Page newPageInstance(String path, Class<? extends Page> pageClass, HttpServletRequest request) throws Exception { Page page = null; String beanName = toBeanName(pageClass); if (getApplicationContext().containsBean(beanName)) { page = (Page) getApplicationContext().getBean(beanName); } else if (getApplicationContext().containsBean(pageClass.getName())) { page = (Page) getApplicationContext().getBean(pageClass.getName()); } else { page = pageClass.newInstance(); } return page; } /** * Return the configured Spring application context. * * @return the configured Spring application context. */ protected ApplicationContext getApplicationContext() { return applicationContext; } /** * This method associates the <tt>ApplicationContext</tt> with any * <tt>ApplicationContextAware</tt> pages and supports the deserialization * of stateful pages. * * @see ClickServlet#activatePageInstance(Page) * * @param page the page instance to activate */ @Override protected void activatePageInstance(Page page) { ApplicationContext applicationContext = getApplicationContext(); if (page instanceof ApplicationContextAware) { ApplicationContextAware aware = (ApplicationContextAware) page; aware.setApplicationContext(applicationContext); } Class<? extends Page> pageClass = page.getClass(); String beanName = toBeanName(pageClass); if (applicationContext.containsBean(beanName) || applicationContext.containsBean(pageClass.getName())) { // Beans are injected through Spring } else { // Inject any Spring beans into the page instance if (!pageSetterBeansMap.isEmpty()) { // In development mode, lazily loaded page classes won't have // their bean setters methods mapped, thus beans won't be injected List<BeanNameAndMethod> beanList = pageSetterBeansMap.get(page.getClass()); if (beanList != null) { for (BeanNameAndMethod bnam : beanList) { Object bean = applicationContext.getBean(bnam.beanName); try { Object[] args = { bean }; bnam.method.invoke(page, args); } catch (Exception error) { throw new RuntimeException(error); } } } } } } /** * Return the Spring beanName for the given class. * * @param aClass the class to get the Spring bean name from * @return the class bean name */ protected String toBeanName(Class<?> aClass) { String className = aClass.getName(); String beanName = className.substring(className.lastIndexOf(".") + 1); return Character.toLowerCase(beanName.charAt(0)) + beanName.substring(1); } // Package Private Inner Classes ------------------------------------------ /** * Provides a Spring bean name and page bean property setter method holder. */ static class BeanNameAndMethod { /** The Spring bean name. */ protected final String beanName; /** The page bean property setter method. */ protected final Method method; /** * Create a new String bean name and page setter method object. * * @param beanName the spring bean name * @param method the page setter method for the bean */ protected BeanNameAndMethod(String beanName, Method method) { this.beanName = beanName; this.method = method; } } // Private Methods -------------------------------------------------------- /** * Load the pageClass bean setter methods * * @param pageClass the page class */ private void loadSpringBeanSetterMethods(Class<? extends Page> pageClass) { Method[] methods = pageClass.getMethods(); for (int j = 0; j < methods.length; j++) { Method method = methods[j]; String methodName = method.getName(); if (methodName.startsWith("set") && !SETTER_METHODS_IGNORE_SET.contains(methodName) && method.getParameterTypes().length == 1) { // Get the bean name from the setter method name HtmlStringBuffer buffer = new HtmlStringBuffer(); buffer.append(Character.toLowerCase(methodName.charAt(3))); buffer.append(methodName.substring(4)); String beanName = buffer.toString(); // If Spring contains the bean then cache in map list if (getApplicationContext().containsBean(beanName)) { List<BeanNameAndMethod> beanList = pageSetterBeansMap.get(pageClass); if (beanList == null) { beanList = new ArrayList<BeanNameAndMethod>(); pageSetterBeansMap.put(pageClass, beanList); } beanList.add(new BeanNameAndMethod(beanName, method)); } } } } }