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}