/*
* 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.
*
* $Header:$
*/
package org.apache.beehive.netui.tags.tree;
import org.apache.beehive.netui.util.internal.InternalStringBuilder;
import org.apache.beehive.netui.core.URLCodec;
import org.apache.beehive.netui.pageflow.PageFlowUtils;
import org.apache.beehive.netui.pageflow.internal.AdapterManager;
import org.apache.beehive.netui.pageflow.internal.InternalUtils;
import org.apache.beehive.netui.pageflow.scoping.ScopedServletUtils;
import org.apache.beehive.netui.tags.HtmlUtils;
import org.apache.beehive.netui.tags.html.HtmlConstants;
import org.apache.beehive.netui.tags.internal.PageFlowTagUtils;
import org.apache.beehive.netui.tags.javascript.ScriptRequestState;
import org.apache.beehive.netui.tags.rendering.*;
import org.apache.beehive.netui.util.Bundle;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;
abstract public class TreeRenderer implements HtmlConstants
{
private TagRenderingBase _imageRenderer;
private TagRenderingBase _anchorRenderer;
private TagRenderingBase _divRenderer;
private TagRenderingBase _spanRenderer;
private TreeRenderState _trs;
private ImageTag.State _imgState = new ImageTag.State();
private AnchorTag.State _anchorState = new AnchorTag.State();
private DivTag.State _divState = new DivTag.State();
private SpanTag.State _spanState = new SpanTag.State();
private ServletContext _servletContext;
private HttpServletRequest _req;
private HttpServletResponse _res;
TreeRenderer(TreeRenderState trs, HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext)
{
_trs = trs;
_imageRenderer = TagRenderingBase.Factory.getRendering(TagRenderingBase.IMAGE_TAG, request);
_anchorRenderer = TagRenderingBase.Factory.getRendering(TagRenderingBase.ANCHOR_TAG, request);
_divRenderer = TagRenderingBase.Factory.getRendering(TagRenderingBase.DIV_TAG, request);
_spanRenderer = TagRenderingBase.Factory.getRendering(TagRenderingBase.SPAN_TAG, request);
_servletContext = servletContext;
_req = request;
_res = response;
}
abstract protected void registerTagError(String message, Throwable e)
throws JspException;
abstract protected String renderTagId(HttpServletRequest request, String tagId, AbstractHtmlState state);
protected void renderBeforeNode(AbstractRenderAppender writer, TreeElement node)
{
}
protected void renderAfterNode(AbstractRenderAppender writer, TreeElement node)
{
}
/**
* This is a recursive method which generates the markup for the tree.
* @param sb
* @param node
* @param level
* @param attrs
* @param state
* @throws javax.servlet.jsp.JspException
*/
protected void render(InternalStringBuilder sb, TreeElement node, int level, AttributeRenderer attrs,
InheritableState state)
throws JspException
{
// assert the values...
assert(sb != null);
assert(node != null);
String encoding = _res.getCharacterEncoding();
String nodeName = node.getName();
assert(nodeName != null);
// HACK to take into account special characters like = and &
// in the node name, could remove this code if encode URL
// and later request.getParameter() could deal with = and &
// character in parameter values.
String encodedNodeName = null;
try {
encodedNodeName = URLCodec.encode(nodeName, encoding);
assert(encodedNodeName != null);
}
catch (IOException e) {
// report the exception and return.
String s = Bundle.getString("Tags_TreeEncodingError", null);
registerTagError(s, e);
return;
}
// add any attributes to the renderer
AttributeRenderer.RemoveInfo removes = attrs.addElement(node);
// get the renderers for this tree...
InternalStringBuilder img = new InternalStringBuilder(32);
// Render the beginning of this node
_divState.clear();
String tagId = node.getTagId();
String script = null;
if (tagId != null) {
script = renderTagId(_req, tagId, _divState);
}
attrs.renderDiv(_divState, node);
if (_trs.runAtClient) {
_divState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_LEVEL, Integer.toString(level));
}
sb.append(" ");
StringBuilderRenderAppender writer = new StringBuilderRenderAppender(sb);
renderBeforeNode(writer, node);
_divRenderer.doStartTag(writer, _divState);
sb.append("\n");
if (script != null)
sb.append(script);
// In devMode we will verify the structure of the tree. This will not run in
// production mode.
ServletContext servletContext = InternalUtils.getServletContext(_req);
boolean devMode = !AdapterManager.getServletContainerAdapter(servletContext).isInProductionMode();
if (devMode) {
boolean error = false;
InternalStringBuilder errorText = new InternalStringBuilder(64);
if (node.getName() == null) {
errorText.append("name");
error = true;
}
if (node.getParent() == null) {
if (error)
errorText.append(", ");
errorText.append("parent");
}
if (error)
registerTagError(Bundle.getString("Tags_TreeStructureError", errorText.toString()), null);
}
// check for tree override properties, the second
// case here is because the root runs through this an by definitions
// has InheritableState == state
InheritableState is = node.getInheritableState();
if (is != null && is != state) {
is.setParent(state);
state = is;
}
// Create the appropriate number of indents
// These are either the spacer.gif if the parent is the last in the line or the
// vertical line gif if the parent is not the last child.
_imgState.clear();
_imgState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, WIDTH, "16px");
_imgState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, BORDER, "0");
_imgState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, ALT, "", false);
for (int i = 0; i < level; i++) {
int levels = level - i;
TreeElement parent = node;
for (int j = 1; j <= levels; j++)
parent = parent.getParent();
img.setLength(0);
img.append(state.getImageRoot());
img.append('/');
if (parent.isLast()) {
img.append(state.getImageSpacer());
_imgState.style = null;
}
else {
img.append(state.getVerticalLineImage());
_imgState.style = "vertical-align:bottom;";
}
sb.append(" ");
_imgState.src = img.toString();
_imageRenderer.doStartTag(writer, _imgState);
_imageRenderer.doEndTag(writer);
sb.append("\n");
}
// boolean flag that will indicate if there is an open anchor created
boolean closeAnchor = false;
if (!_trs.runAtClient)
closeAnchor = renderExpansionAnchor(sb, _anchorRenderer, node, nodeName, state);
else {
// Render client expansion and initialize the tree JavaScript support
closeAnchor = renderClientExpansionAnchor(sb, _anchorRenderer, node, encodedNodeName, state);
}
// place the image into the anchor....
// The type of the image depends upon the position and the type of the node.
String alt = "";
img.setLength(0);
img.append(state.getImageRoot());
img.append('/');
if (node.isLeaf()) { // leaf node either last or middle
if (node.isLast())
img.append(state.getLastLineJoinImage());
else
img.append(state.getLineJoinImage());
}
else if (node.isExpanded()) { // interior node that is expanded
alt = Bundle.getString("Tags_TreeAltTextCollapse", null);
String rImg = null;
if (node.getParent() == null && node instanceof ITreeRootElement) {
rImg = ((ITreeRootElement) node).getRootNodeExpandedImage();
if (rImg != null) {
img.append(rImg);
}
}
if (rImg == null) {
if (node.isLast())
img.append(state.getLastNodeExpandedImage());
else
img.append(state.getNodeExpandedImage());
}
}
else { // interior not expanded
alt = Bundle.getString("Tags_TreeAltTextExpand", null);
String rImg = null;
if (node.getParent() == null && node instanceof ITreeRootElement) {
rImg = ((ITreeRootElement) node).getRootNodeCollapsedImage();
if (rImg != null) {
img.append(rImg);
}
}
if (rImg == null) {
if (node.isLast())
img.append(state.getLastNodeCollapsedImage());
else
img.append(state.getNodeCollapsedImage());
}
}
// write out the image which occurs next to the icon
if (!closeAnchor)
sb.append(" ");
_imgState.clear();
_imgState.src = img.toString();
_imgState.style = "vertical-align:bottom;";
_imgState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, BORDER, "0");
_imgState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, ALT, alt, false);
_imageRenderer.doStartTag(writer, _imgState);
_imageRenderer.doEndTag(writer);
// close the anchor if one was openned...
if (closeAnchor)
_anchorRenderer.doEndTag(writer);
sb.append("\n");
// Calculate the selection link for this node, if the node is disabled, we can skip
// this because a disabled node may not be selected.
String selectionLink = null;
if (!node.isDisabled()) {
// The action on the node overrides all. Otherwise, check to see if there
// is either an href or clientAction. If neither is set, then we inherit the
// action defined on the trees inheritable state.
String action = node.getAction();
if (action == null) {
selectionLink = node.getHref();
if (selectionLink == null && node.getClientAction() != null)
selectionLink = "";
if (selectionLink == null)
action = state.getSelectionAction();
}
// create the selection link
if (action != null && selectionLink == null) {
HashMap params = null;
boolean remove = false;
params = node.getParams();
if (params == null) {
params = new HashMap();
remove = true;
}
params.put(TreeElement.SELECTED_NODE, nodeName);
if (_trs.tagId != null) {
params.put(TreeElement.TREE_ID, _trs.tagId);
}
// Add the jpf ScopeID param if necessary.
String scope = node.getScope();
if (scope != null) {
params.put(ScopedServletUtils.SCOPE_ID_PARAM, scope);
}
String uri = null;
try {
boolean xml = TagRenderingBase.Factory.isXHTML(_req);
uri = PageFlowUtils.getRewrittenActionURI(_servletContext, _req, _res, action, params, null, xml);
}
catch (URISyntaxException e) {
// report the error...
String s = Bundle.getString("Tags_Tree_Node_URLException",
new Object[]{action, e.getMessage()});
registerTagError(s, e);
}
if (remove) {
params.remove(TreeElement.SELECTED_NODE);
if (_trs.tagId != null) {
params.remove(TreeElement.TREE_ID);
}
if (scope != null) {
params.remove(ScopedServletUtils.SCOPE_ID_PARAM);
}
}
if (uri != null) {
selectionLink = _res.encodeURL(uri);
}
}
}
TagRenderingBase endRender = null;
// if there is a selection link we need to put an anchor out.
if (selectionLink != null) {
_anchorState.clear();
_anchorState.href = selectionLink;
String target = node.getTarget();
if (target == null) {
target = state.getSelectionTarget();
}
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TARGET, target);
String title = node.getTitle();
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TITLE, title);
// set the selection styles
if (node.isSelected()) {
_anchorState.style = _trs.selectedStyle;
_anchorState.styleClass = _trs.selectedStyleClass;
}
else {
_anchorState.style = _trs.unselectedStyle;
_anchorState.styleClass = _trs.unselectedStyleClass;
}
if (_anchorState.style == null && _anchorState.styleClass == null) {
_anchorState.style = "text-decoration: none";
}
// render any attributes applied to the HTML
attrs.renderSelectionLink(_anchorState, node);
// render the runAtClient attributes
if (_trs.runAtClient) {
String action = node.getClientAction();
if (action != null) {
action = HtmlUtils.escapeEscapes(action);
action = ScriptRequestState.getString("netuiAction", new Object[]{action});
}
_anchorState.registerAttribute(AbstractHtmlState.ATTR_JAVASCRIPT, ONCLICK, action);
// Jira 299
//_anchorState.onClick = action;
}
// actually render the anchor.
sb.append(" ");
_anchorRenderer.doStartTag(writer, _anchorState);
endRender = _anchorRenderer;
}
else {
// This node doesn's support selection. This means we consider it disabled. We will
// put a span around it and set the style/class to indicate that it is disabled.
_spanState.clear();
_spanState.styleClass = _trs.disabledStyleClass;
_spanState.style = _trs.disabledStyle;
sb.append(" ");
_spanRenderer.doStartTag(writer, _spanState);
endRender = _spanRenderer;
}
sb.append(" ");
// Render the icon for this node, there will always unless the tree turns off default
// icons by setting the useDefaultIcons attribute to false.
String icon = node.getIcon();
if (icon == null) {
icon = state.getIconRoot() + "/" + state.getItemIcon();
}
else {
icon = state.getIconRoot() + "/" + icon;
}
// write out the icon
String label = node.getLabel();
if (icon != null) {
_imgState.clear();
_imgState.src = icon;
_imgState.style = "vertical-align:text-top";
alt = null;
if (label != null && node.isLabelLegalAsAlt())
alt = label;
else
alt = node.getTitle();
if (alt == null)
alt = Bundle.getString("Tags_TreeAltText", null);
_imgState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, ALT, alt, false);
_imgState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, BORDER, "0");
// set the inheritted attributes
attrs.renderIconImage(_imgState, node);
_imageRenderer.doStartTag(writer, _imgState);
_imageRenderer.doEndTag(writer);
sb.append(" ");
}
// Render the label for this node (if any)
if (label != null) {
if (_trs.escapeContent) {
InternalStringBuilder s = new InternalStringBuilder(label.length() + 16);
StringBuilderRenderAppender sbAppend = new StringBuilderRenderAppender(sb);
HtmlUtils.filter(label, sbAppend);
label = s.toString();
}
sb.append(label);
sb.append(" ");
}
endRender.doEndTag(writer);
// if there is content then we should render that here...
String ctnt = node.getContent();
if (ctnt != null) {
if (_trs.escapeContent) {
InternalStringBuilder s = new InternalStringBuilder(ctnt.length() + 16);
StringBuilderRenderAppender sbAppend = new StringBuilderRenderAppender(sb);
HtmlUtils.filter(ctnt, sbAppend);
ctnt = s.toString();
}
sb.append("\n ");
sb.append(ctnt);
}
// Render the end of this node
sb.append("\n ");
_divRenderer.doEndTag(writer);
sb.append("\n");
renderAfterNode(writer, node);
// now remove all of the attributes scoped with this...
attrs.removeElementScoped(node, removes);
// Render the children of this node
// If the node is expanded we render it
// If we are runAtClient and the node is Not expandOnServer then render it
if (node.isExpanded() || (_trs.runAtClient && !node.isExpandOnServer())) {
TreeElement children[] = node.getChildren();
int newLevel = level + 1;
for (int i = 0; i < children.length; i++) {
render(sb, children[i], newLevel, attrs, state);
}
}
attrs.removeElement(node, removes);
}
private boolean renderExpansionAnchor(InternalStringBuilder sb, TagRenderingBase anchorRenderer,
TreeElement node, String nodeName, InheritableState state)
throws JspException
{
// Render the tree state image for this node
String action = state.getExpansionAction();
if (action == null) {
action = state.getSelectionAction();
}
boolean isAction = PageFlowTagUtils.isAction(_req, action);
if (!isAction) {
registerTagError(Bundle.getString("Tags_BadAction", action), null);
return false;
}
// encode the tree parameters into the action.
HashMap params = new HashMap();
params.put(TreeElement.EXPAND_NODE, nodeName);
assert (_trs.tagId != null);
params.put(TreeElement.TREE_ID, _trs.tagId);
String uri = null;
try {
boolean xml = TagRenderingBase.Factory.isXHTML(_req);
uri = PageFlowUtils.getRewrittenActionURI(_servletContext, _req, _res, action, params, null, xml);
}
catch (URISyntaxException e) {
// report the error...
String s = Bundle.getString("Tags_Tree_Node_URLException",
new Object[]{action, e.getMessage()});
registerTagError(s, e);
}
boolean ret = false;
if ((uri != null) && !node.isLeaf()) {
_anchorState.clear();
_anchorState.href = _res.encodeURL(uri);
sb.append(" ");
StringBuilderRenderAppender writer = new StringBuilderRenderAppender(sb);
anchorRenderer.doStartTag(writer, _anchorState);
ret = true;
}
return ret;
}
/**
* @param sb
* @param node
* @param encodedNodeName
* @return
*/
private boolean renderClientExpansionAnchor(InternalStringBuilder sb, TagRenderingBase anchorRenderer,
TreeElement node, String encodedNodeName,
InheritableState state)
{
boolean imgOverride = (state != state.getParent() && state.getParent() != null) ||
(node.getParent() == null);
if (!node.isLeaf()) {
boolean expanded = node.isExpanded();
_anchorState.clear();
_anchorState.href = "";
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_ANCHOR,
(expanded ? TreeElement.TREE_EXPAND_STATE : TreeElement.TREE_COLLAPSE_STATE));
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_ANCHOR_INIT, "true");
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_ANCHOR_ID, encodedNodeName);
if (node.isLast()) {
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_NODE_LAST, "true");
}
// Does this node have it's images being overridden?
if (imgOverride) {
if (node.getParent() == null) {
String rootImg = ((ITreeRootElement) node).getRootNodeCollapsedImage();
if (rootImg != null)
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_COLLAPSE_IMAGE,
state.getImageRoot() + "/" + rootImg);
else
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_COLLAPSE_IMAGE,
state.getImageRoot() + "/" + state.getLastNodeCollapsedImage());
rootImg = ((ITreeRootElement) node).getRootNodeExpandedImage();
if (rootImg != null)
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_EXPAND_IMAGE,
state.getImageRoot() + "/" + rootImg);
else
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_EXPAND_IMAGE,
state.getImageRoot() + "/" + state.getLastNodeExpandedImage());
}
else if (node.isLast()) {
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_COLLAPSE_IMAGE,
state.getImageRoot() + "/" + state.getLastNodeCollapsedImage());
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_EXPAND_IMAGE,
state.getImageRoot() + "/" + state.getLastNodeExpandedImage());
}
else {
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_COLLAPSE_IMAGE,
state.getImageRoot() + "/" + state.getNodeCollapsedImage());
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_EXPAND_IMAGE,
state.getImageRoot() + "/" + state.getNodeExpandedImage());
}
}
if (node.isExpandOnServer() && !node.isExpanded()) {
String path = _req.getServletPath();
int idx = path.lastIndexOf('/');
if (idx != -1) {
path = path.substring(1, idx);
}
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_EXPAND, "true");
_anchorState.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TreeElement.TREE_EXPAND_PATH, path);
}
sb.append(" ");
StringBuilderRenderAppender writer = new StringBuilderRenderAppender(sb);
anchorRenderer.doStartTag(writer, _anchorState);
return true;
}
return false;
}
}