001    /**
002     * Copyright 2005-2013 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     */
016    package org.kuali.rice.krad.uif.view;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.krad.uif.container.CollectionGroup;
020    import org.kuali.rice.krad.uif.component.Component;
021    import org.kuali.rice.krad.uif.field.DataField;
022    import org.kuali.rice.krad.uif.field.InputField;
023    import org.kuali.rice.krad.uif.util.ComponentUtils;
024    import org.kuali.rice.krad.uif.util.ViewCleaner;
025    
026    import java.beans.PropertyEditor;
027    import java.io.Serializable;
028    import java.util.HashMap;
029    import java.util.HashSet;
030    import java.util.Map;
031    import java.util.Set;
032    
033    /**
034     * Holds field indexes of a <code>View</code> instance for retrieval
035     *
036     * @author Kuali Rice Team (rice.collab@kuali.org)
037     */
038    public class ViewIndex implements Serializable {
039        private static final long serialVersionUID = 4700818801272201371L;
040    
041        private Map<String, Component> index;
042        private Map<String, DataField> dataFieldIndex;
043        private Map<String, CollectionGroup> collectionsIndex;
044    
045        private Map<String, Component> initialComponentStates;
046    
047        private Map<String, PropertyEditor> fieldPropertyEditors;
048        private Map<String, PropertyEditor> secureFieldPropertyEditors;
049        private Map<String, Integer> idSequenceSnapshot;
050        private Map<String, Map<String, String>> componentExpressionGraphs;
051    
052        /**
053         * Constructs new instance
054         */
055        public ViewIndex() {
056            index = new HashMap<String, Component>();
057            dataFieldIndex = new HashMap<String, DataField>();
058            collectionsIndex = new HashMap<String, CollectionGroup>();
059            initialComponentStates = new HashMap<String, Component>();
060            fieldPropertyEditors = new HashMap<String, PropertyEditor>();
061            secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
062            idSequenceSnapshot = new HashMap<String, Integer>();
063            componentExpressionGraphs = new HashMap<String, Map<String, String>>();
064        }
065    
066        /**
067         * Walks through the View tree and indexes all components found. All components
068         * are indexed by their IDs with the special indexing done for certain components
069         *
070         * <p>
071         * <code>DataField</code> instances are indexed by the attribute path.
072         * This is useful for retrieving the InputField based on the incoming
073         * request parameter
074         * </p>
075         *
076         * <p>
077         * <code>CollectionGroup</code> instances are indexed by the collection
078         * path. This is useful for retrieving the CollectionGroup based on the
079         * incoming request parameter
080         * </p>
081         */
082        protected void index(View view) {
083            index = new HashMap<String, Component>();
084            dataFieldIndex = new HashMap<String, DataField>();
085            collectionsIndex = new HashMap<String, CollectionGroup>();
086            fieldPropertyEditors = new HashMap<String, PropertyEditor>();
087            secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
088    
089            indexComponent(view);
090        }
091    
092        /**
093         * Adds an entry to the main index for the given component. If the component
094         * is of type <code>DataField</code> or <code>CollectionGroup</code> an
095         * entry is created in the corresponding indexes for those types as well. Then
096         * the #indexComponent method is called for each of the component's children
097         *
098         * <p>
099         * If the component is already contained in the indexes, it will be replaced
100         * </p>
101         *
102         * <p>
103         * Special processing is done for DataField instances to register their property editor which will
104         * be used for form binding
105         * </p>
106         *
107         * @param component - component instance to index
108         */
109        public void indexComponent(Component component) {
110            if (component == null) {
111                return;
112            }
113    
114            index.put(component.getId(), component);
115    
116            if (component instanceof DataField) {
117                DataField field = (DataField) component;
118                dataFieldIndex.put(field.getBindingInfo().getBindingPath(), field);
119    
120                // pull out information we will need to support the form post
121                if (component.isRender()) {
122                    if (field.hasSecureValue()) {
123                        secureFieldPropertyEditors.put(field.getBindingInfo().getBindingPath(), field.getPropertyEditor());
124                    } else {
125                        fieldPropertyEditors.put(field.getBindingInfo().getBindingPath(), field.getPropertyEditor());
126                    }
127                }
128            } else if (component instanceof CollectionGroup) {
129                CollectionGroup collectionGroup = (CollectionGroup) component;
130                collectionsIndex.put(collectionGroup.getBindingInfo().getBindingPath(), collectionGroup);
131            }
132    
133            for (Component nestedComponent : component.getComponentsForLifecycle()) {
134                indexComponent(nestedComponent);
135            }
136        }
137    
138        /**
139         * Invoked after the view lifecycle or component refresh has run to clear indexes that are not
140         * needed for the post
141         */
142        public void clearIndexesAfterRender() {
143            // build list of factory ids for components whose initial state needs to be keep
144            Set<String> holdIds = new HashSet<String>();
145            Set<String> holdFactoryIds = new HashSet<String>();
146            for (Component component : index.values()) {
147                if (component != null) {
148                    // if component has a refresh condition we need to keep it
149                    if ((StringUtils.isNotBlank(component.getProgressiveRender()) || StringUtils.isNotBlank(
150                            component.getConditionalRefresh()) || component.getRefreshTimer() > 0 ||
151                            (component.getRefreshWhenChangedPropertyNames() != null &&
152                                    !component.getRefreshWhenChangedPropertyNames().isEmpty()) ||
153                            component.isRefreshedByAction() || component.isDisclosedByAction()) &&
154                            !component.isDisableSessionPersistence()) {
155                        holdFactoryIds.add(component.getBaseId());
156                        holdIds.add(component.getId());
157                    }
158                    // if component is marked as persist in session we need to keep it
159                    else if (component.isForceSessionPersistence()) {
160                        holdFactoryIds.add(component.getBaseId());
161                        holdIds.add(component.getId());
162                    }
163                    // if component is a collection we need to keep it
164                    else if (component instanceof CollectionGroup && !component.isDisableSessionPersistence()) {
165                        ViewCleaner.cleanCollectionGroup((CollectionGroup) component);
166                        holdFactoryIds.add(component.getBaseId());
167                        holdIds.add(component.getId());
168                    }
169                    // if component is input field and has a query we need to keep the final state
170                    else if ((component instanceof InputField) && !component.isDisableSessionPersistence()) {
171                        InputField inputField = (InputField) component;
172                        if ((inputField.getAttributeQuery() != null) || ((inputField.getSuggest() != null) && inputField
173                                .getSuggest().isRender())) {
174                            holdIds.add(component.getId());
175                        }
176                    }
177                }
178            }
179    
180            // remove initial states for components we don't need for post
181            Map<String, Component> holdInitialComponentStates = new HashMap<String, Component>();
182            for (String factoryId : initialComponentStates.keySet()) {
183                if (holdFactoryIds.contains(factoryId)) {
184                    holdInitialComponentStates.put(factoryId, initialComponentStates.get(factoryId));
185                }
186            }
187            initialComponentStates = holdInitialComponentStates;
188    
189            // remove final states for components we don't need for post
190            Map<String, Component> holdComponentStates = new HashMap<String, Component>();
191            for (String id : index.keySet()) {
192                if (holdIds.contains(id)) {
193                    Component component = index.get(id);
194                    holdComponentStates.put(id, component);
195    
196                    // hold expressions for refresh (since they could have been pushed from a parent)
197                    if (!component.getRefreshExpressionGraph().isEmpty()) {
198                        componentExpressionGraphs.put(component.getBaseId(), component.getRefreshExpressionGraph());
199                    }
200                }
201            }
202            index = holdComponentStates;
203    
204            dataFieldIndex = new HashMap<String, DataField>();
205        }
206    
207        /**
208         * Retrieves a <code>Component</code> from the view index by Id
209         *
210         * @param id - id for the component to retrieve
211         * @return Component instance found in index, or null if no such component exists
212         */
213        public Component getComponentById(String id) {
214            return index.get(id);
215        }
216    
217        /**
218         * Retrieves a <code>DataField</code> instance from the index
219         *
220         * @param propertyPath - full path of the data field (from the form)
221         * @return DataField instance for the path or Null if not found
222         */
223        public DataField getDataFieldByPath(String propertyPath) {
224            return dataFieldIndex.get(propertyPath);
225        }
226    
227        /**
228         * Retrieves a <code>DataField</code> instance that has the given property name
229         * specified (note this is not the full binding path and first match is returned)
230         *
231         * @param propertyName - property name for field to retrieve
232         * @return DataField instance found or null if not found
233         */
234        public DataField getDataFieldByPropertyName(String propertyName) {
235            DataField dataField = null;
236    
237            for (DataField field : dataFieldIndex.values()) {
238                if (StringUtils.equals(propertyName, field.getPropertyName())) {
239                    dataField = field;
240                    break;
241                }
242            }
243    
244            return dataField;
245        }
246    
247        /**
248         * Gets the Map that contains attribute field indexing information. The Map
249         * key points to an attribute binding path, and the Map value is the
250         * <code>DataField</code> instance
251         *
252         * @return Map<String, DataField> data fields index map
253         */
254        public Map<String, DataField> getDataFieldIndex() {
255            return this.dataFieldIndex;
256        }
257    
258        /**
259         * Gets the Map that contains collection indexing information. The Map key
260         * gives the binding path to the collection, and the Map value givens the
261         * <code>CollectionGroup</code> instance
262         *
263         * @return Map<String, CollectionGroup> collection index map
264         */
265        public Map<String, CollectionGroup> getCollectionsIndex() {
266            return this.collectionsIndex;
267        }
268    
269        /**
270         * Retrieves a <code>CollectionGroup</code> instance from the index
271         *
272         * @param collectionPath - full path of the collection (from the form)
273         * @return CollectionGroup instance for the collection path or Null if not
274         *         found
275         */
276        public CollectionGroup getCollectionGroupByPath(String collectionPath) {
277            return collectionsIndex.get(collectionPath);
278        }
279    
280        /**
281         * Preserves initial state of components needed for doing component refreshes
282         *
283         * <p>
284         * Some components, such as those that are nested or created in code cannot be requested from the
285         * spring factory to get new instances. For these a copy of the component in its initial state is
286         * set in this map which will be used when doing component refreshes (which requires running just that
287         * component's lifecycle)
288         * </p>
289         *
290         * <p>
291         * Map entries are added during the perform initialize phase from {@link org.kuali.rice.krad.uif.service.ViewHelperService}
292         * </p>
293         *
294         * @return Map<String, Component> - map with key giving the factory id for the component and the value the
295         *         component
296         *         instance
297         */
298        public Map<String, Component> getInitialComponentStates() {
299            return initialComponentStates;
300        }
301    
302        /**
303         * Adds a copy of the given component instance to the map of initial component states keyed
304         *
305         * <p>
306         * Component is only added if its factory id is not set yet (which would happen if it had a spring bean id
307         * and we can get the state from Spring). Once added the factory id will be set to the component id
308         * </p>
309         *
310         * @param component - component instance to add
311         */
312        public void addInitialComponentStateIfNeeded(Component component) {
313            if (StringUtils.isBlank(component.getBaseId())) {
314                component.setBaseId(component.getId());
315                initialComponentStates.put(component.getBaseId(), ComponentUtils.copy(component));
316            }
317        }
318    
319        /**
320         * Setter for the map holding initial component states
321         *
322         * @param initialComponentStates
323         */
324        public void setInitialComponentStates(Map<String, Component> initialComponentStates) {
325            this.initialComponentStates = initialComponentStates;
326        }
327    
328        /**
329         * Maintains configuration of properties that have been configured for the view (if render was set to
330         * true) and there corresponding PropertyEdtior (if configured)
331         *
332         * <p>
333         * Information is pulled out of the View during the lifecycle so it can be used when a form post is done
334         * from the View. Note if a field is secure, it will be placed in the {@link #getSecureFieldPropertyEditors()} map
335         * instead
336         * </p>
337         *
338         * @return Map<String, PropertyEditor> map of property path (full) to PropertyEditor
339         */
340        public Map<String, PropertyEditor> getFieldPropertyEditors() {
341            return fieldPropertyEditors;
342        }
343    
344        /**
345         * Maintains configuration of secure properties that have been configured for the view (if render was set to
346         * true) and there corresponding PropertyEdtior (if configured)
347         *
348         * <p>
349         * Information is pulled out of the View during the lifecycle so it can be used when a form post is done
350         * from the View. Note if a field is non-secure, it will be placed in the {@link #getFieldPropertyEditors()} map
351         * instead
352         * </p>
353         *
354         * @return Map<String, PropertyEditor> map of property path (full) to PropertyEditor
355         */
356        public Map<String, PropertyEditor> getSecureFieldPropertyEditors() {
357            return secureFieldPropertyEditors;
358        }
359    
360        /**
361         * Map of components ids to starting id sequences used for the component refresh process
362         *
363         * @return Map<String, Integer> key is component id and value is id sequence value
364         */
365        public Map<String, Integer> getIdSequenceSnapshot() {
366            return idSequenceSnapshot;
367        }
368    
369        /**
370         * Adds a sequence value to the id snapshot map for the given component id
371         *
372         * @param componentId - id for the component the id sequence value is associated it
373         * @param sequenceVal - current sequence value to insert into the snapshot
374         */
375        public void addSequenceValueToSnapshot(String componentId, int sequenceVal) {
376            idSequenceSnapshot.put(componentId, sequenceVal);
377        }
378    
379        /**
380         * Map of components with their associated expression graphs that will be used during
381         * the component refresh process
382         *
383         * <p>
384         * Because expressions that impact a component being refreshed might be on a parent component, a special
385         * map needs to be held around that contains expressions that apply to the component and all its nested
386         * components. This map is populated during the initial view processing and populating of the property
387         * expressions from the initial expression graphs
388         * </p>
389         *
390         * @return Map<String, Map<String, String>> key is component id and value is expression graph map
391         * @see org.kuali.rice.krad.uif.util.ExpressionUtils#populatePropertyExpressionsFromGraph(org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean,
392         *      boolean)
393         */
394        public Map<String, Map<String, String>> getComponentExpressionGraphs() {
395            return componentExpressionGraphs;
396        }
397    
398    }