org.apache.click.service.ClickClickConfigService.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.click.service.ClickClickConfigService.java

Source

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.click.service;

import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.click.Page;
import org.apache.click.util.ClickUtils;
import org.apache.click.util.HtmlStringBuffer;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Element;

/**
 * This class provides a ConfigService that does not cache Page metadata
 * and enables hot reloading of Page classes.
 */
public class ClickClickConfigService extends XmlConfigService {

    // -------------------------------------------------------- Constants

    private static final Object PAGE_LOAD_LOCK = new Object();

    // -------------------------------------------------------------- Variables

    private Map manualPageByPathMap = new HashMap();

    private Map manualPageByClassNameMap = new HashMap();

    // --------------------------------------------------------- Public Methods

    /**
     * Return the list of page packages.
     *
     * @return the list of page packages.
     */
    public List getPagesPackage() {
        return pagePackages;
    }

    /**
     * @see org.apache.click.service.ConfigService#getPageClass(java.lang.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 (isProductionMode() || isProfileMode()) {
            return super.getPageClass(path);

        } else {
            // Else in development, debug or trace mode
            synchronized (PAGE_LOAD_LOCK) {

                // Try and load the manually mapped page first
                PageMetaData page = lookupManuallyStoredMetaData(path);

                if (page != null) {
                    try {
                        return (Class<? extends Page>) ClickUtils.classForName(page.getPageClassName());
                    } catch (ClassNotFoundException ex) {
                        // ignore, this class is not available, so try and load
                        // it from the classpath
                    }
                }

                Class pageClass = null;

                try {
                    URL resource = getServletContext().getResource(path);

                    if (resource != null) {
                        for (int i = 0; i < pagePackages.size(); i++) {
                            String pagesPackage = pagePackages.get(i).toString();

                            pageClass = getPageClass(path, pagesPackage);
                        }
                    } else {
                        // No caching of this class or fields are done here.
                    }

                } catch (MalformedURLException ex) {
                    // ignore, will return null
                }

                return pageClass;
            }
        }
    }

    /**
     * @see org.apache.click.service.ConfigService#getPagePath(java.lang.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 pageClass) {
        // Try to lookup path from manually mapped pages first
        PageMetaData page = lookupManuallyStoredMetaData(pageClass);
        if (page != null) {
            return page.getPath();
        }

        // If not found we do a reverse algorithm lookup for the path
        return new PathLookupAlgorithm().getPagePath(pageClass);
    }

    /**
     * @see org.apache.click.service.ConfigService#getPageHeaders(java.lang.String)
     *
     * @param path the path of the page
     * @return a Map of headers for the given page path
     */
    public Map getPageHeaders(String path) {
        // Try and load the manually mapped page first
        PageMetaData page = lookupManuallyStoredMetaData(path);

        if (page != null) {
            return page.getHeaders();
        } else {
            // If path was not found in the manually loaded pages, return common headers
            return Collections.unmodifiableMap(commonHeaders);
        }
    }

    /**
     * @see org.apache.click.service.ConfigService#getPageField(java.lang.Class, java.lang.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 pageClass, String fieldName) {
        if (isProductionMode() || isProfileMode()) {
            return super.getPageField(pageClass, fieldName);
        }
        return (Field) getPageFields(pageClass).get(fieldName);
    }

    /**
     * @see org.apache.click.service.ConfigService#getPageFieldArray(java.lang.Class)
     *
     * @param pageClass the page class
     * @return an array public fields for the given page class
     */
    public Field[] getPageFieldArray(Class pageClass) {
        if (isProductionMode() || isProfileMode()) {
            return super.getPageFieldArray(pageClass);
        }
        return pageClass.getFields();
    }

    /**
     * @see org.apache.click.service.ConfigService#getPageFields(java.lang.Class)
     *
     * @param pageClass the page class
     * @return a Map of public fields for the given page class
     */
    public Map getPageFields(Class pageClass) {
        if (isProductionMode() || isProfileMode()) {
            return super.getPageFields(pageClass);
        }
        Field[] fieldArray = getPageFieldArray(pageClass);
        Map fields = new HashMap();
        for (int i = 0; i < fieldArray.length; i++) {
            Field field = fieldArray[i];
            fields.put(field.getName(), field);
        }
        return fields;
    }

    // ------------------------------------------------ Package Private Methods

