001 /** 002 * Copyright 2005-2013 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.rice.krad.uif.view; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.kuali.rice.krad.uif.container.CollectionGroup; 020 import org.kuali.rice.krad.uif.component.Component; 021 import org.kuali.rice.krad.uif.field.DataField; 022 import org.kuali.rice.krad.uif.field.InputField; 023 import org.kuali.rice.krad.uif.util.ComponentUtils; 024 import org.kuali.rice.krad.uif.util.ViewCleaner; 025 026 import java.beans.PropertyEditor; 027 import java.io.Serializable; 028 import java.util.HashMap; 029 import java.util.HashSet; 030 import java.util.Map; 031 import java.util.Set; 032 033 /** 034 * Holds field indexes of a <code>View</code> instance for retrieval 035 * 036 * @author Kuali Rice Team (rice.collab@kuali.org) 037 */ 038 public class ViewIndex implements Serializable { 039 private static final long serialVersionUID = 4700818801272201371L; 040 041 private Map<String, Component> index; 042 private Map<String, DataField> dataFieldIndex; 043 private Map<String, CollectionGroup> collectionsIndex; 044 045 private Map<String, Component> initialComponentStates; 046 047 private Map<String, PropertyEditor> fieldPropertyEditors; 048 private Map<String, PropertyEditor> secureFieldPropertyEditors; 049 private Map<String, Integer> idSequenceSnapshot; 050 private Map<String, Map<String, String>> componentExpressionGraphs; 051 052 /** 053 * Constructs new instance 054 */ 055 public ViewIndex() { 056 index = new HashMap<String, Component>(); 057 dataFieldIndex = new HashMap<String, DataField>(); 058 collectionsIndex = new HashMap<String, CollectionGroup>(); 059 initialComponentStates = new HashMap<String, Component>(); 060 fieldPropertyEditors = new HashMap<String, PropertyEditor>(); 061 secureFieldPropertyEditors = new HashMap<String, PropertyEditor>(); 062 idSequenceSnapshot = new HashMap<String, Integer>(); 063 componentExpressionGraphs = new HashMap<String, Map<String, String>>(); 064 } 065 066 /** 067 * Walks through the View tree and indexes all components found. All components 068 * are indexed by their IDs with the special indexing done for certain components 069 * 070 * <p> 071 * <code>DataField</code> instances are indexed by the attribute path. 072 * This is useful for retrieving the InputField based on the incoming 073 * request parameter 074 * </p> 075 * 076 * <p> 077 * <code>CollectionGroup</code> instances are indexed by the collection 078 * path. This is useful for retrieving the CollectionGroup based on the 079 * incoming request parameter 080 * </p> 081 */ 082 protected void index(View view) { 083 index = new HashMap<String, Component>(); 084 dataFieldIndex = new HashMap<String, DataField>(); 085 collectionsIndex = new HashMap<String, CollectionGroup>(); 086 fieldPropertyEditors = new HashMap<String, PropertyEditor>(); 087 secureFieldPropertyEditors = new HashMap<String, PropertyEditor>(); 088 089 indexComponent(view); 090 } 091 092 /** 093 * Adds an entry to the main index for the given component. If the component 094 * is of type <code>DataField</code> or <code>CollectionGroup</code> an 095 * entry is created in the corresponding indexes for those types as well. Then 096 * the #indexComponent method is called for each of the component's children 097 * 098 * <p> 099 * 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 }