com.sun.jsftemplating.layout.LayoutDefinitionManager Maven / Gradle / Ivy
/*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the license at
* https://jsftemplating.dev.java.net/cddl1.html or
* jsftemplating/cddl1.txt.
* See the License for the specific language governing
* permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at jsftemplating/cddl1.txt.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* you own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
*/
package com.sun.jsftemplating.layout;
import com.sun.jsftemplating.annotation.FormatDefinitionAPFactory;
import com.sun.jsftemplating.annotation.HandlerAPFactory;
import com.sun.jsftemplating.annotation.HandlerInput;
import com.sun.jsftemplating.annotation.UIComponentFactoryAPFactory;
import com.sun.jsftemplating.component.factory.basic.GenericFactory;
import com.sun.jsftemplating.layout.descriptors.ComponentType;
import com.sun.jsftemplating.layout.descriptors.LayoutComponent;
import com.sun.jsftemplating.layout.descriptors.LayoutComposition;
import com.sun.jsftemplating.layout.descriptors.LayoutDefinition;
import com.sun.jsftemplating.layout.descriptors.LayoutElement;
import com.sun.jsftemplating.layout.descriptors.LayoutInsert;
import com.sun.jsftemplating.layout.descriptors.Resource;
import com.sun.jsftemplating.layout.descriptors.handler.HandlerDefinition;
import com.sun.jsftemplating.layout.descriptors.handler.IODescriptor;
import com.sun.jsftemplating.layout.facelets.DbFactory;
import com.sun.jsftemplating.layout.facelets.NSContext;
import com.sun.jsftemplating.layout.template.TemplateLayoutDefinitionManager;
import com.sun.jsftemplating.util.FileUtil;
import com.sun.jsftemplating.util.LogUtil;
import com.sun.jsftemplating.util.Util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.faces.context.FacesContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* This abstract class provides the base functionality for all
* LayoutDefinitionManager
implementations. It provides a
* static method used to obtain an instance of a concrete
* LayoutDefinitionManager
:
* {@link #getLayoutDefinitionManager(FacesContext,String)}. However, in
* most cases is makes the most sense to call the static method:
* {@link #getLayoutDefinition(FacesContext,String)}. This method
* ensures that the cache is checked first before going through the effort
* of finding a LayoutDefinitionManager
instance.
*
* This class also provides access to global {@link HandlerDefinition}s,
* {@link Resource}s, and {@link ComponentType}s.
*
* @author Ken Paulsen ([email protected])
*/
public abstract class LayoutDefinitionManager {
/**
* Constructor.
*/
protected LayoutDefinitionManager() {
super();
}
/**
* This method is responsible for finding/creating the requested
* {@link LayoutDefinition}.
*
* @param key The key used to identify the requested
* {@link LayoutDefinition}.
*/
public abstract LayoutDefinition getLayoutDefinition(String key) throws LayoutDefinitionException;
/**
* This method is used to determine if this
* LayoutDefinitionManager
should process the given key.
* It does not necessarily mean that the
* LayoutDefinitionManager
can process it.
* Parser errors do not necessarily mean that it should not process
* the file. In order to provide meaningful error messages, this
* method should return true if the format of the template matches the
* type that this LayoutDefinitionManager
processes. It
* is understood that at times it may not be recognizable; in the case
* where no LayoutDefinitionManager
s return
* true
from this method, the parent
* ViewHandler
will be used, which likely means that it
* will look for a .jsp and give error messages accordingly. Also,
* the existance of a file should not be used as a meassure of success
* as other LayoutDefinitionManager
s may be more
* appropriate.
*/
public abstract boolean accepts(String key);
/**
* This method should be used to obtain a {@link LayoutDefinition}.
* It first checks to see if a cached {@link LayoutDefinition}
* already exists, if so it returns it. If one does not already
* exist, it will obtain the appropriate
* LayoutDefinitionManager
instance and call
* {@link #getLayoutDefinition} and return the result.
*/
public static LayoutDefinition getLayoutDefinition(FacesContext ctx, String key) throws LayoutDefinitionException {
// Determine the key we should use to cache this
String cacheKey = FileUtil.cleanUpPath(key.startsWith("/") ?
key : FileUtil.getAbsolutePath(ctx, key));
// Check to see if we already have it.
LayoutDefinition def = getCachedLayoutDefinition(ctx, cacheKey);
//System.out.println("GET LD (" + cacheKey + ", " + isDebug(ctx) + "):" + def);
if (def == null) {
// Obtain the correct LDM, and get the LD
def = getLayoutDefinitionManager(ctx, key).getLayoutDefinition(key);
//System.out.println(" Found LD (" + cacheKey + ")?:" + def);
putCachedLayoutDefinition(ctx, cacheKey, def);
} else {
// In the case where we found a cached version,
// ensure we invoke "initPage" handlers
def.dispatchInitPageHandlers(ctx, def);
}
// FIXME: Flag a page as *not found* for performance reasons when JSP is used (or other view technologies)
// Return the LD
return def;
}
/**
* This method finds the (closest) requested
* LayoutComponent
for the given clientId
.
* If the viewId
is not supplied, the current
* UIViewRoot
will be used. If an exact match is not
* found, it will return the last {@link LayoutComponent} found while
* walking the tree -- this represents the last {@link LayoutComponent}
* in the hierarchy of the specified component. If nothing matches the
* given clientId
, null
will be returned.
*
* This is not an easy process since JSF components are not all
* NamingContainer
s, so the clientId
is not
* sufficient to find it. This is unfortunate, but we we deal with
* it.
*
* @param ctx The FacesContext
.
* @param ldKey The {@link LayoutDefinition} key to identify the
* {@link LayoutDefinition} tree to be searched.
* @param clientId The component clientId
for which to
* obtain a {@link LayoutComponent}.
*/
public static LayoutComponent getLayoutComponent(FacesContext ctx, String ldKey, String clientId) throws LayoutDefinitionException {
// Find the page first...
LayoutElement layElt = null;
if (ldKey != null) {
// FIXME: This fixme probably belongs in getLD(ctx, key): initPage should only be invoked if the page is accessed for the first time on the request. This potentially calls it multiple times.
layElt = getLayoutDefinition(ctx, ldKey);
if (layElt == null) {
throw new LayoutDefinitionException(
"Unable to find LayoutDefinition ('" + ldKey + "')");
}
} else {
layElt = ViewRootUtil.getLayoutDefinition(
FacesContext.getCurrentInstance().getViewRoot());
}
// Save the current LayoutComposition Stack
// - This is needed b/c we may be in the middle of walking the tree
// - already and we need ot use this Stack... so we must save the
// - Stack and use a fresh one. We must restore it later.
Stack oldStack = LayoutComposition.getCompositionStack(ctx);
try {
LayoutComposition.setCompositionStack(
ctx, new Stack());
// Create a StringTokenizer over the clientId
StringTokenizer tok = new StringTokenizer(clientId, ":");
// Walk the LD looking for the individual id's specified in the
// clientId.
String id = null;
LayoutElement match = null;
while (tok.hasMoreTokens()) {
// I don't want to create a bunch of objects to check for
// instanceof NamingContainer. I can't check the class file
// b/c there is no way for me to know what class gets created
// before actually creating the UIComponent. This is because
// either the ComponentFactory can decide how to create the
// UIComponent, which it often uses the Application. The
// Application is driven off the faces-config.xml file(s).
//
// I will instead do a brute force search for a match. This
// has the potential to fail if non-naming containers have the
// same id's as naming containers. It may also fail for
// components with dynamic id's.
id = tok.nextToken();
match = findById(ctx, layElt, id);
if (match == null) {
// Can't go any further! We're as close as we're getting.
break;
}
layElt = match;
}
} finally {
// Restore the previous LayoutComposition Stack
LayoutComposition.setCompositionStack(ctx, oldStack);
}
// Make sure we're not still at the LayoutDefinition, if so do NOT
// accept this as a match.
if (layElt instanceof LayoutDefinition) {
layElt = null;
}
// Return the closest match (or null if nothing found)
return (LayoutComponent) layElt;
}
/**
* This method performs a breadth-first search for a child
* {@link LayoutComponent} with the given id
of the given
* {@link LayoutElement} (elt
). It will return null if
* none of the children (or children's children, etc.) equal the given
* id
.
*/
private static LayoutComponent findById(FacesContext ctx, LayoutElement elt, String id) {
boolean shouldPop = false;
// Check for special LE's
if (elt instanceof LayoutComposition) {
// We have a LayoutComposition, this includes another file... we
// need to look there as well.
String viewId = ((LayoutComposition) elt).getTemplate();
if (viewId != null) {
// Add LayoutComposition to the stack
LayoutComposition.push(ctx, elt);
shouldPop = true;
// Get the new LD to walk
try {
elt = LayoutDefinitionManager.getLayoutDefinition(ctx, viewId);
} catch (LayoutDefinitionException ex) {
if (((LayoutComposition) elt).isRequired()) {
throw ex;
}
}
}
} else if (elt instanceof LayoutInsert) {
// We found a LayoutInsert, this includes content from a previous
// file... we need to go back there and look now.
}
// First search the direct child LayoutElement
LayoutComponent comp = null;
for (LayoutElement child : elt.getChildLayoutElements()) {
// I am *NOT* providing the parent UIComponent as it may not be
// available, this function is *not* guaranteed to work for
// dynamic ids
if (child.getId(ctx, null).equals(id)
&& (child instanceof LayoutComponent)) {
// Found it!
comp = (LayoutComponent) child;
}
}
// Not found directly under it, search children...
// NOTE: Must do a breadth first search, so 2 loops are necessary
if (comp == null) {
for (LayoutElement child : elt.getChildLayoutElements()) {
comp = findById(ctx, child, id);
if (comp != null) {
// Found it!
break;
}
}
}
// Remove the LayoutComposition from the stack
if (shouldPop) {
LayoutComposition.pop(ctx);
}
// Return the result, or null if not found
return comp;
}
/**
* This method obtains the LayoutDefinitionManager
that
* is able to process the given key
.
*
* This implementation uses the ExternalContext
's
* initParams to look for the LayoutDefinitionManager
* class. If it exists, the specified concrete
* LayoutDefinitionManager
class will be used as the
* "default" (i.e. the first LayoutDefinitionManager
* checked). "{@link #LAYOUT_DEFINITION_MANAGER_KEY}" is the
* initParam key.
*
* The key
is used to test if desired
* LayoutDefinitionManager
is able to read the requested
* {@link LayoutDefinition}.
*
* @param ctx The FacesContext
.
* @param key The desired {@link LayoutDefinition}.
* @see #LAYOUT_DEFINITION_MANAGER_KEY
*/
public static LayoutDefinitionManager getLayoutDefinitionManager(FacesContext ctx, String key) throws LayoutDefinitionException {
List ldms = getLayoutDefinitionManagers(ctx);
//System.out.println("LDMS: " + ldms);
LayoutDefinitionManager mgr = null;
for (String className : ldms) {
mgr = getLayoutDefinitionManagerByClass(ctx, className);
//System.out.println("LDM ("+className+"): " + mgr);
if (mgr.accepts(key)) {
//System.out.println("Accepts!");
return mgr;
}
}
throw new LayoutDefinitionException("No LayoutDefinitionManager "
+ "available for '" + key + "'. This may mean the file cannot "
+ "be found, or is unrecognizable.");
}
/**
* This method is responsible for returning a List
of
* known LayoutDefinitionManager
instances. Each value
* of the list is a String
representing the classname of
* a LayoutDefinitionManager
implementation.
*/
public static List getLayoutDefinitionManagers(FacesContext ctx) {
if (ctx == null) {
ctx = FacesContext.getCurrentInstance();
}
List keys = null;
if (ctx != null) {
keys = (List) ctx.getExternalContext().
getApplicationMap().get(LDM_KEYS);
}
if (keys == null) {
// 1st time... initialize it
keys = new ArrayList();
// Check to see what the default should be...
if (ctx != null) {
Map initParams = ctx.getExternalContext().getInitParameterMap();
if (initParams.containsKey(LAYOUT_DEFINITION_MANAGER_KEY)) {
keys.add(((String) initParams.
get(LAYOUT_DEFINITION_MANAGER_KEY)).trim());
}
}
// Make "template" format the default (if none specified)
String tplFormat = TemplateLayoutDefinitionManager.class.getName();
if (!keys.contains(tplFormat)) {
keys.add(tplFormat);
}
try {
// Get all the files that define them
BufferedReader rdr = null;
InputStream is = null;
String line = null;
Enumeration urls = Util.getClassLoader(ctx).
getResources(FormatDefinitionAPFactory.FACTORY_FILE);
while (urls.hasMoreElements()) {
// Add all lines in each file to the list of LDMs
try {
is = urls.nextElement().openStream();
rdr = new BufferedReader(new InputStreamReader(is));
for (line = rdr.readLine(); line != null; line = rdr.readLine()) {
line = line.trim();
if (line.equals("") || line.startsWith("#")) {
// Skip comments
continue;
}
if (keys.contains(line)) {
// Skip ones already added...
continue;
}
// Add it!
keys.add(line);
}
} finally {
Util.closeStream(is);
}
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
if (ctx != null) {
// Save the result in Application Scope
ctx.getExternalContext().getApplicationMap().
put(LDM_KEYS, keys);
}
}
// Return the LDM keys
return keys;
}
/**
* This method is a singleton factory method for obtaining an instance
* of a LayoutDefintionManager
. It is possible that
* multiple different implementations of
* LayoutDefinitionManager
s will be used within the same
* application. This is OK. Someone may provide a different
* LayoutDefinitionManager
to locate
* {@link LayoutDefinition}'s in a different way (XML, database, file,
* java code, new file format, etc.).
*/
public static LayoutDefinitionManager getLayoutDefinitionManagerByClass(FacesContext ctx, String className) {
if (ctx == null) {
ctx = FacesContext.getCurrentInstance();
}
Map ldms = null;
if (ctx != null) {
ldms = (Map)
ctx.getExternalContext().getApplicationMap().get(LDMS);
}
if (ldms == null) {
ldms = new HashMap(4);
if (ctx != null) {
ctx.getExternalContext().getApplicationMap().put(LDMS, ldms);
}
}
LayoutDefinitionManager ldm = ldms.get(className);
if (ldm == null) {
try {
ldm = (LayoutDefinitionManager)
Util.loadClass(className, className).
getMethod("getInstance", (Class[]) null).
invoke((Object) null, (Object[]) null);
} catch (ClassNotFoundException ex) {
throw new LayoutDefinitionException(
"Unable to find LDM: '" + className + "'.", ex);
} catch (NoSuchMethodException ex) {
throw new LayoutDefinitionException("LDM '" + className
+ "' does not have a 'getInstance()' method!", ex);
} catch (IllegalAccessException ex) {
throw new LayoutDefinitionException("Unable to access LDM: '"
+ className + "'!", ex);
} catch (InvocationTargetException ex) {
throw new LayoutDefinitionException("Error while attempting "
+ "to get LDM: '" + className + "'!", ex);
} catch (ClassCastException ex) {
throw new LayoutDefinitionException("LDM '" + className
+ "' must extend from '"
+ LayoutDefinitionManager.class.getName() + " and must "
+ "be loaded from the same ClassLoader!", ex);
} catch (NullPointerException ex) {
throw new LayoutDefinitionException(ex);
}
ldms.put(className, ldm);
}
return ldm;
}
/**
* This method may be used to obtain a cached
* {@link LayoutDefinition}. If it has not been cached, this method
* returns null
.
*
* @param ctx The FacesContext
.
* @param key Key for the cached {@link LayoutDefinition} to obtain.
*
* @return The {@link LayoutDefinition} or null
.
*/
private static LayoutDefinition getCachedLayoutDefinition(FacesContext ctx, String key) {
if (ctx == null) {
ctx = FacesContext.getCurrentInstance();
}
if (isDebug(ctx)) {
if (ctx != null) {
// Make sure we cache during the life of the request, even
// in Debug mode
return (LayoutDefinition) ctx.getExternalContext().
getRequestMap().get(CACHE_PREFIX + key);
}
// Disable caching for debug mode
return null;
}
return getLayoutDefinitionMap(ctx).get(key);
}
/**
* This method returns the LD Map which is stored in application
* scope. If it has not been created yet, it will be created as a
* ConcurrentHashMap
.
*/
private static Map getLayoutDefinitionMap(FacesContext ctx) {
if (ctx == null) {
ctx = FacesContext.getCurrentInstance();
}
Map ldMap = null;
if (ctx != null) {
ldMap = (Map)
ctx.getExternalContext().getApplicationMap().get(LD_MAP);
}
if (ldMap == null) {
// 1st time... initialize it
// Consider using a SoftReference here...
ldMap = new ConcurrentHashMap(
400, 0.75f, 2);
if (ctx != null) {
ctx.getExternalContext().getApplicationMap().put(LD_MAP, ldMap);
}
}
// Return the map...
return ldMap;
}
/**
* In general, this method should be used by sub-classes to store a
* cached {@link LayoutDefinition}. It may also be used, however, to
* define {@link LayoutDefinition}s on the fly (not recommended unless
* you know what you're doing. ;)
*
* @param ctx The FacesContext
.
* @param key The {@link LayoutDefinition} key to cache.
* @param value The {@link LayoutDefinition} to cache.
*/
public static void putCachedLayoutDefinition(FacesContext ctx, String key, LayoutDefinition value) {
//System.out.println("CACHING LD: " + key);
if (isDebug(ctx)) {
if (ctx != null) {
// Make sure we cache during the life of the request, even
// in Debug mode
ctx.getExternalContext().getRequestMap().
put(CACHE_PREFIX + key, value);
}
} else {
getLayoutDefinitionMap(ctx).put(key, value);
}
}
/**
* Retrieve an attribute by key.
*
* @param key The key used to retrieve the attribute.
*
* @return The requested attribute or null
*/
public Object getAttribute(String key) {
return _attributes.get(key);
}
/**
* Associate the given key with the given Object as an attribute.
*
* @param key The key associated with the given object (if this key
* is already in use, it will replace the previously set
* attribute object).
* @param value The Object to store.
*/
public void setAttribute(String key, Object value) {
_attributes.put(key, value);
}
/**
* This method returns the Map
of global
* {@link ComponentType}s (the {@link ComponentType}s available across
* the application).
*
* It is recommended that this method not be used directly. The map
* returned by this method is shared across the application and is not
* thread safe. Instead access this Map via
* {@link LayoutDefinitionManager#getGlobalComponentType(FacesContext, String)}.
*
* This method will initialize the global {@link ComponentType}s if
* they are not initialized. It does this by finding all files in the
* classpath named:
* {@link UIComponentFactoryAPFactory#FACTORY_FILE}. It then reads
* each of these files (which must be Properties
files)
* and stores each identifier / fully qualified classname as an entry
* in the Map<String, {@link ComponentType}>
.
*
* @param ctx The FacesContext
.
*/
public static Map getGlobalComponentTypes(FacesContext ctx) {
if (ctx == null) {
ctx = FacesContext.getCurrentInstance();
}
Map types = null;
if (ctx != null) {
types = (Map) ctx.getExternalContext().
getApplicationMap().get(CT_MAP);
}
if (types == null) {
// We haven't initialized the global ComponentTypes yet...
types = new ConcurrentHashMap(200, 0.75f, 2);
try {
Properties props = null;
URL url = null;
String id = null;
// Get all the properties files that define them
Enumeration urls =
Util.getClassLoader(types).
getResources(UIComponentFactoryAPFactory.FACTORY_FILE);
while (urls.hasMoreElements()) {
url = urls.nextElement();
props = new Properties();
// Load each Properties file
InputStream is = null;
try {
is = url.openStream();
props.load(is);
for (Map.Entry