1 /**
2 * Copyright 2005-2012 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 }