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/* 017* Copyright 2006-2012 The Kuali Foundation 018* 019* Licensed under the Educational Community License, Version 2.0 (the "License"); 020* you may not use this file except in compliance with the License. 021* You may obtain a copy of the License at 022* 023* http://www.opensource.org/licenses/ecl2.php 024* 025* Unless required by applicable law or agreed to in writing, software 026* distributed under the License is distributed on an "AS IS" BASIS, 027* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 028* See the License for the specific language governing permissions and 029* limitations under the License. 030*/ 031package org.kuali.rice.krad.datadictionary.validation; 032 033import org.apache.commons.beanutils.PropertyUtils; 034import org.apache.commons.collections.CollectionUtils; 035import org.apache.commons.lang.StringUtils; 036import org.kuali.rice.core.api.CoreApiServiceLocator; 037import org.kuali.rice.core.api.config.property.ConfigurationService; 038import org.kuali.rice.core.api.exception.RiceIllegalArgumentException; 039import org.kuali.rice.core.api.uif.RemotableAttributeError; 040import org.kuali.rice.core.api.uif.RemotableAttributeField; 041import org.kuali.rice.core.api.util.RiceKeyConstants; 042import org.kuali.rice.core.api.util.Truth; 043import org.kuali.rice.core.api.util.type.TypeUtils; 044import org.kuali.rice.core.web.format.Formatter; 045import org.kuali.rice.krad.bo.BusinessObject; 046import org.kuali.rice.krad.data.DataObjectWrapper; 047import org.kuali.rice.krad.data.KradDataServiceLocator; 048import org.kuali.rice.krad.datadictionary.PrimitiveAttributeDefinition; 049import org.kuali.rice.krad.datadictionary.RelationshipDefinition; 050import org.kuali.rice.krad.service.DataDictionaryRemoteFieldService; 051import org.kuali.rice.krad.service.DataDictionaryService; 052import org.kuali.rice.krad.service.DictionaryValidationService; 053import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 054import org.kuali.rice.krad.util.ErrorMessage; 055import org.kuali.rice.krad.util.GlobalVariables; 056import org.kuali.rice.krad.util.KRADUtils; 057 058import java.beans.PropertyDescriptor; 059import java.text.MessageFormat; 060import java.util.ArrayList; 061import java.util.Collections; 062import java.util.HashMap; 063import java.util.List; 064import java.util.Map; 065import java.util.regex.Pattern; 066 067/** 068 * <p>An abstract base class for type service implementations which provides default validation of attributes from the Data 069 * Dictionary. It attempts to remain module independent by requiring the translation of the attribute definitions to a 070 * generic format that includes the required {@link RemotableAttributeField}s as an unimplemented template method, 071 * see{@link #getTypeAttributeDefinitions(String)}. 072 * </p> 073 * <p>Note that any {@link RemotableAttributeError}s returned from here should be fully resolved to the messages to be 074 * displayed to the user (in other words, they should not contain error keys). <b>The same approach should be taken by 075 * subclasses since the message resources may not be present on the remote server that is invoking this service</b>. 076 * There is a {@link #createErrorString(String, String...)} utility method that can be used to resolve 077 * errorKeys and format them appropriately.</p> 078 * 079 * @author Kuali Rice Team (rice.collab@kuali.org) 080 */ 081public abstract class AttributeValidatingTypeServiceBase { 082 083 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AttributeValidatingTypeServiceBase.class); 084 private static final String ANY_CHAR_PATTERN_S = ".*"; 085 private static final Pattern ANY_CHAR_PATTERN = Pattern.compile(ANY_CHAR_PATTERN_S); 086 087 private DictionaryValidationService dictionaryValidationService; 088 private DataDictionaryService dataDictionaryService; 089 private DataDictionaryRemoteFieldService dataDictionaryRemoteFieldService; 090 091 /** 092 * Retrieves active type attribute definitions and translates them into a module-independent representation. Note 093 * that they should be returned in the order desired for display. 094 * 095 * @param typeId the identifier for the type 096 * @return a correctly ordered List of active, module-independent type attribute definitions 097 */ 098 protected abstract List<TypeAttributeDefinition> getTypeAttributeDefinitions(String typeId); 099 100 /** 101 * Validates an attribute that is *not* mapped to a data dictionary component via 102 * {@link TypeAttributeDefinition#componentName} and {@link TypeAttributeDefinition#name}. 103 * 104 * @param attr the RemotableAttributeField for which to validate. 105 * @param key the attribute name 106 * @param value the attribute value 107 * @return a List of {@link RemotableAttributeError}s with fully resolved error messages (not error keys). May 108 * return null or an empty List if no errors are encountered. 109 */ 110 protected abstract List<RemotableAttributeError> 111 validateNonDataDictionaryAttribute(RemotableAttributeField attr, String key, String value); 112 113 114 115 /** 116 * <p>This is the default implementation. It calls into the service for each attribute to 117 * validate it there. No combination validation is done. That should be done 118 * by overriding this method.</p> 119 * <p>This implementation calls {@link #getTypeAttributeDefinitions(String)} to retrieve module-agnostic 120 * representations. It then iterates through the entry set of attributes, and calls 121 * {@link #validateNonDataDictionaryAttribute(org.kuali.rice.core.api.uif.RemotableAttributeField, String, String)} 122 * or {@link #validateDataDictionaryAttribute(AttributeValidatingTypeServiceBase.TypeAttributeDefinition, String, String)} 123 * as appropriate. Lastly it calls {@link #validateReferencesExistAndActive(java.util.Map, java.util.Map, java.util.List)}. 124 * </p> 125 * 126 * @param typeId the identifier for the type 127 * @param attributes the Map of attribute names to values 128 * @return the List of errors ({@link RemotableAttributeError}s) encountered during validation. 129 */ 130 public List<RemotableAttributeError> validateAttributes(String typeId, Map<String, String> attributes) { 131 132 if (StringUtils.isBlank(typeId)) { 133 throw new RiceIllegalArgumentException("typeId was null or blank"); 134 } 135 136 if (attributes == null) { 137 throw new RiceIllegalArgumentException("attributes was null or blank"); 138 } 139 140 List<TypeAttributeDefinition> definitions = getTypeAttributeDefinitions(typeId); 141 Map<String, TypeAttributeDefinition> typeAttributeDefinitionMap = 142 buildTypeAttributeDefinitionMapByName(definitions); 143 144 final List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>(); 145 146 for ( Map.Entry<String, String> entry : attributes.entrySet() ) { 147 148 TypeAttributeDefinition typeAttributeDefinition = typeAttributeDefinitionMap.get(entry.getKey()); 149 150 final List<RemotableAttributeError> attributeErrors; 151 if (typeAttributeDefinition != null) { 152 if (typeAttributeDefinition.getComponentName() == null) { 153 attributeErrors = validateNonDataDictionaryAttribute(typeAttributeDefinition.getField(), entry.getKey(), entry.getValue()); 154 } else { 155 attributeErrors = validateDataDictionaryAttribute(typeAttributeDefinition, entry.getKey(), entry.getValue()); 156 } 157 158 if ( attributeErrors != null ) { 159 validationErrors.addAll(attributeErrors); 160 } 161 } 162 } 163 164 165 final List<RemotableAttributeError> referenceCheckErrors = validateReferencesExistAndActive(typeAttributeDefinitionMap, attributes, validationErrors); 166 validationErrors.addAll(referenceCheckErrors); 167 168 return Collections.unmodifiableList(validationErrors); 169 } 170 171 private Map<String, TypeAttributeDefinition> buildTypeAttributeDefinitionMapByName( 172 List<TypeAttributeDefinition> definitions) {// throw them into a map by name 173 Map<String, TypeAttributeDefinition> typeAttributeDefinitionMap; 174 if (definitions == null || definitions.size() == 0) { 175 typeAttributeDefinitionMap = Collections.<String, TypeAttributeDefinition>emptyMap(); 176 } else { 177 typeAttributeDefinitionMap = new HashMap<String, TypeAttributeDefinition>(); 178 179 for (TypeAttributeDefinition definition : definitions) { 180 typeAttributeDefinitionMap.put(definition.getName(), definition); 181 } 182 } 183 return typeAttributeDefinitionMap; 184 } 185 186 /** 187 * <p>Cross-validates referenced components amongst attributes to ensure they refer to existing and active 188 * business objects.</p> 189 * <p>This implementation instantiates any components mapped by attributes, populates them as best it can, and then 190 * uses the {@link DataDictionaryService} to get relationship information. Then, through the 191 * {@link DictionaryValidationService} it attempts to ensure that any referenced business objects mapped by other 192 * attributes exist and are active. It pulls any errors encountered out of the global error map via calls to 193 * {@link #extractErrorsFromGlobalVariablesErrorMap(String)}</p> 194 * <p>TODO: who can explain this? :-)</p> 195 * 196 * @param typeAttributeDefinitionMap a Map from attribute name to {@link TypeAttributeDefinition} containing all of 197 * the attribute definitions for this type. 198 * @param attributes the Map of attribute names to values 199 * @param previousValidationErrors a List of previously encountered errors used to short circuit testing on 200 * attributes that are already known to have errors. 201 * @return the List of errors encountered. Cannot return null. 202 */ 203 protected List<RemotableAttributeError> validateReferencesExistAndActive( Map<String, TypeAttributeDefinition> typeAttributeDefinitionMap, Map<String, String> attributes, List<RemotableAttributeError> previousValidationErrors) { 204 // 205 // Here there be dragons -- adapted from DataDictionaryTypeServiceBase, please excuse X-. 206 // 207 208 Map<String, BusinessObject> componentClassInstances = new HashMap<String, BusinessObject>(); 209 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 210 211 // Create an instance of each component and shove it into the componentClassInstances 212 for ( String attributeName : attributes.keySet() ) { 213 TypeAttributeDefinition attr = typeAttributeDefinitionMap.get(attributeName); 214 215 if ((attr != null) && StringUtils.isNotBlank(attr.getComponentName())) { 216 if (!componentClassInstances.containsKey(attr.getComponentName())) { 217 try { 218 Class<?> componentClass = Class.forName(attr.getComponentName()); 219 if (!BusinessObject.class.isAssignableFrom(componentClass)) { 220 LOG.warn("Class " + componentClass.getName() + " does not implement BusinessObject. Unable to perform reference existence and active validation"); 221 continue; 222 } 223 BusinessObject componentInstance = (BusinessObject) componentClass.newInstance(); 224 componentClassInstances.put(attr.getComponentName(), componentInstance); 225 } catch (Exception e) { 226 LOG.error("Unable to instantiate class for attribute: " + attributeName, e); 227 } 228 } 229 } 230 } 231 232 // now that we have instances for each component class, try to populate them with any attribute we can, 233 // assuming there were no other validation errors associated with it 234 for ( Map.Entry<String, String> entry : attributes.entrySet() ) { 235 if (!RemotableAttributeError.containsAttribute(entry.getKey(), previousValidationErrors)) { 236 for (Object componentInstance : componentClassInstances.values()) { 237 try { 238 DataObjectWrapper wrapper = KradDataServiceLocator.getDataObjectService().wrap(componentInstance); 239 wrapper.setPropertyValues(Collections.singletonMap(entry.getKey(), entry.getValue())); 240 } catch (Exception e) { 241 LOG.error("Unable to set object property class: " + componentInstance.getClass().getName() + " property: " + entry.getKey(), e); 242 } 243 } 244 } 245 } 246 247 for (Map.Entry<String, BusinessObject> entry : componentClassInstances.entrySet()) { 248 List<RelationshipDefinition> relationships = getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(entry.getKey()).getRelationships(); 249 if (relationships == null) { 250 continue; 251 } 252 253 for (RelationshipDefinition relationshipDefinition : relationships) { 254 List<PrimitiveAttributeDefinition> primitiveAttributes = relationshipDefinition.getPrimitiveAttributes(); 255 256 // this code assumes that the last defined primitiveAttribute is the attributeToHighlightOnFail 257 String attributeToHighlightOnFail = primitiveAttributes.get(primitiveAttributes.size() - 1).getSourceName(); 258 259 // TODO: will this work for user ID attributes? 260 261 if (attributes.containsKey(attributeToHighlightOnFail)) { 262 263 TypeAttributeDefinition attr = typeAttributeDefinitionMap.get(attributeToHighlightOnFail); 264 if (attr != null) { 265 final String attributeDisplayLabel; 266 if (StringUtils.isNotBlank(attr.getComponentName())) { 267 attributeDisplayLabel = getDataDictionaryService().getAttributeLabel(attr.getComponentName(), attributeToHighlightOnFail); 268 } else { 269 attributeDisplayLabel = attr.getLabel(); 270 } 271 272 getDictionaryValidationService().validateReferenceExistsAndIsActive(entry.getValue(), relationshipDefinition.getObjectAttributeName(), 273 attributeToHighlightOnFail, attributeDisplayLabel); 274 } 275 List<String> extractedErrors = extractErrorsFromGlobalVariablesErrorMap(attributeToHighlightOnFail); 276 if (CollectionUtils.isNotEmpty(extractedErrors)) { 277 errors.add(RemotableAttributeError.Builder.create(attributeToHighlightOnFail, extractedErrors).build()); 278 } 279 } 280 } 281 } 282 return errors; 283 } 284 285 /** 286 * <p>Returns a String suitable for use in error messages to represent the given attribute.</p> 287 * <p>This implementation returns a String of the format "longLabel (shortLabel)" where those fields are pulled 288 * from the passed in definition.</p> 289 * 290 * @param definition the definition for which to create an error label. 291 * @return the error label String. 292 */ 293 protected static String getAttributeErrorLabel(RemotableAttributeField definition) { 294 String longAttributeLabel = definition.getLongLabel(); 295 String shortAttributeLabel = definition.getShortLabel(); 296 297 return longAttributeLabel + " (" + shortAttributeLabel + ")"; 298 } 299 300 /** 301 * <p>creates an error String from the given errorKey and parameters.</p> 302 * <p>This implementation will attempt to resolve the errorKey using the {@link ConfigurationService}, and format it 303 * with the provided params using {@link MessageFormat#format(String, Object...)}. If the errorKey can't be 304 * resolved, it will return a string like the following: errorKey:param1;param2;param3; 305 * </p> 306 * 307 * @param errorKey the errorKey 308 * @param params the error params 309 * @return error string 310 */ 311 protected String createErrorString(String errorKey, String... params) { 312 313 String errorString = getConfigurationService().getPropertyValueAsString(errorKey); 314 if (StringUtils.isEmpty(errorString)) { 315 final StringBuilder s = new StringBuilder(errorKey).append(':'); 316 if (params != null) { 317 for (String p : params) { 318 if (p != null) { 319 s.append(p); 320 s.append(';'); 321 } 322 } 323 } 324 errorString = s.toString(); 325 } else { 326 errorString = MessageFormat.format(errorString, params); 327 } 328 return errorString; 329 } 330 331 /** 332 * <p>Validates a data dictionary mapped attribute for a primitive property.</p> 333 * <p>This implementation checks that the attribute is defined using the {@link DataDictionaryService} if it is 334 * from a specific set of types defined in TypeUtils. Then, if the value is not blank, it checks for errors by 335 * calling 336 * {@link #validateAttributeFormat(org.kuali.rice.core.api.uif.RemotableAttributeField, String, String, String, String)}. 337 * If it is blank, it checks for errors by calling 338 * {@link #validateAttributeRequired(org.kuali.rice.core.api.uif.RemotableAttributeField, String, String, Object, String)} 339 * .</p> 340 * 341 * @param typeAttributeDefinition the definition for the attribute 342 * @param componentName the data dictionary component name 343 * @param object the instance of the component 344 * @param propertyDescriptor the descriptor for the property that the attribute maps to 345 * @return a List of errors ({@link RemotableAttributeError}s) encountered during validation. Cannot return null. 346 */ 347 protected List<RemotableAttributeError> validatePrimitiveAttributeFromDescriptor ( 348 TypeAttributeDefinition typeAttributeDefinition, String componentName, Object object, 349 PropertyDescriptor propertyDescriptor) { 350 351 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 352 // validate the primitive attributes if defined in the dictionary 353 if (null != propertyDescriptor 354 && getDataDictionaryService().isAttributeDefined(componentName, propertyDescriptor.getName())) { 355 356 DataObjectWrapper wrapper = KradDataServiceLocator.getDataObjectService().wrap(object); 357 Object value = wrapper.getPropertyValue(propertyDescriptor.getName()); 358 Class<?> propertyType = propertyDescriptor.getPropertyType(); 359 360 if (TypeUtils.isStringClass(propertyType) 361 || TypeUtils.isIntegralClass(propertyType) 362 || TypeUtils.isDecimalClass(propertyType) 363 || TypeUtils.isTemporalClass(propertyType)) { 364 365 // check value format against dictionary 366 if (value != null && StringUtils.isNotBlank(value.toString())) { 367 if (!TypeUtils.isTemporalClass(propertyType)) { 368 errors.addAll(validateAttributeFormat(typeAttributeDefinition.getField(), componentName, 369 propertyDescriptor.getName(), value.toString(), propertyDescriptor.getName())); 370 } 371 } else { 372 // if it's blank, then we check whether the attribute should be required 373 errors.addAll(validateAttributeRequired(typeAttributeDefinition.getField(), componentName, 374 propertyDescriptor.getName(), value, propertyDescriptor.getName())); 375 } 376 } 377 } 378 return errors; 379 } 380 381 382 /** 383 * <p>Validates required-ness of an attribute against its corresponding value</p> 384 * <p>This implementation checks if an attribute value is null or blank, and if so checks if the 385 * {@link RemotableAttributeField} is required. If it is, a {@link RemotableAttributeError} is created 386 * with the message populated by a call to {@link #createErrorString(String, String...)}.</p> 387 * 388 * @param field the field for the attribute being tested 389 * @param objectClassName the class name for the component 390 * @param attributeName the name of the attribute 391 * @param attributeValue the value of the attribute 392 * @param errorKey the errorKey used to identify the field 393 * @return the List of errors ({@link RemotableAttributeError}s) encountered during validation. Cannot return null. 394 */ 395 protected List<RemotableAttributeError> validateAttributeRequired(RemotableAttributeField field, 396 String objectClassName, String attributeName, Object attributeValue, String errorKey) { 397 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 398 // check if field is a required field for the business object 399 400 if (attributeValue == null 401 || (attributeValue instanceof String && StringUtils.isBlank((String) attributeValue))) { 402 403 boolean required = field.isRequired(); 404 if (required) { 405 // get label of attribute for message 406 String errorLabel = getAttributeErrorLabel(field); 407 errors.add(RemotableAttributeError.Builder.create(errorKey, 408 createErrorString(RiceKeyConstants.ERROR_REQUIRED, errorLabel)).build()); 409 } 410 } 411 412 return errors; 413 } 414 415 /** 416 * <p>Gets the validation {@link Pattern} for the given {@link RemotableAttributeField}.</p> 417 * <p>This implementation checks if there is a regexConstraint set on the field, and if so 418 * it compiles a Pattern (with no special flags) using it. Otherwise, it returns a pattern that 419 * always matches.</p> 420 * 421 * @param field the field for which to return a validation {@link Pattern}. 422 * @return the compiled {@link Pattern} to use in validation the given field. 423 */ 424 protected Pattern getAttributeValidatingExpression(RemotableAttributeField field) { 425 if (field == null || StringUtils.isBlank(field.getRegexConstraint())) { 426 return ANY_CHAR_PATTERN; 427 } 428 429 return Pattern.compile(field.getRegexConstraint()); 430 } 431 432 /** 433 * <p>Gets a {@link Formatter} appropriate for the data type of the given field.</p> 434 * <p>This implementation returns null if {@link org.kuali.rice.core.api.uif.RemotableAttributeField#getDataType()} 435 * returns null. Otherwise, it returns the result of calling {@link Formatter#getFormatter(Class)} on the 436 * {@link org.kuali.rice.core.api.data.DataType}'s type</p> 437 * 438 * @param field the field for which to provide a {@link Formatter}. 439 * @return an applicable {@link Formatter}, or null if one can't be found. 440 */ 441 protected Formatter getAttributeFormatter(RemotableAttributeField field) { 442 if (field.getDataType() == null) { 443 return null; 444 } 445 446 return Formatter.getFormatter(field.getDataType().getType()); 447 } 448 449 /** 450 * <p>Validates the format of the value for the given attribute field.</p> 451 * <p>This implementation checks if the attribute value is not blank, in which case it checks (as applicable) the 452 * max length, min length, min value, max value, and format (using the {@link Pattern} returned by 453 * {@link #getAttributeValidatingExpression(org.kuali.rice.core.api.uif.RemotableAttributeField)}). If that doesn't 454 * match, it will use the Formatter returned by 455 * {@link #getAttributeFormatter(org.kuali.rice.core.api.uif.RemotableAttributeField)} to format the value and try 456 * matching against it again. For each format error that is found, 457 * {@link #createErrorString(String, String...)} is called to prepare the text for the 458 * {@link RemotableAttributeError} that is generated. 459 * 460 * @param field the field for the attribute whose value we are validating 461 * @param objectClassName the name of the class to which the attribute belongs 462 * @param attributeName the name of the attribute 463 * @param attributeValue the String value whose format we are validating 464 * @param errorKey the name of the property on the object class that this attribute maps to 465 * @return a List containing any errors ({@link RemotableAttributeError}s) that are detected. 466 */ 467 protected List<RemotableAttributeError> validateAttributeFormat(RemotableAttributeField field, String objectClassName, String attributeName, String attributeValue, String errorKey) { 468 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 469 470 String errorLabel = getAttributeErrorLabel(field); 471 472 if ( LOG.isDebugEnabled() ) { 473 LOG.debug("(bo, attributeName, attributeValue) = (" + objectClassName + "," + attributeName + "," + attributeValue + ")"); 474 } 475 476 if (StringUtils.isNotBlank(attributeValue)) { 477 Integer maxLength = field.getMaxLength(); 478 if ((maxLength != null) && (maxLength.intValue() < attributeValue.length())) { 479 errors.add(RemotableAttributeError.Builder.create(errorKey, 480 createErrorString(RiceKeyConstants.ERROR_MAX_LENGTH, errorLabel, maxLength.toString())).build()); 481 return errors; 482 } 483 Integer minLength = field.getMinLength(); 484 if ((minLength != null) && (minLength.intValue() > attributeValue.length())) { 485 errors.add(RemotableAttributeError.Builder.create(errorKey, 486 createErrorString(RiceKeyConstants.ERROR_MIN_LENGTH, errorLabel, minLength.toString())).build()); 487 return errors; 488 } 489 Pattern validationExpression = getAttributeValidatingExpression(field); 490 if (!ANY_CHAR_PATTERN_S.equals(validationExpression.pattern())) { 491 if ( LOG.isDebugEnabled() ) { 492 LOG.debug("(bo, attributeName, validationExpression) = (" + objectClassName + "," + attributeName + "," + validationExpression + ")"); 493 } 494 495 if (!validationExpression.matcher(attributeValue).matches()) { 496 boolean isError=true; 497 final Formatter formatter = getAttributeFormatter(field); 498 if (formatter != null) { 499 Object o = formatter.format(attributeValue); 500 isError = !validationExpression.matcher(String.valueOf(o)).matches(); 501 } 502 if (isError) { 503 errors.add(RemotableAttributeError.Builder.create(errorKey, createErrorString(field.getRegexContraintMsg(), errorLabel)) 504 .build()); 505 } 506 return errors; 507 } 508 } 509 Double min = field.getMinValue(); 510 if (min != null) { 511 try { 512 if (Double.parseDouble(attributeValue) < min) { 513 errors.add(RemotableAttributeError.Builder.create(errorKey, createErrorString( 514 RiceKeyConstants.ERROR_INCLUSIVE_MIN, errorLabel, min.toString())).build()); 515 return errors; 516 } 517 } 518 catch (NumberFormatException e) { 519 // quash; this indicates that the DD contained a min for a non-numeric attribute 520 } 521 } 522 Double max = field.getMaxValue(); 523 if (max != null) { 524 try { 525 526 if (Double.parseDouble(attributeValue) > max) { 527 errors.add(RemotableAttributeError.Builder.create(errorKey, createErrorString( 528 RiceKeyConstants.ERROR_INCLUSIVE_MAX, errorLabel, max.toString())).build()); 529 return errors; 530 } 531 } 532 catch (NumberFormatException e) { 533 // quash; this indicates that the DD contained a max for a non-numeric attribute 534 } 535 } 536 } 537 return errors; 538 } 539 540 541 /** 542 * <p>Removes all errors for the given attributeName from the global error map, transforms them as appropriate and 543 * returns them as a List of Strings.</p> 544 * <p>This implementation iterates through any errors found in the error map, transforms them by calling 545 * {@link #createErrorString(String, String...)} and adds them to the List that is then returned</p> 546 * 547 * @param attributeName the attribute name for which to extract errors from the global error map. 548 * @return a List of error Strings 549 */ 550 protected List<String> extractErrorsFromGlobalVariablesErrorMap(String attributeName) { 551 Object results = GlobalVariables.getMessageMap().getErrorMessagesForProperty(attributeName); 552 List<String> errors = new ArrayList<String>(); 553 if (results instanceof String) { 554 errors.add(createErrorString((String) results)); 555 } else if ( results != null) { 556 if (results instanceof List) { 557 List<?> errorList = (List<?>)results; 558 for (Object msg : errorList) { 559 ErrorMessage errorMessage = (ErrorMessage)msg; 560 errors.add(createErrorString(errorMessage.getErrorKey(), errorMessage.getMessageParameters())); 561 } 562 } else { 563 String [] temp = (String []) results; 564 for (String string : temp) { 565 errors.add(createErrorString(string)); 566 } 567 } 568 } 569 GlobalVariables.getMessageMap().removeAllErrorMessagesForProperty(attributeName); 570 return errors; 571 } 572 573 /** 574 * <p>Validates the attribute value for the given {@link TypeAttributeDefinition} having a componentName.</p> 575 * <p>This implementation instantiates a component object using reflection on the class name specified in the 576 * {@link TypeAttributeDefinition}s componentName, gets a {@link PropertyDescriptor} for the attribute of the 577 * component object, hydrates the attribute's value from it's String form, sets that value on the component object, 578 * and then delegates to 579 * {@link #validatePrimitiveAttributeFromDescriptor(AttributeValidatingTypeServiceBase.TypeAttributeDefinition, String, Object, java.beans.PropertyDescriptor)}. 580 * </p> 581 * 582 * @param typeAttributeDefinition 583 * @param attributeName 584 * @param value 585 * @return 586 */ 587 protected List<RemotableAttributeError> validateDataDictionaryAttribute(TypeAttributeDefinition typeAttributeDefinition, String attributeName, String value) { 588 try { 589 // create an object of the proper type per the component 590 Object componentObject = Class.forName( typeAttributeDefinition.getComponentName() ).newInstance(); 591 592 if ( attributeName != null ) { 593 // get the bean utils descriptor for accessing the attribute on that object 594 PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(componentObject, attributeName); 595 if ( propertyDescriptor != null ) { 596 // set the value on the object so that it can be checked 597 Object attributeValue = getAttributeValue(propertyDescriptor, value); 598 propertyDescriptor.getWriteMethod().invoke( componentObject, attributeValue); 599 return validatePrimitiveAttributeFromDescriptor(typeAttributeDefinition, 600 typeAttributeDefinition.getComponentName(), componentObject, propertyDescriptor); 601 } 602 } 603 } catch (Exception e) { 604 throw new TypeAttributeValidationException(e); 605 } 606 return Collections.emptyList(); 607 } 608 609 private Object getAttributeValue(PropertyDescriptor propertyDescriptor, String attributeValue){ 610 Object attributeValueObject = null; 611 if (propertyDescriptor!=null && attributeValue!=null) { 612 Class<?> propertyType = propertyDescriptor.getPropertyType(); 613 if (String.class.equals(propertyType)) { 614 // it's already a String 615 attributeValueObject = attributeValue; 616 } // KULRICE-6808: Kim Role Maintenance - Custom boolean role qualifier values are not being converted properly 617 else if (Boolean.class.equals(propertyType) || Boolean.TYPE.equals(propertyType)) { 618 attributeValueObject = Truth.strToBooleanIgnoreCase(attributeValue); 619 } else { 620 // try to create one with KRADUtils for other misc data types 621 attributeValueObject = KRADUtils.createObject(propertyType, new Class[]{String.class}, new Object[]{attributeValue}); 622 // if that didn't work, we'll get a null back 623 if (attributeValueObject == null ) { 624 // this doesn't seem like a great option, since we know the property isn't of type String 625 attributeValueObject = attributeValue; 626 } 627 } 628 } 629 return attributeValueObject; 630 } 631 632 633 protected DictionaryValidationService getDictionaryValidationService() { 634 if (dictionaryValidationService == null) { 635 dictionaryValidationService = KRADServiceLocatorWeb.getDictionaryValidationService(); 636 } 637 return dictionaryValidationService; 638 } 639 640 // lazy initialization holder class 641 private static class DataDictionaryServiceHolder { 642 public static DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService(); 643 } 644 645 protected DataDictionaryService getDataDictionaryService() { 646 return DataDictionaryServiceHolder.dataDictionaryService; 647 } 648 649 // lazy initialization holder class 650 private static class DataDictionaryRemoteFieldServiceHolder { 651 public static DataDictionaryRemoteFieldService dataDictionaryRemoteFieldService = 652 KRADServiceLocatorWeb.getDataDictionaryRemoteFieldService(); 653 } 654 655 protected DataDictionaryRemoteFieldService getDataDictionaryRemoteFieldService() { 656 return DataDictionaryRemoteFieldServiceHolder.dataDictionaryRemoteFieldService; 657 } 658 659 // lazy initialization holder class 660 private static class ConfigurationServiceHolder { 661 public static ConfigurationService configurationService = CoreApiServiceLocator.getKualiConfigurationService(); 662 } 663 664 protected ConfigurationService getConfigurationService() { 665 return ConfigurationServiceHolder.configurationService; 666 } 667 668 669 protected static class TypeAttributeValidationException extends RuntimeException { 670 671 protected TypeAttributeValidationException(String message) { 672 super( message ); 673 } 674 675 protected TypeAttributeValidationException(Throwable cause) { 676 super( cause ); 677 } 678 679 private static final long serialVersionUID = 8220618846321607801L; 680 681 } 682 683 684 /** 685 * A module-independent representation of a type attribute containing all the information that we need 686 * in order to validate data dictionary-based attributes. 687 */ 688 protected static class TypeAttributeDefinition { 689 690 private final RemotableAttributeField field; 691 private final String name; 692 private final String componentName; 693 private final String label; 694 private final Map<String, String> properties; 695 696 /** 697 * Constructs a {@link TypeAttributeDefinition} 698 * @param field the RemotableAttributeField corresponding to this definition. Must not be null. 699 * @param name the name for this attribute. Must not be empty or null. 700 * @param componentName The name of a data dictionary component that this field refers to. May be null. 701 * @param label The label to use for this attribute. May be null. 702 * @param properties a catch all for properties important to a module's type attrbute definitions 703 * that aren't directly supported by {@link TypeAttributeDefinition}. 704 */ 705 public TypeAttributeDefinition(RemotableAttributeField field, String name, String componentName, String label, Map<String, String> properties) { 706 if (field == null) throw new RiceIllegalArgumentException("field must not be null"); 707 if (StringUtils.isEmpty(name)) throw new RiceIllegalArgumentException("name must not be empty or null"); 708 this.field = field; 709 this.name = name; 710 this.componentName = componentName; 711 this.label = label; 712 713 if (properties == null || properties.isEmpty()) { 714 this.properties = Collections.emptyMap(); 715 } else { 716 // make our local variable into a copy of the passed in Map 717 properties = new HashMap<String, String>(properties); 718 // assign in in immutable form to our class member variable 719 this.properties = Collections.unmodifiableMap(properties); 720 } 721 } 722 723 public RemotableAttributeField getField() { 724 return field; 725 } 726 727 public String getName() { 728 return name; 729 } 730 731 public String getComponentName() { 732 return componentName; 733 } 734 735 public String getLabel() { 736 return label; 737 } 738 739 /** 740 * @return an unmodifiable map of properties for this attribute. Will never be null. 741 */ 742 public Map<String, String> getProperties() { 743 return properties; 744 } 745 } 746}