org.apache.click.extras.control.MenuFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.click.extras.control.MenuFactory.java

Source

/*
 * 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.control;

import java.io.InputStream;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletContext;

import org.apache.click.Context;
import org.apache.click.extras.security.AccessController;
import org.apache.click.extras.security.RoleAccessController;
import org.apache.click.service.ConfigService;
import org.apache.click.util.ClickUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Provides a Menu factory for creating application menus from configuration
 * files.
 * <p/>
 * Menu factory provides a variety of <tt>getRootMenu()</tt> methods for
 * loading the menus. The default {@link #getRootMenu()} method creates menus
 * from the configuration file <tt>/WEB-INF/menu.xml</tt>, or the classpath
 * resource <tt>/menu.xml</tt> if <tt>WEB-INF/menu.xml</tt> was not resolved.
 * <p/>
 * Below is an example <tt>menu.xml</tt> configuration file:
 *
 * <pre class="prettyprint">
 * &lt;?xml version="1.0" encoding="UTF-8" standalone="yes"?&gt;
 * &lt;menu&gt;
 *    &lt;menu label="Home" path="user/home.htm" roles="tomcat, role1"/&gt;
 *    &lt;menu label="User" path="user/home.htm" roles="tomcat, role1"&gt;
 *        &lt;menu label="User Page 1" path="user/user-1.htm" roles="tomcat, role1"/&gt;
 *        &lt;menu label="User Page 2" path="user/user-2.htm" roles="tomcat, role1"/&gt;
 *    &lt;/menu&gt;
 *    &lt;menu label="Admin" path="admin/admin-1.htm" roles="role1"&gt;
 *        &lt;menu label="Admin Page 1" path="admin/admin-1.htm" roles="tomcat, role1"/&gt;
 *        &lt;menu label="Admin Page 2" path="admin/admin-2.htm" roles="tomcat, role1"/&gt;
 *    &lt;/menu&gt;
 * &lt;/menu&gt; </pre>
 *
 * You can also specify an alternative configuration file name to load your
 * menus from. Just use one of the <tt>getRootMenu</tt> methods that accept a
 * configuration file name, for example {@link #getRootMenu(java.lang.String, java.lang.String)
 * getRootMenu(name, fileName)}.
 *
 * <h3><a name="examples"></a>MenuFactory Examples</h3>
 *
 * Below is an example of a MenuFactory being used to set the rootMenu on a
 * border page. Typically a border page will define a page template which
 * contain the surrounding page chrome including the header and the application
 * menu. Application page classes will subclass the BorderPage an inherit
 * the application rootMenu.
 *
 * <pre class="prettyprint">
 * public abstract class BorderPage extends Page {
 *
 *     private Menu rootMenu;
 *
 *     public BorderPage() {
 *         MenuFactory menuFactory = new MenuFactory();
 *         rootMenu = menuFactory.getRootMenu();
 *         addControl(rootMenu);
 *     }
 *
 *     &#64;Override
 *     public String getTemplate() {
 *         return "/border-template.htm";
 *     }
 *
 * } </pre>
 *
 * <h3><a name="stateful-pages"></a>Stateful pages</h3>
 * Please note if you use stateful pages that are serialized, you probably
 * won't want your application menu being serialized to disk or across a cluster
 * with your page as well. In these scenarios please follow the pattern below.
 *
 * <pre class="prettyprint">
 * public abstract class BorderPage extends Page {
 *
 *     // Note the transient keyword
 *     private transient Menu rootMenu;
 *
 *     &#64;Override
 *     public void onInit() {
 *         super.onInit();
 *
 *         MenuFactory menuFactory = new MenuFactory();
 *         rootMenu = menuFactory.getRootMenu();
 *         addControl(rootMenu);
 *     }
 *
 *     &#64;Override
 *     public void onDestroy() {
 *         if (rootMenu != null) {
 *             removeControl(rootMenu);
 *         }
 *
 *         super.onDestroy();
 *     }
 *
 * } </pre>
 *
 * <h3><a name="caching"></a>Caching</h3>
 * Loading Menus using {@link #getRootMenu()} will automatically cache the
 * menus for improved performance (technically the menus are only cached when
 * Click is in <tt>production</tt> or <tt>profile</tt> mode).
 * <p/>
 * If you want to manage Menu caching yourself, use one of the
 * {@link #getRootMenu(boolean) getRootMenu} methods that accepts a boolean
 * controlling whether or not the menus are cached.
 * <p/>
 * A common use case for caching menus yourself is when you need to customize
 * the menus based on the logged in user. For this scenario you would load the
 * Menus using {@link #getRootMenu(boolean) getRootMenu(false)}, customize the
 * menus according to the user profile, and cache the menus in the HttpSession.
 *
 * @see Menu
 */
public class MenuFactory implements Serializable {

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

