001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.kim.impl.identity; 017 018import java.security.GeneralSecurityException; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.commons.beanutils.PropertyUtils; 030import org.apache.commons.lang.StringUtils; 031import org.apache.log4j.Logger; 032import org.kuali.rice.core.api.CoreApiServiceLocator; 033import org.kuali.rice.core.api.criteria.CountFlag; 034import org.kuali.rice.core.api.criteria.Predicate; 035import org.kuali.rice.core.api.criteria.PredicateUtils; 036import org.kuali.rice.core.api.criteria.QueryByCriteria; 037import org.kuali.rice.kim.api.identity.IdentityService; 038import org.kuali.rice.kim.api.identity.Person; 039import org.kuali.rice.kim.api.identity.PersonService; 040import org.kuali.rice.kim.api.identity.entity.EntityDefault; 041import org.kuali.rice.kim.api.identity.entity.EntityDefaultQueryResults; 042import org.kuali.rice.kim.api.identity.external.EntityExternalIdentifierType; 043import org.kuali.rice.kim.api.identity.principal.Principal; 044import org.kuali.rice.kim.api.identity.type.EntityTypeContactInfoDefault; 045import org.kuali.rice.kim.api.role.RoleService; 046import org.kuali.rice.kim.api.services.KimApiServiceLocator; 047import org.kuali.rice.kim.impl.KIMPropertyConstants; 048import org.kuali.rice.kim.impl.identity.principal.PrincipalBo; 049import org.kuali.rice.kns.service.BusinessObjectMetaDataService; 050import org.kuali.rice.kns.service.KNSServiceLocator; 051import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService; 052import org.kuali.rice.krad.bo.BusinessObject; 053import org.kuali.rice.krad.bo.DataObjectRelationship; 054import org.kuali.rice.krad.data.DataObjectWrapper; 055import org.kuali.rice.krad.data.KradDataServiceLocator; 056import org.kuali.rice.krad.lookup.CollectionIncomplete; 057import org.kuali.rice.krad.util.KRADConstants; 058import org.kuali.rice.krad.util.KRADPropertyConstants; 059import org.kuali.rice.krad.util.KRADUtils; 060import org.springframework.beans.PropertyAccessorUtils; 061 062/** 063 * This is a description of what this class does - kellerj don't forget to fill this in. 064 * 065 * @author Kuali Rice Team (rice.collab@kuali.org) 066 * 067 */ 068public class PersonServiceImpl implements PersonService { 069 070 private static Logger LOG = Logger.getLogger( PersonServiceImpl.class ); 071 protected static final String ENTITY_EXT_ID_PROPERTY_PREFIX = "externalIdentifiers."; 072 protected static final String ENTITY_AFFILIATION_PROPERTY_PREFIX = "affiliations."; 073 protected static final String ENTITY_TYPE_PROPERTY_PREFIX = "entityTypeContactInfos."; 074 protected static final String ENTITY_EMAIL_PROPERTY_PREFIX = "entityTypeContactInfos.emailAddresses."; 075 protected static final String ENTITY_PHONE_PROPERTY_PREFIX = "entityTypeContactInfos.phoneNumbers."; 076 protected static final String ENTITY_ADDRESS_PROPERTY_PREFIX = "entityTypeContactInfos.addresses."; 077 protected static final String ENTITY_NAME_PROPERTY_PREFIX = "names."; 078 protected static final String PRINCIPAL_PROPERTY_PREFIX = "principals."; 079 protected static final String ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX = "employmentInformation."; 080 // KULRICE-4442 Special handling for extension objects 081 protected static final String EXTENSION = "extension"; 082 083 private IdentityService identityService; 084 private RoleService roleService; 085 private BusinessObjectMetaDataService businessObjectMetaDataService; 086 private MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService; 087 088 protected List<String> personEntityTypeCodes = new ArrayList<String>( 4 ); 089 // String that can be passed to the lookup framework to create an type = X OR type = Y criteria 090 private String personEntityTypeLookupCriteria = null; 091 092 protected Map<String,String> baseLookupCriteria = new HashMap<String,String>(); 093 protected Map<String,String> criteriaConversion = new HashMap<String,String>(); 094 protected ArrayList<String> personCachePropertyNames = new ArrayList<String>(); 095 { 096 // init the criteria which will need to be applied to every lookup against 097 // the identity data tables 098 baseLookupCriteria.put( KIMPropertyConstants.Person.ACTIVE, "Y" ); 099 baseLookupCriteria.put( ENTITY_TYPE_PROPERTY_PREFIX + KRADPropertyConstants.ACTIVE, "Y" ); 100 101 // create the field mappings between the Person object and the KimEntity object 102 criteriaConversion.put( KIMPropertyConstants.Person.ENTITY_ID, KIMPropertyConstants.Entity.ID); 103 criteriaConversion.put( KIMPropertyConstants.Person.ACTIVE, PRINCIPAL_PROPERTY_PREFIX + KRADPropertyConstants.ACTIVE ); 104 criteriaConversion.put( KIMPropertyConstants.Person.PRINCIPAL_ID, PRINCIPAL_PROPERTY_PREFIX + KIMPropertyConstants.Person.PRINCIPAL_ID ); 105 criteriaConversion.put( KIMPropertyConstants.Person.PRINCIPAL_NAME, PRINCIPAL_PROPERTY_PREFIX + KIMPropertyConstants.Person.PRINCIPAL_NAME ); 106 criteriaConversion.put( KIMPropertyConstants.Person.FIRST_NAME, "names.firstName" ); 107 criteriaConversion.put( KIMPropertyConstants.Person.LAST_NAME, "names.lastName" ); 108 criteriaConversion.put( KIMPropertyConstants.Person.MIDDLE_NAME, "names.middleName" ); 109 criteriaConversion.put( KIMPropertyConstants.Person.EMAIL_ADDRESS, "entityTypeContactInfos.emailAddresses.emailAddress" ); 110 criteriaConversion.put( KIMPropertyConstants.Person.PHONE_NUMBER, "entityTypeContactInfos.phoneNumbers.phoneNumber" ); 111 criteriaConversion.put( KIMPropertyConstants.Person.ADDRESS_LINE_1, "entityTypeContactInfos.addresses.line1" ); 112 criteriaConversion.put( KIMPropertyConstants.Person.ADDRESS_LINE_2, "entityTypeContactInfos.addresses.line2" ); 113 criteriaConversion.put( KIMPropertyConstants.Person.ADDRESS_LINE_3, "entityTypeContactInfos.addresses.line3" ); 114 criteriaConversion.put( KIMPropertyConstants.Person.CITY, "entityTypeContactInfos.addresses.city" ); 115 criteriaConversion.put( KIMPropertyConstants.Person.STATE_CODE, "entityTypeContactInfos.addresses.stateProvinceCode" ); 116 criteriaConversion.put( KIMPropertyConstants.Person.POSTAL_CODE, "entityTypeContactInfos.addresses.postalCode" ); 117 criteriaConversion.put( KIMPropertyConstants.Person.COUNTRY_CODE, "entityTypeContactInfos.addresses.countryCode" ); 118 criteriaConversion.put( KIMPropertyConstants.Person.CAMPUS_CODE, "affiliations.campusCode" ); 119 criteriaConversion.put( KIMPropertyConstants.Person.AFFILIATION_TYPE_CODE, "affiliations.affiliationTypeCode" ); 120 criteriaConversion.put( KIMPropertyConstants.Person.EXTERNAL_IDENTIFIER_TYPE_CODE, "externalIdentifiers.externalIdentifierTypeCode" ); 121 criteriaConversion.put( KIMPropertyConstants.Person.EXTERNAL_ID, "externalIdentifiers.externalId" ); 122 criteriaConversion.put( KIMPropertyConstants.Person.EMPLOYEE_TYPE_CODE, "employmentInformation.employeeTypeCode" ); 123 criteriaConversion.put( KIMPropertyConstants.Person.EMPLOYEE_STATUS_CODE, "employmentInformation.employeeStatusCode" ); 124 criteriaConversion.put( KIMPropertyConstants.Person.EMPLOYEE_ID, "employmentInformation.employeeId" ); 125 criteriaConversion.put( KIMPropertyConstants.Person.BASE_SALARY_AMOUNT, "employmentInformation.baseSalaryAmount" ); 126 criteriaConversion.put( KIMPropertyConstants.Person.PRIMARY_DEPARTMENT_CODE, "employmentInformation.primaryDepartmentCode" ); 127 128 personCachePropertyNames.add( KIMPropertyConstants.Person.PRINCIPAL_ID ); 129 personCachePropertyNames.add( KIMPropertyConstants.Person.PRINCIPAL_NAME ); 130 personCachePropertyNames.add( KIMPropertyConstants.Person.ENTITY_ID ); 131 personCachePropertyNames.add( KIMPropertyConstants.Person.FIRST_NAME ); 132 personCachePropertyNames.add( KIMPropertyConstants.Person.LAST_NAME ); 133 personCachePropertyNames.add( KIMPropertyConstants.Person.MIDDLE_NAME ); 134 personCachePropertyNames.add( KIMPropertyConstants.Person.CAMPUS_CODE ); 135 personCachePropertyNames.add( KIMPropertyConstants.Person.EMPLOYEE_ID ); 136 personCachePropertyNames.add( KIMPropertyConstants.Person.PRIMARY_DEPARTMENT_CODE ); 137 } 138 139 140 /** 141 * @see org.kuali.rice.kim.api.identity.PersonService#getPerson(java.lang.String) 142 */ 143 @Override 144 public Person getPerson(String principalId) { 145 if ( StringUtils.isBlank(principalId) ) { 146 return null; 147 } 148 149 // get the corresponding principal 150 final Principal principal = getIdentityService().getPrincipal( principalId ); 151 // get the identity 152 if ( principal != null ) { 153 final EntityDefault entity = getIdentityService().getEntityDefault(principal.getEntityId()); 154 // convert the principal and identity to a Person 155 // skip if the person was created from the DB cache 156 if (entity != null ) { 157 return convertEntityToPerson( entity, principal ); 158 } 159 } 160 return null; 161 } 162 163 protected PersonImpl convertEntityToPerson( EntityDefault entity, Principal principal ) { 164 try { 165 // get the EntityEntityType for the EntityType corresponding to a Person 166 for ( String entityTypeCode : personEntityTypeCodes ) { 167 EntityTypeContactInfoDefault entType = entity.getEntityType( entityTypeCode ); 168 // if no "person" identity type present for the given principal, skip to the next type in the list 169 if ( entType == null ) { 170 continue; 171 } 172 // attach the principal and identity objects 173 // PersonImpl has logic to pull the needed elements from the KimEntity-related classes 174 return new PersonImpl( principal, entity, entityTypeCode ); 175 } 176 return null; 177 } catch ( Exception ex ) { 178 // allow runtime exceptions to pass through 179 if ( ex instanceof RuntimeException ) { 180 throw (RuntimeException)ex; 181 } 182 throw new RuntimeException( "Problem building person object", ex ); 183 } 184 } 185 186 187 /** 188 * @see org.kuali.rice.kim.api.identity.PersonService#getPersonByPrincipalName(java.lang.String) 189 */ 190 @Override 191 public Person getPersonByPrincipalName(String principalName) { 192 if ( StringUtils.isBlank(principalName) ) { 193 return null; 194 } 195 196 // get the corresponding principal 197 final Principal principal = getIdentityService().getPrincipalByPrincipalName( principalName ); 198 // get the identity 199 if ( principal != null ) { 200 final EntityDefault entity = getIdentityService().getEntityDefault(principal.getEntityId()); 201 202 // convert the principal and identity to a Person 203 if ( entity != null ) { 204 return convertEntityToPerson( entity, principal ); 205 } 206 } 207 return null; 208 } 209 210 @Override 211 public Person getPersonByEmployeeId(String employeeId) { 212 if ( StringUtils.isBlank( employeeId ) ) { 213 return null; 214 } 215 216 final List<Person> people = findPeople( Collections.singletonMap(KIMPropertyConstants.Person.EMPLOYEE_ID, employeeId) ); 217 if ( !people.isEmpty() ) { 218 return people.get(0); 219 220 } 221 222 // If no person was found above, check for inactive records 223 EntityDefault entity = getIdentityService().getEntityDefaultByEmployeeId(employeeId); 224 if (entity != null) { 225 if ( !entity.getPrincipals().isEmpty() ) { 226 Principal principal = getIdentityService().getPrincipal(entity.getPrincipals().get(0).getPrincipalId()); 227 if (principal != null) { 228 return convertEntityToPerson( entity, principal ); 229 } 230 } 231 } 232 233 return null; 234 } 235 236 /** 237 * @see org.kuali.rice.kim.api.identity.PersonService#findPeople(Map) 238 */ 239 @Override 240 public List<Person> findPeople(Map<String, String> criteria) { 241 return findPeople(criteria, true); 242 } 243 244 /** 245 * @see org.kuali.rice.kim.api.identity.PersonService#findPeople(java.util.Map, boolean) 246 */ 247 @Override 248 public List<Person> findPeople(Map<String, String> criteria, boolean unbounded) { 249 List<Person> people = null; 250 // protect from NPEs 251 if ( criteria == null ) { 252 criteria = Collections.emptyMap(); 253 } 254 // make a copy so it can be modified safely in this method 255 criteria = new HashMap<String, String>( criteria ); 256 257 // extract the role lookup parameters and then remove them since later code will not know what to do with them 258 String roleName = criteria.get( "lookupRoleName" ); 259 String namespaceCode = criteria.get( "lookupRoleNamespaceCode" ); 260 criteria.remove("lookupRoleName"); 261 criteria.remove("lookupRoleNamespaceCode"); 262 if ( StringUtils.isNotBlank(namespaceCode) && StringUtils.isNotBlank(roleName) ) { 263 Integer searchResultsLimit = org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit(PersonImpl.class); 264 int searchResultsLimitInt = Integer.MAX_VALUE; 265 if (searchResultsLimit != null) { 266 searchResultsLimitInt = searchResultsLimit.intValue(); 267 } 268 if ( LOG.isDebugEnabled() ) { 269 LOG.debug("Performing Person search including role filter: " + namespaceCode + "/" + roleName ); 270 } 271 if ( criteria.size() == 1 && criteria.containsKey(KIMPropertyConstants.Person.ACTIVE) ) { // if only active is specified 272 if ( LOG.isDebugEnabled() ) { 273 LOG.debug( "Only active criteria specified, running role search first" ); 274 } 275 // in this case, run the role lookup first and pass those results to the person lookup 276 Collection<String> principalIds = getRoleService().getRoleMemberPrincipalIds(namespaceCode, roleName, Collections.<String, String>emptyMap()); 277 StringBuffer sb = new StringBuffer(principalIds.size()*15); 278 Iterator<String> pi = principalIds.iterator(); 279 while ( pi.hasNext() ) { 280 sb.append( pi.next() ); 281 if ( pi.hasNext() ) { 282 sb.append( '|' ); 283 } 284 } 285 // add the list of principal IDs to the lookup so that only matching Person objects are returned 286 criteria.put( KIMPropertyConstants.Person.PRINCIPAL_ID, sb.toString() ); 287 people = findPeopleInternal(criteria, false); // can allow internal method to filter here since no more filtering necessary 288 } else if ( !criteria.isEmpty() ) { // i.e., person criteria are specified 289 if ( LOG.isDebugEnabled() ) { 290 LOG.debug( "Person criteria also specified, running that search first" ); 291 } 292 // run the person lookup first 293 people = findPeopleInternal(criteria, true); // get all, since may need to be filtered 294 // TODO - now check if these people have the given role 295 // build a principal list 296 List<String> principalIds = peopleToPrincipalIds( people ); 297 // get sublist of principals that have the given roles 298 principalIds = getRoleService().getPrincipalIdSubListWithRole(principalIds, namespaceCode, roleName, Collections.<String, String>emptyMap()); 299 // re-convert into people objects, wrapping in CollectionIncomplete if needed 300 if ( !unbounded && principalIds.size() > searchResultsLimitInt ) { 301 int actualResultSize = principalIds.size(); 302 // trim the list down before converting to people 303 principalIds = new ArrayList<String>(principalIds).subList(0, searchResultsLimitInt); // yes, this is a little wasteful 304 people = getPeople(principalIds); // convert the results to people 305 people = new CollectionIncomplete<Person>( people.subList(0, searchResultsLimitInt), new Long(actualResultSize) ); 306 } else { 307 people = getPeople(principalIds); 308 } 309 } else { // only role criteria specified 310 if ( LOG.isDebugEnabled() ) { 311 LOG.debug( "No Person criteria specified - only using role service." ); 312 } 313 // run the role criteria to get the principals with the role 314 Collection<String> principalIds = getRoleService().getRoleMemberPrincipalIds(namespaceCode, roleName, Collections.<String, String>emptyMap()); 315 if ( !unbounded && principalIds.size() > searchResultsLimitInt ) { 316 int actualResultSize = principalIds.size(); 317 // trim the list down before converting to people 318 principalIds = new ArrayList<String>(principalIds).subList(0, searchResultsLimitInt); // yes, this is a little wasteful 319 people = getPeople(principalIds); // convert the results to people 320 people = new CollectionIncomplete<Person>( people.subList(0, searchResultsLimitInt), new Long(actualResultSize) ); 321 } else { 322 people = getPeople(principalIds); // convert the results to people 323 } 324 } 325 } else { 326 if ( LOG.isDebugEnabled() ) { 327 LOG.debug( "No Role criteria specified, running person lookup as normal." ); 328 } 329 people = findPeopleInternal(criteria, unbounded); 330 } 331 332 // The following change is for KULRICE-5694 - It prevents duplicate rows from being returned for the 333 // person inquiry (In this case, duplicate meaning same entityId, principalId, and principalNm). 334 // This allows for multiple rows to be returned if an entityID has more then one principal name 335 // or more than one principal ID. 336 Set<String> peopleNoDupsSet = new HashSet<String>(); 337 List<Person> peopleNoDupsList = new ArrayList<Person>(); 338 339 for (Iterator<Person> iter = people.iterator(); iter.hasNext(); ) { 340 Person person = iter.next(); 341 if (peopleNoDupsSet.add(person.getEntityId() + person.getPrincipalId() + person.getPrincipalName())) { 342 peopleNoDupsList.add(person); 343 } 344 } 345 346 people.clear(); 347 people.addAll(peopleNoDupsList); 348 349 return people; 350 } 351 352 @SuppressWarnings("unchecked") 353 protected List<Person> findPeopleInternal(Map<String,String> criteria, boolean unbounded ) { 354 // convert the criteria to a form that can be used by the ORM layer 355 356 //TODO convert this to the new criteria predicates 357 Map<String,String> entityCriteria = convertPersonPropertiesToEntityProperties( criteria ); 358 359 Predicate predicate = PredicateUtils.convertMapToPredicate(entityCriteria); 360 361 QueryByCriteria.Builder queryBuilder = QueryByCriteria.Builder.create(); 362 queryBuilder.setPredicates(predicate); 363 364 if (!unbounded) { 365 Integer searchResultsLimit = org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit(PersonImpl.class); 366 if (searchResultsLimit != null && searchResultsLimit >= 0) { 367 queryBuilder.setMaxResults(searchResultsLimit); 368 queryBuilder.setCountFlag(CountFlag.INCLUDE); 369 } 370 } 371 372 List<Person> people = new ArrayList<Person>(); 373 374 EntityDefaultQueryResults qr = getIdentityService().findEntityDefaults( queryBuilder.build() ); 375 376 if (qr.getResults().size() > 0) { 377 378 for ( EntityDefault e : qr.getResults() ) { 379 // get to get all principals for the identity as well 380 if (e.getPrincipals().isEmpty()) { 381 PrincipalBo principalBo = new PrincipalBo(); 382 principalBo.setActive(false); 383 principalBo.setEntityId(e.getEntityId()); 384 principalBo.setPrincipalName("No Principal Name"); 385 // If the principal ID is not set, the person inquiry will not work 386 principalBo.setPrincipalId(e.getEntityId()); 387 people.add( convertEntityToPerson( e, PrincipalBo.to(principalBo) ) ); 388 } else { 389 for ( Principal p : e.getPrincipals() ) { 390 people.add( convertEntityToPerson( e, p ) ); 391 } 392 } 393 } 394 } else if (!qr.isMoreResultsAvailable() && entityCriteria.containsKey("principals.principalId")) { 395 HashMap<String,String> newEntityCriteria = new HashMap<String,String>(); 396 String principalID = entityCriteria.get("principals.principalId"); 397 newEntityCriteria.put( KIMPropertyConstants.Person.ACTIVE, "Y" ); 398 newEntityCriteria.put(KIMPropertyConstants.Entity.ID, principalID); 399 400 Predicate newPredicate = PredicateUtils.convertMapToPredicate(newEntityCriteria); 401 QueryByCriteria.Builder newQueryBuilder = QueryByCriteria.Builder.create(); 402 newQueryBuilder.setMaxResults(1); 403 newQueryBuilder.setCountFlag(CountFlag.INCLUDE); 404 newQueryBuilder.setPredicates(newPredicate); 405 EntityDefaultQueryResults newQr = getIdentityService().findEntityDefaults( newQueryBuilder.build() ); 406 String activeCriteria = entityCriteria.get(KIMPropertyConstants.Entity.ACTIVE); 407 if (activeCriteria != null && activeCriteria.equalsIgnoreCase(KRADConstants.YES_INDICATOR_VALUE)) { 408 if (newQr.getResults().size() > 0) { 409 for ( EntityDefault e : newQr.getResults() ) { 410 if (e.getPrincipals().isEmpty()) { 411 PrincipalBo principalBo = new PrincipalBo(); 412 principalBo.setActive(false); 413 principalBo.setEntityId(e.getEntityId()); 414 principalBo.setPrincipalName("No Principal Name"); 415 // if the principal ID is not set, the person inquiry will not work 416 principalBo.setPrincipalId(e.getEntityId()); 417 people.add( convertEntityToPerson( e, PrincipalBo.to(principalBo) ) ); 418 } 419 } 420 } 421 } else if (!(entityCriteria.containsKey(KIMPropertyConstants.Person.ACTIVE)) || (criteria.get(KIMPropertyConstants.Person.ACTIVE).equals("N"))) { 422 if (newQr.getResults().isEmpty()) { 423 424 String principalId = entityCriteria.get("principals.principalId"); 425 try { 426 EntityDefault entityDefault = getIdentityService().getEntityDefaultByPrincipalId(principalId); 427 for ( Principal p : entityDefault.getPrincipals() ) { 428 if (!p.isActive()){ 429 people.add( convertEntityToPerson(entityDefault, p ) ); 430 } 431 } 432 } catch ( Exception e ) { 433 LOG.info( "A principal Id of " + principalId + " dose not exist in the system"); 434 } 435 } 436 } 437 } else if (!qr.isMoreResultsAvailable() && entityCriteria.containsKey("principals.principalName")) { 438 if (!(entityCriteria.containsKey(KIMPropertyConstants.Person.ACTIVE)) || (criteria.get(KIMPropertyConstants.Person.ACTIVE).equals("N"))) { 439 440 HashMap<String,String> newEntityCriteria = new HashMap<String,String>(); 441 String principalName = entityCriteria.get("principals.principalName"); 442 newEntityCriteria.put( KIMPropertyConstants.Person.ACTIVE, KRADConstants.YES_INDICATOR_VALUE ); 443 newEntityCriteria.put("principals.principalName", principalName); 444 445 Predicate newPredicate = PredicateUtils.convertMapToPredicate(newEntityCriteria); 446 QueryByCriteria.Builder newQueryBuilder = QueryByCriteria.Builder.create(); 447 newQueryBuilder.setMaxResults(1); 448 newQueryBuilder.setCountFlag(CountFlag.INCLUDE); 449 newQueryBuilder.setPredicates(newPredicate); 450 EntityDefaultQueryResults newQr = getIdentityService().findEntityDefaults( newQueryBuilder.build() ); 451 452 if (newQr.getResults().isEmpty()) { 453 String principalNm = entityCriteria.get("principals.principalName"); 454 try { 455 EntityDefault entityDefault = getIdentityService().getEntityDefaultByPrincipalName(principalNm); 456 for ( Principal p : entityDefault.getPrincipals() ) { 457 if (!p.isActive()){ 458 people.add( convertEntityToPerson(entityDefault, p ) ); 459 } 460 } 461 } catch ( Exception e ) { 462 LOG.info( "A principal name of " + principalNm + " dose not exist in the system"); 463 } 464 } 465 } 466 } 467 return people; 468 } 469 470 public Map<String,String> convertPersonPropertiesToEntityProperties( Map<String,String> criteria ) { 471 if ( LOG.isDebugEnabled() ) { 472 LOG.debug( "convertPersonPropertiesToEntityProperties: " + criteria ); 473 } 474 boolean nameCriteria = false; 475 boolean addressCriteria = false; 476 boolean externalIdentifierCriteria = false; 477 boolean affiliationCriteria = false; 478 boolean affiliationDefaultOnlyCriteria = false; 479 boolean phoneCriteria = false; 480 boolean emailCriteria = false; 481 boolean employeeIdCriteria = false; 482 // add base lookups for all person lookups 483 HashMap<String,String> newCriteria = new HashMap<String,String>(); 484 newCriteria.putAll( baseLookupCriteria ); 485 486 newCriteria.put( "entityTypeContactInfos.entityTypeCode", personEntityTypeLookupCriteria ); 487 488 if ( criteria != null ) { 489 for ( String key : criteria.keySet() ) { 490 //check active radio button 491 // The following if statement enables the "both" button to work correctly. 492 if (!(criteria.containsKey(KIMPropertyConstants.Person.ACTIVE))) { 493 newCriteria.remove( KIMPropertyConstants.Person.ACTIVE ); 494 } 495 496 497 // if no value was passed, skip the entry in the Map 498 if ( StringUtils.isEmpty( criteria.get(key) ) ) { 499 continue; 500 } 501 // check if the value needs to be encrypted 502 // handle encrypted external identifiers 503 if ( key.equals( KIMPropertyConstants.Person.EXTERNAL_ID ) && StringUtils.isNotBlank(criteria.get(key)) ) { 504 // look for a ext ID type property 505 if ( criteria.containsKey( KIMPropertyConstants.Person.EXTERNAL_IDENTIFIER_TYPE_CODE ) ) { 506 String extIdTypeCode = criteria.get(KIMPropertyConstants.Person.EXTERNAL_IDENTIFIER_TYPE_CODE); 507 if ( StringUtils.isNotBlank(extIdTypeCode) ) { 508 // if found, load that external ID Type via service 509 EntityExternalIdentifierType extIdType = getIdentityService().getExternalIdentifierType(extIdTypeCode); 510 // if that type needs to be encrypted, encrypt the value in the criteria map 511 if ( extIdType != null && extIdType.isEncryptionRequired() ) { 512 try { 513 if(CoreApiServiceLocator.getEncryptionService().isEnabled()) { 514 criteria.put(key, 515 CoreApiServiceLocator.getEncryptionService().encrypt(criteria.get(key)) 516 ); 517 } 518 } catch (GeneralSecurityException ex) { 519 LOG.error("Unable to encrypt value for external ID search of type " + extIdTypeCode, ex ); 520 } 521 } 522 } 523 } 524 } 525 526 // convert the property to the Entity data model 527 String entityProperty = criteriaConversion.get( key ); 528 if ( entityProperty != null ) { 529 newCriteria.put( entityProperty, criteria.get( key ) ); 530 } else { 531 entityProperty = key; 532 // just pass it through if no translation present 533 newCriteria.put( key, criteria.get( key ) ); 534 } 535 // check if additional criteria are needed based on the types of properties specified 536 if ( isNameEntityCriteria( entityProperty ) ) { 537 nameCriteria = true; 538 } 539 if ( isExternalIdentifierEntityCriteria( entityProperty ) ) { 540 externalIdentifierCriteria = true; 541 } 542 if ( isAffiliationEntityCriteria( entityProperty ) ) { 543 affiliationCriteria = true; 544 } 545 if ( isAddressEntityCriteria( entityProperty ) ) { 546 addressCriteria = true; 547 } 548 if ( isPhoneEntityCriteria( entityProperty ) ) { 549 phoneCriteria = true; 550 } 551 if ( isEmailEntityCriteria( entityProperty ) ) { 552 emailCriteria = true; 553 } 554 if ( isEmployeeIdEntityCriteria( entityProperty ) ) { 555 employeeIdCriteria = true; 556 } 557 // special handling for the campus code, since that forces the query to look 558 // at the default affiliation record only 559 if ( key.equals( "campusCode" ) ) { 560 affiliationDefaultOnlyCriteria = true; 561 } 562 } 563 564 if ( nameCriteria ) { 565 newCriteria.put( ENTITY_NAME_PROPERTY_PREFIX + "active", "Y" ); 566 newCriteria.put( ENTITY_NAME_PROPERTY_PREFIX + "defaultValue", "Y" ); 567 //newCriteria.put(ENTITY_NAME_PROPERTY_PREFIX + "nameCode", "PRFR");//so we only display 1 result 568 } 569 if ( addressCriteria ) { 570 newCriteria.put( ENTITY_ADDRESS_PROPERTY_PREFIX + "active", "Y" ); 571 newCriteria.put( ENTITY_ADDRESS_PROPERTY_PREFIX + "defaultValue", "Y" ); 572 } 573 if ( phoneCriteria ) { 574 newCriteria.put( ENTITY_PHONE_PROPERTY_PREFIX + "active", "Y" ); 575 newCriteria.put( ENTITY_PHONE_PROPERTY_PREFIX + "defaultValue", "Y" ); 576 } 577 if ( emailCriteria ) { 578 newCriteria.put( ENTITY_EMAIL_PROPERTY_PREFIX + "active", "Y" ); 579 newCriteria.put( ENTITY_EMAIL_PROPERTY_PREFIX + "defaultValue", "Y" ); 580 } 581 if ( employeeIdCriteria ) { 582 newCriteria.put( ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX + "active", "Y" ); 583 newCriteria.put( ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX + "primary", "Y" ); 584 //KULRICE-12405: There is no reason to verify the person is a system or person when searching by only empl ID. 585 // Do not check the KRIM_ENTITY_ENT_TYP_T table if the employeeId is the only criteria passed in. 586 if (criteria.size() == 1) { 587 newCriteria.remove("entityTypeContactInfos.active"); 588 newCriteria.remove("entityTypeContactInfos.entityTypeCode"); 589 } 590 } 591 if ( affiliationCriteria ) { 592 newCriteria.put( ENTITY_AFFILIATION_PROPERTY_PREFIX + "active", "Y" ); 593 } 594 if ( affiliationDefaultOnlyCriteria ) { 595 newCriteria.put( ENTITY_AFFILIATION_PROPERTY_PREFIX + "defaultValue", "Y" ); 596 } 597 } 598 599 if ( LOG.isDebugEnabled() ) { 600 LOG.debug("Converted: " + newCriteria); 601 } 602 return newCriteria; 603 } 604 605 protected boolean isNameEntityCriteria( String propertyName ) { 606 return propertyName.startsWith( ENTITY_NAME_PROPERTY_PREFIX ); 607 } 608 protected boolean isAddressEntityCriteria( String propertyName ) { 609 return propertyName.startsWith( ENTITY_ADDRESS_PROPERTY_PREFIX ); 610 } 611 protected boolean isPhoneEntityCriteria( String propertyName ) { 612 return propertyName.startsWith( ENTITY_PHONE_PROPERTY_PREFIX ); 613 } 614 protected boolean isEmailEntityCriteria( String propertyName ) { 615 return propertyName.startsWith( ENTITY_EMAIL_PROPERTY_PREFIX ); 616 } 617 protected boolean isEmployeeIdEntityCriteria( String propertyName ) { 618 return propertyName.startsWith( ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX ); 619 } 620 protected boolean isAffiliationEntityCriteria( String propertyName ) { 621 return propertyName.startsWith( ENTITY_AFFILIATION_PROPERTY_PREFIX ); 622 } 623 protected boolean isExternalIdentifierEntityCriteria( String propertyName ) { 624 return propertyName.startsWith( ENTITY_EXT_ID_PROPERTY_PREFIX ); 625 } 626 627 /** 628 * Get the entityTypeCode that can be associated with a Person. This will determine 629 * where EntityType-related data is pulled from within the KimEntity object. The codes 630 * in the list will be examined in the order present. 631 */ 632 public List<String> getPersonEntityTypeCodes() { 633 return this.personEntityTypeCodes; 634 } 635 636 public void setPersonEntityTypeCodes(List<String> personEntityTypeCodes) { 637 this.personEntityTypeCodes = personEntityTypeCodes; 638 personEntityTypeLookupCriteria = null; 639 for ( String entityTypeCode : personEntityTypeCodes ) { 640 if ( personEntityTypeLookupCriteria == null ) { 641 personEntityTypeLookupCriteria = entityTypeCode; 642 } else { 643 personEntityTypeLookupCriteria = personEntityTypeLookupCriteria + "|" + entityTypeCode; 644 } 645 } 646 } 647 648 649 protected List<Person> getPeople( Collection<String> principalIds ) { 650 List<Person> people = new ArrayList<Person>( principalIds.size() ); 651 for ( String principalId : principalIds ) { 652 people.add( getPerson(principalId) ); 653 } 654 return people; 655 } 656 657 protected List<String> peopleToPrincipalIds( List<Person> people ) { 658 List<String> principalIds = new ArrayList<String>(); 659 660 for ( Person person : people ) { 661 principalIds.add( person.getPrincipalId() ); 662 } 663 664 return principalIds; 665 } 666 667 /** 668 * @see org.kuali.rice.kim.api.identity.PersonService#getPersonByExternalIdentifier(java.lang.String, java.lang.String) 669 */ 670 @Override 671 public List<Person> getPersonByExternalIdentifier(String externalIdentifierTypeCode, String externalId) { 672 if (StringUtils.isBlank( externalIdentifierTypeCode ) || StringUtils.isBlank( externalId ) ) { 673 return null; 674 } 675 Map<String,String> criteria = new HashMap<String,String>( 2 ); 676 criteria.put( KIMPropertyConstants.Person.EXTERNAL_IDENTIFIER_TYPE_CODE, externalIdentifierTypeCode ); 677 criteria.put( KIMPropertyConstants.Person.EXTERNAL_ID, externalId ); 678 return findPeople( criteria ); 679 } 680 681 /** 682 * @see org.kuali.rice.kim.api.identity.PersonService#updatePersonIfNecessary(java.lang.String, org.kuali.rice.kim.api.identity.Person) 683 */ 684 @Override 685 public Person updatePersonIfNecessary(String sourcePrincipalId, Person currentPerson ) { 686 if (currentPerson == null // no person set 687 || !StringUtils.equals(sourcePrincipalId, currentPerson.getPrincipalId() ) // principal ID mismatch 688 || currentPerson.getEntityId() == null ) { // syntheticially created Person object 689 Person person = getPerson( sourcePrincipalId ); 690 // if a synthetically created person object is present, leave it - required for property derivation and the UI layer for 691 // setting the principal name 692 if ( person == null ) { 693 if ( currentPerson != null && currentPerson.getEntityId() == null ) { 694 return currentPerson; 695 } 696 } 697 // if both are null, create an empty object for property derivation 698 if ( person == null && currentPerson == null ) { 699 try { 700 return new PersonImpl(); 701 } catch ( Exception ex ) { 702 LOG.error( "unable to instantiate an object of type: " + getPersonImplementationClass() + " - returning null", ex ); 703 return null; 704 } 705 } 706 return person; 707 } 708 // otherwise, no need to change the given object 709 return currentPerson; 710 } 711 712 /** 713 * Builds a map containing entries from the passed in Map that do NOT represent properties on an embedded 714 * Person object. 715 */ 716 private Map<String,String> getNonPersonSearchCriteria( Object bo, Map<String,String> fieldValues) { 717 Map<String,String> nonUniversalUserSearchCriteria = new HashMap<String,String>(); 718 for ( String propertyName : fieldValues.keySet() ) { 719 if (!isPersonProperty(bo, propertyName)) { 720 nonUniversalUserSearchCriteria.put(propertyName, fieldValues.get(propertyName)); 721 } 722 } 723 return nonUniversalUserSearchCriteria; 724 } 725 726 727 private boolean isPersonProperty(Object bo, String propertyName) { 728 try { 729 if (PropertyAccessorUtils.isNestedOrIndexedProperty( propertyName ) // is a nested property 730 && !StringUtils.contains(propertyName, "add.") ) {// exclude add line properties (due to path parsing problems in PropertyUtils.getPropertyType) 731 int lastIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(propertyName); 732 String propertyTypeName = lastIndex != -1 ? StringUtils.substring(propertyName, 0, lastIndex) : StringUtils.EMPTY; 733 Class<?> type = PropertyUtils.getPropertyType(bo, propertyTypeName); 734 // property type indicates a Person object 735 if ( type != null ) { 736 return Person.class.isAssignableFrom(type); 737 } 738 LOG.warn( "Unable to determine type of nested property: " + bo.getClass().getName() + " / " + propertyName ); 739 } 740 } catch (Exception ex) { 741 if ( LOG.isDebugEnabled() ) { 742 LOG.debug("Unable to determine if property on " + bo.getClass().getName() + " to a person object: " + propertyName, ex ); 743 } 744 } 745 return false; 746 } 747 748 /** 749 * @see org.kuali.rice.kim.api.identity.PersonService#resolvePrincipalNamesToPrincipalIds(org.kuali.rice.krad.bo.BusinessObject, java.util.Map) 750 */ 751 @Override 752 @SuppressWarnings("unchecked") 753 public Map<String,String> resolvePrincipalNamesToPrincipalIds(BusinessObject businessObject, Map<String,String> fieldValues) { 754 if ( fieldValues == null ) { 755 return null; 756 } 757 if ( businessObject == null ) { 758 return fieldValues; 759 } 760 StringBuffer resolvedPrincipalIdPropertyName = new StringBuffer(); 761 // save off all criteria which are not references to Person properties 762 // leave person properties out so they can be resolved and replaced by this method 763 Map<String,String> processedFieldValues = getNonPersonSearchCriteria(businessObject, fieldValues); 764 for ( String propertyName : fieldValues.keySet() ) { 765 if ( !StringUtils.isBlank(fieldValues.get(propertyName)) // property has a value 766 && isPersonProperty(businessObject, propertyName) // is a property on a Person object 767 ) { 768 // strip off the prefix on the property 769 int lastPropertyIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(propertyName); 770 String personPropertyName = lastPropertyIndex != -1 ? StringUtils.substring(propertyName, lastPropertyIndex + 1) : propertyName; 771 // special case - the user ID 772 if ( StringUtils.equals( KIMPropertyConstants.Person.PRINCIPAL_NAME, personPropertyName) ) { 773 Class targetBusinessObjectClass = null; 774 BusinessObject targetBusinessObject = null; 775 resolvedPrincipalIdPropertyName.setLength( 0 ); // clear the buffer without requiring a new object allocation on each iteration 776 // get the property name up until the ".principalName" 777 // this should be a reference to the Person object attached to the BusinessObject 778 String personReferenceObjectPropertyName = lastPropertyIndex != -1 ? StringUtils.substring(propertyName, 0, lastPropertyIndex) : StringUtils.EMPTY; 779 // check if the person was nested within another BO under the master BO. If so, go up one more level 780 // otherwise, use the passed in BO class as the target class 781 if ( PropertyAccessorUtils.isNestedOrIndexedProperty(personReferenceObjectPropertyName) ) { 782 int lastTargetIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(personReferenceObjectPropertyName); 783 String targetBusinessObjectPropertyName = lastTargetIndex != -1 ? StringUtils.substring(personReferenceObjectPropertyName, 0, lastTargetIndex) : StringUtils.EMPTY; 784 DataObjectWrapper<BusinessObject> wrapper = KradDataServiceLocator.getDataObjectService().wrap(businessObject); 785 targetBusinessObject = (BusinessObject) wrapper.getPropertyValueNullSafe(targetBusinessObjectPropertyName); 786 if (targetBusinessObject != null) { 787 targetBusinessObjectClass = targetBusinessObject.getClass(); 788 resolvedPrincipalIdPropertyName.append(targetBusinessObjectPropertyName).append("."); 789 } else { 790 LOG.error("Could not find target property '"+propertyName+"' in class "+businessObject.getClass().getName()+". Property value was null."); 791 } 792 } else { // not a nested Person property 793 targetBusinessObjectClass = businessObject.getClass(); 794 targetBusinessObject = businessObject; 795 } 796 797 if (targetBusinessObjectClass != null) { 798 // use the relationship metadata in the KNS to determine the property on the 799 // host business object to put back into the map now that the principal ID 800 // (the value stored in application tables) has been resolved 801 int lastIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(personReferenceObjectPropertyName); 802 String propName = lastIndex != -1 ? StringUtils.substring(personReferenceObjectPropertyName, lastIndex + 1) : personReferenceObjectPropertyName; 803 DataObjectRelationship rel = getBusinessObjectMetaDataService().getBusinessObjectRelationship( targetBusinessObject, propName ); 804 if ( rel != null ) { 805 String sourcePrimitivePropertyName = rel.getParentAttributeForChildAttribute(KIMPropertyConstants.Person.PRINCIPAL_ID); 806 resolvedPrincipalIdPropertyName.append(sourcePrimitivePropertyName); 807 // get the principal - for translation of the principalName to principalId 808 String principalName = fieldValues.get( propertyName ); 809 Principal principal = getIdentityService().getPrincipalByPrincipalName( principalName ); 810 if (principal != null ) { 811 processedFieldValues.put(resolvedPrincipalIdPropertyName.toString(), principal.getPrincipalId()); 812 } else { 813 processedFieldValues.put(resolvedPrincipalIdPropertyName.toString(), null); 814 try { 815 // if the principalName is bad, then we need to clear out the Person object 816 // and base principalId property 817 // so that their values are no longer accidentally used or re-populate 818 // the object 819 KRADUtils.setObjectProperty(targetBusinessObject, 820 resolvedPrincipalIdPropertyName.toString(), null); 821 KRADUtils.setObjectProperty(targetBusinessObject, propName, null ); 822 KRADUtils.setObjectProperty(targetBusinessObject, propName + ".principalName", principalName ); 823 } catch ( Exception ex ) { 824 LOG.error( "Unable to blank out the person object after finding that the person with the given principalName does not exist.", ex ); 825 } 826 } 827 } else { 828 LOG.error( "Missing relationship for " + propName + " on " + targetBusinessObjectClass.getName() ); 829 } 830 } else { // no target BO class - the code below probably will not work 831 processedFieldValues.put(resolvedPrincipalIdPropertyName.toString(), null); 832 } 833 } 834 // if the property does not seem to match the definition of a Person property but it 835 // does end in principalName then... 836 // this is to handle the case where the user ID is on an ADD line - a case excluded from isPersonProperty() 837 } else if (propertyName.endsWith("." + KIMPropertyConstants.Person.PRINCIPAL_NAME)){ 838 // if we're adding to a collection and we've got the principalName; let's populate universalUser 839 String principalName = fieldValues.get(propertyName); 840 if ( StringUtils.isNotEmpty( principalName ) ) { 841 String containerPropertyName = propertyName; 842 if (containerPropertyName.startsWith(KRADConstants.MAINTENANCE_ADD_PREFIX)) { 843 containerPropertyName = StringUtils.substringAfter( propertyName, KRADConstants.MAINTENANCE_ADD_PREFIX ); 844 } 845 // get the class of the object that is referenced by the property name 846 // if this is not true then there's a principalName collection or primitive attribute 847 // directly on the BO on the add line, so we just ignore that since something is wrong here 848 if ( PropertyAccessorUtils.isNestedOrIndexedProperty(containerPropertyName) ) { 849 // the first part of the property is the collection name 850 String collectionName = StringUtils.substringBefore( containerPropertyName, "." ); 851 // what is the class held by that collection? 852 // JHK: I don't like this. This assumes that this method is only used by the maintenance 853 // document service. If that will always be the case, this method should be moved over there. 854 Class<? extends BusinessObject> collectionBusinessObjectClass = getMaintenanceDocumentDictionaryService() 855 .getCollectionBusinessObjectClass( 856 getMaintenanceDocumentDictionaryService() 857 .getDocumentTypeName(businessObject.getClass()), collectionName); 858 if (collectionBusinessObjectClass != null) { 859 // we are adding to a collection; get the relationships for that object; 860 // is there one for personUniversalIdentifier? 861 List<DataObjectRelationship> relationships = 862 getBusinessObjectMetaDataService().getBusinessObjectRelationships( collectionBusinessObjectClass ); 863 // JHK: this seems like a hack - looking at all relationships for a BO does not guarantee that we get the right one 864 // JHK: why not inspect the objects like above? Is it the property path problems because of the .add. portion? 865 for ( DataObjectRelationship rel : relationships ) { 866 String parentAttribute = rel.getParentAttributeForChildAttribute( KIMPropertyConstants.Person.PRINCIPAL_ID ); 867 if ( parentAttribute == null ) { 868 continue; 869 } 870 // there is a relationship for personUserIdentifier; use that to find the universal user 871 processedFieldValues.remove( propertyName ); 872 String fieldPrefix = StringUtils.substringBeforeLast( StringUtils.substringBeforeLast( propertyName, "." + KIMPropertyConstants.Person.PRINCIPAL_NAME ), "." ); 873 String relatedPrincipalIdPropertyName = fieldPrefix + "." + parentAttribute; 874 // KR-683 Special handling for extension objects 875 if(EXTENSION.equals(StringUtils.substringAfterLast(fieldPrefix, ".")) && EXTENSION.equals(StringUtils.substringBefore(parentAttribute, "."))) 876 { 877 relatedPrincipalIdPropertyName = fieldPrefix + "." + StringUtils.substringAfter(parentAttribute, "."); 878 } 879 String currRelatedPersonPrincipalId = processedFieldValues.get(relatedPrincipalIdPropertyName); 880 if ( StringUtils.isBlank( currRelatedPersonPrincipalId ) ) { 881 Principal principal = getIdentityService().getPrincipalByPrincipalName( principalName ); 882 if ( principal != null ) { 883 processedFieldValues.put(relatedPrincipalIdPropertyName, principal.getPrincipalId()); 884 } else { 885 processedFieldValues.put(relatedPrincipalIdPropertyName, null); 886 } 887 } 888 } // relationship loop 889 } else { 890 if ( LOG.isDebugEnabled() ) { 891 LOG.debug( "Unable to determine class for collection referenced as part of property: " + containerPropertyName + " on " + businessObject.getClass().getName() ); 892 } 893 } 894 } else { 895 if ( LOG.isDebugEnabled() ) { 896 LOG.debug( "Non-nested property ending with 'principalName': " + containerPropertyName + " on " + businessObject.getClass().getName() ); 897 } 898 } 899 } 900 } 901 } 902 return processedFieldValues; 903 } 904 905 // OTHER METHODS 906 907 protected IdentityService getIdentityService() { 908 if ( identityService == null ) { 909 identityService = KimApiServiceLocator.getIdentityService(); 910 } 911 return identityService; 912 } 913 914 protected RoleService getRoleService() { 915 if ( roleService == null ) { 916 roleService = KimApiServiceLocator.getRoleService(); 917 } 918 return roleService; 919 } 920 921 922 @Override 923 public Class<? extends Person> getPersonImplementationClass() { 924 return PersonImpl.class; 925 } 926 927 protected BusinessObjectMetaDataService getBusinessObjectMetaDataService() { 928 if ( businessObjectMetaDataService == null ) { 929 businessObjectMetaDataService = KNSServiceLocator.getBusinessObjectMetaDataService(); 930 } 931 return businessObjectMetaDataService; 932 } 933 934 protected MaintenanceDocumentDictionaryService getMaintenanceDocumentDictionaryService() { 935 if ( maintenanceDocumentDictionaryService == null ) { 936 maintenanceDocumentDictionaryService = KNSServiceLocator.getMaintenanceDocumentDictionaryService(); 937 } 938 return maintenanceDocumentDictionaryService; 939 } 940}