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 */ 016package org.kuali.rice.kns.kim.type; 017 018import java.beans.PropertyDescriptor; 019import java.util.AbstractMap; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.regex.Pattern; 029 030import org.apache.commons.beanutils.PropertyUtils; 031import org.apache.commons.collections.CollectionUtils; 032import org.apache.commons.lang.StringUtils; 033import org.kuali.rice.core.api.exception.RiceIllegalArgumentException; 034import org.kuali.rice.core.api.uif.RemotableAbstractWidget; 035import org.kuali.rice.core.api.uif.RemotableAttributeError; 036import org.kuali.rice.core.api.uif.RemotableAttributeField; 037import org.kuali.rice.core.api.uif.RemotableQuickFinder; 038import org.kuali.rice.core.api.util.RiceKeyConstants; 039import org.kuali.rice.core.api.util.type.TypeUtils; 040import org.kuali.rice.core.web.format.Formatter; 041import org.kuali.rice.kew.api.KewApiServiceLocator; 042import org.kuali.rice.kew.api.doctype.DocumentType; 043import org.kuali.rice.kew.api.doctype.DocumentTypeService; 044import org.kuali.rice.kim.api.services.KimApiServiceLocator; 045import org.kuali.rice.kim.api.type.KimAttributeField; 046import org.kuali.rice.kim.api.type.KimType; 047import org.kuali.rice.kim.api.type.KimTypeAttribute; 048import org.kuali.rice.kim.api.type.KimTypeInfoService; 049import org.kuali.rice.kim.framework.type.KimTypeService; 050import org.kuali.rice.kns.lookup.LookupUtils; 051import org.kuali.rice.kns.service.DictionaryValidationService; 052import org.kuali.rice.kns.service.KNSServiceLocator; 053import org.kuali.rice.kns.util.FieldUtils; 054import org.kuali.rice.kns.web.ui.Field; 055import org.kuali.rice.krad.bo.BusinessObject; 056import org.kuali.rice.krad.comparator.StringValueComparator; 057import org.kuali.rice.krad.datadictionary.AttributeDefinition; 058import org.kuali.rice.krad.datadictionary.PrimitiveAttributeDefinition; 059import org.kuali.rice.krad.datadictionary.RelationshipDefinition; 060import org.kuali.rice.krad.service.BusinessObjectService; 061import org.kuali.rice.krad.service.DataDictionaryService; 062import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 063import org.kuali.rice.krad.service.ModuleService; 064import org.kuali.rice.krad.util.ErrorMessage; 065import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils; 066import org.kuali.rice.krad.util.GlobalVariables; 067import org.kuali.rice.krad.util.KRADUtils; 068import org.kuali.rice.krad.util.ObjectUtils; 069 070import com.google.common.base.Function; 071import com.google.common.collect.Lists; 072 073/** 074 * A base class for {@code KimTypeService} implementations which read attribute-related information from the Data 075 * Dictionary. This implementation is currently written against the KNS apis for Data Dictionary. Additionally, it 076 * supports the ability to read non-Data Dictionary attribute information from the {@link KimTypeInfoService}. 077 * 078 * @author Kuali Rice Team (rice.collab@kuali.org) 079 * 080 * @deprecated A krad integrated type service base class will be provided in the future. 081 */ 082@Deprecated 083public class DataDictionaryTypeServiceBase implements KimTypeService { 084 085 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DataDictionaryTypeServiceBase.class); 086 private static final String ANY_CHAR_PATTERN_S = ".*"; 087 private static final Pattern ANY_CHAR_PATTERN = Pattern.compile(ANY_CHAR_PATTERN_S); 088 089 private BusinessObjectService businessObjectService; 090 private DictionaryValidationService dictionaryValidationService; 091 private DataDictionaryService dataDictionaryService; 092 private KimTypeInfoService typeInfoService; 093 private DocumentTypeService documentTypeService; 094 095 @Override 096 public String getWorkflowDocumentTypeName() { 097 return null; 098 } 099 100 @Override 101 public List<String> getWorkflowRoutingAttributes(String routeLevel) { 102 if (StringUtils.isBlank(routeLevel)) { 103 throw new RiceIllegalArgumentException("routeLevel was blank or null"); 104 } 105 106 return Collections.emptyList(); 107 } 108 109 @Override 110 public List<KimAttributeField> getAttributeDefinitions(String kimTypeId) { 111 final KimType kimType = getTypeInfoService().getKimType(kimTypeId); 112 if ( kimType == null ) { 113 LOG.warn("Unable to retrieve a KimTypeInfo for kimTypeId=" + kimTypeId + " in getAttributeDefinitions()"); 114 return Collections.emptyList(); 115 } 116 final List<String> uniqueAttributes = getUniqueAttributes(kimTypeId); 117 118 //using map.entry as a 2-item tuple 119 final List<Map.Entry<String,KimAttributeField>> definitions = new ArrayList<Map.Entry<String,KimAttributeField>>(); 120 if ( kimType == null ) { 121 LOG.warn( "Unable to find KimType for ID: " + kimTypeId); 122 return Collections.emptyList(); 123 } 124 final String nsCode = kimType.getNamespaceCode(); 125 126 for (KimTypeAttribute typeAttribute : kimType.getAttributeDefinitions()) { 127 final KimAttributeField definition; 128 if (typeAttribute.getKimAttribute().getComponentName() == null) { 129 definition = getNonDataDictionaryAttributeDefinition(nsCode,kimTypeId,typeAttribute, uniqueAttributes); 130 } else { 131 definition = getDataDictionaryAttributeDefinition(nsCode,kimTypeId,typeAttribute, uniqueAttributes); 132 } 133 134 if (definition != null) { 135 definitions.add(new AbstractMap.SimpleEntry<String,KimAttributeField>(typeAttribute.getSortCode() != null ? typeAttribute.getSortCode() : "", definition)); 136 } 137 } 138 139 //sort by sortCode 140 Collections.sort(definitions, new Comparator<Map.Entry<String, KimAttributeField>>() { 141 @Override 142 public int compare(Map.Entry<String, KimAttributeField> o1, Map.Entry<String, KimAttributeField> o2) { 143 return o1.getKey().compareTo(o2.getKey()); 144 } 145 }); 146 147 //transform removing sortCode 148 return Collections.unmodifiableList(Lists.transform(definitions, new Function<Map.Entry<String, KimAttributeField>, KimAttributeField>() { 149 @Override 150 public KimAttributeField apply(Map.Entry<String, KimAttributeField> v) { 151 return v.getValue(); 152 } 153 })); 154 } 155 156 /** 157 * This is the default implementation. It calls into the service for each attribute to 158 * validate it there. No combination validation is done. That should be done 159 * by overriding this method. 160 */ 161 @Override 162 public List<RemotableAttributeError> validateAttributes(String kimTypeId, Map<String, String> attributes) { 163 if (StringUtils.isBlank(kimTypeId)) { 164 throw new RiceIllegalArgumentException("kimTypeId was null or blank"); 165 } 166 167 if (attributes == null) { 168 throw new RiceIllegalArgumentException("attributes was null or blank"); 169 } 170 171 final List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>(); 172 KimType kimType = getTypeInfoService().getKimType(kimTypeId); 173 174 for ( Map.Entry<String, String> entry : attributes.entrySet() ) { 175 KimTypeAttribute attr = kimType.getAttributeDefinitionByName(entry.getKey()); 176 final List<RemotableAttributeError> attributeErrors; 177 if ( attr.getKimAttribute().getComponentName() == null) { 178 attributeErrors = validateNonDataDictionaryAttribute(attr, entry.getKey(), entry.getValue()); 179 } else { 180 attributeErrors = validateDataDictionaryAttribute(attr, entry.getKey(), entry.getValue()); 181 } 182 183 if ( attributeErrors != null ) { 184 validationErrors.addAll(attributeErrors); 185 } 186 } 187 188 189 final List<RemotableAttributeError> referenceCheckErrors = validateReferencesExistAndActive(kimType, attributes, validationErrors); 190 validationErrors.addAll(referenceCheckErrors); 191 192 return Collections.unmodifiableList(validationErrors); 193 } 194 195 @Override 196 public List<RemotableAttributeError> validateAttributesAgainstExisting(String kimTypeId, Map<String, String> newAttributes, Map<String, String> oldAttributes){ 197 if (StringUtils.isBlank(kimTypeId)) { 198 throw new RiceIllegalArgumentException("kimTypeId was null or blank"); 199 } 200 201 if (newAttributes == null) { 202 throw new RiceIllegalArgumentException("newAttributes was null or blank"); 203 } 204 205 if (oldAttributes == null) { 206 throw new RiceIllegalArgumentException("oldAttributes was null or blank"); 207 } 208 return Collections.emptyList(); 209 //final List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 210 //errors.addAll(validateUniqueAttributes(kimTypeId, newAttributes, oldAttributes)); 211 //return Collections.unmodifiableList(errors); 212 213 } 214 215 /** 216 * 217 * This method matches input attribute set entries and standard attribute set entries using literal string match. 218 * 219 */ 220 protected boolean performMatch(Map<String, String> inputAttributes, Map<String, String> storedAttributes) { 221 if ( storedAttributes == null || inputAttributes == null ) { 222 return true; 223 } 224 for ( Map.Entry<String, String> entry : storedAttributes.entrySet() ) { 225 if (inputAttributes.containsKey(entry.getKey()) && !StringUtils.equals(inputAttributes.get(entry.getKey()), entry.getValue()) ) { 226 return false; 227 } 228 } 229 return true; 230 } 231 232 protected Map<String, String> translateInputAttributes(Map<String, String> qualification){ 233 return qualification; 234 } 235 236 protected List<RemotableAttributeError> validateReferencesExistAndActive( KimType kimType, Map<String, String> attributes, List<RemotableAttributeError> previousValidationErrors) { 237 Map<String, BusinessObject> componentClassInstances = new HashMap<String, BusinessObject>(); 238 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 239 240 for ( String attributeName : attributes.keySet() ) { 241 KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeName); 242 243 if (StringUtils.isNotBlank(attr.getKimAttribute().getComponentName())) { 244 if (!componentClassInstances.containsKey(attr.getKimAttribute().getComponentName())) { 245 try { 246 Class<?> componentClass = Class.forName( attr.getKimAttribute().getComponentName() ); 247 if (!BusinessObject.class.isAssignableFrom(componentClass)) { 248 LOG.warn("Class " + componentClass.getName() + " does not implement BusinessObject. Unable to perform reference existence and active validation"); 249 continue; 250 } 251 BusinessObject componentInstance = (BusinessObject) componentClass.newInstance(); 252 componentClassInstances.put(attr.getKimAttribute().getComponentName(), componentInstance); 253 } catch (Exception e) { 254 LOG.error("Unable to instantiate class for attribute: " + attributeName, e); 255 } 256 } 257 } 258 } 259 260 // now that we have instances for each component class, try to populate them with any attribute we can, assuming there were no other validation errors associated with it 261 for ( Map.Entry<String, String> entry : attributes.entrySet() ) { 262 if (!RemotableAttributeError.containsAttribute(entry.getKey(), previousValidationErrors)) { 263 for (Object componentInstance : componentClassInstances.values()) { 264 try { 265 ObjectUtils.setObjectProperty(componentInstance, entry.getKey(), entry.getValue()); 266 } catch (NoSuchMethodException e) { 267 // this is expected since not all attributes will be in all components 268 } catch (Exception e) { 269 LOG.error("Unable to set object property class: " + componentInstance.getClass().getName() + " property: " + entry.getKey(), e); 270 } 271 } 272 } 273 } 274 275 for (Map.Entry<String, BusinessObject> entry : componentClassInstances.entrySet()) { 276 List<RelationshipDefinition> relationships = getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(entry.getKey()).getRelationships(); 277 if (relationships == null) { 278 relationships = getDataDictionaryService().getDataDictionary().getDataObjectEntry(entry.getKey()).getRelationships(); 279 if (relationships == null) { 280 continue; 281 } 282 } 283 284 for (RelationshipDefinition relationshipDefinition : relationships) { 285 List<PrimitiveAttributeDefinition> primitiveAttributes = relationshipDefinition.getPrimitiveAttributes(); 286 287 // this code assumes that the last defined primitiveAttribute is the attributeToHighlightOnFail 288 String attributeToHighlightOnFail = primitiveAttributes.get(primitiveAttributes.size() - 1).getSourceName(); 289 290 // TODO: will this work for user ID attributes? 291 292 if (!attributes.containsKey(attributeToHighlightOnFail)) { 293 // if the attribute to highlight wasn't passed in, don't bother validating 294 continue; 295 } 296 297 298 KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeToHighlightOnFail); 299 if (attr != null) { 300 final String attributeDisplayLabel; 301 if (StringUtils.isNotBlank(attr.getKimAttribute().getComponentName())) { 302 attributeDisplayLabel = getDataDictionaryService().getAttributeLabel(attr.getKimAttribute().getComponentName(), attributeToHighlightOnFail); 303 } else { 304 attributeDisplayLabel = attr.getKimAttribute().getAttributeLabel(); 305 } 306 307 getDictionaryValidationService().validateReferenceExistsAndIsActive(entry.getValue(), relationshipDefinition.getObjectAttributeName(), 308 attributeToHighlightOnFail, attributeDisplayLabel); 309 } 310 List<String> extractedErrors = extractErrorsFromGlobalVariablesErrorMap(attributeToHighlightOnFail); 311 if (CollectionUtils.isNotEmpty(extractedErrors)) { 312 errors.add(RemotableAttributeError.Builder.create(attributeToHighlightOnFail, extractedErrors).build()); 313 } 314 } 315 } 316 return errors; 317 } 318 319 protected List<RemotableAttributeError> validateAttributeRequired(String kimTypeId, String objectClassName, String attributeName, Object attributeValue, String errorKey) { 320 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 321 // check if field is a required field for the business object 322 if (attributeValue == null || (attributeValue instanceof String && StringUtils.isBlank((String) attributeValue))) { 323 List<KimAttributeField> map = getAttributeDefinitions(kimTypeId); 324 KimAttributeField definition = DataDictionaryTypeServiceHelper.findAttributeField(attributeName, map); 325 326 boolean required = definition.getAttributeField().isRequired(); 327 if (required) { 328 // get label of attribute for message 329 String errorLabel = DataDictionaryTypeServiceHelper.getAttributeErrorLabel(definition); 330 errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper 331 .createErrorString(RiceKeyConstants.ERROR_REQUIRED, errorLabel)).build()); 332 } 333 } 334 return errors; 335 } 336 337 protected List<RemotableAttributeError> validateDataDictionaryAttribute(String kimTypeId, String entryName, Object object, PropertyDescriptor propertyDescriptor) { 338 return validatePrimitiveFromDescriptor(kimTypeId, entryName, object, propertyDescriptor); 339 } 340 341 protected List<RemotableAttributeError> validatePrimitiveFromDescriptor(String kimTypeId, String entryName, Object object, PropertyDescriptor propertyDescriptor) { 342 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 343 // validate the primitive attributes if defined in the dictionary 344 if (null != propertyDescriptor && getDataDictionaryService().isAttributeDefined(entryName, propertyDescriptor.getName())) { 345 Object value = ObjectUtils.getPropertyValue(object, propertyDescriptor.getName()); 346 Class<?> propertyType = propertyDescriptor.getPropertyType(); 347 348 if (TypeUtils.isStringClass(propertyType) || TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) || TypeUtils.isTemporalClass(propertyType)) { 349 350 // check value format against dictionary 351 if (value != null && StringUtils.isNotBlank(value.toString())) { 352 if (!TypeUtils.isTemporalClass(propertyType)) { 353 errors.addAll(validateAttributeFormat(kimTypeId, entryName, propertyDescriptor.getName(), value.toString(), propertyDescriptor.getName())); 354 } 355 } 356 else { 357 // if it's blank, then we check whether the attribute should be required 358 errors.addAll(validateAttributeRequired(kimTypeId, entryName, propertyDescriptor.getName(), value, propertyDescriptor.getName())); 359 } 360 } 361 } 362 return errors; 363 } 364 365 protected Pattern getAttributeValidatingExpression(KimAttributeField definition) { 366 if (definition == null || StringUtils.isBlank(definition.getAttributeField().getRegexConstraint())) { 367 return ANY_CHAR_PATTERN; 368 } 369 370 return Pattern.compile(definition.getAttributeField().getRegexConstraint()); 371 } 372 373 protected Formatter getAttributeFormatter(KimAttributeField definition) { 374 if (definition.getAttributeField().getDataType() == null) { 375 return null; 376 } 377 378 return Formatter.getFormatter(definition.getAttributeField().getDataType().getType()); 379 } 380 381 382 383 protected Double getAttributeMinValue(KimAttributeField definition) { 384 return definition == null ? null : definition.getAttributeField().getMinValue(); 385 } 386 387 protected Double getAttributeMaxValue(KimAttributeField definition) { 388 return definition == null ? null : definition.getAttributeField().getMaxValue(); 389 } 390 391 protected List<RemotableAttributeError> validateAttributeFormat(String kimTypeId, String objectClassName, String attributeName, String attributeValue, String errorKey) { 392 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 393 394 List<KimAttributeField> attributeDefinitions = getAttributeDefinitions(kimTypeId); 395 KimAttributeField definition = DataDictionaryTypeServiceHelper.findAttributeField(attributeName, 396 attributeDefinitions); 397 398 String errorLabel = DataDictionaryTypeServiceHelper.getAttributeErrorLabel(definition); 399 400 if ( LOG.isDebugEnabled() ) { 401 LOG.debug("(bo, attributeName, attributeValue) = (" + objectClassName + "," + attributeName + "," + attributeValue + ")"); 402 } 403 404 if (StringUtils.isNotBlank(attributeValue)) { 405 Integer maxLength = definition.getAttributeField().getMaxLength(); 406 if ((maxLength != null) && (maxLength.intValue() < attributeValue.length())) { 407 errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper 408 .createErrorString(RiceKeyConstants.ERROR_MAX_LENGTH, errorLabel, maxLength.toString())).build()); 409 return errors; 410 } 411 Pattern validationExpression = getAttributeValidatingExpression(definition); 412 if (!ANY_CHAR_PATTERN_S.equals(validationExpression.pattern())) { 413 if ( LOG.isDebugEnabled() ) { 414 LOG.debug("(bo, attributeName, validationExpression) = (" + objectClassName + "," + attributeName + "," + validationExpression + ")"); 415 } 416 417 if (!validationExpression.matcher(attributeValue).matches()) { 418 boolean isError=true; 419 final Formatter formatter = getAttributeFormatter(definition); 420 if (formatter != null) { 421 Object o = formatter.format(attributeValue); 422 isError = !validationExpression.matcher(String.valueOf(o)).matches(); 423 } 424 if (isError) { 425 errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper 426 .createErrorString(definition)).build()); 427 } 428 return errors; 429 } 430 } 431 Double min = getAttributeMinValue(definition); 432 if (min != null) { 433 try { 434 if (Double.parseDouble(attributeValue) < min) { 435 errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper 436 .createErrorString(RiceKeyConstants.ERROR_INCLUSIVE_MIN, errorLabel, min.toString())).build()); 437 return errors; 438 } 439 } 440 catch (NumberFormatException e) { 441 // quash; this indicates that the DD contained a min for a non-numeric attribute 442 } 443 } 444 Double max = getAttributeMaxValue(definition); 445 if (max != null) { 446 try { 447 448 if (Double.parseDouble(attributeValue) > max) { 449 errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper 450 .createErrorString(RiceKeyConstants.ERROR_INCLUSIVE_MAX, errorLabel, max.toString())).build()); 451 return errors; 452 } 453 } 454 catch (NumberFormatException e) { 455 // quash; this indicates that the DD contained a max for a non-numeric attribute 456 } 457 } 458 } 459 return errors; 460 } 461 462 463 464 /* 465 * will create a list of errors in the following format: 466 * 467 * 468 * error_key:param1;param2;param3; 469 */ 470 protected List<String> extractErrorsFromGlobalVariablesErrorMap(String attributeName) { 471 Object results = GlobalVariables.getMessageMap().getErrorMessagesForProperty(attributeName); 472 List<String> errors = new ArrayList<String>(); 473 if (results instanceof String) { 474 errors.add((String)results); 475 } else if ( results != null) { 476 if (results instanceof List) { 477 List<?> errorList = (List<?>)results; 478 for (Object msg : errorList) { 479 ErrorMessage errorMessage = (ErrorMessage)msg; 480 errors.add(DataDictionaryTypeServiceHelper.createErrorString(errorMessage.getErrorKey(), 481 errorMessage.getMessageParameters())); 482 } 483 } else { 484 String [] temp = (String []) results; 485 for (String string : temp) { 486 errors.add(string); 487 } 488 } 489 } 490 GlobalVariables.getMessageMap().removeAllErrorMessagesForProperty(attributeName); 491 return errors; 492 } 493 494 protected List<RemotableAttributeError> validateNonDataDictionaryAttribute(KimTypeAttribute attr, String key, String value) { 495 return Collections.emptyList(); 496 } 497 498 protected List<RemotableAttributeError> validateDataDictionaryAttribute(KimTypeAttribute attr, String key, String value) { 499 try { 500 // create an object of the proper type per the component 501 Object componentObject = Class.forName( attr.getKimAttribute().getComponentName() ).newInstance(); 502 503 if ( attr.getKimAttribute().getAttributeName() != null ) { 504 // get the bean utils descriptor for accessing the attribute on that object 505 PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(componentObject, attr.getKimAttribute().getAttributeName()); 506 if ( propertyDescriptor != null ) { 507 // set the value on the object so that it can be checked 508 Object attributeValue = KRADUtils.hydrateAttributeValue(propertyDescriptor.getPropertyType(), value); 509 if (attributeValue == null) { 510 attributeValue = value; // not a super-awesome fallback strategy, but... 511 } 512 propertyDescriptor.getWriteMethod().invoke( componentObject, attributeValue); 513 return validateDataDictionaryAttribute(attr.getKimTypeId(), attr.getKimAttribute().getComponentName(), componentObject, propertyDescriptor); 514 } 515 } 516 } catch (Exception e) { 517 throw new KimTypeAttributeValidationException(e); 518 } 519 return Collections.emptyList(); 520 } 521 522 523 /** 524 * @param namespaceCode 525 * @param typeAttribute 526 * @return an AttributeDefinition for the given KimTypeAttribute, or null no base AttributeDefinition 527 * matches the typeAttribute parameter's attributeName. 528 */ 529 protected KimAttributeField getDataDictionaryAttributeDefinition( String namespaceCode, String kimTypeId, KimTypeAttribute typeAttribute, List<String> uniqueAttributes) { 530 531 final String componentClassName = typeAttribute.getKimAttribute().getComponentName(); 532 final String attributeName = typeAttribute.getKimAttribute().getAttributeName(); 533 final Class<? extends BusinessObject> componentClass; 534 final AttributeDefinition baseDefinition; 535 536 // try to resolve the component name - if not possible - try to pull the definition from the app mediation service 537 try { 538 if (StringUtils.isNotBlank(componentClassName)) { 539 componentClass = (Class<? extends BusinessObject>) Class.forName(componentClassName); 540 AttributeDefinition baseDefinitionTemp = 541 getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(componentClassName) 542 .getAttributeDefinition(attributeName); 543 if (baseDefinitionTemp == null) { 544 baseDefinition = getDataDictionaryService().getDataDictionary().getDataObjectEntry( 545 componentClassName).getAttributeDefinition(attributeName); 546 } else { 547 baseDefinition = baseDefinitionTemp; 548 } 549 } else { 550 baseDefinition = null; 551 componentClass = null; 552 } 553 } catch (ClassNotFoundException ex) { 554 throw new KimTypeAttributeException(ex); 555 } 556 557 if (baseDefinition == null) { 558 return null; 559 } 560 final RemotableAttributeField.Builder definition = RemotableAttributeField.Builder.create(baseDefinition.getName()); 561 562 definition.setLongLabel(baseDefinition.getLabel()); 563 definition.setShortLabel(baseDefinition.getShortLabel()); 564 definition.setMaxLength(baseDefinition.getMaxLength()); 565 566 if (baseDefinition.isRequired() != null) { 567 definition.setRequired(baseDefinition.isRequired()); 568 } else { 569 definition.setRequired(false); 570 } 571 572 if (baseDefinition.getForceUppercase() != null) { 573 definition.setForceUpperCase(baseDefinition.getForceUppercase()); 574 } 575 definition.setControl(DataDictionaryTypeServiceHelper.toRemotableAbstractControlBuilder( 576 baseDefinition)); 577 final RemotableQuickFinder.Builder qf = createQuickFinder(componentClass, attributeName); 578 if (qf != null) { 579 definition.setWidgets(Collections.<RemotableAbstractWidget.Builder>singletonList(qf)); 580 } 581 final KimAttributeField.Builder kimField = KimAttributeField.Builder.create(definition, typeAttribute.getKimAttribute().getId()); 582 583 if(uniqueAttributes!=null && uniqueAttributes.contains(definition.getName())){ 584 kimField.setUnique(true); 585 } 586 587 return kimField.build(); 588 } 589 590 private RemotableQuickFinder.Builder createQuickFinder(Class<? extends BusinessObject> componentClass, String attributeName) { 591 592 Field field = FieldUtils.getPropertyField(componentClass, attributeName, false); 593 if ( field != null ) { 594 final BusinessObject sampleComponent; 595 try { 596 sampleComponent = componentClass.newInstance(); 597 } catch(InstantiationException e) { 598 throw new KimTypeAttributeException(e); 599 } catch (IllegalAccessException e) { 600 throw new KimTypeAttributeException(e); 601 } 602 603 field = LookupUtils.setFieldQuickfinder( sampleComponent, attributeName, field, Collections.singletonList(attributeName) ); 604 if ( StringUtils.isNotBlank( field.getQuickFinderClassNameImpl() ) ) { 605 final Class<? extends BusinessObject> lookupClass; 606 try { 607 lookupClass = (Class<? extends BusinessObject>) Class.forName( field.getQuickFinderClassNameImpl() ); 608 } catch (ClassNotFoundException e) { 609 throw new KimTypeAttributeException(e); 610 } 611 612 String baseLookupUrl = LookupUtils.getBaseLookupUrl(false) + "?methodToCall=start"; 613 614 if (ExternalizableBusinessObjectUtils.isExternalizableBusinessObject(lookupClass)) { 615 ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(lookupClass); 616 if (moduleService.isExternalizableBusinessObjectLookupable(lookupClass)) { 617 baseLookupUrl = moduleService.getExternalizableBusinessObjectLookupUrl(lookupClass, Collections.<String,String>emptyMap()); 618 // XXX: I'm not proud of this: 619 baseLookupUrl = baseLookupUrl.substring(0,baseLookupUrl.indexOf("?")) + "?methodToCall=start"; 620 } 621 } 622 623 final RemotableQuickFinder.Builder builder = 624 RemotableQuickFinder.Builder.create(baseLookupUrl, lookupClass.getName()); 625 builder.setLookupParameters(toMap(field.getLookupParameters())); 626 builder.setFieldConversions(toMap(field.getFieldConversions())); 627 return builder; 628 } 629 } 630 return null; 631 } 632 633 private static Map<String, String> toMap(String s) { 634 if (StringUtils.isBlank(s)) { 635 return Collections.emptyMap(); 636 } 637 final Map<String, String> map = new HashMap<String, String>(); 638 for (String string : s.split(",")) { 639 String [] keyVal = string.split(":"); 640 map.put(keyVal[0], keyVal[1]); 641 } 642 return Collections.unmodifiableMap(map); 643 } 644 645 protected KimAttributeField getNonDataDictionaryAttributeDefinition(String namespaceCode, String kimTypeId, KimTypeAttribute typeAttribute, List<String> uniqueAttributes) { 646 RemotableAttributeField.Builder field = RemotableAttributeField.Builder.create(typeAttribute.getKimAttribute().getAttributeName()); 647 field.setLongLabel(typeAttribute.getKimAttribute().getAttributeLabel()); 648 649 //KULRICE-9143 shortLabel must be set for KIM to render attribute 650 field.setShortLabel(typeAttribute.getKimAttribute().getAttributeLabel()); 651 652 KimAttributeField.Builder definition = KimAttributeField.Builder.create(field, typeAttribute.getKimAttribute().getId()); 653 654 if(uniqueAttributes!=null && uniqueAttributes.contains(typeAttribute.getKimAttribute().getAttributeName())){ 655 definition.setUnique(true); 656 } 657 return definition.build(); 658 } 659 660 protected static final String COMMA_SEPARATOR = ", "; 661 662 protected void validateRequiredAttributesAgainstReceived(Map<String, String> receivedAttributes){ 663 // abort if type does not want the qualifiers to be checked 664 if ( !isCheckRequiredAttributes() ) { 665 return; 666 } 667 // abort if the list is empty, no attributes need to be checked 668 if ( getRequiredAttributes() == null || getRequiredAttributes().isEmpty() ) { 669 return; 670 } 671 List<String> missingAttributes = new ArrayList<String>(); 672 // if attributes are null or empty, they're all missing 673 if ( receivedAttributes == null || receivedAttributes.isEmpty() ) { 674 return; 675 } else { 676 for( String requiredAttribute : getRequiredAttributes() ) { 677 if( !receivedAttributes.containsKey(requiredAttribute) ) { 678 missingAttributes.add(requiredAttribute); 679 } 680 } 681 } 682 if(!missingAttributes.isEmpty()) { 683 StringBuilder errorMessage = new StringBuilder(); 684 Iterator<String> attribIter = missingAttributes.iterator(); 685 while ( attribIter.hasNext() ) { 686 errorMessage.append( attribIter.next() ); 687 if( attribIter.hasNext() ) { 688 errorMessage.append( COMMA_SEPARATOR ); 689 } 690 } 691 errorMessage.append( " not found in required attributes for this type." ); 692 throw new KimTypeAttributeValidationException(errorMessage.toString()); 693 } 694 } 695 696 697 @Override 698 public List<RemotableAttributeError> validateUniqueAttributes(String kimTypeId, Map<String, String> newAttributes, Map<String, String> oldAttributes) { 699 if (StringUtils.isBlank(kimTypeId)) { 700 throw new RiceIllegalArgumentException("kimTypeId was null or blank"); 701 } 702 703 if (newAttributes == null) { 704 throw new RiceIllegalArgumentException("newAttributes was null or blank"); 705 } 706 707 if (oldAttributes == null) { 708 throw new RiceIllegalArgumentException("oldAttributes was null or blank"); 709 } 710 List<String> uniqueAttributes = getUniqueAttributes(kimTypeId); 711 if(uniqueAttributes==null || uniqueAttributes.isEmpty()){ 712 return Collections.emptyList(); 713 } else{ 714 List<RemotableAttributeError> m = new ArrayList<RemotableAttributeError>(); 715 if(areAttributesEqual(uniqueAttributes, newAttributes, oldAttributes)){ 716 //add all unique attrs to error map 717 for (String a : uniqueAttributes) { 718 m.add(RemotableAttributeError.Builder.create(a, RiceKeyConstants.ERROR_DUPLICATE_ENTRY).build()); 719 } 720 721 return m; 722 } 723 } 724 return Collections.emptyList(); 725 } 726 727 protected boolean areAttributesEqual(List<String> uniqueAttributeNames, Map<String, String> aSet1, Map<String, String> aSet2){ 728 StringValueComparator comparator = StringValueComparator.getInstance(); 729 for(String uniqueAttributeName: uniqueAttributeNames){ 730 String attrVal1 = getAttributeValue(aSet1, uniqueAttributeName); 731 String attrVal2 = getAttributeValue(aSet2, uniqueAttributeName); 732 if(comparator.compare(attrVal1, attrVal2)!=0){ 733 return false; 734 } 735 } 736 return true; 737 } 738 739 protected String getAttributeValue(Map<String, String> aSet, String attributeName){ 740 if(StringUtils.isEmpty(attributeName)) { 741 return null; 742 } 743 for(Map.Entry<String, String> entry : aSet.entrySet()){ 744 if(attributeName.equals(entry.getKey())) { 745 return entry.getValue(); 746 } 747 } 748 return null; 749 } 750 751 protected List<String> getUniqueAttributes(String kimTypeId){ 752 KimType kimType = getTypeInfoService().getKimType(kimTypeId); 753 List<String> uniqueAttributes = new ArrayList<String>(); 754 if ( kimType != null ) { 755 for(KimTypeAttribute attributeDefinition: kimType.getAttributeDefinitions()){ 756 uniqueAttributes.add(attributeDefinition.getKimAttribute().getAttributeName()); 757 } 758 } else { 759 LOG.error("Unable to retrieve a KimTypeInfo for a null kimTypeId in getUniqueAttributes()"); 760 } 761 return Collections.unmodifiableList(uniqueAttributes); 762 } 763 764 @Override 765 public List<RemotableAttributeError> validateUnmodifiableAttributes(String kimTypeId, Map<String, String> originalAttributes, Map<String, String> newAttributes){ 766 if (StringUtils.isBlank(kimTypeId)) { 767 throw new RiceIllegalArgumentException("kimTypeId was null or blank"); 768 } 769 770 if (newAttributes == null) { 771 throw new RiceIllegalArgumentException("newAttributes was null or blank"); 772 } 773 774 if (originalAttributes == null) { 775 throw new RiceIllegalArgumentException("oldAttributes was null or blank"); 776 } 777 List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>(); 778 KimType kimType = getTypeInfoService().getKimType(kimTypeId); 779 List<String> uniqueAttributes = getUniqueAttributes(kimTypeId); 780 for(String attributeNameKey: uniqueAttributes){ 781 KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeNameKey); 782 String mainAttributeValue = getAttributeValue(originalAttributes, attributeNameKey); 783 String delegationAttributeValue = getAttributeValue(newAttributes, attributeNameKey); 784 785 if(!StringUtils.equals(mainAttributeValue, delegationAttributeValue)){ 786 validationErrors.add(RemotableAttributeError.Builder.create(attributeNameKey, DataDictionaryTypeServiceHelper 787 .createErrorString(RiceKeyConstants.ERROR_CANT_BE_MODIFIED, 788 dataDictionaryService.getAttributeLabel(attr.getKimAttribute().getComponentName(), 789 attributeNameKey))).build()); 790 } 791 } 792 return validationErrors; 793 } 794 795 protected List<String> getRequiredAttributes() { 796 return Collections.emptyList(); 797 } 798 799 protected boolean isCheckRequiredAttributes() { 800 return false; 801 } 802 803 protected String getClosestParentDocumentTypeName( 804 DocumentType documentType, 805 Set<String> potentialParentDocumentTypeNames) { 806 if ( potentialParentDocumentTypeNames == null || documentType == null ) { 807 return null; 808 } 809 if (potentialParentDocumentTypeNames.contains(documentType.getName())) { 810 return documentType.getName(); 811 } 812 if ((documentType.getParentId() == null) 813 || documentType.getParentId().equals( 814 documentType.getId())) { 815 return null; 816 } 817 return getClosestParentDocumentTypeName(getDocumentTypeService().getDocumentTypeById(documentType 818 .getParentId()), potentialParentDocumentTypeNames); 819 } 820 821 protected static class KimTypeAttributeValidationException extends RuntimeException { 822 823 protected KimTypeAttributeValidationException(String message) { 824 super( message ); 825 } 826 827 protected KimTypeAttributeValidationException(Throwable cause) { 828 super( cause ); 829 } 830 831 private static final long serialVersionUID = 8220618846321607801L; 832 833 } 834 835 protected static class KimTypeAttributeException extends RuntimeException { 836 837 protected KimTypeAttributeException(String message) { 838 super( message ); 839 } 840 841 protected KimTypeAttributeException(Throwable cause) { 842 super( cause ); 843 } 844 845 private static final long serialVersionUID = 8220618846321607801L; 846 847 } 848 849 protected KimTypeInfoService getTypeInfoService() { 850 if ( typeInfoService == null ) { 851 typeInfoService = KimApiServiceLocator.getKimTypeInfoService(); 852 } 853 return typeInfoService; 854 } 855 856 protected BusinessObjectService getBusinessObjectService() { 857 if ( businessObjectService == null ) { 858 businessObjectService = KNSServiceLocator.getBusinessObjectService(); 859 } 860 return businessObjectService; 861 } 862 863 protected DictionaryValidationService getDictionaryValidationService() { 864 if ( dictionaryValidationService == null ) { 865 dictionaryValidationService = KNSServiceLocator.getKNSDictionaryValidationService(); 866 } 867 return dictionaryValidationService; 868 } 869 870 protected DataDictionaryService getDataDictionaryService() { 871 if ( dataDictionaryService == null ) { 872 dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService(); 873 } 874 return this.dataDictionaryService; 875 } 876 877 878 protected DocumentTypeService getDocumentTypeService() { 879 if ( documentTypeService == null ) { 880 documentTypeService = KewApiServiceLocator.getDocumentTypeService(); 881 } 882 return this.documentTypeService; 883 } 884}