    private static final long serialVersionUID = 1L;

    /**
     * The default root menu name: &nbsp; "<tt>rootMenu</tt>".
     */
    public final static String DEFAULT_ROOT_MENU_NAME = "rootMenu";

    /**
     * The menu configuration filename: &nbsp; "<tt>menu.xml</tt>".
     */
    protected static final String DEFAULT_CONFIG_FILE = "menu.xml";

    // Class Variables --------------------------------------------------------

    /** The default Menu XML attributes loaded into menu properties. */
    protected final static Set<String> DEFAULT_ATTRIBUTES = new HashSet<String>();

    /** The menu cache. */
    protected static final Map<String, Menu> MENU_CACHE = new ConcurrentHashMap<String, Menu>();

    static {
        DEFAULT_ATTRIBUTES.add("name");
        DEFAULT_ATTRIBUTES.add("label");
        DEFAULT_ATTRIBUTES.add("path");
        DEFAULT_ATTRIBUTES.add("target");
        DEFAULT_ATTRIBUTES.add("title");
        DEFAULT_ATTRIBUTES.add("imageSrc");
        DEFAULT_ATTRIBUTES.add("external");
        DEFAULT_ATTRIBUTES.add("separator");
        DEFAULT_ATTRIBUTES.add("roles");
        DEFAULT_ATTRIBUTES.add("pages");
    }

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

    /**
    * Return cached root menu item defined in the WEB-INF/menu.xml or classpath
     * menu.xml, creating menu items using the Menu class and the JEE
     * RoleAccessController.
     *
     * @see RoleAccessController
     *
     * @return the cached root menu item defined in the WEB-INF/menu.xml file
     * or menu.xml in the root classpath
     */
    public Menu getRootMenu() {
        return getRootMenu(DEFAULT_ROOT_MENU_NAME, DEFAULT_CONFIG_FILE);
    }

    /**
     * Return root menu item defined in the WEB-INF/menu.xml or classpath
     * menu.xml, creating menu items using the provided menu class and the JEE
     * RoleAccessController.
     *
     * @param menuClass the menu class to create new Menu instances from
     * @return the cached root menu item defined in the WEB-INF/menu.xml file
     * or menu.xml in the root classpath
     */
    public Menu getRootMenu(Class<? extends Menu> menuClass) {
        return getRootMenu(DEFAULT_ROOT_MENU_NAME, DEFAULT_CONFIG_FILE, new RoleAccessController(), true,
                menuClass);
    }

    /**
     * Return root menu item defined in the WEB-INF/menu.xml or classpath
     * menu.xml, creating menu items using the Menu class and the provided
     * AccessController.
     *
     * @param accessController the menu access controller
     * @return the root menu item defined in the WEB-INF/menu.xml file or menu.xml
     * in the root classpath
     */
    public Menu getRootMenu(AccessController accessController) {
        return getRootMenu(DEFAULT_ROOT_MENU_NAME, DEFAULT_CONFIG_FILE, accessController, true, null);
    }

    /**
     * Return root menu item defined in the WEB-INF/menu.xml or classpath
     * menu.xml, creating menu items using the Menu class and the JEE
     * RoleAccessController. The cached option specifies whether the loaded
     * menus will be cached or not.
     *
     * @param cached return the cached menu if in production or profile mode,
     * otherwise create and return a new root menu instance
     * @return the root menu item defined in the WEB-INF/menu.xml file or menu.xml
     * in the root classpath
     */
    public Menu getRootMenu(boolean cached) {
        return getRootMenu(DEFAULT_ROOT_MENU_NAME, DEFAULT_CONFIG_FILE, new RoleAccessController(), cached, null);
    }

    /**
     * Return root menu item defined by the given name and fileName under
     * WEB-INF or the classpath, creating menu items using the Menu class and
     * the JEE RoleAccessController.
     *
     * @param name the name of the root menu
     * @param fileName the fileName defining the menu definitions
     * @return the root menu item defined by the fileName under WEB-INF or the
     * classpath
     */
    public Menu getRootMenu(String name, String fileName) {
        return getRootMenu(name, fileName, new RoleAccessController(), true, null);
    }

