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}