001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.container; 017 018import java.util.ArrayList; 019import java.util.List; 020import java.util.Map; 021 022import org.apache.commons.lang.StringUtils; 023import org.kuali.rice.core.api.util.tree.Node; 024import org.kuali.rice.core.api.util.tree.Tree; 025import org.kuali.rice.krad.datadictionary.parse.BeanTag; 026import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 027import org.kuali.rice.krad.datadictionary.parse.BeanTags; 028import org.kuali.rice.krad.uif.UifConstants; 029import org.kuali.rice.krad.uif.component.BindingInfo; 030import org.kuali.rice.krad.uif.component.Component; 031import org.kuali.rice.krad.uif.component.DataBinding; 032import org.kuali.rice.krad.uif.element.Message; 033import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 034import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleRestriction; 035import org.kuali.rice.krad.uif.util.ComponentUtils; 036import org.kuali.rice.krad.uif.util.ContextUtils; 037import org.kuali.rice.krad.uif.util.CopyUtils; 038import org.kuali.rice.krad.uif.util.LifecycleElement; 039import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 040 041/** 042 * Group component that is backed by a <code>Tree</code> data structure and typically 043 * rendered as a tree in the user interface 044 * 045 * @author Kuali Rice Team (rice.collab@kuali.org) 046 */ 047@BeanTags({@BeanTag(name = "treeGroup", parent = "Uif-TreeGroup"), 048 @BeanTag(name = "treeSection", parent = "Uif-TreeSection"), 049 @BeanTag(name = "treeSubSection", parent = "Uif-TreeSubSection")}) 050public class TreeGroup extends GroupBase implements DataBinding { 051 private static final long serialVersionUID = 5841343037089286740L; 052 053 private String propertyName; 054 private BindingInfo bindingInfo; 055 056 private Map<Class<?>, NodePrototype> nodePrototypeMap; 057 private NodePrototype defaultNodePrototype; 058 059 private Tree<Group, Message> treeGroups; 060 061 private org.kuali.rice.krad.uif.widget.Tree tree; 062 063 public TreeGroup() { 064 super(); 065 066 treeGroups = new Tree<Group, Message>(); 067 } 068 069 /** 070 * The following actions are performed: 071 * 072 * <ul> 073 * <li>Set fieldBindModelPath to the collection model path (since the fields 074 * have to belong to the same model as the collection)</li> 075 * <li>Set defaults for binding</li> 076 * <li>Calls view helper service to initialize prototypes</li> 077 * </ul> 078 */ 079 @Override 080 public void performInitialization(Object model) { 081 setFieldBindingObjectPath(getBindingInfo().getBindingObjectPath()); 082 083 super.performInitialization(model); 084 085 if (bindingInfo != null) { 086 bindingInfo.setDefaults(ViewLifecycle.getActiveLifecycle().getView(), getPropertyName()); 087 } 088 089 // TODO: set object path for prototypes equal to the tree group object path? 090 } 091 092 @Override 093 public void performApplyModel(Object model, LifecycleElement parent) { 094 super.performApplyModel(model, parent); 095 096 buildTreeGroups(model); 097 } 098 099 /** 100 * Builds the components that will be rendered as part of the tree group 101 * 102 * <p> 103 * The component tree group mirrors the tree data structure on the model. For each node of 104 * the data structure, a corresponding <code>Message</code> will be created for the node 105 * label, and a <code>Group</code> component for the node data. These are placed into a new 106 * node for the component tree. After the tree is built it is set as a property on the tree group 107 * to be read by the renderer 108 * </p> 109 * 110 * @param model object containing the view data from which the tree data will be retrieved 111 */ 112 protected void buildTreeGroups(Object model) { 113 // get Tree data property 114 Tree<Object, String> treeData = ObjectPropertyUtils.getPropertyValue(model, getBindingInfo().getBindingPath()); 115 116 // build component tree that corresponds with tree data 117 Tree<Group, Message> treeGroups = new Tree<Group, Message>(); 118 119 String bindingPrefix = getBindingInfo().getBindingPrefixForNested(); 120 Node<Group, Message> rootNode = buildTreeNode(treeData.getRootElement(), 121 bindingPrefix + /* TODO: hack */ ".rootElement", "root"); 122 treeGroups.setRootElement(rootNode); 123 124 setTreeGroups(treeGroups); 125 } 126 127 protected Node<Group, Message> buildTreeNode(Node<Object, String> nodeData, String bindingPrefix, 128 String parentNode) { 129 if (nodeData == null) { 130 return null; 131 } 132 133 Node<Group, Message> node = new Node<Group, Message>(); 134 node.setNodeType(nodeData.getNodeType()); 135 136 NodePrototype prototype = getNodePrototype(nodeData); 137 138 Message message = ComponentUtils.copy(prototype.getLabelPrototype(), parentNode); 139 ContextUtils.pushObjectToContextDeep(message, UifConstants.ContextVariableNames.NODE, nodeData); 140 message.setMessageText(nodeData.getNodeLabel()); 141 node.setNodeLabel(message); 142 143 Group nodeGroup = ComponentUtils.copyComponent(prototype.getDataGroupPrototype(), bindingPrefix + ".data", 144 parentNode); 145 ContextUtils.pushObjectToContextDeep(nodeGroup, UifConstants.ContextVariableNames.NODE, nodeData); 146 147 String nodePath = bindingPrefix + ".data"; 148 if (StringUtils.isNotBlank(getBindingInfo().getBindingObjectPath())) { 149 nodePath = getBindingInfo().getBindingObjectPath() + "." + nodePath; 150 } 151 ContextUtils.pushObjectToContextDeep(nodeGroup, UifConstants.ContextVariableNames.NODE_PATH, nodePath); 152 node.setData(nodeGroup); 153 154 List<Node<Group, Message>> nodeChildren = new ArrayList<Node<Group, Message>>(); 155 156 int childIndex = 0; 157 for (Node<Object, String> childDataNode : nodeData.getChildren()) { 158 String nextBindingPrefix = bindingPrefix + ".children[" + childIndex + "]"; 159 Node<Group, Message> childNode = buildTreeNode(childDataNode, nextBindingPrefix, 160 "_node_" + childIndex + ("root".equals(parentNode) ? "_parent_" : "_parent") + parentNode); 161 162 nodeChildren.add(childNode); 163 164 // Don't forget about me: 165 ++childIndex; 166 } 167 node.setChildren(nodeChildren); 168 169 return node; 170 } 171 172 /** 173 * Gets the NodePrototype to use for the given Node 174 */ 175 private NodePrototype getNodePrototype(Node<Object, String> nodeData) { 176 NodePrototype result = null; 177 if (nodeData != null && nodeData.getData() != null) { 178 Class<?> dataClass = nodeData.getData().getClass(); 179 result = nodePrototypeMap.get(dataClass); 180 181 // somewhat lame fallback - to do this right we'd find all entries that are assignable from the data class 182 // and then figure out which one is the closest relative 183 if (result == null) { 184 for (Map.Entry<Class<?>, NodePrototype> prototypeEntry : nodePrototypeMap.entrySet()) { 185 if (prototypeEntry.getKey().isAssignableFrom(dataClass)) { 186 result = prototypeEntry.getValue(); 187 break; 188 } 189 } 190 } 191 } 192 193 if (result == null) { 194 result = defaultNodePrototype; 195 } 196 197 return result; 198 } 199 200 /** 201 * Gets all node components within the tree. 202 * 203 * @return list of node components 204 */ 205 public List<Component> getNodeComponents() { 206 List<Component> components = new ArrayList<Component>(); 207 addNodeComponents(treeGroups.getRootElement(), components); 208 return components; 209 } 210 211 /** 212 * Gets all node components prototypes within the tree. 213 * 214 * @return list of node component prototypes 215 */ 216 @ViewLifecycleRestriction(UifConstants.ViewPhases.INITIALIZE) 217 public List<Component> getComponentPrototypes() { 218 List<Component> components = new ArrayList<Component>(); 219 220 if (defaultNodePrototype != null) { 221 components.add(defaultNodePrototype.getLabelPrototype()); 222 components.add(defaultNodePrototype.getDataGroupPrototype()); 223 } 224 225 if (nodePrototypeMap != null) { 226 for (Map.Entry<Class<?>, NodePrototype> prototypeEntry : nodePrototypeMap.entrySet()) { 227 NodePrototype prototype = prototypeEntry.getValue(); 228 if (prototype != null) { 229 components.add(prototype.getLabelPrototype()); 230 components.add(prototype.getDataGroupPrototype()); 231 } 232 } 233 } 234 235 return components; 236 } 237 238 /** 239 * Retrieves the <code>Component</code> instances from the node for building the nested 240 * components list 241 * 242 * @param node node to pull components from 243 * @param components list to add components to 244 */ 245 protected void addNodeComponents(Node<Group, Message> node, List<Component> components) { 246 if (node != null) { 247 components.add(node.getNodeLabel()); 248 components.add(node.getData()); 249 250 for (Node<Group, Message> nodeChild : node.getChildren()) { 251 addNodeComponents(nodeChild, components); 252 } 253 } 254 } 255 256 @BeanTagAttribute 257 public String getPropertyName() { 258 return propertyName; 259 } 260 261 public void setPropertyName(String propertyName) { 262 this.propertyName = propertyName; 263 } 264 265 @BeanTagAttribute 266 public BindingInfo getBindingInfo() { 267 return bindingInfo; 268 } 269 270 public void setBindingInfo(BindingInfo bindingInfo) { 271 this.bindingInfo = bindingInfo; 272 } 273 274 /** 275 * @return the defaultNodePrototype 276 */ 277 @BeanTagAttribute(type = BeanTagAttribute.AttributeType.DIRECTORBYTYPE) 278 public NodePrototype getDefaultNodePrototype() { 279 return this.defaultNodePrototype; 280 } 281 282 /** 283 * @param defaultNodePrototype the defaultNodePrototype to set 284 */ 285 public void setDefaultNodePrototype(NodePrototype defaultNodePrototype) { 286 this.defaultNodePrototype = defaultNodePrototype; 287 } 288 289 /** 290 * @return the nodePrototypeMap 291 */ 292 @BeanTagAttribute 293 public Map<Class<?>, NodePrototype> getNodePrototypeMap() { 294 return this.nodePrototypeMap; 295 } 296 297 /** 298 * @param nodePrototypeMap the nodePrototypeMap to set 299 */ 300 public void setNodePrototypeMap(Map<Class<?>, NodePrototype> nodePrototypeMap) { 301 this.nodePrototypeMap = nodePrototypeMap; 302 } 303 304 @BeanTagAttribute 305 public Tree<Group, Message> getTreeGroups() { 306 return treeGroups; 307 } 308 309 public void setTreeGroups(Tree<Group, Message> treeGroups) { 310 this.treeGroups = treeGroups; 311 } 312 313 @BeanTagAttribute 314 public org.kuali.rice.krad.uif.widget.Tree getTree() { 315 return tree; 316 } 317 318 public void setTree(org.kuali.rice.krad.uif.widget.Tree tree) { 319 this.tree = tree; 320 } 321 322 /** 323 * Copies a {@link Node} instance and then recursively copies each of its child nodes 324 * 325 * @param node node instance to copy 326 * @return new node instance copied from given node 327 */ 328 protected Node<Group, Message> copyNode(Node<Group, Message> node) { 329 Node<Group, Message> nodeCopy = new Node<Group, Message>(); 330 331 if (node == null) { 332 return null; 333 } 334 335 nodeCopy.setNodeType(node.getNodeType()); 336 337 if (node.getData() != null) { 338 nodeCopy.setData((Group) CopyUtils.copy(node.getData())); 339 } 340 341 if (node.getNodeLabel() != null) { 342 nodeCopy.setNodeLabel((Message) CopyUtils.copy(node.getNodeLabel())); 343 } 344 345 if (node.getChildren() != null) { 346 List<Node<Group, Message>> childrenCopy = new ArrayList<Node<Group, Message>>(); 347 for (Node<Group, Message> childNode : node.getChildren()) { 348 childrenCopy.add(copyNode(childNode)); 349 } 350 351 nodeCopy.setChildren(childrenCopy); 352 } 353 354 return nodeCopy; 355 } 356}