    /**
     * Return root menu item defined by the given name and fileName under WEB-INF
     * or the classpath, creating menu items using the provided menu class and
     * AccessController. The cached option specifies whether the loaded
     * menus will be cached or not.
     * <p/>
     * Example usage:
     * <pre class="prettyprint">
     * public void onInit() {
     *     MenuFactory factory = new MenuFactory();
     *     String menuName = "mymenu";
     *     String fileName = "mymenu.xml";
     *     AccessController accessController = new RoleAccessController();
     *     boolean cached = true;
     *
     *     factory.getRootMenu(menuName, fileName, accessController, cached, MyMenu.class);
     * } </pre>
     *
     * @param name the name of the root menu
     * @param fileName the fileName defining the menu definitions
     * @param accessController the menu access controller
     * @param cached return the cached menu if in production or profile mode,
     * otherwise create and return a new root menu instance
     * @param menuClass the menu class to create new Menu instances from
     * @return the root menu item defined by the fileName under WEB-INF or the
     * classpath
     */
    public Menu getRootMenu(String name, String fileName, AccessController accessController, boolean cached,
            Class<? extends Menu> menuClass) {

        Validate.notNull(name, "Null name parameter");
        Validate.notNull(fileName, "Null fileName parameter");
        Validate.notNull(accessController, "Null accessController parameter");

        if (cached) {

            Menu cachedMenu = retrieveRootMenu(name);
            if (cachedMenu != null) {
                return cachedMenu;
            }
        }

        Menu rootMenu = loadFromMenuXml(name, fileName, accessController, menuClass);

        // Retrieve headElements to guard against race conditions when initializing
        // menus from multiple threads. CLK-713
        rootMenu.getHeadElements();

        ServletContext servletContext = Context.getThreadLocalContext().getServletContext();
        ConfigService configService = ClickUtils.getConfigService(servletContext);

        if (cached) {

            if (configService.isProductionMode() || configService.isProfileMode()) {
                // Cache menu in production modes
                cacheRootMenu(rootMenu);
            }
        }

        return rootMenu;
    }

    // Protected Methods ------------------------------------------------------

    /**
     * Build a new Menu from the given menu item XML Element and recurse through
     * all the menu-items children. If the menuClass is specified, menus will
     * be created of that type, otherwise an instance of {@link Menu} will be
     * created.
     *
     * @param menuElement the menu item XML Element
     * @param accessController the menu access controller
     * @param menuClass the menu class to instantiate
     * @return new Menu instance for the given XML menuElement
     */
    protected Menu buildMenu(Element menuElement, AccessController accessController,
            Class<? extends Menu> menuClass) {

        Validate.notNull(menuElement, "Null menuElement parameter");
        Validate.notNull(accessController, "Null accessController parameter");

        Menu menu = null;
        if (menuClass == null) {
            menu = new Menu();
        } else {
            menu = createMenu(menuClass);
        }

        menu.setAccessController(accessController);

        String nameAtr = menuElement.getAttribute("name");
        if (StringUtils.isNotBlank(nameAtr)) {
            menu.setName(nameAtr);
        }

        String labelAtr = menuElement.getAttribute("label");
        if (StringUtils.isNotBlank(labelAtr)) {
            menu.setLabel(labelAtr);
        }

        String imageSrcAtr = menuElement.getAttribute("imageSrc");
        if (StringUtils.isNotBlank(imageSrcAtr)) {
            menu.setImageSrc(imageSrcAtr);
        }

        String pathAtr = menuElement.getAttribute("path");
        if (StringUtils.isNotBlank(pathAtr)) {
            menu.setPath(pathAtr);
        }

        String titleAtr = menuElement.getAttribute("title");
        if (StringUtils.isNotBlank(titleAtr)) {
            menu.setTitle(titleAtr);
        }

        String targetAtr = menuElement.getAttribute("target");
        if (StringUtils.isNotBlank(targetAtr)) {
            menu.setTarget(targetAtr);
        }

        String externalAtr = menuElement.getAttribute("external");
        if ("true".equalsIgnoreCase(externalAtr)) {
            menu.setExternal(true);
        }

        String separatorAtr = menuElement.getAttribute("separator");
        if ("true".equalsIgnoreCase(separatorAtr)) {
            menu.setSeparator(true);
        }

        /*
        String visibilityAtr = menuElement.getAttribute("visible");
        if ("false".equalsIgnoreCase(visibilityAtr)) {
        menu.setVisible(false);
        }
            
        String enablingAtr = menuElement.getAttribute("enabled");
        if ("false".equalsIgnoreCase(enablingAtr)) {
        menu.setEnabled(false);
        }*/

        String pagesValue = menuElement.getAttribute("pages");
        if (StringUtils.isNotBlank(pagesValue)) {
            StringTokenizer tokenizer = new StringTokenizer(pagesValue, ",");
            while (tokenizer.hasMoreTokens()) {
                String path = tokenizer.nextToken().trim();
                path = (path.startsWith("/")) ? path : "/" + path;
                menu.getPages().add(path);
            }
        }

        String rolesValue = menuElement.getAttribute("roles");
        if (StringUtils.isNotBlank(rolesValue)) {
            StringTokenizer tokenizer = new StringTokenizer(rolesValue, ",");
            while (tokenizer.hasMoreTokens()) {
                menu.getRoles().add(tokenizer.nextToken().trim());
            }
        }

        // Load other attributes
        NamedNodeMap attributeNodeMap = menuElement.getAttributes();
        for (int i = 0; i < attributeNodeMap.getLength(); i++) {
            Node attribute = attributeNodeMap.item(i);
            String name = attribute.getNodeName();
            if (!DEFAULT_ATTRIBUTES.contains(name)) {
                String value = attribute.getNodeValue();
                menu.getAttributes().put(name, value);
            }
        }

        NodeList childElements = menuElement.getChildNodes();
        for (int i = 0, size = childElements.getLength(); i < size; i++) {
            Node node = childElements.item(i);
            if (node instanceof Element) {
                Menu childMenu = buildMenu((Element) node, accessController, menuClass);
                menu.add(childMenu);
            }
        }
        return menu;
    }

