/** * The contents of this file are subject to the Mozilla Public License * Version 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations under * the License. * * The Original Code is OpenELIS code. * * Copyright (C) The Minnesota Department of Health. All Rights Reserved. */ package us.mn.state.health.lims.common.valueholder.tree; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Properties; import us.mn.state.health.lims.common.log.LogEvent; /** * This class represents each node in the tree. A TreeNode is the basic building node in a Tree. Each * TreeNode can exist only in one Tree at a time and cannot be shared across different trees. * <br> * For each TreeNode object, an equivalent JSTreeNode javascript object is created on the client. The following * javascript attributes will be available for the treeNode on the client : * <br><br> * node.name - the name of the tree <br> * node.path - the path to the tree <br> * node.parent - the parent node to the tree node <br> * node.treeName - the name of the tree the treenode belongs to <br> * node.isExpanded - whether or not the node is expanded or not <br> * node.hasChildren - whether or not the node has any children or not <br> * node.data - the data field assocaited with the node <br> * node.preSubmitAction- Optional presubmit action associated with the node <br> * * @author jalpesh * */ public class TreeNode { private String name; // the name of the node private String displayName; // the display name of the node private String iconOpen; // the open image icon src private String iconClosed; // the closed image icon src private TreePopup popup; // the popup associated with the node private String popupText; // the popup text associated when user hovers over the node image private Properties props; // if the user wants to set extra properties on the TreeNode. private String data; // the data field associated with the node. private String url; // the url associated with the node private String urlTargetName; // optional target name associated with the node. private String submitFormName; // the form to be submitted when user clicks on the node private String preSubmitScript; // optional javascript script to be executed before submitting the form private List children; // a list of children of the tree node private boolean isExpanded; // whether or not a node is expanded or not private boolean hasChildren; // whether or not a node has children private Tree tree; // the tree this node belongs to protected String nodePath; // the path to the node protected TreeNode parent; // keep an internal reference to the parent of the tree node /** pagination support for the node. */ private boolean isPaginated; // whether or not a node supports pagination for its children private int pageSize = 10; // the default page size. private int paginatedNodeChildCount; // the total children of this paginated node. private int pageRangeBegin=-1; // the begin index of the page range private String varName; // the variable name of the parent javascript object of this tree node // The delimeter for the node path - do not change this value here // use the setter to change it if you want a different one private static char pathDelimeter = '.'; // the constant used for the javascript variable protected static String VAR_NAME_CONSTANT = "_a"; protected static String VAR_NAME_INCREMENT = "a"; /** * Create a tree node with a given name * @param name The name of the tree node. Note that the name should not contain the path delimeter. */ public TreeNode(String name) { // if the name contains the path delimeter, let us throw an exception if(name.indexOf(pathDelimeter) != -1) throw new IllegalArgumentException("The name of the TreeNode should not contain the path Delimeter - '" + pathDelimeter + "'"); this.name = name; this.nodePath = name; this.children = new ArrayList(); this.props = new Properties(); this.varName = VAR_NAME_CONSTANT; } /** * Method used by the addChild methods which construct a new tree node. It uses reflection to create the subclass of TreeNode * (if any) instead of a tree node. * * @param name The name of the tree node. * @return An instance which is the subclass of the tree node. */ private TreeNode createTreeNode(String name) { // System.out.println(">>> The class calling addChild is :" +this.getClass().getName()); try { Constructor constructor = this.getClass().getConstructor(new Class[] { String.class }); return (TreeNode)constructor.newInstance((Object[])new String[] {name}); } catch (IllegalArgumentException e) { //bugzilla 2154 LogEvent.logError("TreeNode","createTreeNode()",e.toString()); throw e; } catch (InstantiationException e) { //bugzilla 2154 LogEvent.logError("TreeNode","createTreeNode()",e.toString()); throw new IllegalArgumentException(e.getMessage()); } catch (IllegalAccessException e) { //bugzilla 2154 LogEvent.logError("TreeNode","createTreeNode()",e.toString()); throw new IllegalArgumentException(e.getMessage()); } catch (InvocationTargetException e) { //bugzilla 2154 LogEvent.logError("TreeNode","createTreeNode()",e.toString()); throw new IllegalArgumentException(e.getMessage()); } catch (SecurityException e) { //bugzilla 2154 LogEvent.logError("TreeNode","createTreeNode()",e.toString()); throw new IllegalArgumentException(e.getMessage()); } catch (NoSuchMethodException e) { //bugzilla 2154 LogEvent.logError("TreeNode","createTreeNode()","Your subclass should have a constructor which takes in a string argument: " + e.toString()); throw new IllegalArgumentException("Your subclass should have a constructor which takes in a string argument."); } } /** * @return Returns the tree associated with the node. This is set automatically by the framework when the tree is generated */ protected Tree getTree() { return tree; } /** * @param tree the tree which is associated with this node. */ protected void setTree(Tree tree) { this.tree = tree; } /** * Add a child node to the current node * @param name The name of the node. Note that the name should not contain the path delimeter character in it. * @return The newly added node. Note that if you have extended the TreeNode, the addChild method will return an * instance of your subclass. So you can cast down to the subclass if desired. */ public TreeNode addChild(String name) { return addChild(name, null, null); } /** * Add a child node to the current node. * @param name The name of the node. Note that the name should not contain the path delimeter character in it. * @param action The action associated with the node. This could be a url or a form name which should be submitted. * @param icon The icon src which will be used to display the tree node. The same icon will be used to display * expanded/collapsed tree. * @return The newly added node. Note that if you have extended the TreeNode, the addChild method will return an * instance of your subclass. So you can cast down to the subclass if desired. */ public TreeNode addChild(String name, String action, String iconSrcOpen, String iconSrcClosed) { TreeNode tn = addChild(name, action, iconSrcOpen); if(tn == null) return null; // we should set the closed icon also. tn.setIconSrcClosed(iconSrcClosed); return tn; } /** * Add a tree node as a child to the current node. This will create new child node * out of the current node and add it to the child list of the current node. * @param node The child node * @return The added child node. Note that if you have extended the TreeNode, the addChild method will return an * instance of your subclass. So you can cast down to the subclass if desired. */ public TreeNode addChild(TreeNode node) { TreeNode tn = addChild(node.getName()); // copy required properties. copyNode(node, tn); // now copy all the children for(int i=0; i<node.children.size(); i++) tn.addChild((TreeNode)node.children.get(i)); return tn; } /** * Add a child node to the current node. * @param name The name of the node. Note that the name should not contain the path delimeter character in it. * @param action The action associated with the node. This could be a url or a form name which should be submitted. * @param icon The icon src which will be used to display the tree node. The same icon will be used to display * expanded/collapsed tree. * @return The newly added node. Note that if you have extended the TreeNode, the addChild method will return an * instance of your subclass. So you can cast down to the subclass if desired. */ public TreeNode addChild(String name, String action, String icon) { if(name == null) return null; TreeNode tn = createTreeNode(name); // action can either be url or a form name to submit if(action == null) tn.setUrl(null, null); else if(action.startsWith("http")) tn.setUrl(action, null); else tn.setSubmitFormName(action); tn.nodePath = this.nodePath + pathDelimeter + name; tn.setIconSrcOpen(icon); tn.setIconSrcClosed(icon); tn.parent = this; tn.varName = varName + VAR_NAME_INCREMENT; children.add(tn); this.hasChildren = true; return tn; } private void copyNode(TreeNode original, TreeNode target) { target.name = new String(original.name); target.hasChildren = original.hasChildren; target.isExpanded = original.isExpanded; target.isPaginated = original.isPaginated; target.pageRangeBegin= original.pageRangeBegin; target.pageSize = original.pageSize; target.paginatedNodeChildCount = original.paginatedNodeChildCount; if(original.data != null) target.data = new String(original.data); if(original.displayName != null) target.displayName = new String(original.displayName); if(original.iconClosed != null) target.iconClosed = new String(original.iconClosed); if(original.iconOpen != null) target.iconOpen = new String(original.iconOpen); if(original.popup != null) target.popup = original.popup.clonePopup(); if(original.popupText != null) target.popupText = new String(original.popupText); if(original.preSubmitScript != null) target.preSubmitScript = new String(original.preSubmitScript); if(original.props != null) target.props = copyProps(original.props); if(original.submitFormName != null) target.submitFormName = new String(original.submitFormName); if(original.url != null) target.url = new String(original.url); if(original.urlTargetName != null) target.urlTargetName = new String(original.urlTargetName); // ignored variables in copy. // path, tree, parent, varName; } private Properties copyProps(Properties props) { Properties newProps = new Properties(); Enumeration keys = props.keys(); while(keys.hasMoreElements()) { Object key = keys.nextElement(); newProps.put(key, props.get(key)); } return newProps; } /** * Return whether or not a node has children or not * @return */ public boolean hasChildren() { return hasChildren; } /** * Over-write whether or not a node has children or not. * @param hasChildren */ public void setHasChildren(boolean hasChildren) { this.hasChildren = hasChildren; } /** * Reference to the parent object of the tree node * @return */ public TreeNode getParent() { return parent; } /** * Return true if the node is expanded. * @return Returns whether or not the node is expanded or not */ public boolean isExpanded() { return isExpanded; } /** * If true, sets the state of the node to be expanded when displayed in the tree * @param isExpanded The expanded state to be set. */ public void setExpanded(boolean isExpanded) { this.isExpanded = isExpanded; } /** * Return the path associated with the node. This is essentially the path separated by the node delimeter. * @return Returns the nodePath. */ public String getNodePath() { return nodePath; } /** * @return Returns the pathDelimeter. */ public static char getPathDelimeter() { return pathDelimeter; } /** * @param pathDelimeter The pathDelimeter to set. */ public static void setPathDelimeter(char pathDelimeter) { TreeNode.pathDelimeter = pathDelimeter; } /** * @return Returns the displayed name of the TreeNode. * This is an optional field and if it is not set, the tree will be displayed with the name */ public String getDisplayName() { return displayName; } /** * Sets the displayed name of the TreeNode. If not set, the node will be displayed with the original name * @param displayName The displayed name of the tree node. */ public void setDisplayName(String displayName) { this.displayName = displayName; } /** * Return the javascript variable name associated with the object * @return String */ protected String getVarName() { return varName; } /** * This is an optional method on the TreeNode which sets the preSubmitAction. If you want some * javascript to be executed before the form associated with the node is submitted, * you can use the setPreSubmitAction method. Please ensure that the javascript specified by this method does not submit a * form, or else the tree will be in an inconsistent state. * * @return Returns the preSubmitScript. */ public String getPreSubmitScript() { return preSubmitScript; } /** * This is an optional method on the TreeNode which sets the preSubmitAction. If you want some * javascript to be executed before the form associated with the node is submitted when the node is clicked, * you can use the setPreSubmitScript method. Please ensure that the javascript specified by this method does not submit a * form, or else the tree will be in an inconsistent state. * * Eg: node.setPreSubmitScript("javascript:askMeFirst(currentNode)"); * * @param preSubmitScript The preSubmitScript to set. Please make sure that the preSubmitScript returns a true or false. * If the script returns false, the node submit form / url will not be submitted/executed. */ public void setPreSubmitScript(String preSubmitAction) { this.preSubmitScript = preSubmitAction; } /** * @return Returns the submitForm. */ public String getSubmitFormName() { return submitFormName; } /** * Sets the form to be submitted when this node is clicked. Each node can either have a url or a form name * associated with it. If you want to set a url, use the setUrl method. If you want to set * the form name, use setSubmitForm method. * * @param submitForm The name of the form to be submitted. The form must exist on the page, or else an error will be thrown */ public void setSubmitFormName(String submitFormName) { this.submitFormName = submitFormName; } /** * @return Returns the url associated with the node. */ public String getUrl() { return url; } /** * @return Returns the target of the url, associated with the node. The urlTargetName can * optionally be set when you set the url for the node. */ public String getUrlTargetName() { return urlTargetName; } /** * Sets the url associated with the node. Each node can either have a url or a form name associated * with it. If you want to set a url, use the setUrl method. If you want to set the form name, use setSubmitForm method. * * @param url The url to set. * @param urlTargetName If you want the url to open in a separate frame or a window, please specify the name here. * If not specified, when the user clicks on the node, the url will replace the window's contents. * * Typically this is used when you are displaying the tree in a webpage with frames. * You would want to specify the frame name as this parameter where you want the contents to be displayed. * */ public void setUrl(String url, String urlTargetName) { // note that if the user creates a url, we still will submit a form, after creating a new form // whose name is going to be the nodePath, separated by the underscore character - "_". this.url = url; this.urlTargetName = urlTargetName; } /** * @return Returns the icon source for the image shown when the node is closed. Should be a 16x16 pixel image */ public String getIconSrcClosed() { return iconClosed; } /** * Set the icon source for the image shown when the node is closed. Should be a 16x16 pixel image * @param iconClosed The iconClosed to set. */ public void setIconSrcClosed(String iconClosed) { this.iconClosed = iconClosed; } /** * @return Returns the icon source for the image shown when the node is expanded. Should be a 16x16 pixel image */ public String getIconSrcOpen() { return iconOpen; } /** * Returns the icon source for the image shown when the node is expanded. Should be a 16x16 pixel image * @param iconOpen The iconOpen to set. */ public void setIconSrcOpen(String iconOpen) { this.iconOpen = iconOpen; } /** * @return Returns the name of the node */ public String getName() { return name; } /** * @param name Set the name of the node */ public void setName(String name) { this.name = name; } /** * Return a list of all the children TreeNode objects of this tree node. * @return An list containing children of the tree node. */ public List getChildren() { return children; } /** * @return Returns the popup associated with the node, which is invoked when the user right clicks on the node. */ public TreePopup getPopup() { return popup; } /** * @param popup Set the popup associated with the node. */ public void setPopup(TreePopup popup) { this.popup = popup; } /** * @return Returns the text which is displayed when the user hovers the mouse over the node image */ public String getPopupText() { return popupText; } /** * @param popupText Sets the text which is displayed when the user hovers the mouse over the node image. */ public void setPopupText(String popupText) { this.popupText = popupText; } /** * @return Returns the data field of the tree node. Each tree node can have an string * data field, which can have any arbitrary string data. This can be accessed in the javascript by using the node.data modifier. */ public String getData() { return data; } /** * Set the data field of the tree node. Each TreeNode can optionally have an abitrary * data field if required. User can then access this field in the page javascript if required * @param data The data to set. */ public void setData(String data) { this.data = data; } /** * Return whether or not this node supports pagination for its children. * @return Returns boolean indicating pagination support. Default is false. */ public boolean isPaginated() { return isPaginated; } /** * Sets up a node to support pagination for its children. By default a node does not * support pagination for the children. If pagination support is enabled, the user is * required to fill in the paginatedNodeChildCount variable also. This is set using the * setPaginatedNodeChildCount method. * * @param isPaginated The isPaginated to set. */ public void setPaginated(boolean isPaginated) { this.isPaginated = isPaginated; } /** * Returns the pageSize of the paginated node. The default page size of a paginated node is 10. * @return the page size */ public int getPageSize() { return pageSize; } /** * Set the page size of a paginated node. Default is 10, which means that if a node has more than * 10 children, the node will be displayed with the prev,next links. * @param pageSize The pageSize to set. */ public void setPageSize(int pageSize) { this.pageSize = pageSize; } /** * Return the number of children a paginated node has. * @return Returns the paginatedNodeChildCount. */ public int getPaginatedNodeChildCount() { return paginatedNodeChildCount; } /** * Sets the number of children a paginated node has. This is required to be set if a node is designated to be paginated. * @param paginatedNodeChildCount The paginatedNodeChildCount to set. */ public void setPaginatedNodeChildCount(int paginatedNodeChildCount) { this.paginatedNodeChildCount = paginatedNodeChildCount; } /** * Sets the page range for the node. This indicates the tree node renderer what is the current child range * being displayed * in the browser. * @param begin The begin index of the child being displayed. */ public void setPageRangeBegin(int begin) { this.pageRangeBegin = begin; } /** * Return the begin page range of the node. The page range of a paginated node * is set by using the setPageRange method. * @return The begin page range. For a non-paginated node, the default is -1. */ public int getPageRangeBegin() { return pageRangeBegin; } /** * Add an extra property on the tree node. This can then be accessed in the page by using the node.<property name> method. * @param name * @param value */ public void addProperty(String name, String value) { if(name == null) return; props.put(name, value); } /** * Get a named property for the node, which has been added by the addProperty method. * @param name * @return The value of the property, or null if property does not exist. */ public String getProperty(String name) { return (String)props.get(name); } /** * Convert some characters into html specific chars. * '<' becomes < * '>' becomes > * '&' becomes & * '"' becomes " * "'" becomes ' * @param source * @return String */ public static String htmlChars(String source) { if(source == null) return source; String target = source; target = target.replaceAll("&", "&"); target = target.replaceAll("<", "<"); target = target.replaceAll(">", ">"); // target = target.replaceAll("\"", """); // target = target.replaceAll("'", "'"); return target; } /** * Escape quotes from the passed parameter * @return String */ public static String escapeQuotes(String source) { String retVal = source; if(source == null) return retVal; retVal = retVal.replaceAll("'", "\\\\u0027"); retVal = retVal.replaceAll("\"", "\\\\u0022"); retVal = retVal.replaceAll("\\n", "\\\\u000A"); retVal = retVal.replaceAll("\\t", "\\\\u0009"); return retVal; } /** * Remove the unicode characters from the string and replace them with real quotes * @param source * @return String */ public static String unescapeQuotes(String source) { if(source == null) return source; source = source.replaceAll("\\\\u0027", "'"); source = source.replaceAll("\\\\u0022", "\""); return source; } /** * Get the JavascriptDefinition of the TreeNode * @param parent - This is the javascript representation of the parent node. Should be null for the root node * @return String */ protected String getJavascriptDefinition(String parentVarName) { StringBuffer scriptBuffer = new StringBuffer(100); String script = ""; if(parentVarName == null) scriptBuffer.append(" " + getVarName() + " = " + "new JSTreeNode("); else scriptBuffer.append(" " + getVarName() + " = " + parentVarName + ".addChild("); scriptBuffer.append("'" + escapeQuotes(getName()) + "'"); if(getUrl() != null) { scriptBuffer.append(", '" + getUrl() + "'"); } else if(getSubmitFormName() != null) { scriptBuffer.append(", '" + getSubmitFormName() + "'"); } else { scriptBuffer.append(", null"); } if(getIconSrcOpen() != null) { scriptBuffer.append(", '" + getIconSrcOpen() + "'"); } else { scriptBuffer.append(", null"); } if(getIconSrcClosed() != null) { scriptBuffer.append(", '" + getIconSrcClosed() + "'"); } else { scriptBuffer.append(", null"); } if(getPopupText() != null) { scriptBuffer.append(", '" + escapeQuotes(getPopupText()) + "'"); } else { scriptBuffer.append(", null"); } if(getPopup() != null) { scriptBuffer.append(", '" + getPopup().getName() + "'"); } else { scriptBuffer.append(", null"); } if(isExpanded || parentVarName == null) { // the root is always expanded scriptBuffer.append(", true"); } else { scriptBuffer.append(", false"); } if(hasChildren) { scriptBuffer.append(", true"); } else { scriptBuffer.append(", false"); } if(tree == null || tree.getName() == null) throw new IllegalArgumentException("Each node should have a tree defined. Either the tree is null, or the name of tree is null."); scriptBuffer.append(", '" + escapeQuotes(tree.getName()) + "'"); if(preSubmitScript != null) { scriptBuffer.append(", '" + preSubmitScript + "'"); } scriptBuffer.append(");\n"); /** Display name should be set here */ if(getDisplayName() != null) scriptBuffer.append(" " + getVarName() + ".displayName = '" + getDisplayName() + "';\n"); /** Set the data field */ if(getData() != null) scriptBuffer.append(" " + getVarName() + ".data = '" + getData() + "';\n"); /** Let us generate pagination code for a node which supports pagination */ if(isPaginated()) { if(getPaginatedNodeChildCount() == 0) throw new IllegalArgumentException("Please set the paginated node child count for the node. (using the setPaginatedNodeChildCount() method.)"); scriptBuffer.append(" " + getVarName() + ".isPaginated=true;\n"); scriptBuffer.append(" " + getVarName() + ".paginatedNodeChildCount="+ getPaginatedNodeChildCount()+";\n"); scriptBuffer.append(" " + getVarName() + ".pageSize="+ getPageSize()+";\n"); scriptBuffer.append(" " + getVarName() + ".pageRangeBegin=" + (getPageRangeBegin() == -1 ? "0" : String.valueOf(getPageRangeBegin())) +";\n"); } // we need to set the target for the url if it is specified. if(getUrl() != null && getUrlTargetName() != null) scriptBuffer.append(" " + getVarName() + "._target = '" + getUrlTargetName() + "';\n"); /** since the tree could have extra properties, let us generate those.... */ Enumeration enumVar = props.propertyNames(); while (enumVar.hasMoreElements()) { String element = (String) enumVar.nextElement(); scriptBuffer.append(" " + getVarName() + "." + element + "='" + escapeQuotes(props.getProperty(element)) + "';\n"); } return scriptBuffer.toString(); } /** * The equals method is over-ridden for the TreeNode. If the passed in object is a TreeNode, * the equality test is based on the node path of the tree nodes. */ public boolean equals(Object obj) { if(obj == null || !(obj instanceof TreeNode)) return false; return getNodePath().equals(((TreeNode)obj).getNodePath()); } public String toString() { return "TreeNode: name="+name + ", path="+nodePath+", submitFormName="+submitFormName+", data="+data+", displayName="+displayName; } }