/* | |
* 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"> | |
* <?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
* <menu> | |
* <menu label="Home" path="user/home.htm" roles="tomcat, role1"/> | |
* <menu label="User" path="user/home.htm" roles="tomcat, role1"> | |
* <menu label="User Page 1" path="user/user-1.htm" roles="tomcat, role1"/> | |
* <menu label="User Page 2" path="user/user-2.htm" roles="tomcat, role1"/> | |
* </menu> | |
* <menu label="Admin" path="admin/admin-1.htm" roles="role1"> | |
* <menu label="Admin Page 1" path="admin/admin-1.htm" roles="tomcat, role1"/> | |
* <menu label="Admin Page 2" path="admin/admin-2.htm" roles="tomcat, role1"/> | |
* </menu> | |
* </menu> </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); | |
* } | |
* | |
* @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; | |
* | |
* @Override | |
* public void onInit() { | |
* super.onInit(); | |
* | |
* MenuFactory menuFactory = new MenuFactory(); | |
* rootMenu = menuFactory.getRootMenu(); | |
* addControl(rootMenu); | |
* } | |
* | |
* @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: "<tt>rootMenu</tt>". | |
*/ | |
public final static String DEFAULT_ROOT_MENU_NAME = "rootMenu"; | |
/** | |
* The menu configuration filename: "<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); | |
} | |
} |