    /**
     * In Production modes delegate to the super implementation. In development
     * modes add manually defined Pages to the {@link #manualPageByPathMap}.
     *
     * @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 {
        if (isProductionMode() || isProfileMode()) {
            super.buildManualPageMapping(pagesElm, pagesPackage);
            return;
        }

        List pageList = ClickUtils.getChildren(pagesElm, "page");

        if (!pageList.isEmpty() && getLogService().isDebugEnabled()) {
            getLogService().debug("click.xml pages:");
        }

        for (int i = 0; i < pageList.size(); i++) {
            Element pageElm = (Element) pageList.get(i);

            PageMetaData page = new PageMetaData(pageElm, pagesPackage, commonHeaders);
            manualPageByPathMap.put(page.getPath(), page);
        }
    }

    /**
     * In Production modes delegate to the super implementation. In development
     * modes this method does <b>not</b> associate 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 {

        if (isProductionMode() || isProfileMode()) {
            //Build and cache in production modes.
            super.buildAutoPageMapping(pagesElm, pagesPackage, templates);
            return;
        }

        // 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()));
        }
    }

    /**
     * In Production modes delegate to the super implementation. In development
     * modes this method builds the {@link #manualPageByClassNameMap} by using
     * the Page Class name instead of the Page Class. Thus no reference to the
     * class is stored and it can be reloaded. Further only manually mapped
     * pages will be stored by this method as automapped pages are looked up
     * dynamically.
     */
    void buildClassMap() {
        if (isProductionMode() || isProfileMode()) {
            super.buildClassMap();
            return;
        }

        // Build pages by className map
        for (Iterator i = pageByPathMap.values().iterator(); i.hasNext();) {
            PageMetaData page = (PageMetaData) i.next();

            Object value = manualPageByClassNameMap.get(page.pageClassName);

            if (value == null) {
                manualPageByClassNameMap.put(page.pageClassName, page);

            } else if (value instanceof List) {
                ((List) value).add(value);

            } else if (value instanceof PageMetaData) {
                List list = new ArrayList();
                list.add(value);
                list.add(page);
                manualPageByClassNameMap.put(page.pageClassName, list);

            } else {
                // should never occur
                throw new IllegalStateException();
            }
        }
    }

    /**
     * Return the {@link PageMetaData} associated with the given path
     *
     * @param path the page path
     * @return the PageMetaData object for the given path
     */
    private PageMetaData lookupManuallyStoredMetaData(String path) {
        //Try and load the manually mapped page
        PageMetaData page = (PageMetaData) manualPageByPathMap.get(path);
        if (page == null) {
            String jspPath = StringUtils.replace(path, ".htm", ".jsp");
            page = (PageMetaData) manualPageByPathMap.get(jspPath);
        }
        return page;
    }

    /**
     * Return the {@link PageMetaData} associated with the given page class.
     *
     * @param pageClass the page class
     * @return the PageMetaData object for the given page class
     */
    private PageMetaData lookupManuallyStoredMetaData(Class pageClass) {
        //Try and load the manually mapped page
        PageMetaData page = null;
        Object object = (PageMetaData) manualPageByClassNameMap.get(pageClass);
        if (object instanceof PageMetaData) {
            page = (PageMetaData) object;
            return page;

        } else if (object instanceof List) {
            String msg = "Page class resolves to multiple paths: " + pageClass.getName();
            throw new IllegalArgumentException(msg);

        }

        return page;
    }

    // ---------------------------------------------------------- Inner Classes

    /**
     * Encapsulate a Page metadata such as headers, classname and path.
     */
    static class PageMetaData {

        final Map headers;

        final String pageClassName;

        final String path;

        public PageMetaData(Element element, String pagesPackage, Map commonHeaders) 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;
            }

            // Set pageClass
            String value = element.getAttribute("classname");
            if (value != null) {
                if (pagesPackage.trim().length() > 0) {
                    value = pagesPackage + "." + value;
                }
            } else {
                String msg = "No classname defined for page path " + path;
                throw new RuntimeException(msg);
            }

