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