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