            pageClassName = value;
        }

        public Map getHeaders() {
            return headers;
        }

        public String getPageClassName() {
            return pageClassName;
        }

        public String getPath() {
            return path;
        }
    }

    /**
     * Encapsulates the algorithm used to lookup the Page path from a given
     * Page class.
     */
    class PathLookupAlgorithm {

        public String getPagePath(Class pageClass) {
            return constructPathFromClass(pageClass);
        }

        public String constructPathFromClass(Class pageClass) {
            String className = ClassUtils.getShortClassName(pageClass);
            String pageDir = calcPageDir(pageClass);
            String result = null;

            result = constructPathFromClass(pageDir, className);

            if (result != null) {
                return result;
            }

            //Not found? Try with/without 'Page'
            if (className.endsWith("Page")) {
                //Chop off the 'Page' string and try again
                String noPageClassName = className.substring(0, className.lastIndexOf("Page"));
                result = constructPathFromClass(pageDir, noPageClassName);
            } else {
                //Append the 'Page' string and try again
                String pageClassName = className + "Page";
                result = constructPathFromClass(pageDir, pageClassName);
            }

            return result;
        }

        /**
         * This method strips the pagesPackage from the class package.
         * Thus if clickApp.pagesPackage is 'com.mycorp', then the packageName
         * 'com.mycorp.contacts' becomes 'contacts'
         */
        public String calcPageDir(Class clazz) {
            int indexOfClassName = clazz.getName().lastIndexOf('.');
            String pageDir = "/";

            // If the clazz does not have a package return the root path
            if (indexOfClassName < 0) {
                return pageDir;
            }

            // Note the addition of the '.' after the package name below. This
            // ensures to check for a legal package instead of a false positive
            // like 'com.mycorp.con' which would also have qualified if the
            // package name was 'com.mycorp.contacts'.

            // The '+ 1' in the substring argument ensures the packageName
            // ends with '.' ie 'com.mycorp.'
            final String packageName = clazz.getName().substring(0, indexOfClassName + 1);

            // If the pagePackages is not specified return the converted packageName
            if (getPagePackages().isEmpty()) {
                return convertToAbsoluteDir(packageName);
            }

            boolean matchFound = false;

            for (int i = 0; i < getPagePackages().size(); i++) {
                String pagesPackage = pagePackages.get(i).toString();

                // Also append a '.' at the end of the pagesPackage
                // final String pagesPackage = getPagesPackage() + ".";

                // Check that pagesPackage is a substring of packageName
                if (packageName.startsWith(pagesPackage)) {

                    matchFound = true;

                    // Check that the pagesPackage and packageName is not equal
                    if (packageName.length() != pagesPackage.length()) {

                        // Subtract the pagesPackage from the specified class package
                        pageDir = packageName.substring(pagesPackage.length() + 1);
                        break;
                    }
                }
            }

            // If page directory was matched, return the pageDir
            if (matchFound) {
                return convertToAbsoluteDir(pageDir);
            } else {
                // If the page directory was not matched, return the packageName
                // as the path
                return convertToAbsoluteDir(packageName);
            }
        }

        public List getPagePackages() {
            return pagePackages;
        }

        /**
         * Prefix the name with a '/' and change any '.' to '/'
         */
        public String convertToAbsoluteDir(String packageName) {
            packageName = ensurePathStartsWithSlash(packageName);
            return packageName.replace('.', '/');
        }

        protected String constructPathFromClass(String pageDir, String className) {
            String result = _constructPathFromClass(pageDir, className + ".htm");
            if (result == null) {
                result = _constructPathFromClass(pageDir, className + ".jsp");
            }
            return result;
        }

        private String _constructPathFromClass(String pageDir, String className) {
            pageDir = ensurePathStartsWithSlash(pageDir);

            //The 'path from class' lookup strategy is as follows
            //1. do not change the classname, just do lookup
            String path = pageDir + className;
            URL resource = tryAndFindEntryForPath(path);
            if (resource != null) {
                return path;
            }

            //2. lower case the first character
            String lowercase = Character.toString(Character.toLowerCase(className.charAt(0)));
            path = pageDir + lowercase + className.substring(1);
            resource = tryAndFindEntryForPath(path);
            if (resource != null) {
                return path;
            }

            //3. normalize camel case class to path tokenized on '-'
            path = pageDir + camelCaseToPath(className, "-");
            resource = tryAndFindEntryForPath(path);
            if (resource != null) {
                return path;
            }

            //3. normalize camel case class to path tokenized on '-'
            path = pageDir + camelCaseToPath(className, "_");
            resource = tryAndFindEntryForPath(path);
            if (resource != null) {
                return path;
            }
            return null;
        }

        protected String camelCaseToPath(String camelString, String token) {
            HtmlStringBuffer buffer = new HtmlStringBuffer();
            char[] chars = camelString.toCharArray();
            int length = chars.length;

            //Append first char
            buffer.append(Character.toLowerCase(chars[0]));

            for (int i = 1; i < length; i++) {
                if (Character.isUpperCase(chars[i])) {
                    buffer.append(token);
                    buffer.append(Character.toLowerCase(chars[i]));
                } else {
                    buffer.append(chars[i]);
                }
            }
            return buffer.toString();
        }

        protected URL tryAndFindEntryForPath(String path) {
            try {
                //path = ensurePathStartsWithSlash(path);
                URL resource = getServletContext().getResource(path);
                return resource;
            } catch (Exception ex) {
            }
            return null;
        }

        protected String ensurePathStartsWithSlash(String path) {
            if (StringUtils.isBlank(path)) {
                return "/";
            }
            if (path.charAt(0) != '/') {
                return '/' + path;
            } else {
                return path;
            }
        }
    }
}