001    /**
002     * Copyright 2005-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.kim.impl.identity;
017    
018    import org.apache.commons.beanutils.PropertyUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.apache.log4j.Logger;
021    import org.kuali.rice.core.api.CoreApiServiceLocator;
022    import org.kuali.rice.core.api.criteria.Predicate;
023    import org.kuali.rice.core.api.criteria.PredicateUtils;
024    import org.kuali.rice.core.api.criteria.QueryByCriteria;
025    import org.kuali.rice.kim.api.identity.IdentityService;
026    import org.kuali.rice.kim.api.identity.Person;
027    import org.kuali.rice.kim.api.identity.PersonService;
028    import org.kuali.rice.kim.api.identity.entity.EntityDefault;
029    import org.kuali.rice.kim.api.identity.entity.EntityDefaultQueryResults;
030    import org.kuali.rice.kim.api.identity.external.EntityExternalIdentifierType;
031    import org.kuali.rice.kim.api.identity.principal.Principal;
032    import org.kuali.rice.kim.api.identity.type.EntityTypeContactInfoDefault;
033    import org.kuali.rice.kim.api.role.RoleService;
034    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
035    import org.kuali.rice.kim.impl.KIMPropertyConstants;
036    import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
037    import org.kuali.rice.kns.service.KNSServiceLocator;
038    import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService;
039    import org.kuali.rice.krad.bo.BusinessObject;
040    import org.kuali.rice.krad.bo.DataObjectRelationship;
041    import org.kuali.rice.krad.lookup.CollectionIncomplete;
042    import org.kuali.rice.krad.util.KRADConstants;
043    import org.kuali.rice.krad.util.KRADPropertyConstants;
044    import org.kuali.rice.krad.util.ObjectUtils;
045    
046    import java.security.GeneralSecurityException;
047    import java.util.ArrayList;
048    import java.util.Collection;
049    import java.util.Collections;
050    import java.util.HashMap;
051    import java.util.HashSet;
052    import java.util.Iterator;
053    import java.util.List;
054    import java.util.Map;
055    import java.util.Set;
056    
057    /**
058     * This is a description of what this class does - kellerj don't forget to fill this in. 
059     * 
060     * @author Kuali Rice Team (rice.collab@kuali.org)
061     *
062     */
063    public class PersonServiceImpl implements PersonService {
064    
065            private static Logger LOG = Logger.getLogger( PersonServiceImpl.class );
066            protected static final String ENTITY_EXT_ID_PROPERTY_PREFIX = "externalIdentifiers.";
067            protected static final String ENTITY_AFFILIATION_PROPERTY_PREFIX = "affiliations.";
068            protected static final String ENTITY_TYPE_PROPERTY_PREFIX = "entityTypeContactInfos.";
069            protected static final String ENTITY_EMAIL_PROPERTY_PREFIX = "entityTypeContactInfos.emailAddresses.";
070            protected static final String ENTITY_PHONE_PROPERTY_PREFIX = "entityTypeContactInfos.phoneNumbers.";
071            protected static final String ENTITY_ADDRESS_PROPERTY_PREFIX = "entityTypeContactInfos.addresses.";
072            protected static final String ENTITY_NAME_PROPERTY_PREFIX = "names.";
073            protected static final String PRINCIPAL_PROPERTY_PREFIX = "principals.";
074            protected static final String ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX = "employmentInformation.";
075            // KULRICE-4442 Special handling for extension objects
076            protected static final String EXTENSION = "extension";
077            
078            private IdentityService identityService;
079            private RoleService roleService;
080            private BusinessObjectMetaDataService businessObjectMetaDataService;
081            private MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService;
082    
083            protected List<String> personEntityTypeCodes = new ArrayList<String>( 4 );
084            // String that can be passed to the lookup framework to create an type = X OR type = Y criteria
085            private String personEntityTypeLookupCriteria = null;
086        
087            protected Map<String,String> baseLookupCriteria = new HashMap<String,String>();
088            protected Map<String,String> criteriaConversion = new HashMap<String,String>();
089            protected ArrayList<String> personCachePropertyNames = new ArrayList<String>();
090            {
091                    // init the criteria which will need to be applied to every lookup against
092                    // the identity data tables
093                    baseLookupCriteria.put( KIMPropertyConstants.Person.ACTIVE, "Y" );
094                    baseLookupCriteria.put( ENTITY_TYPE_PROPERTY_PREFIX + KRADPropertyConstants.ACTIVE, "Y" );
095                    
096                    // create the field mappings between the Person object and the KimEntity object
097                    criteriaConversion.put( KIMPropertyConstants.Person.ENTITY_ID, KIMPropertyConstants.Entity.ID);
098                    criteriaConversion.put( KIMPropertyConstants.Person.ACTIVE, PRINCIPAL_PROPERTY_PREFIX + KRADPropertyConstants.ACTIVE );
099                    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                    
214                // If no person was found above, check for inactive records
215            EntityDefault entity = getIdentityService().getEntityDefaultByEmployeeId(employeeId);
216            if (entity != null) {
217                if ( !entity.getPrincipals().isEmpty() ) {
218                    Principal principal = getIdentityService().getPrincipal(entity.getPrincipals().get(0).getPrincipalId());
219                    if (principal != null) {
220                        return convertEntityToPerson( entity, principal );
221                    }  
222                }
223            }
224    
225                    return null;
226            }
227            
228            /**
229             * @see org.kuali.rice.kim.api.identity.PersonService#findPeople(Map)
230             */
231            public List<Person> findPeople(Map<String, String> criteria) {
232                    return findPeople(criteria, true);
233            }
234            
235            /**
236             * @see org.kuali.rice.kim.api.identity.PersonService#findPeople(java.util.Map, boolean)
237             */
238            public List<Person> findPeople(Map<String, String> criteria, boolean unbounded) {
239                    List<Person> people = null;
240                    // protect from NPEs
241                    if ( criteria == null ) {
242                            criteria = Collections.emptyMap();
243                    }
244                    // make a copy so it can be modified safely in this method
245                    criteria = new HashMap<String, String>( criteria );
246                    
247                    // extract the role lookup parameters and then remove them since later code will not know what to do with them
248                    String roleName = criteria.get( "lookupRoleName" );
249                    String namespaceCode = criteria.get( "lookupRoleNamespaceCode" );
250                    criteria.remove("lookupRoleName");
251                    criteria.remove("lookupRoleNamespaceCode");
252                    if ( StringUtils.isNotBlank(namespaceCode) && StringUtils.isNotBlank(roleName) ) {
253                            Integer searchResultsLimit = org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit(PersonImpl.class);
254                            int searchResultsLimitInt = Integer.MAX_VALUE;
255                            if (searchResultsLimit != null) {
256                                    searchResultsLimitInt = searchResultsLimit.intValue();
257                            }
258                            if ( LOG.isDebugEnabled() ) {
259                                    LOG.debug("Performing Person search including role filter: " + namespaceCode + "/" + roleName );
260                            }
261                            if ( criteria.size() == 1 && criteria.containsKey(KIMPropertyConstants.Person.ACTIVE) ) { // if only active is specified
262                                    if ( LOG.isDebugEnabled() ) {
263                                            LOG.debug( "Only active criteria specified, running role search first" );
264                                    }
265                                    // in this case, run the role lookup first and pass those results to the person lookup
266                                    Collection<String> principalIds = getRoleService().getRoleMemberPrincipalIds(namespaceCode, roleName,  Collections.<String, String>emptyMap());
267                                    StringBuffer sb = new StringBuffer(principalIds.size()*15);
268                                    Iterator<String> pi = principalIds.iterator();
269                                    while ( pi.hasNext() ) {
270                                            sb.append( pi.next() );
271                                            if ( pi.hasNext() ) sb.append( '|' );
272                                    }
273                                    // add the list of principal IDs to the lookup so that only matching Person objects are returned
274                                    criteria.put( KIMPropertyConstants.Person.PRINCIPAL_ID, sb.toString() );
275                                    people = findPeopleInternal(criteria, false); // can allow internal method to filter here since no more filtering necessary                             
276                            } else if ( !criteria.isEmpty() ) { // i.e., person criteria are specified
277                                    if ( LOG.isDebugEnabled() ) {
278                                            LOG.debug( "Person criteria also specified, running that search first" );
279                                    }
280                                    // run the person lookup first
281                                    people = findPeopleInternal(criteria, true); // get all, since may need to be filtered
282                                    // TODO - now check if these people have the given role
283                                    // build a principal list
284                                    List<String> principalIds = peopleToPrincipalIds( people );
285                                    // get sublist of principals that have the given roles
286                                    principalIds = getRoleService().getPrincipalIdSubListWithRole(principalIds, namespaceCode, roleName,  Collections.<String, String>emptyMap());
287                                    // re-convert into people objects, wrapping in CollectionIncomplete if needed
288                                    if ( !unbounded && principalIds.size() > searchResultsLimitInt ) {
289                                            int actualResultSize = principalIds.size();
290                                            // trim the list down before converting to people
291                                            principalIds = new ArrayList<String>(principalIds).subList(0, searchResultsLimitInt); // yes, this is a little wasteful
292                                            people = getPeople(principalIds); // convert the results to people
293                                            people = new CollectionIncomplete<Person>( people.subList(0, searchResultsLimitInt), new Long(actualResultSize) );
294                                    } else {
295                                            people = getPeople(principalIds);
296                                    }
297                            } else { // only role criteria specified
298                                    if ( LOG.isDebugEnabled() ) {
299                                            LOG.debug( "No Person criteria specified - only using role service." );
300                                    }
301                                    // run the role criteria to get the principals with the role
302                                    Collection<String> principalIds = getRoleService().getRoleMemberPrincipalIds(namespaceCode, roleName,  Collections.<String, String>emptyMap());
303                                    if ( !unbounded && principalIds.size() > searchResultsLimitInt ) {
304                                            int actualResultSize = principalIds.size();
305                                            // trim the list down before converting to people
306                                            principalIds = new ArrayList<String>(principalIds).subList(0, searchResultsLimitInt); // yes, this is a little wasteful
307                                            people = getPeople(principalIds); // convert the results to people
308                                            people = new CollectionIncomplete<Person>( people.subList(0, searchResultsLimitInt), new Long(actualResultSize) );
309                                    } else {
310                                            people = getPeople(principalIds); // convert the results to people
311                                    }
312                            }
313                    } else {
314                            if ( LOG.isDebugEnabled() ) {
315                                    LOG.debug( "No Role criteria specified, running person lookup as normal." );
316                            }
317                            people = findPeopleInternal(criteria, unbounded);
318                    }
319                            
320                    // The following change is for KULRICE-5694 - It prevents duplicate rows from being returned for the 
321                    // person inquiry (In this case, duplicate meaning same entityId, principalId, and principalNm).  
322                    // This allows for multiple rows to be returned if an entityID has more then one principal name
323                    // or more than one principal ID.  
324            Set<String> peopleNoDupsSet = new HashSet<String>();
325            List<Person> peopleNoDupsList = new ArrayList<Person>();
326    
327                for (Iterator<Person> iter = people.iterator(); iter.hasNext(); ) {
328                    Person person = iter.next();
329                    if (peopleNoDupsSet.add(person.getEntityId() + person.getPrincipalId() + person.getPrincipalName())) {
330                        peopleNoDupsList.add(person);
331                    }
332                }
333                 
334                people.clear();
335                people.addAll(peopleNoDupsList);
336                    
337                return people;
338            }
339            
340            @SuppressWarnings("unchecked")
341            protected List<Person> findPeopleInternal(Map<String,String> criteria, boolean unbounded ) {
342                    // convert the criteria to a form that can be used by the ORM layer
343    
344            //TODO convert this to the new criteria predicates
345                    Map<String,String> entityCriteria = convertPersonPropertiesToEntityProperties( criteria );
346    
347            Predicate predicate = PredicateUtils.convertMapToPredicate(entityCriteria);
348    
349            QueryByCriteria.Builder queryBuilder = QueryByCriteria.Builder.create();
350            queryBuilder.setPredicates(predicate);
351    
352                    List<Person> people = new ArrayList<Person>();
353    
354                    EntityDefaultQueryResults qr = getIdentityService().findEntityDefaults( queryBuilder.build() );
355    
356                    for ( EntityDefault e : qr.getResults() ) {
357                            // get to get all principals for the identity as well
358                            for ( Principal p : e.getPrincipals() ) {
359                                    people.add( convertEntityToPerson( e, p ) );
360                            }
361                    }
362    
363                    return people;
364            }
365    
366            public Map<String,String> convertPersonPropertiesToEntityProperties( Map<String,String> criteria ) {
367                    if ( LOG.isDebugEnabled() ) {
368                            LOG.debug( "convertPersonPropertiesToEntityProperties: " + criteria );
369                    }
370                    boolean nameCriteria = false;
371                    boolean addressCriteria = false;
372                    boolean externalIdentifierCriteria = false;
373                    boolean affiliationCriteria = false;
374                    boolean affiliationDefaultOnlyCriteria = false;
375                    boolean phoneCriteria = false;
376                    boolean emailCriteria = false;
377                    boolean employeeIdCriteria = false;
378                    // add base lookups for all person lookups
379                    HashMap<String,String> newCriteria = new HashMap<String,String>();
380                    newCriteria.putAll( baseLookupCriteria );
381    
382                    newCriteria.put( "entityTypeContactInfos.entityTypeCode", personEntityTypeLookupCriteria );
383    
384            if ( criteria != null ) {
385                            for ( String key : criteria.keySet() ) {
386                                //check active radio button
387                        if(key.equals(KIMPropertyConstants.Person.ACTIVE)) {
388                            newCriteria.put(KIMPropertyConstants.Person.ACTIVE, criteria.get(KIMPropertyConstants.Person.ACTIVE));
389                        } else {
390                            // The following if statement enables the "both" button to work correctly.
391                            if (!(criteria.containsKey(KIMPropertyConstants.Person.ACTIVE))) {
392                                newCriteria.remove( KIMPropertyConstants.Person.ACTIVE );
393                            }
394                        }
395                        
396                                    // if no value was passed, skip the entry in the Map
397                                    if ( StringUtils.isEmpty( criteria.get(key) ) ) {
398                                            continue;
399                                    }
400                                    // check if the value needs to be encrypted
401                                    // handle encrypted external identifiers
402                                    if ( key.equals( KIMPropertyConstants.Person.EXTERNAL_ID ) && StringUtils.isNotBlank(criteria.get(key)) ) {
403                                            // look for a ext ID type property
404                                            if ( criteria.containsKey( KIMPropertyConstants.Person.EXTERNAL_IDENTIFIER_TYPE_CODE ) ) {
405                                                    String extIdTypeCode = criteria.get(KIMPropertyConstants.Person.EXTERNAL_IDENTIFIER_TYPE_CODE);
406                                                    if ( StringUtils.isNotBlank(extIdTypeCode) ) {
407                                                            // if found, load that external ID Type via service
408                                                            EntityExternalIdentifierType extIdType = getIdentityService().getExternalIdentifierType(extIdTypeCode);
409                                                            // if that type needs to be encrypted, encrypt the value in the criteria map
410                                                            if ( extIdType != null && extIdType.isEncryptionRequired() ) {
411                                                                    try {
412                                                                            criteria.put(key, 
413                                                                                            CoreApiServiceLocator.getEncryptionService().encrypt(criteria.get(key))
414                                                                                            );
415                                                                    } catch (GeneralSecurityException ex) {
416                                                                            LOG.error("Unable to encrypt value for external ID search of type " + extIdTypeCode, ex );
417                                                                    }                                                               
418                                                            }
419                                                    }
420                                            }
421                                    }
422                                    
423                                    // convert the property to the Entity data model
424                                    String entityProperty = criteriaConversion.get( key );
425                                    if ( entityProperty != null ) {
426                                            newCriteria.put( entityProperty, criteria.get( key ) );
427                                    } else {
428                                            entityProperty = key;
429                                            // just pass it through if no translation present
430                                            newCriteria.put( key, criteria.get( key ) );
431                                    }
432                                    // check if additional criteria are needed based on the types of properties specified
433                                    if ( isNameEntityCriteria( entityProperty ) ) {
434                                            nameCriteria = true;
435                                    }
436                                    if ( isExternalIdentifierEntityCriteria( entityProperty ) ) {
437                                            externalIdentifierCriteria = true;
438                                    }
439                                    if ( isAffiliationEntityCriteria( entityProperty ) ) {
440                                            affiliationCriteria = true;
441                                    }
442                                    if ( isAddressEntityCriteria( entityProperty ) ) {
443                                            addressCriteria = true;
444                                    }
445                                    if ( isPhoneEntityCriteria( entityProperty ) ) {
446                                            phoneCriteria = true;
447                                    }
448                                    if ( isEmailEntityCriteria( entityProperty ) ) {
449                                            emailCriteria = true;
450                                    }
451                                    if ( isEmployeeIdEntityCriteria( entityProperty ) ) {
452                                            employeeIdCriteria = true;
453                                    }                               
454                                    // special handling for the campus code, since that forces the query to look
455                                    // at the default affiliation record only
456                                    if ( key.equals( "campusCode" ) ) {
457                                            affiliationDefaultOnlyCriteria = true;
458                                    }
459                            } 
460                            
461                            if ( nameCriteria ) {
462                                    newCriteria.put( ENTITY_NAME_PROPERTY_PREFIX + "active", "Y" );
463                                    newCriteria.put( ENTITY_NAME_PROPERTY_PREFIX + "defaultValue", "Y" );
464                                    //newCriteria.put(ENTITY_NAME_PROPERTY_PREFIX + "nameCode", "PRFR");//so we only display 1 result
465                            }
466                            if ( addressCriteria ) {
467                                    newCriteria.put( ENTITY_ADDRESS_PROPERTY_PREFIX + "active", "Y" );
468                                    newCriteria.put( ENTITY_ADDRESS_PROPERTY_PREFIX + "defaultValue", "Y" );
469                            }
470                            if ( phoneCriteria ) {
471                                    newCriteria.put( ENTITY_PHONE_PROPERTY_PREFIX + "active", "Y" );
472                                    newCriteria.put( ENTITY_PHONE_PROPERTY_PREFIX + "defaultValue", "Y" );
473                            }
474                            if ( emailCriteria ) {
475                                    newCriteria.put( ENTITY_EMAIL_PROPERTY_PREFIX + "active", "Y" );
476                                    newCriteria.put( ENTITY_EMAIL_PROPERTY_PREFIX + "defaultValue", "Y" );
477                            }
478                            if ( employeeIdCriteria ) {
479                                    newCriteria.put( ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX + "active", "Y" );
480                                    newCriteria.put( ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX + "primary", "Y" );
481                            }
482                            if ( affiliationCriteria ) {
483                                    newCriteria.put( ENTITY_AFFILIATION_PROPERTY_PREFIX + "active", "Y" );
484                            }
485                            if ( affiliationDefaultOnlyCriteria ) {
486                                    newCriteria.put( ENTITY_AFFILIATION_PROPERTY_PREFIX + "defaultValue", "Y" );
487                            } 
488            }   
489                    
490                    if ( LOG.isDebugEnabled() ) {
491                            LOG.debug( "Converted: " + newCriteria );
492                    }
493                    return newCriteria;             
494            }
495    
496            protected boolean isNameEntityCriteria( String propertyName ) {
497                    return propertyName.startsWith( ENTITY_NAME_PROPERTY_PREFIX );
498            }
499            protected boolean isAddressEntityCriteria( String propertyName ) {
500                    return propertyName.startsWith( ENTITY_ADDRESS_PROPERTY_PREFIX );
501            }
502            protected boolean isPhoneEntityCriteria( String propertyName ) {
503                    return propertyName.startsWith( ENTITY_PHONE_PROPERTY_PREFIX );
504            }
505            protected boolean isEmailEntityCriteria( String propertyName ) {
506                    return propertyName.startsWith( ENTITY_EMAIL_PROPERTY_PREFIX );
507            }
508            protected boolean isEmployeeIdEntityCriteria( String propertyName ) {
509                    return propertyName.startsWith( ENTITY_EMPLOYEE_ID_PROPERTY_PREFIX );
510            }
511            protected boolean isAffiliationEntityCriteria( String propertyName ) {
512                    return propertyName.startsWith( ENTITY_AFFILIATION_PROPERTY_PREFIX );
513            }
514            protected boolean isExternalIdentifierEntityCriteria( String propertyName ) {
515                    return propertyName.startsWith( ENTITY_EXT_ID_PROPERTY_PREFIX );
516            }
517            
518            /**
519             * Get the entityTypeCode that can be associated with a Person.  This will determine
520             * where EntityType-related data is pulled from within the KimEntity object.  The codes
521             * in the list will be examined in the order present.
522             */
523            public List<String> getPersonEntityTypeCodes() {
524                    return this.personEntityTypeCodes;
525            }
526    
527            public void setPersonEntityTypeCodes(List<String> personEntityTypeCodes) {
528                    this.personEntityTypeCodes = personEntityTypeCodes;
529                    personEntityTypeLookupCriteria = null;
530                    for ( String entityTypeCode : personEntityTypeCodes ) {
531                            if ( personEntityTypeLookupCriteria == null ) {
532                                    personEntityTypeLookupCriteria = entityTypeCode;
533                            } else {
534                                    personEntityTypeLookupCriteria = personEntityTypeLookupCriteria + "|" + entityTypeCode;
535                            }
536                    }
537            }
538    
539            
540            protected List<Person> getPeople( Collection<String> principalIds ) {
541                    List<Person> people = new ArrayList<Person>( principalIds.size() );
542                    for ( String principalId : principalIds ) {
543                            people.add( getPerson(principalId) );
544                    }
545                    return people;
546            }
547            
548            protected List<String> peopleToPrincipalIds( List<Person> people ) {
549                    List<String> principalIds = new ArrayList<String>();
550                    
551                    for ( Person person : people ) {
552                            principalIds.add( person.getPrincipalId() );
553                    }
554                    
555                    return principalIds;
556            }
557            
558            /**
559             * @see org.kuali.rice.kim.api.identity.PersonService#getPersonByExternalIdentifier(java.lang.String, java.lang.String)
560             */
561            public List<Person> getPersonByExternalIdentifier(String externalIdentifierTypeCode, String externalId) {
562                    if (StringUtils.isBlank( externalIdentifierTypeCode ) || StringUtils.isBlank( externalId ) ) {
563                            return null;
564                    }
565                    Map<String,String> criteria = new HashMap<String,String>( 2 );
566                    criteria.put( KIMPropertyConstants.Person.EXTERNAL_IDENTIFIER_TYPE_CODE, externalIdentifierTypeCode );
567                    criteria.put( KIMPropertyConstants.Person.EXTERNAL_ID, externalId );
568                    return findPeople( criteria );
569            }
570            
571            /**
572             * @see org.kuali.rice.kim.api.identity.PersonService#updatePersonIfNecessary(java.lang.String, org.kuali.rice.kim.api.identity.Person)
573             */
574        public Person updatePersonIfNecessary(String sourcePrincipalId, Person currentPerson ) {
575            if (currentPerson  == null // no person set
576                    || !StringUtils.equals(sourcePrincipalId, currentPerson.getPrincipalId() ) // principal ID mismatch
577                    || currentPerson.getEntityId() == null ) { // syntheticially created Person object
578                Person person = getPerson( sourcePrincipalId );
579                // if a synthetically created person object is present, leave it - required for property derivation and the UI layer for
580                // setting the principal name
581                if ( person == null ) {
582                    if ( currentPerson != null && currentPerson.getEntityId() == null ) {
583                        return currentPerson;
584                    }
585                }
586                // if both are null, create an empty object for property derivation
587                if ( person == null && currentPerson == null ) {
588                    try {
589                            return new PersonImpl();
590                    } catch ( Exception ex ) {
591                            LOG.error( "unable to instantiate an object of type: " + getPersonImplementationClass() + " - returning null", ex );
592                            return null;
593                    }
594                }
595                return person;
596            }
597            // otherwise, no need to change the given object
598            return currentPerson;
599        }
600    
601        /**
602         * Builds a map containing entries from the passed in Map that do NOT represent properties on an embedded
603         * Person object.
604         */
605        private Map<String,String> getNonPersonSearchCriteria( BusinessObject bo, Map<String,String> fieldValues) {
606            Map<String,String> nonUniversalUserSearchCriteria = new HashMap<String,String>();
607            for ( String propertyName : fieldValues.keySet() ) {
608                if (!isPersonProperty(bo, propertyName)) {
609                    nonUniversalUserSearchCriteria.put(propertyName, fieldValues.get(propertyName));
610                }
611            }
612            return nonUniversalUserSearchCriteria;
613        }
614    
615    
616        private boolean isPersonProperty(BusinessObject bo, String propertyName) {
617            try {
618                    if ( ObjectUtils.isNestedAttribute( propertyName ) // is a nested property
619                            && !StringUtils.contains(propertyName, "add.") ) {// exclude add line properties (due to path parsing problems in PropertyUtils.getPropertyType)
620                            Class<?> type = PropertyUtils.getPropertyType(bo, ObjectUtils.getNestedAttributePrefix( propertyName ));
621                            // property type indicates a Person object
622                            if ( type != null ) {
623                                    return Person.class.isAssignableFrom(type);
624                            }
625                            LOG.warn( "Unable to determine type of nested property: " + bo.getClass().getName() + " / " + propertyName );
626                    }
627            } catch (Exception ex) {
628                    if ( LOG.isDebugEnabled() ) {
629                            LOG.debug("Unable to determine if property on " + bo.getClass().getName() + " to a person object: " + propertyName, ex );
630                    }
631            }
632            return false;
633        }
634        
635        /**
636         * @see org.kuali.rice.kim.api.identity.PersonService#resolvePrincipalNamesToPrincipalIds(org.kuali.rice.krad.bo.BusinessObject, java.util.Map)
637         */
638        @SuppressWarnings("unchecked")
639            public Map<String,String> resolvePrincipalNamesToPrincipalIds(BusinessObject businessObject, Map<String,String> fieldValues) {
640            if ( fieldValues == null ) {
641                    return null;
642            }
643            if ( businessObject == null ) {
644                    return fieldValues;
645            }
646            StringBuffer resolvedPrincipalIdPropertyName = new StringBuffer();
647            // save off all criteria which are not references to Person properties
648            // leave person properties out so they can be resolved and replaced by this method
649            Map<String,String> processedFieldValues = getNonPersonSearchCriteria(businessObject, fieldValues);
650            for ( String propertyName : fieldValues.keySet() ) {            
651                if (        !StringUtils.isBlank(fieldValues.get(propertyName))  // property has a value
652                            && isPersonProperty(businessObject, propertyName) // is a property on a Person object
653                            ) {
654                    // strip off the prefix on the property
655                    String personPropertyName = ObjectUtils.getNestedAttributePrimitive( propertyName );
656                    // special case - the user ID 
657                    if ( StringUtils.equals( KIMPropertyConstants.Person.PRINCIPAL_NAME, personPropertyName) ) {
658                        Class targetBusinessObjectClass = null;
659                        BusinessObject targetBusinessObject = null;
660                        resolvedPrincipalIdPropertyName.setLength( 0 ); // clear the buffer without requiring a new object allocation on each iteration
661                            // get the property name up until the ".principalName"
662                            // this should be a reference to the Person object attached to the BusinessObject                       
663                            String personReferenceObjectPropertyName = ObjectUtils.getNestedAttributePrefix( propertyName );
664                            // check if the person was nested within another BO under the master BO.  If so, go up one more level
665                            // otherwise, use the passed in BO class as the target class
666                        if ( ObjectUtils.isNestedAttribute( personReferenceObjectPropertyName ) ) {
667                            String targetBusinessObjectPropertyName = ObjectUtils.getNestedAttributePrefix( personReferenceObjectPropertyName );
668                            targetBusinessObject = (BusinessObject)ObjectUtils.getPropertyValue( businessObject, targetBusinessObjectPropertyName );
669                            if (targetBusinessObject != null) {
670                                targetBusinessObjectClass = targetBusinessObject.getClass();
671                                resolvedPrincipalIdPropertyName.append(targetBusinessObjectPropertyName).append(".");
672                            } else {
673                                LOG.error("Could not find target property '"+propertyName+"' in class "+businessObject.getClass().getName()+". Property value was null.");
674                            }
675                        } else { // not a nested Person property
676                            targetBusinessObjectClass = businessObject.getClass();
677                            targetBusinessObject = businessObject;
678                        }
679                        
680                        if (targetBusinessObjectClass != null) {
681                            // use the relationship metadata in the KNS to determine the property on the
682                            // host business object to put back into the map now that the principal ID
683                            // (the value stored in application tables) has been resolved
684                            String propName = ObjectUtils.getNestedAttributePrimitive( personReferenceObjectPropertyName );
685                            DataObjectRelationship rel = getBusinessObjectMetaDataService().getBusinessObjectRelationship( targetBusinessObject, propName );
686                            if ( rel != null ) {
687                                String sourcePrimitivePropertyName = rel.getParentAttributeForChildAttribute(KIMPropertyConstants.Person.PRINCIPAL_ID);
688                                resolvedPrincipalIdPropertyName.append(sourcePrimitivePropertyName);
689                                    // get the principal - for translation of the principalName to principalId
690                                String principalName = fieldValues.get( propertyName );
691                                    Principal principal = getIdentityService().getPrincipalByPrincipalName( principalName );
692                                if (principal != null ) {
693                                    processedFieldValues.put(resolvedPrincipalIdPropertyName.toString(), principal.getPrincipalId());
694                                } else {
695                                    processedFieldValues.put(resolvedPrincipalIdPropertyName.toString(), null);
696                                    try {
697                                        // if the principalName is bad, then we need to clear out the Person object
698                                        // and base principalId property
699                                        // so that their values are no longer accidentally used or re-populate
700                                        // the object
701                                        ObjectUtils.setObjectProperty(targetBusinessObject, resolvedPrincipalIdPropertyName.toString(), null );
702                                        ObjectUtils.setObjectProperty(targetBusinessObject, propName, null );
703                                        ObjectUtils.setObjectProperty(targetBusinessObject, propName + ".principalName", principalName );
704                                    } catch ( Exception ex ) {
705                                        LOG.error( "Unable to blank out the person object after finding that the person with the given principalName does not exist.", ex );
706                                    }
707                                }
708                            } else {
709                                    LOG.error( "Missing relationship for " + propName + " on " + targetBusinessObjectClass.getName() );
710                            }
711                        } else { // no target BO class - the code below probably will not work
712                            processedFieldValues.put(resolvedPrincipalIdPropertyName.toString(), null);
713                        }
714                    }
715                // if the property does not seem to match the definition of a Person property but it
716                // does end in principalName then...
717                // this is to handle the case where the user ID is on an ADD line - a case excluded from isPersonProperty()
718                } else if (propertyName.endsWith("." + KIMPropertyConstants.Person.PRINCIPAL_NAME)){
719                    // if we're adding to a collection and we've got the principalName; let's populate universalUser
720                    String principalName = fieldValues.get(propertyName);
721                    if ( StringUtils.isNotEmpty( principalName ) ) {
722                        String containerPropertyName = propertyName;
723                        if (containerPropertyName.startsWith(KRADConstants.MAINTENANCE_ADD_PREFIX)) {
724                            containerPropertyName = StringUtils.substringAfter( propertyName, KRADConstants.MAINTENANCE_ADD_PREFIX );
725                        }
726                        // get the class of the object that is referenced by the property name
727                        // if this is not true then there's a principalName collection or primitive attribute 
728                        // directly on the BO on the add line, so we just ignore that since something is wrong here
729                        if ( ObjectUtils.isNestedAttribute( containerPropertyName ) ) {
730                            // the first part of the property is the collection name
731                            String collectionName = StringUtils.substringBefore( containerPropertyName, "." );
732                            // what is the class held by that collection?
733                            // JHK: I don't like this.  This assumes that this method is only used by the maintenance
734                            // document service.  If that will always be the case, this method should be moved over there.
735                            Class<? extends BusinessObject> collectionBusinessObjectClass = getMaintenanceDocumentDictionaryService()
736                                            .getCollectionBusinessObjectClass(
737                                                            getMaintenanceDocumentDictionaryService()
738                                                                            .getDocumentTypeName(businessObject.getClass()), collectionName);
739                            if (collectionBusinessObjectClass != null) {
740                                // we are adding to a collection; get the relationships for that object; 
741                                    // is there one for personUniversalIdentifier?
742                                List<DataObjectRelationship> relationships =
743                                            getBusinessObjectMetaDataService().getBusinessObjectRelationships( collectionBusinessObjectClass );
744                                // JHK: this seems like a hack - looking at all relationships for a BO does not guarantee that we get the right one
745                                // JHK: why not inspect the objects like above?  Is it the property path problems because of the .add. portion?
746                                for ( DataObjectRelationship rel : relationships ) {
747                                    String parentAttribute = rel.getParentAttributeForChildAttribute( KIMPropertyConstants.Person.PRINCIPAL_ID );
748                                    if ( parentAttribute == null ) {
749                                            continue;
750                                    }
751                                    // there is a relationship for personUserIdentifier; use that to find the universal user
752                                    processedFieldValues.remove( propertyName );
753                                            String fieldPrefix = StringUtils.substringBeforeLast( StringUtils.substringBeforeLast( propertyName, "." + KIMPropertyConstants.Person.PRINCIPAL_NAME ), "." );
754                                    String relatedPrincipalIdPropertyName = fieldPrefix + "." + parentAttribute;
755                                    // KR-683 Special handling for extension objects
756                                            if(EXTENSION.equals(StringUtils.substringAfterLast(fieldPrefix, ".")) && EXTENSION.equals(StringUtils.substringBefore(parentAttribute, ".")))
757                                            {
758                                                    relatedPrincipalIdPropertyName = fieldPrefix + "." + StringUtils.substringAfter(parentAttribute, ".");
759                                            }
760                                    String currRelatedPersonPrincipalId = processedFieldValues.get(relatedPrincipalIdPropertyName);
761                                    if ( StringUtils.isBlank( currRelatedPersonPrincipalId ) ) {
762                                            Principal principal = getIdentityService().getPrincipalByPrincipalName( principalName );
763                                            if ( principal != null ) {
764                                                    processedFieldValues.put(relatedPrincipalIdPropertyName, principal.getPrincipalId());
765                                            } else {
766                                                    processedFieldValues.put(relatedPrincipalIdPropertyName, null);
767                                            }
768                                    }
769                                } // relationship loop
770                            } else {
771                                    if ( LOG.isDebugEnabled() ) {
772                                            LOG.debug( "Unable to determine class for collection referenced as part of property: " + containerPropertyName + " on " + businessObject.getClass().getName() );
773                                    }
774                            }
775                        } else {
776                            if ( LOG.isDebugEnabled() ) {
777                                    LOG.debug( "Non-nested property ending with 'principalName': " + containerPropertyName + " on " + businessObject.getClass().getName() );
778                            }
779                        }
780                    }
781                }
782            }
783            return processedFieldValues;
784        }
785            
786            // OTHER METHODS
787    
788            protected IdentityService getIdentityService() {
789                    if ( identityService == null ) {
790                            identityService = KimApiServiceLocator.getIdentityService();
791                    }
792                    return identityService;
793            }
794    
795            protected RoleService getRoleService() {
796                    if ( roleService == null ) {
797                            roleService = KimApiServiceLocator.getRoleService();
798                    }
799                    return roleService;
800            }
801    
802    
803            public Class<? extends Person> getPersonImplementationClass() {
804                    return PersonImpl.class;
805            }
806            
807            protected BusinessObjectMetaDataService getBusinessObjectMetaDataService() {
808                    if ( businessObjectMetaDataService == null ) {
809                            businessObjectMetaDataService = KNSServiceLocator.getBusinessObjectMetaDataService();
810                    }
811                    return businessObjectMetaDataService;
812            }
813    
814            protected MaintenanceDocumentDictionaryService getMaintenanceDocumentDictionaryService() {
815                    if ( maintenanceDocumentDictionaryService == null ) {
816                            maintenanceDocumentDictionaryService = KNSServiceLocator.getMaintenanceDocumentDictionaryService();
817                    }
818                    return maintenanceDocumentDictionaryService;
819            }
820    }