001/**
002 * Copyright 2005-2016 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 org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.util.tree.Node;
020import org.kuali.rice.core.api.util.tree.Tree;
021import org.kuali.rice.krad.uif.UifConstants;
022import org.kuali.rice.krad.uif.component.BindingInfo;
023import org.kuali.rice.krad.uif.component.Component;
024import org.kuali.rice.krad.uif.component.DataBinding;
025import org.kuali.rice.krad.uif.field.MessageField;
026import org.kuali.rice.krad.uif.util.ComponentUtils;
027import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
028import org.kuali.rice.krad.uif.view.View;
029
030import java.util.ArrayList;
031import java.util.List;
032import java.util.Map;
033
034/**
035 * Group component that is backed by a <code>Tree</code> data structure and typically
036 * rendered as a tree in the user interface
037 *
038 * @author Kuali Rice Team (rice.collab@kuali.org)
039 */
040public class TreeGroup extends Group implements DataBinding{
041    private static final long serialVersionUID = 5841343037089286740L;
042
043    private String propertyName;
044    private BindingInfo bindingInfo;
045
046    private Map<Class<?>, NodePrototype> nodePrototypeMap;
047    private NodePrototype defaultNodePrototype;
048
049    private Tree<Group, MessageField> treeGroups;
050
051    private org.kuali.rice.krad.uif.widget.Tree tree;
052
053    public TreeGroup() {
054        super();
055
056        treeGroups = new Tree<Group, MessageField>();
057    }
058
059    /**
060     * The following actions are performed:
061     *
062     * <ul>
063     * <li>Set fieldBindModelPath to the collection model path (since the fields
064     * have to belong to the same model as the collection)</li>
065     * <li>Set defaults for binding</li>
066     * <li>Calls view helper service to initialize prototypes</li>
067     * </ul>
068     *
069     */
070    @Override
071    public void performInitialization(View view, Object model) {
072        setFieldBindingObjectPath(getBindingInfo().getBindingObjectPath());
073
074        super.performInitialization(view, model);
075
076        if (bindingInfo != null) {
077            bindingInfo.setDefaults(view, getPropertyName());
078        }
079
080        // TODO: set object path for prototypes equal to the tree group object path?
081
082        initializeNodePrototypeComponents(view, model);
083    }
084
085    protected void initializeNodePrototypeComponents(View view, Object model) {
086        view.getViewHelperService().performComponentInitialization(view, model,
087                defaultNodePrototype.getLabelPrototype());
088        view.getViewHelperService().performComponentInitialization(view, model,
089                defaultNodePrototype.getDataGroupPrototype());
090
091        if (nodePrototypeMap != null) {
092            for (Map.Entry<Class<?>, NodePrototype> prototypeEntry : nodePrototypeMap.entrySet()) {
093                NodePrototype prototype = prototypeEntry.getValue();
094                if (prototype != null) {
095
096                    if (prototype.getLabelPrototype() != null) {
097                        view.getViewHelperService().performComponentInitialization(view, model,
098                                prototype.getLabelPrototype());
099                    } else {
100                        throw new IllegalStateException("encountered null NodePrototype.labelPrototype");
101                    }
102
103                    if (prototype.getDataGroupPrototype() != null) {
104                        view.getViewHelperService().performComponentInitialization(view, model,
105                                prototype.getDataGroupPrototype());
106                    } else {
107                        throw new IllegalStateException("encountered null NodePrototype.dataGroupPrototype");
108                    }
109                } else {
110                    throw new IllegalStateException("encountered null NodePrototype");
111                }
112            }
113        }
114    }
115
116    @Override
117    public void performApplyModel(View view, Object model, Component parent) {
118        super.performApplyModel(view, model, parent);
119
120        buildTreeGroups(view, model);
121    }
122
123    /**
124     * Builds the components that will be rendered as part of the tree group
125     *
126     * <p>
127     * The component tree group mirrors the tree data structure on the model. For each node of
128     * the data structure, a corresponding <code>MessageField</code>  will be created for the node
129     * label, and a <code>Group</code> component for the node data. These are placed into a new
130     * node for the component tree. After the tree is built it is set as a property on the tree group
131     * to be read by the renderer
132     * </p>
133     *
134     * @param view - view instance the tree group belongs to
135     * @param model - object containing the view data from which the tree data will be retrieved
136     */
137    protected void buildTreeGroups(View view, Object model) {
138        // get Tree data property
139        Tree<Object, String> treeData = ObjectPropertyUtils.getPropertyValue(model, getBindingInfo().getBindingPath());
140
141        // build component tree that corresponds with tree data
142        Tree<Group, MessageField> treeGroups = new Tree<Group, MessageField>();
143
144        String bindingPrefix = getBindingInfo().getBindingPrefixForNested();
145        Node<Group, MessageField> rootNode =
146                buildTreeNode(treeData.getRootElement(), bindingPrefix + /* TODO: hack */ ".rootElement", "root");
147        treeGroups.setRootElement(rootNode);
148
149        setTreeGroups(treeGroups);
150    }
151
152    protected Node<Group, MessageField> buildTreeNode(Node<Object, String> nodeData, String bindingPrefix,
153            String parentNode) {
154        if (nodeData == null) {
155            return null;
156        }
157
158        Node<Group, MessageField> node = new Node<Group, MessageField>();
159        node.setNodeType(nodeData.getNodeType());
160
161        NodePrototype prototype = getNodePrototype(nodeData);
162
163        MessageField messageField = ComponentUtils.copy(prototype.getLabelPrototype(), parentNode);
164        ComponentUtils.pushObjectToContext(messageField, UifConstants.ContextVariableNames.NODE, nodeData);
165        messageField.setMessageText(nodeData.getNodeLabel());
166        node.setNodeLabel(messageField);
167
168        Group nodeGroup = ComponentUtils.copyComponent(prototype.getDataGroupPrototype(), bindingPrefix + ".data",
169                parentNode);
170        ComponentUtils.pushObjectToContext(nodeGroup, UifConstants.ContextVariableNames.NODE, nodeData);
171
172        String nodePath = bindingPrefix + ".data";
173        if (StringUtils.isNotBlank(getBindingInfo().getBindingObjectPath())) {
174            nodePath = getBindingInfo().getBindingObjectPath() + "." + nodePath;
175        }
176        ComponentUtils.pushObjectToContext(nodeGroup, UifConstants.ContextVariableNames.NODE_PATH, nodePath);
177        node.setData(nodeGroup);
178
179        List<Node<Group, MessageField>> nodeChildren = new ArrayList<Node<Group, MessageField>>();
180
181        int childIndex = 0;
182        for (Node<Object, String> childDataNode : nodeData.getChildren()) {
183            String nextBindingPrefix = bindingPrefix + ".children[" + childIndex + "]";
184            Node<Group, MessageField> childNode = buildTreeNode(childDataNode, nextBindingPrefix,
185                    "_node_" + childIndex + ("root".equals(parentNode) ? "_parent_" : "_parent") + parentNode);
186
187            nodeChildren.add(childNode);
188
189            // Don't forget about me:
190            ++childIndex;
191        }
192        node.setChildren(nodeChildren);
193
194        return node;
195    }
196
197    /**
198     * Gets the NodePrototype to use for the given Node
199     */
200    private NodePrototype getNodePrototype(Node<Object, String> nodeData) {
201        NodePrototype result = null;
202        if (nodeData != null && nodeData.getData() != null) {
203            Class<?> dataClass = nodeData.getData().getClass();
204            result = nodePrototypeMap.get(dataClass);
205
206            // somewhat lame fallback - to do this right we'd find all entries that are assignable from the data class
207            // and then figure out which one is the closest relative
208            if (result == null) {
209                for (Map.Entry<Class<?>, NodePrototype> prototypeEntry : nodePrototypeMap.entrySet()) {
210                    if (prototypeEntry.getKey().isAssignableFrom(dataClass)) {
211                        result = prototypeEntry.getValue();
212                        break;
213                    }
214                }
215            }
216        }
217
218        if (result == null) {
219            result = defaultNodePrototype;
220        }
221
222        return result;
223    }
224
225    /**
226     * @see org.kuali.rice.krad.uif.component.Component#getComponentsForLifecycle()
227     */
228    @Override
229    public List<Component> getComponentsForLifecycle() {
230        List<Component> components = super.getComponentsForLifecycle();
231
232        components.add(tree);
233        addNodeComponents(treeGroups.getRootElement(), components);
234
235        return components;
236    }
237
238    /**
239     * @see org.kuali.rice.krad.uif.component.Component#getComponentPrototypes()
240     */
241    @Override
242    public List<Component> getComponentPrototypes() {
243        List<Component> components = super.getComponentPrototypes();
244
245        if (defaultNodePrototype != null) {
246            components.add(defaultNodePrototype.getLabelPrototype());
247            components.add(defaultNodePrototype.getDataGroupPrototype());
248        }
249
250        if (nodePrototypeMap != null) {
251            for (Map.Entry<Class<?>, NodePrototype> prototypeEntry : nodePrototypeMap.entrySet()) {
252                NodePrototype prototype = prototypeEntry.getValue();
253                if (prototype != null) {
254                    components.add(prototype.getLabelPrototype());
255                    components.add(prototype.getDataGroupPrototype());
256                }
257            }
258        }
259
260        return components;
261    }
262
263    /**
264     * Retrieves the <code>Component</code> instances from the node for building the nested
265     * components list
266     *
267     * @param node - node to pull components from
268     * @param components - list to add components to
269     */
270    protected void addNodeComponents(Node<Group, MessageField> node, List<Component> components) {
271        if (node != null) {
272            components.add(node.getNodeLabel());
273            components.add(node.getData());
274
275            for (Node<Group, MessageField> nodeChild : node.getChildren()) {
276                addNodeComponents(nodeChild, components);
277            }
278        }
279    }
280
281    public String getPropertyName() {
282        return propertyName;
283    }
284
285    public void setPropertyName(String propertyName) {
286        this.propertyName = propertyName;
287    }
288
289    public BindingInfo getBindingInfo() {
290        return bindingInfo;
291    }
292
293    public void setBindingInfo(BindingInfo bindingInfo) {
294        this.bindingInfo = bindingInfo;
295    }
296
297    /**
298     * @return the defaultNodePrototype
299     */
300    public NodePrototype getDefaultNodePrototype() {
301        return this.defaultNodePrototype;
302    }
303
304    /**
305     * @param defaultNodePrototype the defaultNodePrototype to set
306     */
307    public void setDefaultNodePrototype(NodePrototype defaultNodePrototype) {
308        this.defaultNodePrototype = defaultNodePrototype;
309    }
310
311    /**
312     * @return the nodePrototypeMap
313     */
314    public Map<Class<?>, NodePrototype> getNodePrototypeMap() {
315        return this.nodePrototypeMap;
316    }
317
318    /**
319     * @param nodePrototypeMap the nodePrototypeMap to set
320     */
321    public void setNodePrototypeMap(Map<Class<?>, NodePrototype> nodePrototypeMap) {
322        this.nodePrototypeMap = nodePrototypeMap;
323    }
324
325    public Tree<Group, MessageField> getTreeGroups() {
326        return treeGroups;
327    }
328
329    public void setTreeGroups(Tree<Group, MessageField> treeGroups) {
330        this.treeGroups = treeGroups;
331    }
332
333    public org.kuali.rice.krad.uif.widget.Tree getTree() {
334        return tree;
335    }
336
337    public void setTree(org.kuali.rice.krad.uif.widget.Tree tree) {
338        this.tree = tree;
339    }
340}