org.apache.myfaces.custom.tree2.HtmlTreeRenderer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tomahawk Show documentation
Show all versions of tomahawk Show documentation
JSF components and utilities that can be used with any JSF implementation.
This library is compatible with both JSF1.1 and JSF1.2; however for JSF1.2 users there
is an alternative build of Tomahawk available that takes advantage of JSF1.2 features to
offer some additional benefits.
/*
* 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.myfaces.custom.tree2;
import org.apache.myfaces.renderkit.html.util.AddResource;
import org.apache.myfaces.renderkit.html.util.AddResourceFactory;
import org.apache.myfaces.shared_tomahawk.renderkit.html.HTML;
import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRendererUtils;
import org.apache.myfaces.shared_tomahawk.renderkit.RendererUtils;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UICommand;
import javax.faces.component.UIGraphic;
import javax.faces.component.UIViewRoot;
import javax.faces.component.UIParameter;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Iterator;
import java.net.URLDecoder;
import javax.servlet.http.Cookie;
import java.util.HashMap;
/**
* @JSFRenderer
* renderKitId = "HTML_BASIC"
* family = "org.apache.myfaces.HtmlTree2"
* type = "org.apache.myfaces.HtmlTree2"
*
* @author Sean Schofield
* @author Chris Barlow
* @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
* @version $Revision: 782532 $ $Date: 2009-06-08 00:38:29 -0500 (Mon, 08 Jun 2009) $
*/
public class HtmlTreeRenderer extends Renderer
{
protected static final String TOGGLE_SPAN = "org.apache.myfaces.tree.TOGGLE_SPAN";
protected static final String ROOT_NODE_ID = "0";
private static final String NAV_COMMAND = "org.apache.myfaces.tree.NAV_COMMAND";
private static final String ENCODING = "UTF-8";
private static final String ATTRIB_DELIM = ";";
private static final String ATTRIB_KEYVAL = "=";
private static final String NODE_STATE_EXPANDED = "x";
private static final String NODE_STATE_CLOSED = "c";
private static final String SEPARATOR = String.valueOf(NamingContainer.SEPARATOR_CHAR);
private static final String IMAGE_PREFIX = "t2";
private static final String TOGGLE_ID = "t2g";
private static final int NOTHING = 0;
private static final int CHILDREN = 1;
private static final int EXPANDED = 2;
private static final int LINES = 4;
private static final int LAST = 8;
private int counter = 0;
// see superclass for documentation
public boolean getRendersChildren()
{
return true;
}
private void restoreStateFromCookies(FacesContext context, UIComponent component) {
String nodeId = null;
HtmlTree tree = (HtmlTree)component;
TreeState state = tree.getDataModel().getTreeState();
Map cookieMap = context.getExternalContext().getRequestCookieMap();
Cookie treeCookie = (Cookie)cookieMap.get(component.getId());
if (treeCookie == null || treeCookie.getValue() == null)
{
return;
}
String nodeState = null;
Map attrMap = getCookieAttr(treeCookie);
Iterator i = attrMap.keySet().iterator();
while (i.hasNext())
{
nodeId = (String)i.next();
nodeState = (String)attrMap.get(nodeId);
if (NODE_STATE_EXPANDED.equals(nodeState))
{
if (!state.isNodeExpanded(nodeId))
{
state.toggleExpanded(nodeId);
}
}
else if (NODE_STATE_CLOSED.equals(nodeState))
{
if (state.isNodeExpanded(nodeId))
{
state.toggleExpanded(nodeId);
}
}
}
}
public void decode(FacesContext context, UIComponent component)
{
super.decode(context, component);
// see if one of the nav nodes was clicked, if so, then toggle appropriate node
String nodeId = null;
HtmlTree tree = (HtmlTree)component;
if (tree.isClientSideToggle() && tree.isPreserveToggle())
{
restoreStateFromCookies(context, component);
}
else
{
nodeId = (String)context.getExternalContext().getRequestParameterMap().get(tree.getId() + SEPARATOR + NAV_COMMAND);
if (nodeId == null || nodeId.equals(""))
{
return;
}
component.queueEvent(new ToggleExpandedEvent(component, nodeId));
}
}
public void encodeBegin(FacesContext context, UIComponent component) throws IOException
{
// Fixes TOMAHAWK-437
//HtmlTree tree = (HtmlTree)component;
//if (!tree.getDataModel().getTreeState().isTransient()
// && tree.isClientSideToggle()
// && tree.isPreserveToggle())
//{
// restoreStateFromCookies(context, component);
//}
// write javascript functions
encodeJavascript(context, component);
}
/**
* Renders the whole tree. It generates a <span>
element with an id
* attribute if the component has been given an explicit ID. The model nodes are rendered
* recursively by the private encodeNodes
method.
*
* @param context FacesContext
* @param component The component whose children are to be rendered
* @throws IOException
*/
public void encodeChildren(FacesContext context, UIComponent component) throws IOException
{
HtmlTree tree = (HtmlTree)component;
if (!component.isRendered()) return;
if (tree.getValue() == null) return;
ResponseWriter out = context.getResponseWriter();
String clientId = null;
if (component.getId() != null && !component.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX))
{
clientId = component.getClientId(context);
}
boolean isOuterSpanUsed = false;
if (clientId != null)
{
isOuterSpanUsed = true;
out.startElement("span", component);
out.writeAttribute("id", clientId, "id");
}
boolean clientSideToggle = tree.isClientSideToggle();
boolean showRootNode = tree.isShowRootNode();
TreeState state = tree.getDataModel().getTreeState();
TreeWalker walker = tree.getDataModel().getTreeWalker();
walker.reset();
walker.setTree(tree);
walker.setCheckState(!clientSideToggle); // walk all nodes in client mode
if (showRootNode)
{
// encode the tree (starting with the root node)
if (walker.next())
{
encodeTree(context, out, tree, walker);
}
}
else
{
// skip the root node
walker.next();
TreeNode rootNode = tree.getNode();
// now mark the root as expanded (so we don't stop there)
String rootNodeId = tree.getNodeId();
if(!state.isNodeExpanded(rootNodeId))
{
state.toggleExpanded(rootNodeId);
}
// now encode each of the nodes in the level immediately below the root
for (int i=0; i < rootNode.getChildCount(); i++)
{
if (walker.next())
{
encodeTree(context, out, tree, walker);
}
}
}
// reset the current node id once we're done encoding everything
tree.setNodeId(null);
if (isOuterSpanUsed)
{
out.endElement("span");
}
}
/**
* Encodes the tree and its children.
*
* @param context FacesContext
* @param out ResponseWriter
* @param tree HtmlTree
* @param walker TreeWalker
* @throws IOException
*/
protected void encodeTree(FacesContext context, ResponseWriter out, HtmlTree tree, TreeWalker walker)
throws IOException
{
boolean clientSideToggle = tree.isClientSideToggle();
// encode the current node
HtmlRendererUtils.writePrettyLineSeparator(context);
beforeNodeEncode(context, out, tree);
encodeCurrentNode(context, out, tree);
afterNodeEncode(context, out);
// if client side toggling is on, add a span to be used for displaying/hiding children
if (clientSideToggle)
{
String spanId = TOGGLE_SPAN + ":" + tree.getClientId(context) + ":" + tree.getNodeId();
out.startElement(HTML.SPAN_ELEM, tree);
out.writeAttribute(HTML.ID_ATTR, spanId, null);
if (tree.isNodeExpanded())
{
out.writeAttribute(HTML.STYLE_ATTR, "display:block", null);
}
else
{
out.writeAttribute(HTML.STYLE_ATTR, "display:none", null);
}
}
TreeNode node = tree.getNode();
for (int i=0; i < node.getChildCount(); i++)
{
if (walker.next())
{
encodeTree(context, out, tree, walker);
}
}
if (clientSideToggle)
{
out.endElement(HTML.SPAN_ELEM);
}
}
/**
* Encodes the current node. It is protected so that custom {@link Renderer}s can extend it. That might be useful
* if you would like to render additional per node information besides the tree node.
*
* @param context FacesContext
* @param out ResponseWriter
* @param tree HtmlTree
* @throws IOException
*/
protected void encodeCurrentNode(FacesContext context, ResponseWriter out, HtmlTree tree)
throws IOException
{
TreeNode node = tree.getNode();
// set configurable values
boolean showRootNode = tree.isShowRootNode();
boolean showNav = tree.isShowNav();
boolean showLines = tree.isShowLines();
boolean clientSideToggle = tree.isClientSideToggle();
if (clientSideToggle)
{
// we must show the nav icons if client side toggle is enabled (regardless of what user says)
showNav = true;
}
UIComponent nodeTypeFacet = tree.getFacet(node.getType());
UIComponent nodeImgFacet = null;
if (nodeTypeFacet == null)
{
throw new IllegalArgumentException("Unable to locate facet with the name: " + node.getType());
}
// render node padding
String[] pathInfo = tree.getPathInformation(tree.getNodeId());
int paddingLevel = pathInfo.length - 1;
for (int i = (showRootNode ? 0 : 1); i < paddingLevel; i++)
{
boolean lastChild = tree.isLastChild((String)pathInfo[i]);
String lineSrc = (!lastChild && showLines)
? getImageSrc(context, tree, "line-trunk.gif", true)
: getImageSrc(context, tree, "spacer.gif", true);
String altString = (!lastChild && showLines)
? "line trunk"
: "spacer";
out.startElement(HTML.TD_ELEM, tree);
out.writeAttribute(HTML.WIDTH_ATTR, "19", null);
out.writeAttribute(HTML.HEIGHT_ATTR, "100%", null);
out.writeAttribute(HTML.STYLE_ATTR, "background-image:" + lineSrc + ";", null); //we use "style" because "background" is no valid xhtml attribute for td
out.startElement(HTML.IMG_ELEM, tree);
out.writeURIAttribute(HTML.SRC_ATTR, lineSrc, null);
out.writeAttribute(HTML.WIDTH_ATTR, "19", null);
out.writeAttribute(HTML.HEIGHT_ATTR, "18", null);
out.writeAttribute(HTML.BORDER_ATTR, "0", null);
out.writeAttribute(HTML.ALT_ATTR, altString, null); // placing a suitable description for the alt.
out.endElement(HTML.IMG_ELEM);
out.endElement(HTML.TD_ELEM);
}
if (showNav)
{
nodeImgFacet = encodeNavigation(context, out, tree);
}
// render node
out.startElement(HTML.TD_ELEM, tree);
if (nodeImgFacet != null)
{
RendererUtils.renderChild(context, nodeImgFacet);
}
RendererUtils.renderChild(context, nodeTypeFacet);
out.endElement(HTML.TD_ELEM);
}
protected void beforeNodeEncode(FacesContext context, ResponseWriter out, HtmlTree tree)
throws IOException
{
out.startElement(HTML.TABLE_ELEM, tree);
out.writeAttribute(HTML.CELLPADDING_ATTR, "0", null);
out.writeAttribute(HTML.CELLSPACING_ATTR, "0", null);
out.writeAttribute(HTML.BORDER_ATTR, "0", null);
out.startElement(HTML.TR_ELEM, tree);
}
protected void afterNodeEncode(FacesContext context, ResponseWriter out)
throws IOException
{
out.endElement(HTML.TR_ELEM);
out.endElement(HTML.TABLE_ELEM);
}
/**
* Handles the encoding related to the navigation functionality.
*
* @param context FacesContext
* @param out ResponseWriter
* @param tree HtmlTree
* @return The additional navigation image to display inside the node (if any). Only used with client-side toggle.
* @throws IOException
*/
private UIComponent encodeNavigation(FacesContext context, ResponseWriter out, HtmlTree tree)
throws IOException
{
TreeNode node = tree.getNode();
String nodeId = tree.getNodeId();
String spanId = TOGGLE_SPAN + ":" + tree.getClientId(context) + ":" + nodeId;//TOGGLE_SPAN + nodeId;
boolean showLines = tree.isShowLines();
boolean clientSideToggle = tree.isClientSideToggle();
UIComponent nodeTypeFacet = tree.getFacet(node.getType());
String navSrc = null;
String altSrc = null;
UIComponent nodeImgFacet = null;
int bitMask = NOTHING;
bitMask += (node.isLeaf()) ? NOTHING : CHILDREN;
if (bitMask == CHILDREN) // if there are no children, ignore expand state -> more flexible with dynamic tree-structures
bitMask += (tree.isNodeExpanded()) ? EXPANDED : NOTHING;
bitMask += (tree.isLastChild(tree.getNodeId())) ? LAST : NOTHING;
bitMask += (showLines) ? LINES : NOTHING;
switch (bitMask)
{
case (NOTHING):
case (LAST):
navSrc = "spacer.gif";
break;
case (LINES):
navSrc = "line-middle.gif";
break;
case (LINES + LAST):
navSrc = "line-last.gif";
break;
case (CHILDREN):
case (CHILDREN + LAST):
navSrc = "nav-plus.gif";
altSrc = "nav-minus.gif";
break;
case (CHILDREN + LINES):
navSrc = "nav-plus-line-middle.gif";
altSrc = "nav-minus-line-middle.gif";
break;
case (CHILDREN + LINES + LAST):
navSrc = "nav-plus-line-last.gif";
altSrc = "nav-minus-line-last.gif";
break;
case (CHILDREN + EXPANDED):
case (CHILDREN + EXPANDED + LAST):
navSrc = "nav-minus.gif";
altSrc = "nav-plus.gif";
break;
case (CHILDREN + EXPANDED + LINES):
navSrc = "nav-minus-line-middle.gif";
altSrc = "nav-plus-line-middle.gif";
break;
case (CHILDREN + EXPANDED + LINES + LAST):
navSrc = "nav-minus-line-last.gif";
altSrc = "nav-plus-line-last.gif";
break;
// unacceptable bitmask combinations
case (EXPANDED + LINES):
case (EXPANDED + LINES + LAST):
case (EXPANDED):
case (EXPANDED + LAST):
throw new IllegalStateException("Encountered a node ["+ nodeId + "] + with an illogical state. " +
"Node is expanded but it is also considered a leaf (a leaf cannot be considered expanded.");
default:
// catch all for any other combinations
throw new IllegalArgumentException("Invalid bit mask of " + bitMask);
}
// adjust navSrc and altSrc so that the images can be retrieved using the extensions filter
String navSrcUrl = getImageSrc(context, tree, navSrc, false);
navSrc = getImageSrc(context, tree, navSrc, true);
altSrc = getImageSrc(context, tree, altSrc, true);
// render nav cell
out.startElement(HTML.TD_ELEM, tree);
out.writeAttribute(HTML.WIDTH_ATTR, "19", null);
out.writeAttribute(HTML.HEIGHT_ATTR, "100%", null);
out.writeAttribute("valign", "top", null);
if ((bitMask & LINES)!=0 && (bitMask & LAST)==0)
{
//out.writeURIAttribute("background", getImageSrc(context, tree, "line-trunk.gif", true), null);
out.writeURIAttribute(HTML.STYLE_ATTR, "background-image:" + getImageSrc(context, tree, "line-trunk.gif", true) + ";", null);
}
// add the appropriate image for the nav control
UIGraphic image = new UIGraphic();
String imageId = IMAGE_PREFIX+(counter++);
image.setId(imageId);
image.setUrl(navSrcUrl);
Map imageAttrs = image.getAttributes();
imageAttrs.put(HTML.WIDTH_ATTR, "19");
imageAttrs.put(HTML.HEIGHT_ATTR, "18");
imageAttrs.put(HTML.BORDER_ATTR, "0");
imageAttrs.put(HTML.ALT_ATTR, "Nav control image"); // placing a suitable description for the alt.
if (clientSideToggle)
{
/**
* With client side toggle, user has the option to specify open/closed images for the node (in addition to
* the navigtion ones provided by the component.)
*/
String expandImgSrc = "";
String collapseImgSrc = "";
String nodeImageId = "";
UIComponent expandFacet = nodeTypeFacet.getFacet("expand");
if (expandFacet != null)
{
UIGraphic expandImg = (UIGraphic)expandFacet;
expandImgSrc = context.getApplication().getViewHandler().getResourceURL(context, expandImg.getUrl());
if (expandImg.isRendered())
{
expandImg.setId(imageId + NODE_STATE_EXPANDED);
expandImg.setParent(tree);
nodeImageId = expandImg.getClientId(context);
nodeImgFacet = expandFacet;
}
}
UIComponent collapseFacet = nodeTypeFacet.getFacet("collapse");
if (collapseFacet != null)
{
UIGraphic collapseImg = (UIGraphic)collapseFacet;
collapseImgSrc = context.getApplication().getViewHandler().getResourceURL(context, collapseImg.getUrl());
if (collapseImg.isRendered())
{
collapseImg.setId(imageId + NODE_STATE_CLOSED);
collapseImg.setParent(tree);
nodeImageId = collapseImg.getClientId(context);
nodeImgFacet = collapseFacet;
}
}
image.setParent(tree);
if (node.getChildCount() > 0)
{
String onClick = new StringBuffer()
.append("treeNavClick('")
.append(spanId)
.append("', '")
.append(image.getClientId(context))
.append("', '")
.append(navSrc)
.append("', '")
.append(altSrc)
.append("', '")
.append(nodeImageId)
.append("', '")
.append(expandImgSrc)
.append("', '")
.append(collapseImgSrc)
.append("', '")
.append(tree.getId())
.append("', '")
.append(nodeId)
.append("');")
.toString();
imageAttrs.put(HTML.ONCLICK_ATTR, onClick);
imageAttrs.put(HTML.STYLE_ATTR, "cursor:pointer;cursor:hand");
}
RendererUtils.renderChild(context, image);
}
else
{
if (node.getChildCount() > 0)
{
// set up the expand control and remove whatever children (if any) this control had previously
UICommand expandControl = tree.getExpandControl();
expandControl.getChildren().clear();
expandControl.setId(TOGGLE_ID);
UIParameter param = new UIParameter();
param.setName(tree.getId() + NamingContainer.SEPARATOR_CHAR + NAV_COMMAND);
param.setValue(tree.getNodeId());
expandControl.getChildren().add(param);
expandControl.getChildren().add(image);
RendererUtils.renderChild(context, expandControl);
}
else
{
RendererUtils.renderChild(context, image);
}
}
out.endElement(HTML.TD_ELEM);
return nodeImgFacet;
}
/**
* Encodes any stand-alone javascript functions that are needed. Uses either the extension filter, or a
* user-supplied location for the javascript files.
*
* @param context FacesContext
* @param component UIComponent
* @throws IOException
*/
private void encodeJavascript(FacesContext context, UIComponent component) throws IOException
{
// render javascript function for client-side toggle (it won't be used if user has opted for server-side toggle)
String javascriptLocation = ((HtmlTree)component).getJavascriptLocation();
AddResource addResource = AddResourceFactory.getInstance(context);
if (javascriptLocation == null)
{
addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlTreeRenderer.class, "javascript/tree.js");
addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlTreeRenderer.class, "javascript/cookielib.js");
}
else
{
addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/tree.js");
addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/cookielib.js");
}
}
/**
* Get the appropriate image src location. Uses the extension filter mechanism for images if no image
* location has been specified.
*
* @param context The {@link FacesContext}. NOTE: If null
then context path information
* will not be used with extensions filter (assuming no imageLocation specified.)
* @param component UIComponent
* @param imageName The name of the image file to use.
* @return The image src information.
*/
private String getImageSrc(FacesContext context, UIComponent component, String imageName, boolean withContextPath)
{
String imageLocation = ((HtmlTree)component).getImageLocation();
AddResource addResource = AddResourceFactory.getInstance(context);
if (imageLocation == null)
{
return addResource.getResourceUri(context, HtmlTreeRenderer.class,
"images/" + imageName, withContextPath);
}
else
{
return addResource.getResourceUri(context, imageLocation + "/" + imageName, withContextPath);
}
}
/**
* Helper method for getting the boolean value of an attribute. If the attribute is not specified,
* then return the default value.
*
* @param component The component for which the attributes are to be checked.
* @param attributeName The name of the boolean attribute.
* @param defaultValue The default value of the attribute (to be returned if no value found).
* @return boolean
*/
protected boolean getBoolean(UIComponent component, String attributeName, boolean defaultValue)
{
Boolean booleanAttr = (Boolean)component.getAttributes().get(attributeName);
if (booleanAttr == null)
{
return defaultValue;
}
else
{
return booleanAttr.booleanValue();
}
}
private Map getCookieAttr(Cookie cookie)
{
Map attribMap = new HashMap();
try
{
String cookieValue = URLDecoder.decode(cookie.getValue(),ENCODING);
String[] attribArray = cookieValue.split(ATTRIB_DELIM);
for (int j = 0; j < attribArray.length; j++)
{
int index = attribArray[j].indexOf(ATTRIB_KEYVAL);
String name = attribArray[j].substring(0, index);
String value = attribArray[j].substring(index + 1);
attribMap.put(name, value);
}
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException("Error parsing tree cookies", e);
}
return attribMap;
}
}