001/**
002 * Copyright 2005-2018 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// begin Kuali Foundation modification
017package org.kuali.rice.kns.web.struts.form.pojo;
018
019import org.apache.commons.beanutils.PropertyUtils;
020import org.apache.commons.lang.StringUtils;
021import org.apache.commons.lang.time.StopWatch;
022import org.apache.log4j.Logger;
023import org.apache.struts.action.ActionForm;
024import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
025import org.kuali.rice.core.web.format.FormatException;
026import org.kuali.rice.core.web.format.Formatter;
027import org.kuali.rice.kns.util.WebUtils;
028import org.kuali.rice.kns.web.EditablePropertiesHistoryHolder;
029import org.kuali.rice.krad.exception.ValidationException;
030import org.kuali.rice.krad.util.GlobalVariables;
031import org.kuali.rice.krad.util.KRADConstants;
032import org.kuali.rice.krad.util.ObjectUtils;
033
034import javax.servlet.http.HttpServletRequest;
035import java.lang.reflect.InvocationTargetException;
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.Comparator;
039import java.util.Enumeration;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.List;
043import java.util.Map;
044import java.util.Set;
045
046/**
047 * This class is the base form which implements the PojoForm interface.
048 *
049 * @deprecated KNS Struts deprecated, use KRAD and the Spring MVC framework.
050 * Kuali Foundation modification: javadoc comments changed
051 */
052// begin Kuali Foundation modification: this class was named SLActionForm
053@Deprecated
054public class PojoFormBase extends ActionForm implements PojoForm {
055    private static final long serialVersionUID = 1L;
056    
057    // begin Kuali Foundation modification
058    private static final Logger LOG = Logger.getLogger(PojoFormBase.class);
059    
060    private static final String PREVIOUS_REQUEST_EDITABLE_PROPERTIES_GUID = "editablePropertiesGuid";
061
062    /**
063     * Used only in the case that no other parameters have been defined for the max file upload size.
064     */
065    private static final String DEFAULT_MAX_FILE_UPLOAD_SIZE = "250M";
066
067        // removed member variables: cachedActionErrors, coder, errorInfo, fieldOrder, formConfig, HEADING_KEY, IGNORED_KEYS,
068        //     invalidValueKeys, logger, messageResourceKey, messageResources, padNonRequiredFields, valueBinder
069         
070    static final String CREATE_ERR_MSG = "Can't create formatter for keypath ";
071    static final String CONVERT_ERR_MSG = "Can't convert value for keypath: ";
072
073    static Map classCache = Collections.synchronizedMap(new HashMap());
074
075    private Map unconvertedValues = new HashMap();
076    private List unknownKeys = new ArrayList();
077    private Map formatterTypes = new HashMap();
078    private List<String> maxUploadFileSizes = new ArrayList<String>();
079    private Set<String> editableProperties = new HashSet<String>();
080    protected Set<String> requiredNonEditableProperties = new HashSet<String>();
081    private String strutsActionMappingScope; 
082    private boolean isNewForm = true;
083    
084    private String populateEditablePropertiesGuid;
085    private String actionEditablePropertiesGuid;
086
087    // removed methods: PojoFormBase()/SLActionForm(), addFormLevelMessageInfo, addGlobalMessage, addIgnoredKey, addIgnoredKeys, addLengthValidation, addMessageIfAbsent
088    //     addPatternValidation, addPropertyValidationRules, addRangeValidation, addRequiredField, addRequiredFields
089    //     addUnknownKey, addValidationRule(String, ValidationRule), addValidationRule(ValidationRule), cachedActionErrors, clearIgnoredKeys,
090    //     clearUnknownKeys, clearValidationErrors, coalesceMessageArgs, containsKey, convertValue, createActionMessage, createMessageResourcesIfNecessary, fieldOrder, fieldValidationRuleOrder,
091    //     formatMessage, formatMessageArgs, formatterSettingsForKeypath, formatterTypeForKeypath, formBeanConfigForKey, formConfig, formValidationRuleOrder,
092    //     generateErrorMessages, getActionErrors, getActionMessages, getErrorMessages, getFieldLabel, getFormatterTypes, getGlobalMessages, getIgnoredKeys, getInvalidValueKeys, getLabels, getLengthValidations, getLocale,
093    //     getMultipartRequestParameters, getPadNonRequiredFields, 
094    //     getPatternValidations, getPropertyConfig, getRangeValidations, getRequiredFields, hasErrorMessageForKey, hasErrors, hasFormatterForKeypath,
095    //     hasGlobalMessageForKey, isMultipart, messageForKey, messageForRule, messageInfoForRule, messageResourcesConfigForKey, messageResourcesKey, messageResourcesPath,
096    //     messagesForFormLevelRule, messagesForKey, moduleConfigForRequest, removeIgnoredKey, removePropertyConfig,
097    //     renderErrorMessages, renderGlobalMessages, renderMessages, setFieldLabel, setFieldOrder, setFormatterType(String, Class, Map)
098    //     setFormConfig, setInvalidValueKeys, setLengthValidations, setMessageResourceKey,setPadNonRequiredFields, setPatternValidations, setPropertyConfig, setRangeValidations,
099    //     setRequiredFields, setValueBinder, shouldFormat, validate, validateForm, validateLength, validatePattern, validateProperty, validateRange, validateRequestValues, validateRequired, valueBinder
100
101        // end Kuali Foundation modification
102        
103
104        // begin Kuali Foundation modification
105    /**
106     * Method is called after parameters from a multipart request have been made accessible to request.getParameter calls, but
107     * before request parameter values are used to instantiate and populate business objects. Important note: parameters in the
108     * given Map which were created from a multipart-encoded parameter will, apparently, be stored in the given Map as String[]
109     * instead of as String.
110     *
111     * @param requestParameters
112     */
113
114    @Override
115        public void postprocessRequestParameters(Map requestParameters) {
116        // do nothing
117    }
118    // end Kuali Foundation modification
119
120
121    private static final String WATCH_NAME = "PojoFormBase.populate";
122
123    /**
124     * Populates the form with values from the current request. Uses instances of Formatter to convert strings to the Java types of
125     * the properties to which they are bound. Values that can't be converted are cached in a map of unconverted values. Returns an
126     * ActionErrors containing ActionMessage instances for each conversion error that occured, if any.
127     */
128    @Override
129        public void populate(HttpServletRequest request) {
130
131        StopWatch watch = null;
132        if (LOG.isDebugEnabled()) {
133            watch = new StopWatch();
134            watch.start();
135            LOG.debug(WATCH_NAME + ": started");
136        }
137        unconvertedValues.clear();
138        unknownKeys = new ArrayList();
139        addRequiredNonEditableProperties();
140        Map params = request.getParameterMap();
141
142        String contentType = request.getContentType();
143        String method = request.getMethod();
144
145        if ("POST".equalsIgnoreCase(method) && contentType != null && contentType.startsWith("multipart/form-data")) {
146            Map fileElements = (HashMap)request.getAttribute(KRADConstants.UPLOADED_FILE_REQUEST_ATTRIBUTE_KEY);
147            Enumeration names = Collections.enumeration(fileElements.keySet());
148            while (names.hasMoreElements()) {
149                String name = (String) names.nextElement();
150                params.put(name, fileElements.get(name));
151            }
152        }
153
154        postprocessRequestParameters(params);
155
156
157        /**
158         * Iterate through request parameters, if parameter matches a form variable, get the property type, formatter and convert,
159         * if not add to the unknowKeys map.
160         */
161        Comparator<String> nestedPathComparator = new Comparator<String>() {
162            public int compare(String prop1, String prop2) {
163                Integer i1 =  new Integer(prop1.split("\\.").length);
164                Integer i2 =  new Integer(prop2.split("\\.").length);
165                return (i1.compareTo(i2));
166            }
167        };
168
169
170        List<String> pathKeyList = new ArrayList<String>(params.keySet());
171        Collections.sort( pathKeyList , nestedPathComparator);
172
173        for (String keypath : pathKeyList) {
174            if (shouldPropertyBePopulatedInForm(keypath, request)) {
175                    Object param = params.get(keypath);
176                    //LOG.debug("(keypath,paramType)=(" + keypath + "," + param.getClass().getName() + ")");
177        
178                    populateForProperty(keypath, param, params);
179            }
180        }
181        this.registerIsNewForm(false);
182        if (LOG.isDebugEnabled()) {
183            watch.stop();
184            LOG.debug(WATCH_NAME + ": " + watch.toString());
185        }
186    }
187
188
189
190        /**
191         * Populates a given parameter value into the given property path
192         * @param paramPath the path to a property within the form
193         * @param paramValue the value of that property
194         * @param params the Map of parameters from the request
195         */
196        public void populateForProperty(String paramPath, Object paramValue,
197                        Map params) {
198                // get type for property
199                Class type = null;
200                try {
201                    // TODO: see KULOWF-194
202                    //testForPojoHack(this, keypath);
203                    type = getPropertyType(paramPath);
204                }
205                catch (Exception e) {
206                    // deleted redundant unknownKeys.add(keypath)
207                }
208
209                // keypath does not match anything on form
210                if (type == null) {
211                    unknownKeys.add(paramPath);
212                }
213                else {
214                    Formatter formatter = null;
215                    try {
216                        formatter = buildFormatter(paramPath, type, params);
217
218                        ObjectUtils.setObjectProperty(formatter, this, paramPath, type, paramValue);
219                        }
220                    catch (FormatException e1) {
221                        GlobalVariables.getMessageMap().putError(paramPath, e1.getErrorKey(), e1.getErrorArgs());
222                        cacheUnconvertedValue(paramPath, paramValue);
223                    }
224                    catch (InvocationTargetException e1) {
225                        if (e1.getTargetException().getClass().equals(FormatException.class)) {
226                            // Handle occasional case where FormatException is wrapped in an InvocationTargetException
227                            FormatException formatException = (FormatException) e1.getTargetException();
228                            GlobalVariables.getMessageMap().putError(paramPath, formatException.getErrorKey(), formatException.getErrorArgs());
229                            cacheUnconvertedValue(paramPath, paramValue);
230                        }
231                        else {
232                            LOG.error("Error occurred in populate " + e1.getMessage());
233                            throw new RuntimeException(e1.getMessage(), e1);
234                        }
235                    }
236                    catch (Exception e1) {
237                        LOG.error("Error occurred in populate " + e1.getMessage());
238                        LOG.error("FormClass:       " + this.getClass().getName() );
239                        LOG.error("keypath:         " + paramPath );
240                        LOG.error("Detected Type:   " + type.getName() );
241                        LOG.error( "Value:          " + paramValue );
242                        if ( paramValue != null ) {
243                                        LOG.error( "Value Class:    " + paramValue.getClass().getName() );
244                        }
245                        throw new RuntimeException(e1.getMessage(), e1);
246                    }
247                }
248        }
249
250        // begin Kuali Foundation modification
251    private Formatter buildFormatter(String keypath, Class propertyType, Map requestParams) {
252        Formatter formatter = buildFormatterForKeypath(keypath, propertyType, requestParams);
253        if (formatter == null) {
254            formatter = buildFormatterForType(propertyType);
255        }
256        return formatter;
257    }
258    // end Kuali Foundation modification
259
260        // begin Kuali Foundation modification
261    private Formatter buildFormatterForKeypath(String keypath, Class propertyType, Map requestParams) {
262        Formatter formatter = null;
263
264        Class formatterClass = formatterClassForKeypath(keypath);
265
266        if (formatterClass != null) {
267            try {
268                formatter = (Formatter) formatterClass.newInstance();
269            }
270            catch (InstantiationException e) {
271                throw new FormatException("unable to instantiate formatter class '" + formatterClass.getName() + "'", e);
272            }
273            catch (IllegalAccessException e) {
274                throw new FormatException("unable to access formatter class '" + formatterClass.getName() + "'", e);
275            }
276            formatter.setPropertyType(propertyType);
277        }
278        return formatter;
279    }
280    // end Kuali Foundation modification
281
282        // begin Kuali Foundation modification
283    private Formatter buildFormatterForType(Class propertyType) {
284        Formatter formatter = null;
285
286        if (Formatter.findFormatter(propertyType) != null) {
287            formatter = Formatter.getFormatter(propertyType);
288        }
289        return formatter;
290    }
291    // end Kuali Foundation modification
292
293        /**
294     * Delegates to {@link PropertyUtils#getPropertyType(Object, String)}to look up the property type for the provided keypath.
295     * Caches the resulting class so that subsequent lookups for the same keypath can be satisfied by looking in the cache.
296     *
297     * @throws NoSuchMethodException
298     * @throws InvocationTargetException
299     * @throws IllegalAccessException
300     */
301    protected Class getPropertyType(String keypath) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
302        Map propertyTypes = (Map) classCache.get(getClass());
303        if (propertyTypes == null) {
304            propertyTypes = new HashMap();
305            classCache.put(getClass(), propertyTypes);
306        }
307
308        // if type has not been retrieve previousely, use ObjectUtils to get type
309        if (!propertyTypes.containsKey(keypath)) {
310            Class type = ObjectUtils.easyGetPropertyType(this, keypath);
311            propertyTypes.put(keypath, type);
312        }
313
314        Class propertyType = (Class) propertyTypes.get(keypath);
315        return propertyType;
316    }
317
318
319    /**
320     * Retrieves a formatter for the keypath and property type.
321     *
322     * @param keypath
323     * @param propertyType
324     * @return
325     */
326    protected Formatter getFormatter(String keypath, Class propertyType) {
327        // check for a formatter associated with the keypath
328        Class type = formatterClassForKeypath(keypath);
329
330        Formatter formatter;
331        if (type == null) {
332            // retrieve formatter based on property type
333            formatter = Formatter.getFormatter(propertyType);
334        }
335        else {
336            try {
337                formatter = (Formatter) type.newInstance();
338                formatter.setPropertyType(propertyType);
339            }
340            catch (Exception e) {
341                throw new ValidationException(CREATE_ERR_MSG, e);
342            }
343        }
344        return formatter;
345    }
346
347
348        // begin Kuali Foundation modification
349    /**
350     * Retrieves any formatters associated specially with the keypath.
351     *
352     * @param keypath
353     * @return
354     */
355    protected Class formatterClassForKeypath(String keypath) {
356        // remove traces of array and map indices from the incoming keypath
357        String indexlessKey = keypath.replaceAll("(\\[[0-9]*+\\]|\\(.*?\\))", "");
358
359        return (Class)formatterTypes.get( indexlessKey );
360    }
361    // end Kuali Foundation modification
362
363    /**
364     * Tries to format the provided value by passing it to a suitable {@link Formatter}. Adds an ActionMessage to the ActionErrors
365     * in the request if a FormatException is thrown.
366     * <p>
367     * Caution should be used when invoking this method. It should never be called prior to {@link #populate(HttpServletRequest)}
368     * because the cached request reference could be stale.
369     */
370    @Override
371        public Object formatValue(Object value, String keypath, Class type) {
372
373        Formatter formatter = getFormatter(keypath, type);
374        if ( LOG.isDebugEnabled() ) {
375            LOG.debug("formatValue (value,keypath,type) = (" + value + "," + keypath + "," + type.getName() + ")");
376        }
377
378        try {
379            return Formatter.isSupportedType(type) ? formatter.formatForPresentation(value) : value;
380        }
381        catch (FormatException e) {
382            GlobalVariables.getMessageMap().putError(keypath, e.getErrorKey(), e.getErrorArgs());
383            return value.toString();
384        }
385    }
386
387    /**
388     * Sets the Formatter class to use for a given keypath. This class will be used by the form instead of the one returned by calls
389     * to {@link Formatter#getFormatter(Class)}, which is the default mechanism.
390     */
391    public void setFormatterType(String keypath, Class type) {
392        formatterTypes.put(keypath, type);
393    }
394
395    @Override
396        public Map getUnconvertedValues() {
397        return unconvertedValues;
398    }
399
400    public void setUnconvertedValues(Map unconvertedValues) {
401        this.unconvertedValues = unconvertedValues;
402    }
403
404    protected List getUnknownKeys() {
405        return unknownKeys;
406    }
407
408    protected void cacheUnconvertedValue(String key, Object value) {
409        Class type = value.getClass();
410        if (type.isArray()) {
411            value = Formatter.isEmptyValue(value) ? null : ((Object[]) value)[0];
412        }
413
414        unconvertedValues.put(key, value);
415    }
416
417        // begin Kuali Foundation modification
418    @Override
419        public void processValidationFail() {
420        // do nothing - subclasses can implement this if they want to.
421    }
422    // end Kuali Foundation modification
423
424
425        // begin Kuali Foundation modification
426    /**
427     * Gets the formatterTypes attribute.
428     * 
429     * @return Returns the formatterTypes.
430     */
431    public Map getFormatterTypes() {
432        return formatterTypes;
433    }
434    // end Kuali Foundation modification
435
436
437        // begin Kuali Foundation modification
438    /**
439     * Sets the formatterTypes attribute value.
440     * @param formatterTypes The formatterTypes to set.
441     */
442    public void setFormatterTypes(Map formatterTypes) {
443        this.formatterTypes = formatterTypes;
444    }
445    // end Kuali Foundation modification
446
447
448        // begin Kuali Foundation modification
449    /**
450     * Adds the given string as a maximum size to the form.  It will be used if a file upload is used.
451     * 
452     * @param sizeString
453     */
454    protected final void addMaxUploadSize( String sizeString ) {
455        maxUploadFileSizes.add( sizeString );
456    }
457
458    /**
459     * Initializes the list of max upload sizes if necessary. 
460     *
461     */
462    protected final void initMaxUploadSizes() {
463        if ( maxUploadFileSizes.isEmpty() ) {
464            customInitMaxUploadSizes();
465            // if it's still empty, add the default
466            if ( maxUploadFileSizes.isEmpty() ) {
467                String systemDefault = CoreFrameworkServiceLocator.getParameterService().getParameterValueAsString(KRADConstants.KNS_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KRADConstants.MAX_UPLOAD_SIZE_PARM_NM);
468                if (StringUtils.isBlank(systemDefault)) {
469                    LOG.error("System parameter " + KRADConstants.KNS_NAMESPACE + ":" + KRADConstants.DetailTypes.ALL_DETAIL_TYPE + ":" + KRADConstants.MAX_UPLOAD_SIZE_PARM_NM + " not defined, using hardcoded default max file upload size");
470                    systemDefault = DEFAULT_MAX_FILE_UPLOAD_SIZE;
471                }
472                addMaxUploadSize(systemDefault);
473            }
474        }       
475    }
476    
477    /**
478     * Subclasses can override this to add their own max upload size to the list.  Only the largest passed will be used.
479     *
480     */
481    protected void customInitMaxUploadSizes() {
482        // nothing here
483    }
484    
485    public final List<String> getMaxUploadSizes() {
486        initMaxUploadSizes();
487        
488        return maxUploadFileSizes;
489    }
490    
491    @Override
492        public void registerEditableProperty(String editablePropertyName){
493        if ( LOG.isDebugEnabled() ) {
494                LOG.debug( "KualiSessionId: " + GlobalVariables.getUserSession().getKualiSessionId() + " -- Registering Property: " + editablePropertyName );
495        }
496        editableProperties.add(editablePropertyName);
497    }
498    
499    public void registerRequiredNonEditableProperty(String requiredNonEditableProperty) {
500        requiredNonEditableProperties.add(requiredNonEditableProperty);
501    }
502    
503    @Override
504        public void clearEditablePropertyInformation(){
505        if ( LOG.isDebugEnabled() ) {
506                LOG.debug( "KualiSessionId: " + GlobalVariables.getUserSession().getKualiSessionId() + " -- Clearing Editable Properties" );
507        }
508        editableProperties = new HashSet<String>();
509    }
510    
511    @Override
512        public Set<String> getEditableProperties(){
513        return editableProperties;
514    }
515 
516    public boolean isPropertyEditable(String propertyName) {
517        final Set<String> populateEditableProperties = getPopulateEditableProperties();
518        return WebUtils.isPropertyEditable(populateEditableProperties, propertyName);
519    }
520    
521    /***
522     * @see PojoForm#addRequiredNonEditableProperties()
523     */
524    @Override
525        public void addRequiredNonEditableProperties(){
526    }
527    
528    public boolean isPropertyNonEditableButRequired(String propertyName) {
529        return WebUtils.isPropertyEditable(requiredNonEditableProperties, propertyName);
530    }
531    
532    protected String getParameter(HttpServletRequest request, String parameterName){
533        return request.getParameter(parameterName);
534    }
535    
536    protected String[] getParameterValues(HttpServletRequest request, String parameterName){
537        return request.getParameterValues(parameterName);
538    }
539    
540    @Override
541        public Set<String> getRequiredNonEditableProperties(){
542        return requiredNonEditableProperties;
543    }
544    
545        /**
546         * @see PojoForm#registerStrutsActionMappingScope(String)
547         */
548        @Override
549        public void registerStrutsActionMappingScope(String strutsActionMappingScope) {
550                this.strutsActionMappingScope = strutsActionMappingScope;
551        }
552        
553        public String getStrutsActionMappingScope() {
554                return strutsActionMappingScope;
555        }
556        
557        /**
558         * @see PojoForm#registerStrutsActionMappingScope(String)
559         */
560        @Override
561        public void registerIsNewForm(boolean isNewForm) {
562                this.isNewForm = isNewForm;
563        }
564        
565        @Override
566        public boolean getIsNewForm() {
567                return this.isNewForm;
568        }
569        
570        
571        /**
572         * @see PojoForm#shouldPropertyBePopulatedInForm(java.lang.String, javax.servlet.http.HttpServletRequest)
573         */
574        @Override
575        public boolean shouldPropertyBePopulatedInForm(String requestParameterName, HttpServletRequest request) {
576                
577                if (requestParameterName.equals(PojoFormBase.PREVIOUS_REQUEST_EDITABLE_PROPERTIES_GUID)) {
578                        return false; // don't repopulate this
579                }
580                else if (StringUtils.equalsIgnoreCase("session",getStrutsActionMappingScope()) && !getIsNewForm()) {
581                        return isPropertyEditable(requestParameterName) || isPropertyNonEditableButRequired(requestParameterName);
582                }
583                return true;
584                
585        }
586
587        /**
588         * Base implementation that returns just "start".  sub-implementations should not add values to Set instance returned
589         * by this method, and should create its own instance.
590         * 
591         * @see PojoForm#getMethodToCallsToBypassSessionRetrievalForGETRequests()
592         */
593        @Override
594        public Set<String> getMethodToCallsToBypassSessionRetrievalForGETRequests() {
595                Set<String> defaultMethodToCalls = new HashSet<String>();
596                defaultMethodToCalls.add(KRADConstants.START_METHOD);
597                return defaultMethodToCalls;
598        }
599
600
601
602        /**
603         * Sets the guid to editable properties consulted during population
604         * 
605         */
606        @Override
607        public void setPopulateEditablePropertiesGuid(String guid) {
608                this.populateEditablePropertiesGuid = guid;
609        }
610        
611        /**
612         * @return the guid for the populate editable properties
613         */
614        public String getPopulateEditablePropertiesGuid() {
615                return this.populateEditablePropertiesGuid;
616        }
617        
618        /**
619         * Sets the guid of the editable properties which were registered by the action
620         * @see PojoForm#setActionEditablePropertiesGuid(java.lang.String)
621         */
622        @Override
623        public void setActionEditablePropertiesGuid(String guid) {
624                this.actionEditablePropertiesGuid = guid;
625        }
626        
627        /**
628         * @return the guid of the editable properties which had been registered by the action processing
629         */
630        public String getActionEditablePropertiesGuid() {
631                return actionEditablePropertiesGuid;
632        }
633        
634        /**
635         * @return the editable properties to be consulted during population
636         */
637        public Set<String> getPopulateEditableProperties() {
638                EditablePropertiesHistoryHolder holder = (EditablePropertiesHistoryHolder) GlobalVariables.getUserSession().getObjectMap().get(
639                KRADConstants.EDITABLE_PROPERTIES_HISTORY_HOLDER_ATTR_NAME);
640            if (holder == null) {
641                holder = new EditablePropertiesHistoryHolder();
642            }
643            GlobalVariables.getUserSession().addObject(KRADConstants.EDITABLE_PROPERTIES_HISTORY_HOLDER_ATTR_NAME, holder);
644                
645                return holder.getEditableProperties(getPopulateEditablePropertiesGuid());
646        }
647        
648        /**
649         * Copies all editable properties in the populate editable properties to the action editable properties
650         */
651        public void copyPopulateEditablePropertiesToActionEditableProperties() {
652                Set<String> populateEditableProperties = getPopulateEditableProperties();
653                for (String property : populateEditableProperties) {
654                        registerEditableProperty(property);
655                }
656        }
657        
658        // end Kuali Foundation modification
659}