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 java.beans.PropertyEditor;
19  import java.io.Serializable;
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.Map;
23  import java.util.Map.Entry;
24  import java.util.Set;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.kuali.rice.krad.uif.component.Component;
28  import org.kuali.rice.krad.uif.container.CollectionGroup;
29  import org.kuali.rice.krad.uif.field.DataField;
30  import org.kuali.rice.krad.uif.field.InputField;
31  import org.kuali.rice.krad.uif.util.ComponentUtils;
32  import org.kuali.rice.krad.uif.util.ViewCleaner;
33  
34  /**
35   * Holds component indexes of a <code>View</code> instance for convenient retrieval during the
36   * lifecycle and persisting components for the refresh process
37   * 
38   * @author Kuali Rice Team (rice.collab@kuali.org)
39   */
40  public class ViewIndex implements Serializable {
41      private static final long serialVersionUID = 4700818801272201371L;
42  
43      protected Map<String, Component> index;
44      protected Map<String, DataField> dataFieldIndex;
45  
46      protected Map<String, CollectionGroup> collectionsIndex;
47  
48      protected Map<String, Component> initialComponentStates;
49  
50      protected Map<String, PropertyEditor> fieldPropertyEditors;
51      protected Map<String, PropertyEditor> secureFieldPropertyEditors;
52      protected Map<String, Map<String, String>> componentExpressionGraphs;
53  
54      protected Map<String, Map<String, Object>> postContext;
55  
56      private Set<String> idsToHoldInIndex;
57      private Set<String> idsToHoldInitialState;
58  
59      private Set<String> assignedIds;
60  
61      /**
62       * Constructs new instance
63       */
64      public ViewIndex() {
65          index = new HashMap<String, Component>();
66          dataFieldIndex = new HashMap<String, DataField>();
67          collectionsIndex = new HashMap<String, CollectionGroup>();
68          initialComponentStates = new HashMap<String, Component>();
69          fieldPropertyEditors = new HashMap<String, PropertyEditor>();
70          secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
71          componentExpressionGraphs = new HashMap<String, Map<String, String>>();
72          postContext = new HashMap<String, Map<String, Object>>();
73          idsToHoldInIndex = new HashSet<String>();
74          idsToHoldInitialState = new HashSet<String>();
75          assignedIds = new HashSet<String>();
76      }
77  
78      /**
79       * Walks through the View tree and indexes all components found. All components are indexed by
80       * their IDs with the special indexing done for certain components
81       * 
82       * <p>
83       * <code>DataField</code> instances are indexed by the attribute path. This is useful for
84       * retrieving the InputField based on the incoming request parameter
85       * </p>
86       * 
87       * <p>
88       * <code>CollectionGroup</code> instances are indexed by the collection path. This is useful for
89       * retrieving the CollectionGroup based on the incoming request parameter
90       * </p>
91       */
92      protected void index(View view) {
93          index = new HashMap<String, Component>();
94          dataFieldIndex = new HashMap<String, DataField>();
95          collectionsIndex = new HashMap<String, CollectionGroup>();
96          fieldPropertyEditors = new HashMap<String, PropertyEditor>();
97          secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
98  
99          indexComponent(view);
100     }
101 
102     /**
103      * Adds an entry to the main index for the given component. If the component is of type
104      * <code>DataField</code> or <code>CollectionGroup</code> an entry is created in the
105      * corresponding indexes for those types as well. Then the #indexComponent method is called for
106      * each of the component's children
107      * 
108      * <p>
109      * If the component is already contained in the indexes, it will be replaced
110      * </p>
111      * 
112      * <p>
113      * Special processing is done for DataField instances to register their property editor which
114      * will be used for form binding
115      * </p>
116      * 
117      * @param component component instance to index
118      */
119     public void indexComponent(Component component) {
120         if (component == null) {
121             return;
122         }
123 
124         synchronized (index) {
125             index.put(component.getId(), component);
126         }
127 
128         if (component instanceof DataField) {
129             DataField field = (DataField) component;
130             synchronized (dataFieldIndex) {
131                 dataFieldIndex.put(field.getBindingInfo().getBindingPath(), field);
132             }
133 
134             // pull out information we will need to support the form post
135             if (component.isRender()) {
136                 if (field.hasSecureValue()) {
137                     synchronized (secureFieldPropertyEditors) {
138                         secureFieldPropertyEditors.put(field.getBindingInfo().getBindingPath(),
139                                 field.getPropertyEditor());
140                     }
141                 } else {
142                     synchronized (fieldPropertyEditors) {
143                         fieldPropertyEditors.put(field.getBindingInfo().getBindingPath(), field.getPropertyEditor());
144                     }
145                 }
146             }
147         } else if (component instanceof CollectionGroup) {
148             CollectionGroup collectionGroup = (CollectionGroup) component;
149             synchronized (collectionsIndex) {
150                 collectionsIndex.put(collectionGroup.getBindingInfo().getBindingPath(), collectionGroup);
151             }
152         }
153 
154         for (Component nestedComponent : component.getComponentsForLifecycle()) {
155             indexComponent(nestedComponent);
156         }
157     }
158 
159     /**
160      * Invoked after the view lifecycle or component refresh has run to clear indexes that are not
161      * needed for the post
162      */
163     public void clearIndexesAfterRender() {
164         
165         // build list of factory ids for components whose initial or final state needs to be kept
166         for (Component component : index.values()) {
167             if (component == null) {
168                 continue;
169             }
170 
171             if (component.isDisableSessionPersistence()) {
172                 continue;
173             }
174 
175             if (component.isForceSessionPersistence() || canBeRefreshed(component) ||
176                     // if component is a collection we need to keep it for 
177                     // add/delete and other collection functions
178                     (component instanceof CollectionGroup)) {
179                 synchronized (idsToHoldInitialState) {
180                     idsToHoldInitialState.add(component.getBaseId());
181                 }
182                 synchronized (idsToHoldInIndex) {
183                     idsToHoldInIndex.add(component.getId());
184                 }
185             }
186             else if ((component instanceof InputField)) {
187                 InputField inputField = (InputField) component;
188 
189                 if ((inputField.getAttributeQuery() != null) || ((inputField.getSuggest() != null) && inputField
190                         .getSuggest().isRender())) {
191                     synchronized (idsToHoldInIndex) {
192                         idsToHoldInIndex.add(component.getId());
193                     }
194                 }
195             }
196         }
197 
198         // now filter the indexes to include only the components that we need (determined above)
199         Map<String, Component> holdInitialComponentStates = new HashMap<String, Component>();
200         synchronized (initialComponentStates) {
201             for (Entry<String, Component> factoryEntry : initialComponentStates.entrySet()) {
202                 if (idsToHoldInitialState.contains(factoryEntry.getKey())) {
203                     holdInitialComponentStates.put(factoryEntry.getKey(), factoryEntry.getValue());
204                 }
205             }
206         }
207         initialComponentStates = holdInitialComponentStates;
208 
209         Map<String, Component> holdComponentStates = new HashMap<String, Component>();
210         synchronized (index) {
211             for (Entry<String, Component> indexEntry : index.entrySet()) {
212                 if (idsToHoldInIndex.contains(indexEntry.getKey())) {
213                     Component component = indexEntry.getValue();
214 
215                     // hold expressions for refresh (since they could have been pushed from a parent)
216                     if ((component.getRefreshExpressionGraph() != null) && !component.getRefreshExpressionGraph()
217                             .isEmpty()) {
218                         synchronized (componentExpressionGraphs) {
219                             componentExpressionGraphs.put(component.getBaseId(), component.getRefreshExpressionGraph());
220                         }
221                     }
222 
223                     ViewCleaner.cleanComponent(component, this);
224 
225                     holdComponentStates.put(indexEntry.getKey(), component);
226                 }
227             }
228         }
229         index = holdComponentStates;
230 
231         for (CollectionGroup collectionGroup : collectionsIndex.values()) {
232             ViewCleaner.cleanComponent(collectionGroup, this);
233         }
234 
235         dataFieldIndex.clear();
236         assignedIds.clear();
237     }
238 
239     /**
240      * Indicates if the given component has configuration that it allows it to be refreshed
241      * 
242      * @param component instance to check
243      * @return true if component can be refreshed, false if not
244      */
245     protected boolean canBeRefreshed(Component component) {
246         boolean canBeRefreshed = false;
247 
248         boolean hasRefreshCondition = StringUtils.isNotBlank(component.getProgressiveRender()) ||
249                 StringUtils.isNotBlank(component.getConditionalRefresh()) || (component.getRefreshTimer() > 0) ||
250                 (component.getRefreshWhenChangedPropertyNames() != null && !component
251                         .getRefreshWhenChangedPropertyNames().isEmpty());
252 
253         canBeRefreshed = hasRefreshCondition || component.isRefreshedByAction() || component.isDisclosedByAction();
254 
255         return canBeRefreshed;
256     }
257 
258     /**
259      * Indicates whether the given component id is for a component maintained by the view index for
260      * the refresh process
261      * 
262      * @param componentId id for the component to check
263      * @return true if the component id is for a refreshed component, false if not
264      */
265     public boolean isIdForRefreshComponent(String componentId) {
266         return idsToHoldInIndex != null && idsToHoldInIndex.contains(componentId);
267     }
268 
269     /**
270      * Retrieves a <code>Component</code> from the view index by Id
271      * 
272      * @param id id for the component to retrieve
273      * @return Component instance found in index, or null if no such component exists
274      */
275     public Component getComponentById(String id) {
276         return index.get(id);
277     }
278 
279     /**
280      * Retrieves a <code>DataField</code> instance from the index
281      * 
282      * @param propertyPath full path of the data field (from the form)
283      * @return DataField instance for the path or Null if not found
284      */
285     public DataField getDataFieldByPath(String propertyPath) {
286         return dataFieldIndex.get(propertyPath);
287     }
288 
289     /**
290      * Retrieves a <code>DataField</code> instance that has the given property name specified (note
291      * this is not the full binding path and first match is returned)
292      * 
293      * @param propertyName property name for field to retrieve
294      * @return DataField instance found or null if not found
295      */
296     public DataField getDataFieldByPropertyName(String propertyName) {
297         DataField dataField = null;
298 
299         for (DataField field : dataFieldIndex.values()) {
300             if (StringUtils.equals(propertyName, field.getPropertyName())) {
301                 dataField = field;
302                 break;
303             }
304         }
305 
306         return dataField;
307     }
308 
309     /**
310      * Gets the Map that contains attribute field indexing information. The Map key points to an
311      * attribute binding path, and the Map value is the <code>DataField</code> instance
312      * 
313      * @return data fields index map
314      */
315     public Map<String, DataField> getDataFieldIndex() {
316         return this.dataFieldIndex;
317     }
318 
319     /**
320      * Gets the Map that contains collection indexing information. The Map key gives the binding
321      * path to the collection, and the Map value givens the <code>CollectionGroup</code> instance
322      * 
323      * @return collection index map
324      */
325     public Map<String, CollectionGroup> getCollectionsIndex() {
326         return this.collectionsIndex;
327     }
328 
329     /**
330      * Retrieves a <code>CollectionGroup</code> instance from the index
331      * 
332      * @param collectionPath full path of the collection (from the form)
333      * @return CollectionGroup instance for the collection path or Null if not found
334      */
335     public CollectionGroup getCollectionGroupByPath(String collectionPath) {
336         return collectionsIndex.get(collectionPath);
337     }
338 
339     /**
340      * Preserves initial state of components needed for doing component refreshes
341      * 
342      * <p>
343      * Some components, such as those that are nested or created in code cannot be requested from
344      * the spring factory to get new instances. For these a copy of the component in its initial
345      * state is set in this map which will be used when doing component refreshes (which requires
346      * running just that component's lifecycle)
347      * </p>
348      * 
349      * <p>
350      * Map entries are added during the perform initialize phase from
351      * {@link org.kuali.rice.krad.uif.service.ViewHelperService}
352      * </p>
353      * 
354      * @return map with key giving the factory id for the component and the value the component
355      *         instance
356      */
357     public Map<String, Component> getInitialComponentStates() {
358         return initialComponentStates;
359     }
360 
361     /**
362      * Adds a copy of the given component instance to the map of initial component states keyed
363      * 
364      * <p>
365      * Component is only added if its factory id is not set yet (which would happen if it had a
366      * spring bean id and we can get the state from Spring). Once added the factory id will be set
367      * to the component id
368      * </p>
369      * 
370      * @param component component instance to add
371      */
372     public void addInitialComponentStateIfNeeded(Component component) {
373         if (StringUtils.isBlank(component.getBaseId())) {
374             component.setBaseId(component.getId());
375             synchronized (initialComponentStates) {
376                 initialComponentStates.put(component.getBaseId(), ComponentUtils.copy(component));
377             }
378         }
379     }
380 
381     /**
382      * Setter for the map holding initial component states
383      * 
384      * @param initialComponentStates
385      */
386     public void setInitialComponentStates(Map<String, Component> initialComponentStates) {
387         this.initialComponentStates = initialComponentStates;
388     }
389 
390     /**
391      * Maintains configuration of properties that have been configured for the view (if render was
392      * set to true) and there corresponding PropertyEdtior (if configured)
393      * 
394      * <p>
395      * Information is pulled out of the View during the lifecycle so it can be used when a form post
396      * is done from the View. Note if a field is secure, it will be placed in the
397      * {@link #getSecureFieldPropertyEditors()} map instead
398      * </p>
399      * 
400      * @return map of property path (full) to PropertyEditor
401      */
402     public Map<String, PropertyEditor> getFieldPropertyEditors() {
403         return fieldPropertyEditors;
404     }
405 
406     /**
407      * Maintains configuration of secure properties that have been configured for the view (if
408      * render was set to true) and there corresponding PropertyEdtior (if configured)
409      * 
410      * <p>
411      * Information is pulled out of the View during the lifecycle so it can be used when a form post
412      * is done from the View. Note if a field is non-secure, it will be placed in the
413      * {@link #getFieldPropertyEditors()} map instead
414      * </p>
415      * 
416      * @return map of property path (full) to PropertyEditor
417      */
418     public Map<String, PropertyEditor> getSecureFieldPropertyEditors() {
419         return secureFieldPropertyEditors;
420     }
421 
422     /**
423      * Map of components with their associated expression graphs that will be used during the
424      * component refresh process
425      * 
426      * <p>
427      * Because expressions that impact a component being refreshed might be on a parent component, a
428      * special map needs to be held around that contains expressions that apply to the component and
429      * all its nested components. This map is populated during the initial view processing and
430      * populating of the property expressions from the initial expression graphs
431      * </p>
432      * 
433      * @return Map<String, Map<String, String>> key is component id and value is expression graph
434      *         map
435      * @see org.kuali.rice.krad.uif.util.ExpressionUtils#populatePropertyExpressionsFromGraph(org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean,
436      *      boolean)
437      */
438     public Map<String, Map<String, String>> getComponentExpressionGraphs() {
439         return componentExpressionGraphs;
440     }
441 
442     /**
443      * A map of state that is held in the session
444      * 
445      * <p>
446      * Instead of storing entire components in the session in order to retrieve information for
447      * posts, just the state that is needed can be added to this map and then retrieve on the post
448      * through the posted view's index
449      * </p>
450      * 
451      * @return Map of post context which is a map of maps. First map is keyed by component id, then
452      *         each map value gives the context for that component
453      */
454     public Map<String, Map<String, Object>> getPostContext() {
455         return postContext;
456     }
457 
458     /**
459      * Adds an entry to the post context for the given component
460      * 
461      * @param componentId id of the component the context is associated with
462      * @param entryKey key for the entry
463      * @param entryValue value for the entry
464      */
465     public void addPostContextEntry(String componentId, String entryKey, Object entryValue) {
466         Map<String, Object> componentContext = null;
467 
468         if (postContext.containsKey(componentId)) {
469             componentContext = postContext.get(componentId);
470         } else {
471             componentContext = new HashMap<String, Object>();
472             synchronized (postContext) {
473                 postContext.put(componentId, componentContext);
474             }
475         }
476 
477         synchronized (postContext) {
478             componentContext.put(entryKey, entryValue);
479         }
480     }
481     
482     /**
483      * Observe an assigned ID.
484      * 
485      * @param id The ID to observe.
486      * 
487      * @return True if the ID is unique, false if the ID has already been observed.
488      */
489     public boolean observeAssignedId(String id) {
490         if (assignedIds.contains(id)) {
491             return false;
492         }
493         
494         synchronized (assignedIds) {
495             return assignedIds.add(id);
496         }
497     }
498 
499     /**
500      * Retrieves a context entry values for the given component and entry key
501      * 
502      * @param componentId id of the component the entry is associated with
503      * @param entryKey key for the entry
504      * @return value associated with the entry, or null if entry is not found
505      */
506     public Object getPostContextEntry(String componentId, String entryKey) {
507         Object entryValue = null;
508 
509         Map<String, Object> componentContext = null;
510 
511         if (postContext.containsKey(componentId)) {
512             componentContext = postContext.get(componentId);
513 
514             entryValue = componentContext.get(entryKey);
515         }
516 
517         return entryValue;
518     }
519 
520     /**
521      * Returns a clone of the view index.
522      * 
523      * @return ViewIndex clone
524      */
525     public ViewIndex copy() {
526         ViewIndex viewIndexCopy = new ViewIndex();
527 
528         if (this.index != null) {
529             Map<String, Component> indexCopy = new HashMap<String, Component>();
530             for (Map.Entry<String, Component> indexEntry : this.index.entrySet()) {
531                 indexCopy.put(indexEntry.getKey(), (Component) indexEntry.getValue().copy());
532             }
533 
534             viewIndexCopy.index = indexCopy;
535         }
536 
537         if (this.dataFieldIndex != null) {
538             Map<String, DataField> dataFieldIndexCopy = new HashMap<String, DataField>();
539             for (Map.Entry<String, DataField> indexEntry : this.dataFieldIndex.entrySet()) {
540                 dataFieldIndexCopy.put(indexEntry.getKey(), (DataField) indexEntry.getValue().copy());
541             }
542 
543             viewIndexCopy.dataFieldIndex = dataFieldIndexCopy;
544         }
545 
546         if (this.collectionsIndex != null) {
547             Map<String, CollectionGroup> collectionsIndexCopy = new HashMap<String, CollectionGroup>();
548             for (Map.Entry<String, CollectionGroup> indexEntry : this.collectionsIndex.entrySet()) {
549                 collectionsIndexCopy.put(indexEntry.getKey(), (CollectionGroup) indexEntry.getValue().copy());
550             }
551 
552             viewIndexCopy.collectionsIndex = collectionsIndexCopy;
553         }
554 
555         if (this.initialComponentStates != null) {
556             Map<String, Component> initialComponentStatesCopy = new HashMap<String, Component>();
557             for (Map.Entry<String, Component> indexEntry : this.initialComponentStates.entrySet()) {
558                 initialComponentStatesCopy.put(indexEntry.getKey(), (Component) indexEntry.getValue().copy());
559             }
560 
561             viewIndexCopy.initialComponentStates = initialComponentStatesCopy;
562         }
563 
564         if (this.fieldPropertyEditors != null) {
565             viewIndexCopy.fieldPropertyEditors = new HashMap<String, PropertyEditor>(this.fieldPropertyEditors);
566         }
567 
568         if (this.secureFieldPropertyEditors != null) {
569             viewIndexCopy.secureFieldPropertyEditors = new HashMap<String, PropertyEditor>(
570                     this.secureFieldPropertyEditors);
571         }
572 
573         if (this.componentExpressionGraphs != null) {
574             viewIndexCopy.componentExpressionGraphs = new HashMap<String, Map<String, String>>(
575                     this.componentExpressionGraphs);
576         }
577 
578         if (this.postContext != null) {
579             viewIndexCopy.postContext = new HashMap<String, Map<String, Object>>(this.postContext);
580         }
581 
582         return viewIndexCopy;
583     }
584 
585 }