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 }