/* * Copyright 2004 The Apache Software Foundation. * * 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 com.idega.content.tree; 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 com.idega.util.CoreConstants; //import org.apache.myfaces.custom.tree2.*; 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; /** * @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: 1.5 $ $Date: 2007/10/17 15:09:42 $ */ 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 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; // 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 <code><span></code> element with an <code>id</code> * attribute if the component has been given an explicit ID. The model nodes are rendered * recursively by the private <code>encodeNodes</code> 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.getId() + ":" + 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(pathInfo[i]); String lineSrc = (!lastChild && showLines) ? getImageSrc(context, tree, "line-trunk.gif", true) : getImageSrc(context, tree, "spacer.gif", true); out.startElement(HTML.TD_ELEM, tree); out.writeAttribute(HTML.WIDTH_ATTR, "19", null); out.writeAttribute(HTML.HEIGHT_ATTR, "100%", null); out.writeURIAttribute("background", lineSrc, null); 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.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); // out.startElement(HTML.UL_ELEM, tree); } protected void afterNodeEncode(FacesContext context, ResponseWriter out) throws IOException { out.endElement(HTML.TR_ELEM); out.endElement(HTML.TABLE_ELEM); // out.endElement(HTML.UL_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.getId() + ":" + 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); } // add the appropriate image for the nav control UIGraphic image = new UIGraphic(); image.setId(IMAGE_PREFIX); 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"); 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(IMAGE_PREFIX + 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(IMAGE_PREFIX + 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:hand;cursor:pointer"); } 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 <code>null</code> 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(), CoreConstants.ENCODING_UTF8); 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; } }