View Javadoc

1   /**
2    * Copyright 2005-2015 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.List;
31  import java.util.Map;
32  import java.util.Set;
33  
34  /**
35   * Holds field indexes of a <code>View</code> instance for retrieval
36   *
37   * @author Kuali Rice Team (rice.collab@kuali.org)
38   */
39  public class ViewIndex implements Serializable {
40      private static final long serialVersionUID = 4700818801272201371L;
41  
42      private Map<String, Component> index;
43      private Map<String, DataField> dataFieldIndex;
44      private Map<String, CollectionGroup> collectionsIndex;
45  
46      private Map<String, Component> initialComponentStates;
47  
48      private Map<String, PropertyEditor> fieldPropertyEditors;
49      private Map<String, PropertyEditor> secureFieldPropertyEditors;
50      private Map<String, Integer> idSequenceSnapshot;
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      }
64  
65      /**
66       * Walks through the View tree and indexes all components found. All components
67       * are indexed by their IDs with the special indexing done for certain components
68       *
69       * <p>
70       * <code>DataField</code> instances are indexed by the attribute path.
71       * This is useful for retrieving the InputField based on the incoming
72       * request parameter
73       * </p>
74       *
75       * <p>
76       * <code>CollectionGroup</code> instances are indexed by the collection
77       * path. This is useful for retrieving the CollectionGroup based on the
78       * incoming request parameter
79       * </p>
80       */
81      protected void index(View view) {
82          index = new HashMap<String, Component>();
83          dataFieldIndex = new HashMap<String, DataField>();
84          collectionsIndex = new HashMap<String, CollectionGroup>();
85          fieldPropertyEditors = new HashMap<String, PropertyEditor>();
86          secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
87  
88          indexComponent(view);
89      }
90  
91      /**
92       * Adds an entry to the main index for the given component. If the component
93       * is of type <code>DataField</code> or <code>CollectionGroup</code> an
94       * entry is created in the corresponding indexes for those types as well. Then
95       * the #indexComponent method is called for each of the component's children
96       *
97       * <p>
98       * If the component is already contained in the indexes, it will be replaced
99       * </p>
100      *
101      * <p>
102      * Special processing is done for DataField instances to register their property editor which will
103      * be used for form binding
104      * </p>
105      *
106      * @param component - component instance to index
107      */
108     public void indexComponent(Component component) {
109         if (component == null) {
110             return;
111         }
112 
113         index.put(component.getId(), component);
114 
115         if (component instanceof DataField) {
116             DataField field = (DataField) component;
117             dataFieldIndex.put(field.getBindingInfo().getBindingPath(), field);
118 
119             // pull out information we will need to support the form post
120             if (component.isRender()) {
121                 if (field.hasSecureValue()) {
122                     secureFieldPropertyEditors.put(field.getBindingInfo().getBindingPath(), field.getPropertyEditor());
123                 } else {
124                     fieldPropertyEditors.put(field.getBindingInfo().getBindingPath(), field.getPropertyEditor());
125                 }
126             }
127         } else if (component instanceof CollectionGroup) {
128             CollectionGroup collectionGroup = (CollectionGroup) component;
129             collectionsIndex.put(collectionGroup.getBindingInfo().getBindingPath(), collectionGroup);
130         }
131 
132         for (Component nestedComponent : component.getComponentsForLifecycle()) {
133             indexComponent(nestedComponent);
134         }
135     }
136 
137     /**
138      * Invoked after the view lifecycle or component refresh has run to clear indexes that are not
139      * needed for the post
140      */
141     public void clearIndexesAfterRender() {
142         // build list of factory ids for components whose initial state needs to be keep
143         Set<String> holdIds = new HashSet<String>();
144         Set<String> holdFactoryIds = new HashSet<String>();
145         for (Component component : index.values()) {
146             if (component != null) {
147                 // if component has a refresh condition we need to keep it
148                 if (StringUtils.isNotBlank(component.getProgressiveRender()) || StringUtils.isNotBlank(
149                         component.getConditionalRefresh()) || StringUtils.isNotBlank(
150                         component.getRefreshWhenChanged()) || component.isRefreshedByAction()) {
151                     holdFactoryIds.add(component.getFactoryId());
152                     holdIds.add(component.getId());
153                 }
154                 // if component is marked as persist in session we need to keep it
155                 else if (component.isPersistInSession()) {
156                     holdFactoryIds.add(component.getFactoryId());
157                     holdIds.add(component.getId());
158                 }
159                 // if component is a collection we need to keep it
160                 else if (component instanceof CollectionGroup) {
161                     ViewCleaner.cleanCollectionGroup((CollectionGroup) component);
162                     holdFactoryIds.add(component.getFactoryId());
163                     holdIds.add(component.getId());
164                 }
165                 // if component is input field and has a query we need to keep the final state
166                 else if ((component instanceof InputField)) {
167                     InputField inputField = (InputField) component;
168                     if ((inputField.getFieldAttributeQuery() != null) || inputField.getFieldSuggest().isRender()) {
169                         holdIds.add(component.getId());
170                     }
171                 }
172             }
173         }
174 
175         // remove initial states for components we don't need for post
176         Map<String, Component> holdInitialComponentStates = new HashMap<String, Component>();
177         for (String factoryId : initialComponentStates.keySet()) {
178             if (holdFactoryIds.contains(factoryId)) {
179                 holdInitialComponentStates.put(factoryId, initialComponentStates.get(factoryId));
180             }
181         }
182         initialComponentStates = holdInitialComponentStates;
183 
184         // remove final states for components we don't need for post
185         Map<String, Component> holdComponentStates = new HashMap<String, Component>();
186         for (String id : index.keySet()) {
187             if (holdIds.contains(id)) {
188                 holdComponentStates.put(id, index.get(id));
189             }
190         }
191         index = holdComponentStates;
192 
193         dataFieldIndex = new HashMap<String, DataField>();
194     }
195 
196     /**
197      * Retrieves a <code>Component</code> from the view index by Id
198      *
199      * @param id - id for the component to retrieve
200      * @return Component instance found in index, or null if no such component exists
201      */
202     public Component getComponentById(String id) {
203         return index.get(id);
204     }
205 
206     /**
207      * Retrieves a <code>DataField</code> instance from the index
208      *
209      * @param propertyPath - full path of the data field (from the form)
210      * @return DataField instance for the path or Null if not found
211      */
212     public DataField getDataFieldByPath(String propertyPath) {
213         return dataFieldIndex.get(propertyPath);
214     }
215 
216     /**
217      * Retrieves a <code>DataField</code> instance that has the given property name
218      * specified (note this is not the full binding path and first match is returned)
219      *
220      * @param propertyName - property name for field to retrieve
221      * @return DataField instance found or null if not found
222      */
223     public DataField getDataFieldByPropertyName(String propertyName) {
224         DataField dataField = null;
225 
226         for (DataField field : dataFieldIndex.values()) {
227             if (StringUtils.equals(propertyName, field.getPropertyName())) {
228                 dataField = field;
229                 break;
230             }
231         }
232 
233         return dataField;
234     }
235 
236     /**
237      * Gets the Map that contains attribute field indexing information. The Map
238      * key points to an attribute binding path, and the Map value is the
239      * <code>DataField</code> instance
240      *
241      * @return Map<String, DataField> data fields index map
242      */
243     public Map<String, DataField> getDataFieldIndex() {
244         return this.dataFieldIndex;
245     }
246 
247     /**
248      * Gets the Map that contains collection indexing information. The Map key
249      * gives the binding path to the collection, and the Map value givens the
250      * <code>CollectionGroup</code> instance
251      *
252      * @return Map<String, CollectionGroup> collection index map
253      */
254     public Map<String, CollectionGroup> getCollectionsIndex() {
255         return this.collectionsIndex;
256     }
257 
258     /**
259      * Retrieves a <code>CollectionGroup</code> instance from the index
260      *
261      * @param collectionPath - full path of the collection (from the form)
262      * @return CollectionGroup instance for the collection path or Null if not
263      *         found
264      */
265     public CollectionGroup getCollectionGroupByPath(String collectionPath) {
266         return collectionsIndex.get(collectionPath);
267     }
268 
269     /**
270      * Preserves initial state of components needed for doing component refreshes
271      *
272      * <p>
273      * Some components, such as those that are nested or created in code cannot be requested from the
274      * spring factory to get new instances. For these a copy of the component in its initial state is
275      * set in this map which will be used when doing component refreshes (which requires running just that
276      * component's lifecycle)
277      * </p>
278      *
279      * <p>
280      * Map entries are added during the perform initialize phase from {@link org.kuali.rice.krad.uif.service.ViewHelperService}
281      * </p>
282      *
283      * @return Map<String, Component> - map with key giving the factory id for the component and the value the
284      *         component
285      *         instance
286      */
287     public Map<String, Component> getInitialComponentStates() {
288         return initialComponentStates;
289     }
290 
291     /**
292      * Adds a copy of the given component instance to the map of initial component states keyed
293      *
294      * <p>
295      * Component is only added if its factory id is not set yet (which would happen if it had a spring bean id
296      * and we can get the state from Spring). Once added the factory id will be set to the component id
297      * </p>
298      *
299      * @param component - component instance to add
300      */
301     public void addInitialComponentStateIfNeeded(Component component) {
302         if (StringUtils.isBlank(component.getFactoryId())) {
303             component.setFactoryId(component.getId());
304             initialComponentStates.put(component.getFactoryId(), ComponentUtils.copy(component));
305         }
306     }
307 
308     /**
309      * Setter for the map holding initial component states
310      *
311      * @param initialComponentStates
312      */
313     public void setInitialComponentStates(Map<String, Component> initialComponentStates) {
314         this.initialComponentStates = initialComponentStates;
315     }
316 
317     /**
318      * Maintains configuration of properties that have been configured for the view (if render was set to
319      * true) and there corresponding PropertyEdtior (if configured)
320      *
321      * <p>
322      * Information is pulled out of the View during the lifecycle so it can be used when a form post is done
323      * from the View. Note if a field is secure, it will be placed in the {@link #getSecureFieldPropertyEditors()} map
324      * instead
325      * </p>
326      *
327      * @return Map<String, PropertyEditor> map of property path (full) to PropertyEditor
328      */
329     public Map<String, PropertyEditor> getFieldPropertyEditors() {
330         return fieldPropertyEditors;
331     }
332 
333     /**
334      * Setter for the Map that holds view property paths to configured Property Editors (non secure fields only)
335      *
336      * @param fieldPropertyEditors
337      */
338     public void setFieldPropertyEditors(Map<String, PropertyEditor> fieldPropertyEditors) {
339         this.fieldPropertyEditors = fieldPropertyEditors;
340     }
341 
342     /**
343      * Maintains configuration of secure properties that have been configured for the view (if render was set to
344      * true) and there corresponding PropertyEdtior (if configured)
345      *
346      * <p>
347      * Information is pulled out of the View during the lifecycle so it can be used when a form post is done
348      * from the View. Note if a field is non-secure, it will be placed in the {@link #getFieldPropertyEditors()} map
349      * instead
350      * </p>
351      *
352      * @return Map<String, PropertyEditor> map of property path (full) to PropertyEditor
353      */
354     public Map<String, PropertyEditor> getSecureFieldPropertyEditors() {
355         return secureFieldPropertyEditors;
356     }
357 
358     /**
359      * Setter for the Map that holds view property paths to configured Property Editors (secure fields only)
360      *
361      * @param secureFieldPropertyEditors
362      */
363     public void setSecureFieldPropertyEditors(Map<String, PropertyEditor> secureFieldPropertyEditors) {
364         this.secureFieldPropertyEditors = secureFieldPropertyEditors;
365     }
366 
367     public Map<String, Integer> getIdSequenceSnapshot() {
368         return idSequenceSnapshot;
369     }
370 
371     public void setIdSequenceSnapshot(Map<String, Integer> idSequenceSnapshot) {
372         this.idSequenceSnapshot = idSequenceSnapshot;
373     }
374     
375     public void addSequenceValueToSnapshot(String componentId, int sequenceVal) {
376         idSequenceSnapshot.put(componentId, sequenceVal);
377     }
378 }