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.lifecycle;
017
018import org.apache.log4j.Logger;
019import org.kuali.rice.krad.uif.component.Component;
020import org.kuali.rice.krad.web.bind.UifEncryptionPropertyEditorWrapper;
021
022import java.beans.PropertyEditor;
023import java.io.Serializable;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032/**
033 * Holds data about the rendered view that might be needed to handle a post request.
034 *
035 * <p>When an action is requested on a view (for example add/delete line, field query, so on), it might be
036 * necessary to read configuration from the view that was rendered to cary out the action. However, the rendered
037 * view is not stored, and the new view is not rendered until after the controller completes. Therefore it is
038 * necessary to provide this mechanism.</p>
039 *
040 * <p>The post metadata is retrieved in the controller though the {@link org.kuali.rice.krad.web.form.UifFormBase}
041 * instance</p>
042 *
043 * @author Kuali Rice Team (rice.collab@kuali.org)
044 */
045public class ViewPostMetadata implements Serializable {
046
047    private static final long serialVersionUID = -515221881981451818L;
048    private static final Logger LOG = Logger.getLogger(ViewPostMetadata.class);
049
050    private String id;
051
052    private Map<String, ComponentPostMetadata> componentPostMetadataMap;
053
054    private Map<String, PropertyEditor> fieldPropertyEditors;
055    private Map<String, PropertyEditor> secureFieldPropertyEditors;
056
057    private Set<String> inputFieldIds;
058    private Set<String> allRenderedPropertyPaths;
059    private Map<String, List<Object>> addedCollectionObjects;
060
061    private Map<String, Map<String, Object>> lookupCriteria;
062
063    private Set<String> accessibleBindingPaths;
064    private Set<String> accessibleMethodToCalls;
065    private Set<String> availableMethodToCalls;
066
067    /**
068     * Default constructor.
069     */
070    public ViewPostMetadata() {
071        fieldPropertyEditors = Collections.synchronizedMap(new HashMap<String, PropertyEditor>());
072        secureFieldPropertyEditors = Collections.synchronizedMap(new HashMap<String, PropertyEditor>());
073        inputFieldIds = Collections.synchronizedSet(new HashSet<String>());
074        allRenderedPropertyPaths = Collections.synchronizedSet(new HashSet<String>());
075        addedCollectionObjects = Collections.synchronizedMap(new HashMap<String, List<Object>>());
076        lookupCriteria = Collections.synchronizedMap(new HashMap<String, Map<String, Object>>());
077        accessibleBindingPaths = Collections.synchronizedSet(new HashSet<String>());
078        accessibleMethodToCalls = Collections.synchronizedSet(new HashSet<String>());
079        availableMethodToCalls = Collections.synchronizedSet(new HashSet<String>());
080    }
081
082    /**
083     * Constructor that takes the view id.
084     *
085     * @param id id for the view
086     */
087    public ViewPostMetadata(String id) {
088        this();
089
090        this.id = id;
091    }
092
093    /**
094     * Invoked after the lifecycle is complete to perform an necessary cleaning.
095     */
096    public void cleanAfterLifecycle() {
097        allRenderedPropertyPaths = Collections.synchronizedSet(new HashSet<String>());
098        addedCollectionObjects = Collections.synchronizedMap(new HashMap<String, List<Object>>());
099    }
100
101    /**
102     * Id for the view the post metadata is associated with.
103     *
104     * @return view id
105     */
106    public String getId() {
107        return id;
108    }
109
110    /**
111     * @see ViewPostMetadata#getId()
112     */
113    public void setId(String id) {
114        this.id = id;
115    }
116
117    /**
118     * Map containing post metadata for a component keyed by the component id.
119     *
120     * @return post metadata map, key is component id and value is post metadata
121     */
122    public Map<String, ComponentPostMetadata> getComponentPostMetadataMap() {
123        return componentPostMetadataMap;
124    }
125
126    /**
127     * @see ViewPostMetadata#getComponentPostMetadataMap()
128     */
129    public void setComponentPostMetadataMap(Map<String, ComponentPostMetadata> componentPostMetadataMap) {
130        this.componentPostMetadataMap = componentPostMetadataMap;
131    }
132
133    /**
134     * Gets the component post metadata for the given component id.
135     *
136     * @param componentId id for the component whose post metadata should be retrieved
137     * @return post metadata object
138     */
139    public ComponentPostMetadata getComponentPostMetadata(String componentId) {
140        ComponentPostMetadata componentPostMetadata = null;
141
142        if (componentPostMetadataMap != null && (componentPostMetadataMap.containsKey(componentId))) {
143            componentPostMetadata = componentPostMetadataMap.get(componentId);
144        }
145
146        return componentPostMetadata;
147    }
148
149    /**
150     * Adds post data for the given component (this is a convenience method for add component post metadata).
151     *
152     * @param component component instance the data should be added for
153     * @param key key for the post data, this will be used to retrieve the value
154     * @param value value for the post data
155     */
156    public void addComponentPostData(Component component, String key, Object value) {
157        if (component == null) {
158            throw new IllegalArgumentException("Component must not be null for adding post data");
159        }
160
161        addComponentPostData(component.getId(), key, value);
162    }
163
164    /**
165     * Adds post data for the given component id (this is a convenience method for add component post metadata).
166     *
167     * @param componentId id for the component the data should be added for
168     * @param key key for the post data, this will be used to retrieve the value
169     * @param value value for the post data
170     */
171    public void addComponentPostData(String componentId, String key, Object value) {
172        if (value == null) {
173            return;
174        }
175
176        ComponentPostMetadata componentPostMetadata = initializeComponentPostMetadata(componentId);
177
178        componentPostMetadata.addData(key, value);
179    }
180
181    /**
182     * Retrieves post data that has been stored for the given component id and key.
183     *
184     * @param componentId id for the component the data should be retrieved for
185     * @param key key for the post data to retrieve
186     * @return value for the data, or null if the data does not exist
187     */
188    public Object getComponentPostData(String componentId, String key) {
189        ComponentPostMetadata componentPostMetadata = getComponentPostMetadata(componentId);
190
191        if (componentPostMetadata != null) {
192            return componentPostMetadata.getData(key);
193        }
194
195        return null;
196    }
197
198    /**
199     * Initializes a component post metadata instance for the given component.
200     *
201     * @param component component instance to initialize post metadata for
202     * @return post metadata instance
203     */
204    public ComponentPostMetadata initializeComponentPostMetadata(Component component) {
205        if (component == null) {
206            throw new IllegalArgumentException("Component must not be null to initialize post metadata");
207        }
208
209        return initializeComponentPostMetadata(component.getId());
210    }
211
212    /**
213     * Initializes a component post metadata instance for the given component id.
214     *
215     * @param componentId id for the component to initialize post metadata for
216     * @return post metadata instance
217     */
218    public ComponentPostMetadata initializeComponentPostMetadata(String componentId) {
219        if (componentPostMetadataMap == null) {
220            synchronized (this) {
221                if (componentPostMetadataMap == null) {
222                    componentPostMetadataMap = new HashMap<String, ComponentPostMetadata>();
223                }
224            }
225        }
226
227        ComponentPostMetadata componentPostMetadata;
228        if (componentPostMetadataMap.containsKey(componentId)) {
229            componentPostMetadata = componentPostMetadataMap.get(componentId);
230        } else {
231            synchronized (componentPostMetadataMap) {
232                componentPostMetadata = new ComponentPostMetadata(componentId);
233                componentPostMetadataMap.put(componentId, componentPostMetadata);
234            }
235        }
236
237        return componentPostMetadata;
238    }
239
240    /**
241     * Iterates over the componentPostMetadataMap to find a componentPostMetadata which matches the path given.
242     *
243     * @param path the path of the component to find componentPostMetadata for
244     * @return returns the componentPostMetadata that matches the path, null if not found or metadata for path not found
245     */
246    public ComponentPostMetadata getComponentPostMetadataForPath(String path) {
247        if (componentPostMetadataMap == null) {
248            return null;
249        }
250
251        Collection<ComponentPostMetadata> componentPostMetadataCollection = componentPostMetadataMap.values();
252
253        for (ComponentPostMetadata metadata : componentPostMetadataCollection) {
254            if (metadata.getPath() != null && metadata.getPath().equals(path)) {
255                return metadata;
256            }
257        }
258
259        return null;
260    }
261
262    /**
263     * Maintains configuration of properties that have been configured for the view (if render was
264     * set to true) and there corresponding PropertyEdtior (if configured).
265     *
266     * <p>Information is pulled out of the View during the lifecycle so it can be used when a form post
267     * is done from the View. Note if a field is secure, it will be placed in the
268     * {@link #getSecureFieldPropertyEditors()} map instead</p>
269     *
270     * @return map of property path (full) to PropertyEditor
271     */
272    public Map<String, PropertyEditor> getFieldPropertyEditors() {
273        return fieldPropertyEditors;
274    }
275
276    /**
277     * Associates a property editor instance with the given property path.
278     *
279     * @param propertyPath path for the property the editor should be associated with
280     * @param propertyEditor editor instance to use when binding data for the property
281     */
282    public void addFieldPropertyEditor(String propertyPath, PropertyEditor propertyEditor) {
283        if (fieldPropertyEditors == null) {
284            fieldPropertyEditors = new HashMap<String, PropertyEditor>();
285        }
286
287        fieldPropertyEditors.put(propertyPath, propertyEditor);
288    }
289
290    /**
291     * Maintains configuration of secure properties that have been configured for the view (if
292     * render was set to true) and there corresponding PropertyEdtior (if configured).
293     *
294     * <p>Information is pulled out of the View during the lifecycle so it can be used when a form post
295     * is done from the View. Note if a field is non-secure, it will be placed in the
296     * {@link #getFieldPropertyEditors()} map instead</p>
297     *
298     * @return map of property path (full) to PropertyEditor
299     */
300    public Map<String, PropertyEditor> getSecureFieldPropertyEditors() {
301        return secureFieldPropertyEditors;
302    }
303
304    /**
305     * Associates a secure property editor instance with the given property path.
306     *
307     * @param propertyPath path for the property the editor should be associated with
308     * @param propertyEditor secure editor instance to use when binding data for the property
309     */
310    public void addSecureFieldPropertyEditor(String propertyPath, PropertyEditor propertyEditor) {
311        if (secureFieldPropertyEditors == null) {
312            secureFieldPropertyEditors = new HashMap<String, PropertyEditor>();
313        }
314
315        secureFieldPropertyEditors.put(propertyPath, propertyEditor);
316    }
317
318    /**
319     * Set of ids for all input fields rendered with the view.
320     *
321     * @return set of id strings
322     */
323    public Set<String> getInputFieldIds() {
324        return inputFieldIds;
325    }
326
327    /**
328     * @see ViewPostMetadata#getInputFieldIds()
329     */
330    public void setInputFieldIds(Set<String> inputFieldIds) {
331        this.inputFieldIds = inputFieldIds;
332    }
333
334    /**
335     * Set of property paths that have been rendered as part of the lifecycle.
336     *
337     * <p>Note this will include all property paths (of data fields) that were rendered as part of the
338     * last full lifecycle and any component refreshes since then. It will not contain all paths of a view
339     * (which would include all pages)</p>
340     *
341     * @return set of property paths as strings
342     */
343    public Set<String> getAllRenderedPropertyPaths() {
344        return allRenderedPropertyPaths;
345    }
346
347    /**
348     * @see ViewPostMetadata#getAllRenderedPropertyPaths()
349     */
350    public void setAllRenderedPropertyPaths(Set<String> allRenderedPropertyPaths) {
351        this.allRenderedPropertyPaths = allRenderedPropertyPaths;
352    }
353
354    /**
355     * Adds a property path to the list of rendered property paths.
356     *
357     * @param propertyPath property path to add
358     * @see ViewPostMetadata#getAllRenderedPropertyPaths()
359     */
360    public void addRenderedPropertyPath(String propertyPath) {
361        if (this.allRenderedPropertyPaths == null) {
362            this.allRenderedPropertyPaths = new HashSet<String>();
363        }
364
365        this.allRenderedPropertyPaths.add(propertyPath);
366    }
367
368    /**
369     * The collection objects that were added during the current controller call, these will be emptied after
370     * the lifecycle process is run.
371     *
372     * <p>Note: If a list is empty this means that a collection had an addLine call occur and a new line must
373     * be initialized for the collection.</p>
374     *
375     * @return the collection objects that were added during the current controller call if added through a process
376     * other than the collection's own addLine call
377     *
378     * @see org.kuali.rice.krad.uif.container.CollectionGroupBase
379     */
380    public Map<String, List<Object>> getAddedCollectionObjects() {
381        return addedCollectionObjects;
382    }
383
384    /**
385     * @see ViewPostMetadata#getAddedCollectionObjects()
386     */
387    public void setAddedCollectionObjects(Map<String, List<Object>> addedCollectionObjects) {
388        this.addedCollectionObjects = addedCollectionObjects;
389    }
390
391    /**
392     * Set the metadata of the lookup criteria.
393     *
394     * <p>
395     * The lookup criteria property name is the key of the map.  The value is a map of criteria attributes as specified
396     * by {@link org.kuali.rice.krad.uif.UifConstants.LookupCriteriaPostMetadata}.  Not all criteria attribute types
397     * need to be specified.  A missing boolean attribute equals to false.
398     * </p>
399     */
400    public Map<String, Map<String, Object>> getLookupCriteria() {
401        return lookupCriteria;
402    }
403
404    /**
405     * @see ViewPostMetadata#getLookupCriteria()
406     */
407    public void setLookupCriteria(Map<String, Map<String, Object>> lookupCriteria) {
408        this.lookupCriteria = lookupCriteria;
409    }
410
411    /**
412     * Set of property paths from the view that will allow binding to (by default).
413     *
414     * <p>Used by the UIF web infrastructure to provide security during the binding process. By default, binding
415     * will only occur for properties within the view configuration (for properties that allow updating).</p>
416     *
417     * @return Set of property paths
418     */
419    public Set<String> getAccessibleBindingPaths() {
420        return accessibleBindingPaths;
421    }
422
423    /**
424     * @see ViewPostMetadata#getAccessibleBindingPaths()
425     */
426    public void setAccessibleBindingPaths(Set<String> accessibleBindingPaths) {
427        this.accessibleBindingPaths = accessibleBindingPaths;
428    }
429
430    /**
431     * Adds a path to the set of accessible binding paths.
432     *
433     * @param accessibleBindingPath path to add as accessible
434     * @see ViewPostMetadata#getAccessibleBindingPaths()
435     */
436    public void addAccessibleBindingPath(String accessibleBindingPath) {
437        if (this.accessibleBindingPaths == null) {
438            this.accessibleBindingPaths = Collections.synchronizedSet(new HashSet<String>());
439        }
440
441        this.accessibleBindingPaths.add(accessibleBindingPath);
442    }
443
444    /**
445     * Set of method to calls configured within the view that access should be allowed for.
446     *
447     * <p>Used by the UIF web infrastructure to provide security for invoking controller methods. By default,
448     * only methods within the view configuration can be called.</p>
449     *
450     * @return Set of method names
451     */
452    public Set<String> getAccessibleMethodToCalls() {
453        return accessibleMethodToCalls;
454    }
455
456    /**
457     * @see ViewPostMetadata#getAccessibleMethodToCalls()
458     */
459    public void setAccessibleMethodToCalls(Set<String> accessibleMethodToCalls) {
460        this.accessibleMethodToCalls = accessibleMethodToCalls;
461    }
462
463    /**
464     * Adds a method to the set of accessible controller methods.
465     *
466     * @param methodToCall method to add as accessible
467     * @see ViewPostMetadata#getAccessibleMethodToCalls()
468     */
469    public void addAccessibleMethodToCall(String methodToCall) {
470        if (this.accessibleMethodToCalls == null) {
471            this.accessibleMethodToCalls = Collections.synchronizedSet(new HashSet<String>());
472        }
473
474        this.accessibleMethodToCalls.add(methodToCall);
475    }
476
477    /**
478     * Set of available methods to call.
479     *
480     * <p>If a methodToCall belongs to the set of available methods to call, then binding will be allowed only if the
481     * methodToCall, either, has the @MethodAccessible notation, or, is listed as one of the accessible methods to
482     * call on the view.</p>
483     *
484     * @return Set of method names
485     */
486    public Set<String> getAvailableMethodToCalls() {
487        return availableMethodToCalls;
488    }
489
490    /**
491     * @see ViewPostMetadata#getAvailableMethodToCalls()
492     */
493    public void setAvailableMethodToCalls(Set<String> availableMethodToCalls) {
494        this.availableMethodToCalls = availableMethodToCalls;
495    }
496
497    /**
498     * Adds a method to the set of available controller methods.
499     *
500     * @param methodToCall method to add as accessible
501     * @see ViewPostMetadata#getAvailableMethodToCalls()
502     */
503    public void addAvailableMethodToCall(String methodToCall) {
504        if (this.availableMethodToCalls == null) {
505            this.availableMethodToCalls = Collections.synchronizedSet(new HashSet<String>());
506        }
507
508        this.availableMethodToCalls.add(methodToCall);
509    }
510
511    /**
512     * Look up a field editor.
513     *
514     * @param propertyName name of the property to find field and editor for
515     */
516    public PropertyEditor getFieldEditor(String propertyName) {
517        if (LOG.isDebugEnabled()) {
518            LOG.debug("Attempting to find property editor for property '" + propertyName + "'");
519        }
520
521        PropertyEditor propertyEditor = null;
522        boolean requiresEncryption = false;
523
524        if (fieldPropertyEditors != null && fieldPropertyEditors.containsKey(propertyName)) {
525            propertyEditor = fieldPropertyEditors.get(propertyName);
526        } else if (secureFieldPropertyEditors != null && secureFieldPropertyEditors.containsKey(propertyName)) {
527            propertyEditor = secureFieldPropertyEditors.get(propertyName);
528            requiresEncryption = true;
529        }
530
531        if (propertyEditor != null) {
532            if (LOG.isDebugEnabled()) {
533                LOG.debug(
534                        "Registering custom editor for property path '" + propertyName + "' and property editor class '"
535                                + propertyEditor.getClass().getName() + "'");
536            }
537
538            if (requiresEncryption) {
539                if (LOG.isDebugEnabled()) {
540                    LOG.debug("Enabling encryption for custom editor '" + propertyName +
541                            "' and property editor class '" + propertyEditor.getClass().getName() + "'");
542                }
543
544                return new UifEncryptionPropertyEditorWrapper(propertyEditor);
545            }
546        }
547
548        return propertyEditor;
549    }
550}