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}