    /**
     * Create a new menu instance of the given menu class.
     *
     * @param menuClass the menu class to instantiate
     * @return a new menu instance of the given menu class
     */
    protected Menu createMenu(Class<? extends Menu> menuClass) {
        try {
            return menuClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Error occurred create new menu of type " + menuClass);
        }
    }

    /**
     * Return a copy of the Applications root Menu as defined by the
     * configuration file.
     * <p/>
     * If the fileName starts with a '/' character it is assumed to be an
     * absolute path and Click will attempt to load the file from the Servlet
     * context path and if not found from the classpath.
     * <p/>
     * If the fileName does not start with a '/' character it is assumed to be
     * a relative path and Click will load the file from the Servlet context
     * by <tt>prefixing</tt> the fileName with '/WEB-INF'. If not found the
     * file will be loaded from the classpath.
     * <p/>
     * The returned root menu is always selected.
     *
     * @param name the name of the root menu
     * @param fileName the configuration fileName defining the menu definitions
     * @param accessController the menu access controller
     * @param menuClass the menu class to instantiate
     * @return a copy of the application's root Menu
     */
    protected Menu loadFromMenuXml(String name, String fileName, AccessController accessController,
            Class<? extends Menu> menuClass) {

        if (name == null) {
            throw new IllegalArgumentException("Null name parameter");
        }

        if (fileName == null) {
            throw new IllegalArgumentException("Null fileName parameter");
        }

        if (accessController == null) {
            throw new IllegalArgumentException("Null accessController parameter");
        }

        String webinfFileName = null;
        boolean absolute = fileName.startsWith("/");
        if (!absolute) {
            fileName = '/' + fileName;
            webinfFileName = "/WEB-INF" + fileName;
        }

        Context context = Context.getThreadLocalContext();

        Menu menu = null;
        if (menuClass == null) {
            menu = new Menu();
        } else {
            menu = createMenu(menuClass);
        }

        menu.setName(name);
        menu.setAccessController(accessController);

        ServletContext servletContext = context.getServletContext();
        InputStream inputStream = null;

        if (absolute) {
            inputStream = servletContext.getResourceAsStream(fileName);
        } else {
            inputStream = servletContext.getResourceAsStream(webinfFileName);
        }

        if (inputStream == null) {
            if (absolute) {
                inputStream = ClickUtils.getResourceAsStream(fileName, MenuFactory.class);
                if (inputStream == null) {
                    String msg = "could not find configuration file:" + fileName + " on classpath";
                    throw new RuntimeException(msg);
                }
            } else {
                inputStream = ClickUtils.getResourceAsStream(fileName, MenuFactory.class);
                if (inputStream == null) {
                    String msg = "could not find configuration file:" + webinfFileName + " or " + fileName
                            + " on classpath";
                    throw new RuntimeException(msg);
                }
            }
        }

        Document document = ClickUtils.buildDocument(inputStream);

        Element rootElm = document.getDocumentElement();

        NodeList list = rootElm.getChildNodes();

        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);
            if (node instanceof Element) {
                Menu childMenu = buildMenu((Element) node, accessController, menuClass);
                menu.add(childMenu);
            }
        }

        return menu;
    }

    /**
     * Return the map containing menus cached by name.
     *
     * @return the map containing menus cached by name
     */
    protected Map<String, Menu> getMenuCache() {
        return MENU_CACHE;
    }

    /**
     * Return the cached root menu from the
     * {@link #getMenuCache() menu cache}.
     *
     * @param name the name of the root menu to retrieve
     * @return the cache root menu from the menu cache
     */
    protected Menu retrieveRootMenu(String name) {
        if (name == null) {
            throw new IllegalArgumentException("Null name parameter");
        }
        return getMenuCache().get(name);
    }

    /**
     * Cache the given menu in the {@link #getMenuCache() menu cache}.
     *
     * @param menu the menu to store in the cache
     */
    protected void cacheRootMenu(Menu menu) {
        if (menu == null) {
            throw new IllegalArgumentException("Null menu parameter");
        }

        if (menu.getName() == null) {
            throw new IllegalArgumentException("Menu name cannot be null");
        }

        getMenuCache().put(menu.getName(), menu);
    }
}