View Javadoc

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