View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.view;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.krad.uif.container.CollectionGroup;
20  import org.kuali.rice.krad.uif.component.Component;
21  import org.kuali.rice.krad.uif.field.DataField;
22  import org.kuali.rice.krad.uif.field.InputField;
23  import org.kuali.rice.krad.uif.util.ComponentUtils;
24  import org.kuali.rice.krad.uif.util.ViewCleaner;
25  
26  import java.beans.PropertyEditor;
27  import java.io.Serializable;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Map;
31  import java.util.Set;
32  
33  /**
34   * Holds field indexes of a <code>View</code> instance for retrieval
35   *
36   * @author Kuali Rice Team (rice.collab@kuali.org)
37   */
38  public class ViewIndex implements Serializable {
39      private static final long serialVersionUID = 4700818801272201371L;
40  
41      private Map<String, Component> index;
42      private Map<String, DataField> dataFieldIndex;
43      private Map<String, CollectionGroup> collectionsIndex;
44  
45      private Map<String, Component> initialComponentStates;
46  
47      private Map<String, PropertyEditor> fieldPropertyEditors;
48      private Map<String, PropertyEditor> secureFieldPropertyEditors;
49      private Map<String, Integer> idSequenceSnapshot;
50      private Map<String, Map<String, String>> componentExpressionGraphs;
51  
52      /**
53       * Constructs new instance
54       */
55      public ViewIndex() {
56          index = new HashMap<String, Component>();
57          dataFieldIndex = new HashMap<String, DataField>();
58          collectionsIndex = new HashMap<String, CollectionGroup>();
59          initialComponentStates = new HashMap<String, Component>();
60          fieldPropertyEditors = new HashMap<String, PropertyEditor>();
61          secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
62          idSequenceSnapshot = new HashMap<String, Integer>();
63          componentExpressionGraphs = new HashMap<String, Map<String, String>>();
64      }
65  
66      /**
67       * Walks through the View tree and indexes all components found. All components
68       * are indexed by their IDs with the special indexing done for certain components
69       *
70       * <p>
71       * <code>DataField</code> instances are indexed by the attribute path.
72       * This is useful for retrieving the InputField based on the incoming
73       * request parameter
74       * </p>
75       *
76       * <p>
77       * <code>CollectionGroup</code> instances are indexed by the collection
78       * path. This is useful for retrieving the CollectionGroup based on the
79       * incoming request parameter
80       * </p>
81       */
82      protected void index(View view) {
83          index = new HashMap<String, Component>();
84          dataFieldIndex = new HashMap<String, DataField>();
85          collectionsIndex = new HashMap<String, CollectionGroup>();
86          fieldPropertyEditors = new HashMap<String, PropertyEditor>();
87          secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
88  
89          indexComponent(view);
90      }
91  
92      /**
93       * Adds an entry to the main index for the given component. If the component
94       * is of type <code>DataField</code> or <code>CollectionGroup</code> an
95       * entry is created in the corresponding indexes for those types as well. Then
96       * the #indexComponent method is called for each of the component's children
97       *
98       * <p>
99       * 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 }