/* | |
* 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.service; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Modifier; | |
import java.net.MalformedURLException; | |
import java.net.URL; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.Date; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Locale; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.StringTokenizer; | |
import java.util.TreeMap; | |
import javax.servlet.ServletContext; | |
import org.apache.click.Control; | |
import org.apache.click.Page; | |
import org.apache.click.PageInterceptor; | |
import org.apache.click.util.Bindable; | |
import org.apache.click.util.ClickUtils; | |
import org.apache.click.util.Format; | |
import org.apache.click.util.HtmlStringBuffer; | |
import org.apache.commons.lang.StringUtils; | |
import org.apache.commons.lang.Validate; | |
import org.w3c.dom.Document; | |
import org.w3c.dom.Element; | |
import org.xml.sax.EntityResolver; | |
import org.xml.sax.InputSource; | |
import org.xml.sax.SAXException; | |
/** | |
* Provides a Click XML configuration service class. | |
* <p/> | |
* This class reads Click configuration information from a file named | |
* <tt>click.xml</tt>. The service will first lookup the <tt>click.xml</tt> | |
* under the applications <tt>WEB-INF</tt> directory, and if not found | |
* attempt to load the configuration file from the classpath root. | |
* <p/> | |
* Configuring Click through the <tt>click.xml</tt> file is the most common | |
* technique. | |
* <p/> | |
* However you can instruct Click to use a different service implementation. | |
* Please see {@link ConfigService} for more details. | |
*/ | |
public class XmlConfigService implements ConfigService, EntityResolver { | |
/** The name of the Click logger: "<tt>org.apache.click</tt>". */ | |
static final String CLICK_LOGGER = "org.apache.click"; | |
/** The click deployment directory path: "/click". */ | |
static final String CLICK_PATH = "/click"; | |
/** The default common page headers. */ | |
static final Map<String, Object> DEFAULT_HEADERS; | |
/** | |
* The default velocity properties filename: | |
* "<tt>/WEB-INF/velocity.properties</tt>". | |
*/ | |
static final String DEFAULT_VEL_PROPS = "/WEB-INF/velocity.properties"; | |
/** The click DTD file name: "<tt>click.dtd</tt>". */ | |
static final String DTD_FILE_NAME = "click.dtd"; | |
/** | |
* The resource path of the click DTD file: | |
* "<tt>/org/apache/click/click.dtd</tt>". | |
*/ | |
static final String DTD_FILE_PATH = "/org/apache/click/" + DTD_FILE_NAME; | |
/** | |
* The user supplied macro file name: "<tt>macro.vm</tt>". | |
*/ | |
static final String MACRO_VM_FILE_NAME = "macro.vm"; | |
/** The production application mode. */ | |
static final int PRODUCTION = 0; | |
/** The profile application mode. */ | |
static final int PROFILE = 1; | |
/** The development application mode. */ | |
static final int DEVELOPMENT = 2; | |
/** The debug application mode. */ | |
static final int DEBUG = 3; | |
/** The trace application mode. */ | |
static final int TRACE = 4; | |
static final String[] MODE_VALUES = | |
{ "production", "profile", "development", "debug", "trace" }; | |
private static final Object PAGE_LOAD_LOCK = new Object(); | |
/** | |
* The name of the Velocity logger: "<tt>org.apache.velocity</tt>". | |
*/ | |
static final String VELOCITY_LOGGER = "org.apache.velocity"; | |
/** | |
* The global Velocity macro file name: | |
* "<tt>VM_global_library.vm</tt>". | |
*/ | |
static final String VM_FILE_NAME = "VM_global_library.vm"; | |
/** Initialize the default headers. */ | |
static { | |
DEFAULT_HEADERS = new HashMap<String, Object>(); | |
DEFAULT_HEADERS.put("Pragma", "no-cache"); | |
DEFAULT_HEADERS.put("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"); | |
DEFAULT_HEADERS.put("Expires", new Date(1L)); | |
} | |
private static final String GOOGLE_APP_ENGINE = "Google App Engine"; | |
// ------------------------------------------------ Package Private Members | |
/** The Map of global page headers. */ | |
Map commonHeaders; | |
/** The page automapping override page class for path list. */ | |
final List excludesList = new ArrayList(); | |
/** The map of ClickApp.PageElm keyed on path. */ | |
final Map pageByPathMap = new HashMap(); | |
/** The map of ClickApp.PageElm keyed on class. */ | |
final Map pageByClassMap = new HashMap(); | |
/** The list of page packages. */ | |
final List pagePackages = new ArrayList(); | |
// -------------------------------------------------------- Private Members | |
/** The automatically bind controls, request parameters and models flag. */ | |
private AutoBinding autobinding; | |
/** The Commons FileUpload service class. */ | |
private FileUploadService fileUploadService; | |
/** The format class. */ | |
private Class<? extends Format> formatClass; | |
/** The character encoding of this application. */ | |
private String charset; | |
/** The default application locale.*/ | |
private Locale locale; | |
/** The application log service. */ | |
private LogService logService; | |
/** | |
* The application mode: | |
* [ PRODUCTION | PROFILE | DEVELOPMENT | DEBUG | TRACE ]. | |
*/ | |
private int mode; | |
/** The list of application page interceptor instances. */ | |
private List<PageInterceptorConfig> pageInterceptorConfigList | |
= new ArrayList<PageInterceptorConfig>(); | |
/** The ServletContext instance. */ | |
private ServletContext servletContext; | |
/** The application PropertyService. */ | |
private PropertyService propertyService; | |
/** The application ResourceService. */ | |
private ResourceService resourceService; | |
/** The application TemplateService. */ | |
private TemplateService templateService; | |
/** The application TemplateService. */ | |
private MessagesMapService messagesMapService; | |
/** Flag indicating whether Click is running on Google App Engine. */ | |
private boolean onGoogleAppEngine = false; | |
// --------------------------------------------------------- Public Methods | |
/** | |
* @see ConfigService#onInit(ServletContext) | |
* | |
* @param servletContext the application servlet context | |
* @throws Exception if an error occurs initializing the application | |
*/ | |
public void onInit(ServletContext servletContext) throws Exception { | |
Validate.notNull(servletContext, "Null servletContext parameter"); | |
this.servletContext = servletContext; | |
onGoogleAppEngine = servletContext.getServerInfo().startsWith(GOOGLE_APP_ENGINE); | |
// Set default logService early to log errors when services fail. | |
logService = new ConsoleLogService(); | |
messagesMapService = new DefaultMessagesMapService(); | |
InputStream inputStream = ClickUtils.getClickConfig(servletContext); | |
try { | |
Document document = ClickUtils.buildDocument(inputStream, this); | |
Element rootElm = document.getDocumentElement(); | |
// Load the log service | |
loadLogService(rootElm); | |
// Load the application mode and set the logger levels | |
loadMode(rootElm); | |
if (logService.isInfoEnabled()) { | |
logService.info("*** Initializing Click " + ClickUtils.getClickVersion() | |
+ " in " + getApplicationMode() + " mode ***"); | |
String msg = "initialized LogService: " + logService.getClass().getName(); | |
getLogService().info(msg); | |
} | |
// Deploy click resources | |
deployFiles(rootElm); | |
// Load the format class | |
loadFormatClass(rootElm); | |
// Load the common headers | |
loadHeaders(rootElm); | |
// Load the pages | |
loadPages(rootElm); | |
// Load the error and not-found pages | |
loadDefaultPages(); | |
// Load the charset | |
loadCharset(rootElm); | |
// Load the locale | |
loadLocale(rootElm); | |
// Load the Property service | |
loadPropertyService(rootElm); | |
// Load the File Upload service | |
loadFileUploadService(rootElm); | |
// Load the Templating service | |
loadTemplateService(rootElm); | |
// Load the Resource service | |
loadResourceService(rootElm); | |
// Load the Messages Map service | |
loadMessagesMapService(rootElm); | |
// Load the PageInterceptors | |
loadPageInterceptors(rootElm); | |
} finally { | |
ClickUtils.close(inputStream); | |
} | |
} | |
/** | |
* @see ConfigService#onDestroy() | |
*/ | |
public void onDestroy() { | |
if (getFileUploadService() != null) { | |
getFileUploadService().onDestroy(); | |
} | |
if (getPropertyService() != null) { | |
getPropertyService().onDestroy(); | |
} | |
if (getTemplateService() != null) { | |
getTemplateService().onDestroy(); | |
} | |
if (getResourceService() != null) { | |
getResourceService().onDestroy(); | |
} | |
if (getMessagesMapService() != null) { | |
getMessagesMapService().onDestroy(); | |
} | |
if (getLogService() != null) { | |
getLogService().onDestroy(); | |
} | |
} | |
// --------------------------------------------------------- Public Methods | |
/** | |
* Return the application mode String value: <tt>["production", | |
* "profile", "development", "debug"]</tt>. | |
* | |
* @return the application mode String value | |
*/ | |
public String getApplicationMode() { | |
return MODE_VALUES[mode]; | |
} | |
/** | |
* @see ConfigService#getCharset() | |
* | |
* @return the application character encoding | |
*/ | |
public String getCharset() { | |
return charset; | |
} | |
/** | |
* @see ConfigService#getFileUploadService() | |
* | |
* @return the FileUpload service | |
*/ | |
public FileUploadService getFileUploadService() { | |
return fileUploadService; | |
} | |
/** | |
* @see ConfigService#getLogService() | |
* | |
* @return the application log service. | |
*/ | |
public LogService getLogService() { | |
return logService; | |
} | |
/** | |
* @see ConfigService#getProperyService() | |
* | |
* @return the application property service. | |
*/ | |
public PropertyService getPropertyService() { | |
// TODO | |
return new OGNLPropertyService(); | |
} | |
/** | |
* @see ConfigService#getResourceService() | |
* | |
* @return the resource service | |
*/ | |
public ResourceService getResourceService() { | |
return resourceService; | |
} | |
/** | |
* @see ConfigService#getTemplateService() | |
* | |
* @return the template service | |
*/ | |
public TemplateService getTemplateService() { | |
return templateService; | |
} | |
/** | |
* @see ConfigService#getMessagesMapService() | |
* | |
* @return the messages map service | |
*/ | |
public MessagesMapService getMessagesMapService() { | |
return messagesMapService; | |
} | |
/** | |
* @see ConfigService#createFormat() | |
* | |
* @return a new format object | |
*/ | |
public Format createFormat() { | |
try { | |
return formatClass.newInstance(); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
/** | |
* @see ConfigService#getLocale() | |
* | |
* @return the application locale | |
*/ | |
public Locale getLocale() { | |
return locale; | |
} | |
/** | |
* @see ConfigService#getAutoBindingMode() | |
* | |
* @return the Page field auto binding mode { PUBLIC, ANNOTATION, NONE } | |
*/ | |
public AutoBinding getAutoBindingMode() { | |
return autobinding; | |
} | |
/** | |
* @see ConfigService#isProductionMode() | |
* | |
* @return true if the application is in "production" mode | |
*/ | |
public boolean isProductionMode() { | |
return (mode == PRODUCTION); | |
} | |
/** | |
* @see ConfigService#isProfileMode() | |
* | |
* @return true if the application is in "profile" mode | |
*/ | |
public boolean isProfileMode() { | |
return (mode == PROFILE); | |
} | |
/** | |
* @see ConfigService#isJspPage(String) | |
* | |
* @param path the Page ".htm" path | |
* @return true if JSP exists for the given ".htm" path | |
*/ | |
public boolean isJspPage(String path) { | |
HtmlStringBuffer buffer = new HtmlStringBuffer(); | |
int index = StringUtils.lastIndexOf(path, "."); | |
if (index > 0) { | |
buffer.append(path.substring(0, index)); | |
} else { | |
buffer.append(path); | |
} | |
buffer.append(".jsp"); | |
return pageByPathMap.containsKey(buffer.toString()); | |
} | |
/** | |
* Return true if the given path is a Page class template, false | |
* otherwise. By default this method returns true if the path has a | |
* <tt>.htm</tt> or <tt>.jsp</tt> extension. | |
* <p/> | |
* If you want to map alternative templates besides <tt>.htm</tt> and | |
* <tt>.jsp</tt> files you can override this method and provide extra | |
* checks against the given path whether it should be added as a | |
* template or not. | |
* <p/> | |
* Below is an example showing how to allow <tt>.xml</tt> paths to | |
* be recognized as Page class templates. | |
* | |
* <pre class="prettyprint"> | |
* public class MyConfigService extends XmlConfigService { | |
* | |
* protected boolean isTemplate(String path) { | |
* // invoke default implementation | |
* boolean isTemplate = super.isTemplate(path); | |
* | |
* if (!isTemplate) { | |
* // If path has an .xml extension, mark it as a template | |
* isTemplate = path.endsWith(".xml"); | |
* } | |
* return isTemplate; | |
* } | |
* } </pre> | |
* | |
* Here is an example <tt>web.xml</tt> showing how to configure a custom | |
* ConfigService through the context parameter <tt>config-service-class</tt>. | |
* We also map <tt>*.xml</tt> requests to be routed through ClickServlet: | |
* | |
* <pre class="prettyprint"> | |
* <web-app xmlns="http://java.sun.com/xml/ns/j2ee" | |
* xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
* xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" | |
* version="2.4"> | |
* | |
* <!-- Specify a custom ConfigService through the context param 'config-service-class' --> | |
* <context-param> | |
* <param-name>config-service-class</param-name> | |
* <param-value>com.mycorp.service.MyConfigSerivce</param-value> | |
* </context-param> | |
* | |
* <servlet> | |
* <servlet-name>ClickServlet</servlet-name> | |
* <servlet-class>org.apache.click.ClickServlet</servlet-class> | |
* <load-on-startup>0</load-on-startup> | |
* </servlet> | |
* | |
* <!-- NOTE: we still map the .htm extension --> | |
* <servlet-mapping> | |
* <servlet-name>ClickServlet</servlet-name> | |
* <url-pattern>*.htm</url-pattern> | |
* </servlet-mapping> | |
* | |
* <!-- NOTE: we also map .xml extension in order to route xml requests to the ClickServlet --> | |
* <servlet-mapping> | |
* <servlet-name>ClickServlet</servlet-name> | |
* <url-pattern>*.xml</url-pattern> | |
* </servlet-mapping> | |
* | |
* ... | |
* | |
* </web-app> </pre> | |
* | |
* <b>Please note</b>: even though you can add extra template mappings by | |
* overriding this method, it is still recommended to keep the default | |
* <tt>.htm</tt> mapping by invoking <tt>super.isTemplate(String)</tt>. | |
* The reason being that Click ships with some default templates such as | |
* {@link ConfigService#ERROR_PATH} and {@link ConfigService#NOT_FOUND_PATH} | |
* that must be mapped as <tt>.htm</tt>. | |
* <p/> | |
* Please see the ConfigService <a href="#config">javadoc</a> for details | |
* on how to configure a custom ConfigService implementation. | |
* | |
* @see ConfigService#isTemplate(String) | |
* | |
* @param path the path to check if it is a Page class template or not | |
* @return true if the path is a Page class template, false otherwise | |
*/ | |
public boolean isTemplate(String path) { | |
if (path.endsWith(".htm") || path.endsWith(".jsp")) { | |
return true; | |
} | |
return false; | |
} | |
/** | |
* @see ConfigService#getPageClass(String) | |
* | |
* @param path the page path | |
* @return the page class for the given path or null if no class is found | |
*/ | |
public Class<? extends Page> getPageClass(String path) { | |
// If in production or profile mode. | |
if (mode <= PROFILE) { | |
PageElm page = (PageElm) pageByPathMap.get(path); | |
if (page == null) { | |
String jspPath = StringUtils.replace(path, ".htm", ".jsp"); | |
page = (PageElm) pageByPathMap.get(jspPath); | |
} | |
if (page != null) { | |
return page.getPageClass(); | |
} else { | |
return null; | |
} | |
// Else in development, debug or trace mode | |
} else { | |
synchronized (PAGE_LOAD_LOCK) { | |
PageElm page = (PageElm) pageByPathMap.get(path); | |
if (page == null) { | |
String jspPath = StringUtils.replace(path, ".htm", ".jsp"); | |
page = (PageElm) pageByPathMap.get(jspPath); | |
} | |
if (page != null) { | |
return page.getPageClass(); | |
} | |
Class pageClass = null; | |
try { | |
URL resource = servletContext.getResource(path); | |
if (resource != null) { | |
for (int i = 0; i < pagePackages.size(); i++) { | |
String pagesPackage = pagePackages.get(i).toString(); | |
pageClass = getPageClass(path, pagesPackage); | |
if (pageClass != null) { | |
page = new PageElm(path, | |
pageClass, | |
commonHeaders, | |
autobinding); | |
pageByPathMap.put(page.getPath(), page); | |
addToClassMap(page); | |
if (logService.isDebugEnabled()) { | |
String msg = path + " -> " + pageClass.getName(); | |
logService.debug(msg); | |
} | |
break; | |
} | |
} | |
} | |
} catch (MalformedURLException e) { | |
//ignore | |
} | |
return pageClass; | |
} | |
} | |
} | |
/** | |
* @see ConfigService#getPagePath(Class) | |
* | |
* @param pageClass the page class | |
* @return path the page path or null if no path is found | |
* @throws IllegalArgumentException if the Page Class is not configured | |
* with a unique path | |
*/ | |
public String getPagePath(Class<? extends Page> pageClass) { | |
Object object = pageByClassMap.get(pageClass); | |
if (object instanceof XmlConfigService.PageElm) { | |
XmlConfigService.PageElm page = (XmlConfigService.PageElm) object; | |
return page.getPath(); | |
} else if (object instanceof List) { | |
HtmlStringBuffer buffer = new HtmlStringBuffer(); | |
buffer.append("Page class resolves to multiple paths: "); | |
buffer.append(pageClass.getName()); | |
buffer.append(" -> ["); | |
for (Iterator it = ((List) object).iterator(); it.hasNext();) { | |
PageElm pageElm = (PageElm) it.next(); | |
buffer.append(pageElm.getPath()); | |
if (it.hasNext()) { | |
buffer.append(", "); | |
} | |
} | |
buffer.append("]\nUse Context.createPage(String), or Context.getPageClass(String) instead."); | |
throw new IllegalArgumentException(buffer.toString()); | |
} else { | |
return null; | |
} | |
} | |
/** | |
* @see ConfigService#getPageClassList() | |
* | |
* @return the list of configured page classes | |
*/ | |
public List<Class<? extends Page>> getPageClassList() { | |
List<Class<? extends Page>> classList = | |
new ArrayList<Class<? extends Page>>(pageByClassMap.size()); | |
Iterator i = pageByClassMap.keySet().iterator(); | |
while (i.hasNext()) { | |
Class pageClass = (Class) i.next(); | |
classList.add(pageClass); | |
} | |
return classList; | |
} | |
/** | |
* @see ConfigService#getPageHeaders(String) | |
* | |
* @param path the path of the page | |
* @return a Map of headers for the given page path | |
*/ | |
public Map<String, Object> getPageHeaders(String path) { | |
PageElm page = (PageElm) pageByPathMap.get(path); | |
if (page == null) { | |
String jspPath = StringUtils.replace(path, ".htm", ".jsp"); | |
page = (PageElm) pageByPathMap.get(jspPath); | |
} | |
if (page != null) { | |
return page.getHeaders(); | |
} else { | |
return null; | |
} | |
} | |
/** | |
* @see ConfigService#getNotFoundPageClass() | |
* | |
* @return the page not found <tt>Page</tt> <tt>Class</tt> | |
*/ | |
public Class<? extends Page> getNotFoundPageClass() { | |
PageElm page = (PageElm) pageByPathMap.get(NOT_FOUND_PATH); | |
if (page != null) { | |
return page.getPageClass(); | |
} else { | |
return org.apache.click.Page.class; | |
} | |
} | |
/** | |
* @see ConfigService#getErrorPageClass() | |
* | |
* @return the error handling page <tt>Page</tt> <tt>Class</tt> | |
*/ | |
public Class<? extends Page> getErrorPageClass() { | |
PageElm page = (PageElm) pageByPathMap.get(ERROR_PATH); | |
if (page != null) { | |
return page.getPageClass(); | |
} else { | |
return org.apache.click.util.ErrorPage.class; | |
} | |
} | |
/** | |
* @see ConfigService#getPageField(Class, String) | |
* | |
* @param pageClass the page class | |
* @param fieldName the name of the field | |
* @return the public field of the pageClass with the given name or null | |
*/ | |
public Field getPageField(Class<? extends Page> pageClass, String fieldName) { | |
return getPageFields(pageClass).get(fieldName); | |
} | |
/** | |
* @see ConfigService#getPageFieldArray(Class) | |
* | |
* @param pageClass the page class | |
* @return an array public fields for the given page class | |
*/ | |
public Field[] getPageFieldArray(Class<? extends Page> pageClass) { | |
Object object = pageByClassMap.get(pageClass); | |
if (object instanceof XmlConfigService.PageElm) { | |
XmlConfigService.PageElm page = (XmlConfigService.PageElm) object; | |
return page.getFieldArray(); | |
} else if (object instanceof List) { | |
List list = (List) object; | |
XmlConfigService.PageElm page = (XmlConfigService.PageElm) list.get(0); | |
return page.getFieldArray(); | |
} else { | |
return null; | |
} | |
} | |
/** | |
* @see ConfigService#getPageFields(Class) | |
* | |
* @param pageClass the page class | |
* @return a Map of public fields for the given page class | |
*/ | |
public Map<String, Field> getPageFields(Class<? extends Page> pageClass) { | |
Object object = pageByClassMap.get(pageClass); | |
if (object instanceof XmlConfigService.PageElm) { | |
XmlConfigService.PageElm page = (XmlConfigService.PageElm) object; | |
return page.getFields(); | |
} else if (object instanceof List) { | |
List list = (List) object; | |
XmlConfigService.PageElm page = (XmlConfigService.PageElm) list.get(0); | |
return page.getFields(); | |
} else { | |
return Collections.emptyMap(); | |
} | |
} | |
/** | |
* @see ConfigService#getPageInterceptors() | |
* | |
* @return the list of configured PageInterceptor instances | |
*/ | |
public List<PageInterceptor> getPageInterceptors() { | |
if (pageInterceptorConfigList.isEmpty()) { | |
return Collections.emptyList(); | |
} | |
List<PageInterceptor> interceptorList = | |
new ArrayList<PageInterceptor>(pageInterceptorConfigList.size()); | |
for (PageInterceptorConfig pageInterceptorConfig : pageInterceptorConfigList) { | |
interceptorList.add(pageInterceptorConfig.getPageInterceptor()); | |
} | |
return interceptorList; | |
} | |
/** | |
* @see ConfigService#getServletContext() | |
* | |
* @return the application servlet context | |
*/ | |
public ServletContext getServletContext() { | |
return servletContext; | |
} | |
/** | |
* This method resolves the click.dtd for the XML parser using the | |
* classpath resource: <tt>/org/apache/click/click.dtd</tt>. | |
* | |
* @see EntityResolver#resolveEntity(String, String) | |
* | |
* @param publicId the DTD public id | |
* @param systemId the DTD system id | |
* @return resolved entity DTD input stream | |
* @throws SAXException if an error occurs parsing the document | |
* @throws IOException if an error occurs reading the document | |
*/ | |
public InputSource resolveEntity(String publicId, String systemId) | |
throws SAXException, IOException { | |
InputStream inputStream = ClickUtils.getResourceAsStream(DTD_FILE_PATH, getClass()); | |
if (inputStream != null) { | |
return new InputSource(inputStream); | |
} else { | |
throw new IOException("could not load resource: " + DTD_FILE_PATH); | |
} | |
} | |
// ------------------------------------------------------ Protected Methods | |
/** | |
* Find and return the page class for the specified pagePath and | |
* pagesPackage. | |
* <p/> | |
* For example if the pagePath is <tt>'/edit-customer.htm'</tt> and | |
* package is <tt>'com.mycorp'</tt>, the matching page class will be: | |
* <tt>com.mycorp.EditCustomer</tt> or <tt>com.mycorp.EditCustomerPage</tt>. | |
* <p/> | |
* If the page path is <tt>'/admin/add-customer.htm'</tt> and package is | |
* <tt>'com.mycorp'</tt>, the matching page class will be: | |
* <tt>com.mycorp.admin.AddCustomer</tt> or | |
* <tt>com.mycorp.admin.AddCustomerPage</tt>. | |
* | |
* @param pagePath the path used for matching against a page class name | |
* @param pagesPackage the package of the page class | |
* @return the page class for the specified pagePath and pagesPackage | |
*/ | |
protected Class<? extends Page> getPageClass(String pagePath, String pagesPackage) { | |
// To understand this method lets walk through an example as the | |
// code plays out. Imagine this method is called with the arguments: | |
// pagePath='/pages/edit-customer.htm' | |
// pagesPackage='org.apache.click' | |
String packageName = ""; | |
if (StringUtils.isNotBlank(pagesPackage)) { | |
// Append period after package | |
// packageName = 'org.apache.click.' | |
packageName = pagesPackage + "."; | |
} | |
String className = ""; | |
// Strip off extension. | |
// path = '/pages/edit-customer' | |
String path = pagePath.substring(0, pagePath.lastIndexOf(".")); | |
// If page is excluded return the excluded class | |
Class<? extends Page> excludePageClass = getExcludesPageClass(path); | |
if (excludePageClass != null) { | |
return excludePageClass; | |
} | |
// Build complete packageName. | |
// packageName = 'org.apache.click.pages.' | |
// className = 'edit-customer' | |
if (path.indexOf("/") != -1) { | |
StringTokenizer tokenizer = new StringTokenizer(path, "/"); | |
while (tokenizer.hasMoreTokens()) { | |
String token = tokenizer.nextToken(); | |
if (tokenizer.hasMoreTokens()) { | |
packageName = packageName + token + "."; | |
} else { | |
className = token; | |
} | |
} | |
} else { | |
className = path; | |
} | |
// CamelCase className. | |
// className = 'EditCustomer' | |
StringTokenizer tokenizer = new StringTokenizer(className, "_-"); | |
className = ""; | |
while (tokenizer.hasMoreTokens()) { | |
String token = tokenizer.nextToken(); | |
token = Character.toUpperCase(token.charAt(0)) + token.substring(1); | |
className += token; | |
} | |
// className = 'org.apache.click.pages.EditCustomer' | |
className = packageName + className; | |
Class pageClass = null; | |
try { | |
// Attempt to load class. | |
pageClass = ClickUtils.classForName(className); | |
if (!Page.class.isAssignableFrom(pageClass)) { | |
String msg = "Automapped page class " + className | |
+ " is not a subclass of org.apache.click.Page"; | |
throw new RuntimeException(msg); | |
} | |
} catch (ClassNotFoundException cnfe) { | |
boolean classFound = false; | |
// Append "Page" to className and attempt to load class again. | |
// className = 'org.apache.click.pages.EditCustomerPage' | |
if (!className.endsWith("Page")) { | |
String classNameWithPage = className + "Page"; | |
try { | |
// Attempt to load class. | |
pageClass = ClickUtils.classForName(classNameWithPage); | |
if (!Page.class.isAssignableFrom(pageClass)) { | |
String msg = "Automapped page class " + classNameWithPage | |
+ " is not a subclass of org.apache.click.Page"; | |
throw new RuntimeException(msg); | |
} | |
classFound = true; | |
} catch (ClassNotFoundException cnfe2) { | |
} | |
} | |
if (!classFound) { | |
if (logService.isDebugEnabled()) { | |
logService.debug(pagePath + " -> CLASS NOT FOUND"); | |
} | |
if (logService.isTraceEnabled()) { | |
logService.trace("class not found: " + className); | |
} | |
} | |
} | |
return pageClass; | |
} | |
/** | |
* Returns true if Click resources (JavaScript, CSS, images etc) packaged | |
* in jars can be deployed to the root directory of the webapp, false | |
* otherwise. | |
* <p/> | |
* By default this method will return false in restricted environments where | |
* write access to the underlying file system is disallowed. Example | |
* environments where write access is not allowed include the WebLogic JEE | |
* server and Google App Engine. (Note: WebLogic provides the property | |
* <tt>"Archived Real Path Enabled"</tt> that controls whether web | |
* applications can access the file system or not. See the Click user manual | |
* for details). | |
* | |
* @return true if resources can be deployed, false otherwise | |
*/ | |
protected boolean isResourcesDeployable() { | |
// Only deploy if writes are allowed | |
if (onGoogleAppEngine) { | |
// Google doesn't allow writes | |
return false; | |
} | |
return ClickUtils.isResourcesDeployable(servletContext); | |
} | |
// ------------------------------------------------ Package Private Methods | |
/** | |
* Loads all Click Pages defined in the <tt>click.xml</tt> file, including | |
* manually defined Pages, auto mapped Pages and excluded Pages. | |
* | |
* @param rootElm the root xml element containing the configuration | |
* @throws java.lang.ClassNotFoundException if the specified Page class can | |
* not be found on the classpath | |
*/ | |
void loadPages(Element rootElm) throws ClassNotFoundException { | |
List<Element> pagesList = ClickUtils.getChildren(rootElm, "pages"); | |
if (pagesList.isEmpty()) { | |
String msg = "required configuration 'pages' element missing."; | |
throw new RuntimeException(msg); | |
} | |
List templates = getTemplateFiles(); | |
for (Element pagesElm : pagesList) { | |
// Determine whether to use automapping | |
boolean automap = true; | |
String automapStr = pagesElm.getAttribute("automapping"); | |
if (StringUtils.isBlank(automapStr)) { | |
automapStr = "true"; | |
} | |
if ("true".equalsIgnoreCase(automapStr)) { | |
automap = true; | |
} else if ("false".equalsIgnoreCase(automapStr)) { | |
automap = false; | |
} else { | |
String msg = "Invalid pages automapping attribute: " + automapStr; | |
throw new RuntimeException(msg); | |
} | |
// Determine whether to use autobinding. | |
String autobindingStr = pagesElm.getAttribute("autobinding"); | |
if (StringUtils.isBlank(autobindingStr)) { | |
autobinding = AutoBinding.DEFAULT; | |
} else { | |
if ("annotation".equalsIgnoreCase(autobindingStr)) { | |
autobinding = AutoBinding.ANNOTATION; | |
} else if ("public".equalsIgnoreCase(autobindingStr)) { | |
autobinding = AutoBinding.DEFAULT; | |
} else if ("default".equalsIgnoreCase(autobindingStr)) { | |
autobinding = AutoBinding.DEFAULT; | |
} else if ("none".equalsIgnoreCase(autobindingStr)) { | |
autobinding = AutoBinding.NONE; | |
// Provided for backward compatibility | |
} else if ("true".equalsIgnoreCase(autobindingStr)) { | |
autobinding = AutoBinding.DEFAULT; | |
// Provided for backward compatibility | |
} else if ("false".equalsIgnoreCase(autobindingStr)) { | |
autobinding = AutoBinding.NONE; | |
} else { | |
String msg = "Invalid pages autobinding attribute: " | |
+ autobindingStr; | |
throw new RuntimeException(msg); | |
} | |
} | |
// TODO: if autobinding is set to false an there are multiple pages how should this be handled | |
// Perhaps autobinding should be moved to <click-app> and be a application wide setting? | |
// However the way its implemented above is probably fine for backward compatibility | |
// purposes, meaning the last defined autobinding wins | |
String pagesPackage = pagesElm.getAttribute("package"); | |
if (StringUtils.isBlank(pagesPackage)) { | |
pagesPackage = ""; | |
} | |
pagesPackage = pagesPackage.trim(); | |
if (pagesPackage.endsWith(".") && pagesPackage.length() > 1) { | |
pagesPackage = | |
pagesPackage.substring(0, pagesPackage.length() - 2); | |
} | |
// Add the pages package to the list of page packages | |
pagePackages.add(pagesPackage); | |
buildManualPageMapping(pagesElm, pagesPackage); | |
if (automap) { | |
buildAutoPageMapping(pagesElm, pagesPackage, templates); | |
} | |
} | |
buildClassMap(); | |
} | |
/** | |
* Add manually defined Pages to the {@link #pageByPathMap}. | |
* | |
* @param pagesElm the xml element containing manually defined Pages | |
* @param pagesPackage the pages package prefix | |
* | |
* @throws java.lang.ClassNotFoundException if the specified Page class can | |
* not be found on the classpath | |
*/ | |
void buildManualPageMapping(Element pagesElm, String pagesPackage) throws ClassNotFoundException { | |
List pageList = ClickUtils.getChildren(pagesElm, "page"); | |
if (!pageList.isEmpty() && logService.isDebugEnabled()) { | |
logService.debug("click.xml pages:"); | |
} | |
for (int i = 0; i < pageList.size(); i++) { | |
Element pageElm = (Element) pageList.get(i); | |
XmlConfigService.PageElm page = | |
new XmlConfigService.PageElm(pageElm, | |
pagesPackage, | |
commonHeaders, | |
autobinding); | |
pageByPathMap.put(page.getPath(), page); | |
if (logService.isDebugEnabled()) { | |
String msg = | |
page.getPath() + " -> " + page.getPageClass().getName(); | |
logService.debug(msg); | |
} | |
} | |
} | |
/** | |
* Build the {@link #pageByPathMap} by associating template files with | |
* matching Java classes found on the classpath. | |
* <p/> | |
* This method also rebuilds the {@link #excludesList}. This list contains | |
* URL paths that should not be auto-mapped. | |
* | |
* @param pagesElm the xml element containing the excluded URL paths | |
* @param pagesPackage the pages package prefix | |
* @param templates the list of templates to map to Page classes | |
*/ | |
void buildAutoPageMapping(Element pagesElm, String pagesPackage, List templates) throws ClassNotFoundException { | |
// Build list of automap path page class overrides | |
excludesList.clear(); | |
for (Iterator i = ClickUtils.getChildren(pagesElm, "excludes").iterator(); | |
i.hasNext();) { | |
excludesList.add(new XmlConfigService.ExcludesElm((Element) i.next())); | |
} | |
if (logService.isDebugEnabled()) { | |
logService.debug("automapped pages:"); | |
} | |
for (int i = 0; i < templates.size(); i++) { | |
String pagePath = (String) templates.get(i); | |
if (!pageByPathMap.containsKey(pagePath)) { | |
Class pageClass = getPageClass(pagePath, pagesPackage); | |
if (pageClass != null) { | |
XmlConfigService.PageElm page = | |
new XmlConfigService.PageElm(pagePath, | |
pageClass, | |
commonHeaders, | |
autobinding); | |
pageByPathMap.put(page.getPath(), page); | |
if (logService.isDebugEnabled()) { | |
String msg = | |
pagePath + " -> " + pageClass.getName(); | |
logService.debug(msg); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Build the {@link #pageByClassMap} from the {@link #pageByPathMap} and | |
* delegate to {@link #addToClassMap(PageElm)}. | |
*/ | |
void buildClassMap() { | |
// Build pages by class map | |
for (Iterator i = pageByPathMap.values().iterator(); i.hasNext();) { | |
XmlConfigService.PageElm page = (XmlConfigService.PageElm) i.next(); | |
addToClassMap(page); | |
} | |
} | |
/** | |
* Add the specified page to the {@link #pageByClassMap} where the Map's key | |
* holds the Page class and value holds the {@link PageElm}. | |
* | |
* @param page the PageElm containing metadata about a specific page | |
*/ | |
void addToClassMap(PageElm page) { | |
Object value = pageByClassMap.get(page.pageClass); | |
if (value == null) { | |
pageByClassMap.put(page.pageClass, page); | |
} else if (value instanceof List) { | |
((List) value).add(page); | |
} else if (value instanceof XmlConfigService.PageElm) { | |
List list = new ArrayList(); | |
list.add(value); | |
list.add(page); | |
pageByClassMap.put(page.pageClass, list); | |
} else { | |
// should never occur | |
throw new IllegalStateException(); | |
} | |
} | |
/** | |
* Load the Page headers from the specified xml element. | |
* | |
* @param parentElm the element to load the headers from | |
* @return the map of Page headers | |
*/ | |
static Map<String, Object> loadHeadersMap(Element parentElm) { | |
Map<String, Object> headersMap = new HashMap<String, Object>(); | |
for (Element header : ClickUtils.getChildren(parentElm, "header")) { | |
String name = header.getAttribute("name"); | |
String type = header.getAttribute("type"); | |
String propertyValue = header.getAttribute("value"); | |
Object value = null; | |
if ("".equals(type) || "String".equalsIgnoreCase(type)) { | |
value = propertyValue; | |
} else if ("Integer".equalsIgnoreCase(type)) { | |
value = Integer.valueOf(propertyValue); | |
} else if ("Date".equalsIgnoreCase(type)) { | |
value = new Date(Long.parseLong(propertyValue)); | |
} else { | |
value = null; | |
String message = | |
"Invalid property type [String|Integer|Date]: " | |
+ type; | |
throw new IllegalArgumentException(message); | |
} | |
headersMap.put(name, value); | |
} | |
return headersMap; | |
} | |
// -------------------------------------------------------- Private Methods | |
private Element getResourceRootElement(String path) throws IOException { | |
Document document = null; | |
InputStream inputStream = null; | |
try { | |
inputStream = ClickUtils.getResourceAsStream(path, getClass()); | |
if (inputStream != null) { | |
document = ClickUtils.buildDocument(inputStream, this); | |
} | |
} finally { | |
ClickUtils.close(inputStream); | |
} | |
if (document != null) { | |
return document.getDocumentElement(); | |
} else { | |
return null; | |
} | |
} | |
private void deployControls(Element rootElm) throws Exception { | |
if (rootElm == null) { | |
return; | |
} | |
Element controlsElm = ClickUtils.getChild(rootElm, "controls"); | |
if (controlsElm == null) { | |
return; | |
} | |
for (Element deployableElm : ClickUtils.getChildren(controlsElm, "control")) { | |
String classname = deployableElm.getAttribute("classname"); | |
if (StringUtils.isBlank(classname)) { | |
String msg = | |
"'control' element missing 'classname' attribute."; | |
throw new RuntimeException(msg); | |
} | |
Class deployClass = ClickUtils.classForName(classname); | |
Control control = (Control) deployClass.newInstance(); | |
control.onDeploy(servletContext); | |
} | |
} | |
private void deployControlSets(Element rootElm) throws Exception { | |
if (rootElm == null) { | |
return; | |
} | |
Element controlsElm = ClickUtils.getChild(rootElm, "controls"); | |
if (controlsElm == null) { | |
return; | |
} | |
for (Element controlSet : ClickUtils.getChildren(controlsElm, "control-set")) { | |
String name = controlSet.getAttribute("name"); | |
if (StringUtils.isBlank(name)) { | |
String msg = | |
"'control-set' element missing 'name' attribute."; | |
throw new RuntimeException(msg); | |
} | |
deployControls(getResourceRootElement("/" + name)); | |
} | |
} | |
/** | |
* Deploy files from jars and Controls. | |
* | |
* @param rootElm the click.xml configuration DOM element | |
* @throws java.lang.Exception if files cannot be deployed | |
*/ | |
private void deployFiles(Element rootElm) throws Exception { | |
boolean isResourcesDeployable = isResourcesDeployable(); | |
if (isResourcesDeployable) { | |
if (getLogService().isTraceEnabled()) { | |
String deployTarget = servletContext.getRealPath("/"); | |
getLogService().trace("resource deploy folder: " | |
+ deployTarget); | |
} | |
deployControls(getResourceRootElement("/click-controls.xml")); | |
deployControls(getResourceRootElement("/extras-controls.xml")); | |
deployControls(rootElm); | |
deployControlSets(rootElm); | |
deployResourcesOnClasspath(); | |
} | |
if (!isResourcesDeployable) { | |
HtmlStringBuffer buffer = new HtmlStringBuffer(); | |
if (onGoogleAppEngine) { | |
buffer.append("Google App Engine does not support deploying"); | |
buffer.append(" resources to the 'click' web folder.\n"); | |
} else { | |
buffer.append("could not deploy Click resources to the 'click'"); | |
buffer.append(" web folder.\nThis can occur if the call to"); | |
buffer.append(" ServletContext.getRealPath(\"/\") returns null, which means"); | |
buffer.append(" the web application cannot determine the file system path"); | |
buffer.append(" to deploy files to. This issue also occurs if the web"); | |
buffer.append(" application is not allowed to write to the file"); | |
buffer.append(" system.\n"); | |
} | |
buffer.append("To resolve this issue please see the Click user-guide:"); | |
buffer.append(" http://click.apache.org/docs/user-guide/html/ch05s03.html#deploying-restricted-env"); | |
buffer.append(" \nIgnore this warning once you have settled on a"); | |
buffer.append(" deployment strategy"); | |
getLogService().warn(buffer.toString()); | |
} | |
} | |
/** | |
* Deploy from the classpath all resources found under the directory | |
* 'META-INF/resources/'. For backwards compatibility resources under the | |
* directory 'META-INF/web/' are also deployed. | |
* <p/> | |
* Only jars and folders available on the classpath are scanned. | |
* | |
* @throws IOException if the resources cannot be deployed | |
*/ | |
private void deployResourcesOnClasspath() throws IOException { | |
long startTime = System.currentTimeMillis(); | |
// Find all jars and directories on the classpath that contains the | |
// directory "META-INF/resources/", and deploy those resources | |
String resourceDirectory = "META-INF/resources"; | |
List<String> resources = new DeployUtils(logService).findResources(resourceDirectory).getResources(); | |
for (String resource : resources) { | |
deployFile(resource, resourceDirectory); | |
} | |
// For backward compatibility, find all jars and directories on the | |
// classpath that contains the directory "META-INF/web/", and deploy those | |
// resources | |
resourceDirectory = "META-INF/web"; | |
resources = new DeployUtils(logService).findResources(resourceDirectory).getResources(); | |
for (String resource : resources) { | |
deployFile(resource, resourceDirectory); | |
} | |
logService.trace("deployed files from jars and folders - " | |
+ (System.currentTimeMillis() - startTime) + " ms"); | |
} | |
/** | |
* Deploy the specified file. | |
* | |
* @param file the file to deploy | |
* @param prefix the file prefix that must be removed when the file is | |
* deployed | |
*/ | |
private void deployFile(String file, String prefix) { | |
// Only deploy resources containing the prefix | |
int pathIndex = file.indexOf(prefix); | |
if (pathIndex == 0) { | |
pathIndex += prefix.length(); | |
// By default deploy to the web root dir | |
String targetDir = ""; | |
// resourceName example -> click/table.css | |
String resourceName = file.substring(pathIndex); | |
int index = resourceName.lastIndexOf('/'); | |
if (index != -1) { | |
// targetDir example -> click | |
targetDir = resourceName.substring(0, index); | |
} | |
// Copy resources to web folder | |
ClickUtils.deployFile(servletContext, | |
file, | |
targetDir); | |
} | |
} | |
private void loadMode(Element rootElm) { | |
Element modeElm = ClickUtils.getChild(rootElm, "mode"); | |
String modeValue = "development"; | |
if (modeElm != null) { | |
if (StringUtils.isNotBlank(modeElm.getAttribute("value"))) { | |
modeValue = modeElm.getAttribute("value"); | |
} | |
} | |
modeValue = System.getProperty("click.mode", modeValue); | |
if (modeValue.equalsIgnoreCase("production")) { | |
mode = PRODUCTION; | |
} else if (modeValue.equalsIgnoreCase("profile")) { | |
mode = PROFILE; | |
} else if (modeValue.equalsIgnoreCase("development")) { | |
mode = DEVELOPMENT; | |
} else if (modeValue.equalsIgnoreCase("debug")) { | |
mode = DEBUG; | |
} else if (modeValue.equalsIgnoreCase("trace")) { | |
mode = TRACE; | |
} else { | |
logService.error("invalid application mode: '" + modeValue | |
+ "' - defaulted to '" + MODE_VALUES[DEBUG] + "'"); | |
mode = DEBUG; | |
} | |
// Set log levels | |
if (logService instanceof ConsoleLogService) { | |
int logLevel = ConsoleLogService.INFO_LEVEL; | |
if (mode == PRODUCTION) { | |
logLevel = ConsoleLogService.WARN_LEVEL; | |
} else if (mode == DEVELOPMENT) { | |
} else if (mode == DEBUG) { | |
logLevel = ConsoleLogService.DEBUG_LEVEL; | |
} else if (mode == TRACE) { | |
logLevel = ConsoleLogService.TRACE_LEVEL; | |
} | |
((ConsoleLogService) logService).setLevel(logLevel); | |
} | |
} | |
private void loadDefaultPages() throws ClassNotFoundException { | |
if (!pageByPathMap.containsKey(ERROR_PATH)) { | |
XmlConfigService.PageElm page = | |
new XmlConfigService.PageElm("org.apache.click.util.ErrorPage", ERROR_PATH); | |
pageByPathMap.put(ERROR_PATH, page); | |
} | |
if (!pageByPathMap.containsKey(NOT_FOUND_PATH)) { | |
XmlConfigService.PageElm page = | |
new XmlConfigService.PageElm("org.apache.click.Page", NOT_FOUND_PATH); | |
pageByPathMap.put(NOT_FOUND_PATH, page); | |
} | |
} | |
private void loadHeaders(Element rootElm) { | |
Element headersElm = ClickUtils.getChild(rootElm, "headers"); | |
if (headersElm != null) { | |
commonHeaders = | |
Collections.unmodifiableMap(loadHeadersMap(headersElm)); | |
} else { | |
commonHeaders = Collections.unmodifiableMap(DEFAULT_HEADERS); | |
} | |
} | |
private void loadFormatClass(Element rootElm) | |
throws ClassNotFoundException { | |
Element formatElm = ClickUtils.getChild(rootElm, "format"); | |
if (formatElm != null) { | |
String classname = formatElm.getAttribute("classname"); | |
if (classname == null) { | |
String msg = "'format' element missing 'classname' attribute."; | |
throw new RuntimeException(msg); | |
} | |
formatClass = ClickUtils.classForName(classname); | |
} else { | |
formatClass = org.apache.click.util.Format.class; | |
} | |
} | |
private void loadFileUploadService(Element rootElm) throws Exception { | |
Element fileUploadServiceElm = ClickUtils.getChild(rootElm, "file-upload-service"); | |
if (fileUploadServiceElm != null) { | |
Class fileUploadServiceClass = CommonsFileUploadService.class; | |
String classname = fileUploadServiceElm.getAttribute("classname"); | |
if (StringUtils.isNotBlank(classname)) { | |
fileUploadServiceClass = ClickUtils.classForName(classname); | |
} | |
fileUploadService = (FileUploadService) fileUploadServiceClass.newInstance(); | |
Map<String, String> propertyMap = loadPropertyMap(fileUploadServiceElm); | |
for (String name : propertyMap.keySet()) { | |
String value = propertyMap.get(name).toString(); | |
getPropertyService().setValue(fileUploadService, name, value); | |
} | |
} else { | |
fileUploadService = new CommonsFileUploadService(); | |
} | |
if (getLogService().isDebugEnabled()) { | |
String msg = "initializing FileLoadService: " | |
+ fileUploadService.getClass().getName(); | |
getLogService().debug(msg); | |
} | |
fileUploadService.onInit(servletContext); | |
} | |
private void loadLogService(Element rootElm) throws Exception { | |
Element logServiceElm = ClickUtils.getChild(rootElm, "log-service"); | |
if (logServiceElm != null) { | |
Class logServiceClass = ConsoleLogService.class; | |
String classname = logServiceElm.getAttribute("classname"); | |
if (StringUtils.isNotBlank(classname)) { | |
logServiceClass = ClickUtils.classForName(classname); | |
} | |
logService = (LogService) logServiceClass.newInstance(); | |
Map<String, String> propertyMap = loadPropertyMap(logServiceElm); | |
for (String name : propertyMap.keySet()) { | |
String value = propertyMap.get(name).toString(); | |
getPropertyService().setValue(logService, name, value); | |
} | |
} else { | |
logService = new ConsoleLogService(); | |
} | |
logService.onInit(getServletContext()); | |
} | |
private void loadMessagesMapService(Element rootElm) throws Exception { | |
Element messagesMapServiceElm = ClickUtils.getChild(rootElm, "messages-map-service"); | |
if (messagesMapServiceElm != null) { | |
Class messagesMapServiceClass = DefaultMessagesMapService.class; | |
String classname = messagesMapServiceElm.getAttribute("classname"); | |
if (StringUtils.isNotBlank(classname)) { | |
messagesMapServiceClass = ClickUtils.classForName(classname); | |
} | |
messagesMapService = (MessagesMapService) messagesMapServiceClass.newInstance(); | |
Map<String, String> propertyMap = loadPropertyMap(messagesMapServiceElm); | |
for (String name : propertyMap.keySet()) { | |
String value = propertyMap.get(name).toString(); | |
getPropertyService().setValue(messagesMapService, name, value); | |
} | |
} | |
if (getLogService().isDebugEnabled()) { | |
String msg = "initializing MessagesMapService: " | |
+ messagesMapService.getClass().getName(); | |
getLogService().debug(msg); | |
} | |
messagesMapService.onInit(servletContext); | |
} | |
private void loadPageInterceptors(Element rootElm) throws Exception { | |
List<Element> interceptorList = | |
ClickUtils.getChildren(rootElm, "page-interceptor"); | |
for (Element interceptorElm : interceptorList) { | |
String classname = interceptorElm.getAttribute("classname"); | |
String scopeValue = interceptorElm.getAttribute("scope"); | |
boolean applicationScope = "application".equalsIgnoreCase(scopeValue); | |
Class interceptorClass = ClickUtils.classForName(classname); | |
Map<String, String> propertyMap = loadPropertyMap(interceptorElm); | |
List<Property> propertyList = new ArrayList<Property>(); | |
for (String name : propertyMap.keySet()) { | |
String value = propertyMap.get(name).toString(); | |
propertyList.add(new Property(name, value)); | |
} | |
PageInterceptorConfig pageInterceptorConfig = | |
new PageInterceptorConfig(interceptorClass, applicationScope, propertyList); | |
pageInterceptorConfigList.add(pageInterceptorConfig); | |
} | |
} | |
private void loadResourceService(Element rootElm) throws Exception { | |
Element resourceServiceElm = ClickUtils.getChild(rootElm, "resource-service"); | |
if (resourceServiceElm != null) { | |
Class resourceServiceClass = ClickResourceService.class; | |
String classname = resourceServiceElm.getAttribute("classname"); | |
if (StringUtils.isNotBlank(classname)) { | |
resourceServiceClass = ClickUtils.classForName(classname); | |
} | |
resourceService = (ResourceService) resourceServiceClass.newInstance(); | |
Map<String, String> propertyMap = loadPropertyMap(resourceServiceElm); | |
for (String name : propertyMap.keySet()) { | |
String value = propertyMap.get(name).toString(); | |
getPropertyService().setValue(resourceService, name, value); | |
} | |
} else { | |
resourceService = new ClickResourceService(); | |
} | |
if (getLogService().isDebugEnabled()) { | |
String msg = "initializing ResourceService: " | |
+ resourceService.getClass().getName(); | |
getLogService().debug(msg); | |
} | |
resourceService.onInit(servletContext); | |
} | |
private void loadPropertyService(Element rootElm) throws Exception { | |
Element propertyServiceElm = ClickUtils.getChild(rootElm, "property-service"); | |
if (propertyServiceElm != null) { | |
Class propertyServiceClass = OGNLPropertyService.class; | |
String classname = propertyServiceElm.getAttribute("classname"); | |
if (StringUtils.isNotBlank(classname)) { | |
propertyServiceClass = ClickUtils.classForName(classname); | |
} | |
propertyService = (PropertyService) propertyServiceClass.newInstance(); | |
} else { | |
propertyService = new OGNLPropertyService(); | |
} | |
if (getLogService().isDebugEnabled()) { | |
String msg = "initializing PropertyService: " | |
+ propertyService.getClass().getName(); | |
getLogService().debug(msg); | |
} | |
propertyService.onInit(servletContext); | |
} | |
private void loadTemplateService(Element rootElm) throws Exception { | |
Element templateServiceElm = ClickUtils.getChild(rootElm, "template-service"); | |
if (templateServiceElm != null) { | |
Class templateServiceClass = VelocityTemplateService.class; | |
String classname = templateServiceElm.getAttribute("classname"); | |
if (StringUtils.isNotBlank(classname)) { | |
templateServiceClass = ClickUtils.classForName(classname); | |
} | |
templateService = (TemplateService) templateServiceClass.newInstance(); | |
Map<String, String> propertyMap = loadPropertyMap(templateServiceElm); | |
for (String name : propertyMap.keySet()) { | |
String value = propertyMap.get(name).toString(); | |
getPropertyService().setValue(templateService, name, value); | |
} | |
} else { | |
templateService = new VelocityTemplateService(); | |
} | |
if (getLogService().isDebugEnabled()) { | |
String msg = "initializing TemplateService: " | |
+ templateService.getClass().getName(); | |
getLogService().debug(msg); | |
} | |
templateService.onInit(servletContext); | |
} | |
private static Map<String, String> loadPropertyMap(Element parentElm) { | |
Map<String, String> propertyMap = new HashMap<String, String>(); | |
for (Element property : ClickUtils.getChildren(parentElm, "property")) { | |
String name = property.getAttribute("name"); | |
String value = property.getAttribute("value"); | |
propertyMap.put(name, value); | |
} | |
return propertyMap; | |
} | |
private void loadCharset(Element rootElm) { | |
String localCharset = rootElm.getAttribute("charset"); | |
if (localCharset != null && localCharset.length() > 0) { | |
this.charset = localCharset; | |
} | |
} | |
private void loadLocale(Element rootElm) { | |
String value = rootElm.getAttribute("locale"); | |
if (value != null && value.length() > 0) { | |
StringTokenizer tokenizer = new StringTokenizer(value, "_"); | |
if (tokenizer.countTokens() == 1) { | |
String language = tokenizer.nextToken(); | |
locale = new Locale(language); | |
} else if (tokenizer.countTokens() == 2) { | |
String language = tokenizer.nextToken(); | |
String country = tokenizer.nextToken(); | |
locale = new Locale(language, country); | |
} | |
} | |
} | |
/** | |
* Return the list of templates within the web application. | |
* | |
* @return list of all templates within the web application | |
*/ | |
private List getTemplateFiles() { | |
List fileList = new ArrayList(); | |
Set resources = servletContext.getResourcePaths("/"); | |
if (onGoogleAppEngine) { | |
// resources could be immutable so create copy | |
Set tempResources = new HashSet(); | |
// Load the two GAE preconfigured automapped folders | |
tempResources.addAll(servletContext.getResourcePaths("/page")); | |
tempResources.addAll(servletContext.getResourcePaths("/pages")); | |
tempResources.addAll(resources); | |
// assign copy to resources | |
resources = Collections.unmodifiableSet(tempResources); | |
} | |
// Add all resources within web application | |
for (Iterator i = resources.iterator(); i.hasNext();) { | |
String resource = (String) i.next(); | |
if (isTemplate(resource)) { | |
fileList.add(resource); | |
} else if (resource.endsWith("/")) { | |
if (!resource.equalsIgnoreCase("/WEB-INF/")) { | |
processDirectory(resource, fileList); | |
} | |
} | |
} | |
Collections.sort(fileList); | |
return fileList; | |
} | |
private void processDirectory(String dirPath, List fileList) { | |
Set resources = servletContext.getResourcePaths(dirPath); | |
if (resources != null) { | |
for (Iterator i = resources.iterator(); i.hasNext();) { | |
String resource = (String) i.next(); | |
if (isTemplate(resource)) { | |
fileList.add(resource); | |
} else if (resource.endsWith("/")) { | |
processDirectory(resource, fileList); | |
} | |
} | |
} | |
} | |
private Class<? extends Page> getExcludesPageClass(String path) { | |
for (int i = 0; i < excludesList.size(); i++) { | |
XmlConfigService.ExcludesElm override = | |
(XmlConfigService.ExcludesElm) excludesList.get(i); | |
if (override.isMatch(path)) { | |
return override.getPageClass(); | |
} | |
} | |
return null; | |
} | |
/** | |
* Return an array of bindable fields for the given page class based on | |
* the binding mode. | |
* | |
* @param pageClass the page class | |
* @param mode the binding mode | |
* @return the field array of bindable fields | |
*/ | |
private static Field[] getBindablePageFields(Class pageClass, AutoBinding mode) { | |
if (mode == AutoBinding.DEFAULT) { | |
// Get @Bindable fields | |
Map<String, Field> fieldMap = getAnnotatedBindableFields(pageClass); | |
// Add public fields | |
Field[] publicFields = pageClass.getFields(); | |
for (Field field : publicFields) { | |
fieldMap.put(field.getName(), field); | |
} | |
// Copy the field map values into a field list | |
Field[] fieldArray = new Field[fieldMap.size()]; | |
int i = 0; | |
for (Field field : fieldMap.values()) { | |
fieldArray[i++] = field; | |
} | |
return fieldArray; | |
} else if (mode == AutoBinding.ANNOTATION) { | |
Map<String, Field> fieldMap = getAnnotatedBindableFields(pageClass); | |
// Copy the field map values into a field list | |
Field[] fieldArray = new Field[fieldMap.size()]; | |
int i = 0; | |
for (Field field : fieldMap.values()) { | |
fieldArray[i++] = field; | |
} | |
return fieldArray; | |
} else { | |
return new Field[0]; | |
} | |
} | |
/** | |
* Return the fields annotated with the Bindable annotation. | |
* | |
* @param pageClass the page class | |
* @return the map of bindable fields | |
*/ | |
private static Map getAnnotatedBindableFields(Class pageClass) { | |
List<Class> pageClassList = new ArrayList<Class>(); | |
pageClassList.add(pageClass); | |
Class parentClass = pageClass.getSuperclass(); | |
while (parentClass != null) { | |
// Include parent classes up to but excluding Page.class | |
if (parentClass.isAssignableFrom(Page.class)) { | |
break; | |
} | |
pageClassList.add(parentClass); | |
parentClass = parentClass.getSuperclass(); | |
} | |
// Reverse class list so parents are processed first, with the | |
// actual page class fields processed last. This will enable the | |
// page classes fields to override parent class fields | |
Collections.reverse(pageClassList); | |
Map<String, Field> fieldMap = new TreeMap<String, Field>(); | |
for (Class aPageClass : pageClassList) { | |
for (Field field : aPageClass.getDeclaredFields()) { | |
if (field.getAnnotation(Bindable.class) != null) { | |
fieldMap.put(field.getName(), field); | |
// If field is not public set accessibility true | |
if (!Modifier.isPublic(field.getModifiers())) { | |
field.setAccessible(true); | |
} | |
} | |
} | |
} | |
return fieldMap; | |
} | |
// ---------------------------------------------------------- Inner Classes | |
/** | |
* Provide an Excluded Page class. | |
* <p/> | |
* <b>PLEASE NOTE</b> this class is <b>not</b> for public use, and can be | |
* ignored. | |
*/ | |
public static class ExcludePage extends Page { | |
static final Map HEADERS = new HashMap(); | |
static { | |
HEADERS.put("Cache-Control", "max-age=3600, public"); | |
} | |
/** | |
* @see Page#getHeaders() | |
* | |
* @return the map of HTTP header to be set in the HttpServletResponse | |
*/ | |
@Override | |
public Map getHeaders() { | |
return HEADERS; | |
} | |
} | |
static class PageElm { | |
final Map<String, Field> fields; | |
final Field[] fieldArray; | |
final Map headers; | |
final Class<? extends Page> pageClass; | |
final String path; | |
public PageElm(Element element, | |
String pagesPackage, | |
Map commonHeaders, | |
AutoBinding autobinding) | |
throws ClassNotFoundException { | |
// Set headers | |
Map aggregationMap = new HashMap(commonHeaders); | |
Map pageHeaders = loadHeadersMap(element); | |
aggregationMap.putAll(pageHeaders); | |
headers = Collections.unmodifiableMap(aggregationMap); | |
// Set path | |
String pathValue = element.getAttribute("path"); | |
if (pathValue.charAt(0) != '/') { | |
path = "/" + pathValue; | |
} else { | |
path = pathValue; | |
} | |
// Retrieve page classname | |
String classname = element.getAttribute("classname"); | |
if (classname == null) { | |
String msg = "No classname defined for page path " + path; | |
throw new RuntimeException(msg); | |
} | |
Class tmpPageClass = null; | |
String classnameFound = null; | |
try { | |
// First, lookup classname as provided | |
tmpPageClass = ClickUtils.classForName(classname); | |
classnameFound = classname; | |
} catch (ClassNotFoundException cnfe) { | |
if (pagesPackage.trim().length() > 0) { | |
// For backward compatibility prefix classname with package name | |
String prefixedClassname = pagesPackage + "." + classname; | |
try { | |
// CLK-704 | |
// For backward compatibility, lookup classname prefixed with the package name | |
tmpPageClass = | |
ClickUtils.classForName(prefixedClassname); | |
classnameFound = prefixedClassname; | |
} catch (ClassNotFoundException cnfe2) { | |
// Throw original exception which used the given classname | |
String msg = "No class was found for the Page classname: '" | |
+ classname + "'."; | |
throw new RuntimeException(msg, cnfe); | |
} | |
} else { | |
String msg = "No class was found for the Page classname: '" | |
+ classname + "'."; | |
throw new RuntimeException(msg, cnfe); | |
} | |
} | |
pageClass = tmpPageClass; | |
if (!Page.class.isAssignableFrom(pageClass)) { | |
String msg = "Page class '" + classnameFound | |
+ "' is not a subclass of org.apache.click.Page"; | |
throw new RuntimeException(msg); | |
} | |
fieldArray = XmlConfigService.getBindablePageFields(pageClass, autobinding); | |
fields = new HashMap<String, Field>(); | |
for (Field field : fieldArray) { | |
fields.put(field.getName(), field); | |
} | |
} | |
private PageElm(String path, | |
Class pageClass, | |
Map commonHeaders, | |
AutoBinding mode) { | |
headers = Collections.unmodifiableMap(commonHeaders); | |
this.pageClass = pageClass; | |
this.path = path; | |
fieldArray = getBindablePageFields(pageClass, mode); | |
fields = new HashMap<String, Field>(); | |
for (Field field : fieldArray) { | |
fields.put(field.getName(), field); | |
} | |
} | |
public PageElm(String classname, String path) | |
throws ClassNotFoundException { | |
this.fieldArray = new Field[0]; | |
this.fields = Collections.emptyMap(); | |
this.headers = Collections.emptyMap(); | |
pageClass = ClickUtils.classForName(classname); | |
this.path = path; | |
} | |
public Field[] getFieldArray() { | |
return fieldArray; | |
} | |
public Map<String, Field> getFields() { | |
return fields; | |
} | |
public Map getHeaders() { | |
return headers; | |
} | |
public Class<? extends Page> getPageClass() { | |
return pageClass; | |
} | |
public String getPath() { | |
return path; | |
} | |
} | |
static class ExcludesElm { | |
final Set<String> pathSet = new HashSet<String>(); | |
final Set<String> fileSet = new HashSet<String>(); | |
public ExcludesElm(Element element) throws ClassNotFoundException { | |
String pattern = element.getAttribute("pattern"); | |
if (StringUtils.isNotBlank(pattern)) { | |
StringTokenizer tokenizer = new StringTokenizer(pattern, ", "); | |
while (tokenizer.hasMoreTokens()) { | |
String token = tokenizer.nextToken(); | |
if (token.charAt(0) != '/') { | |
token = "/" + token; | |
} | |
int index = token.lastIndexOf("."); | |
if (index != -1) { | |
token = token.substring(0, index); | |
fileSet.add(token); | |
} else { | |
index = token.indexOf("*"); | |
if (index != -1) { | |
token = token.substring(0, index); | |
} | |
pathSet.add(token); | |
} | |
} | |
} | |
} | |
public Class<? extends Page> getPageClass() { | |
return XmlConfigService.ExcludePage.class; | |
} | |
public boolean isMatch(String resourcePath) { | |
if (fileSet.contains(resourcePath)) { | |
return true; | |
} | |
for (String path : pathSet) { | |
if (resourcePath.startsWith(path)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
@Override | |
public String toString() { | |
return getClass().getName() | |
+ "[fileSet=" + fileSet + ",pathSet=" + pathSet + "]"; | |
} | |
} | |
static class PageInterceptorConfig { | |
final Class<? extends PageInterceptor> interceptorClass; | |
final boolean applicationScope; | |
final List<Property> properties; | |
PageInterceptor pageInterceptor; | |
PageInterceptorConfig(Class<? extends PageInterceptor> interceptorClass, | |
boolean applicationScope, | |
List<Property> properties) { | |
this.interceptorClass = interceptorClass; | |
this.applicationScope = applicationScope; | |
this.properties = properties; | |
} | |
public PageInterceptor getPageInterceptor() { | |
PageInterceptor listener = null; | |
// If cached interceptor not already created (application scope) | |
// or is scope request then create a new interceptor | |
if (pageInterceptor == null || !applicationScope) { | |
try { | |
listener = interceptorClass.newInstance(); | |
ConfigService configService = ClickUtils.getConfigService(); | |
PropertyService propertyService = configService.getPropertyService(); | |
for (Property property : properties) { | |
propertyService.setValue(listener, | |
property.getName(), | |
property.getValue()); | |
} | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
if (applicationScope) { | |
pageInterceptor = listener; | |
} | |
} else { | |
listener = pageInterceptor; | |
} | |
return listener; | |
} | |
} | |
static class Property { | |
final String name; | |
final String value; | |
Property(String name, String value) { | |
this.name = name; | |
this.value = value; | |
} | |
public String getName() { | |
return name; | |
} | |
public String getValue() { | |
return value; | |
} | |
} | |
} |