001/** 002 * Copyright 2005-2016 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 */ 016package org.kuali.rice.krad.uif.view; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.krad.uif.container.CollectionGroup; 020import org.kuali.rice.krad.uif.component.Component; 021import org.kuali.rice.krad.uif.field.DataField; 022import org.kuali.rice.krad.uif.field.InputField; 023import org.kuali.rice.krad.uif.util.ComponentUtils; 024import org.kuali.rice.krad.uif.util.ViewCleaner; 025 026import java.beans.PropertyEditor; 027import java.io.Serializable; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033 034/** 035 * Holds field indexes of a <code>View</code> instance for retrieval 036 * 037 * @author Kuali Rice Team (rice.collab@kuali.org) 038 */ 039public class ViewIndex implements Serializable { 040 private static final long serialVersionUID = 4700818801272201371L; 041 042 private Map<String, Component> index; 043 private Map<String, DataField> dataFieldIndex; 044 private Map<String, CollectionGroup> collectionsIndex; 045 046 private Map<String, Component> initialComponentStates; 047 048 private Map<String, PropertyEditor> fieldPropertyEditors; 049 private Map<String, PropertyEditor> secureFieldPropertyEditors; 050 private Map<String, Integer> idSequenceSnapshot; 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 } 064 065 /** 066 * Walks through the View tree and indexes all components found. All components 067 * are indexed by their IDs with the special indexing done for certain components 068 * 069 * <p> 070 * <code>DataField</code> instances are indexed by the attribute path. 071 * This is useful for retrieving the InputField based on the incoming 072 * request parameter 073 * </p> 074 * 075 * <p> 076 * <code>CollectionGroup</code> instances are indexed by the collection 077 * path. This is useful for retrieving the CollectionGroup based on the 078 * incoming request parameter 079 * </p> 080 */ 081 protected void index(View view) { 082 index = new HashMap<String, Component>(); 083 dataFieldIndex = new HashMap<String, DataField>(); 084 collectionsIndex = new HashMap<String, CollectionGroup>(); 085 fieldPropertyEditors = new HashMap<String, PropertyEditor>(); 086 secureFieldPropertyEditors = new HashMap<String, PropertyEditor>(); 087 088 indexComponent(view); 089 } 090 091 /** 092 * Adds an entry to the main index for the given component. If the component 093 * is of type <code>DataField</code> or <code>CollectionGroup</code> an 094 * entry is created in the corresponding indexes for those types as well. Then 095 * the #indexComponent method is called for each of the component's children 096 * 097 * <p> 098 * If the component is already contained in the indexes, it will be replaced 099 * </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}