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.krad.service.impl;
017    
018    import org.apache.commons.beanutils.PropertyUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.kuali.rice.core.api.config.property.ConfigContext;
021    import org.kuali.rice.kim.api.identity.Person;
022    import org.kuali.rice.kim.api.identity.PersonService;
023    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
024    import org.kuali.rice.krad.bo.BusinessObject;
025    import org.kuali.rice.krad.bo.DataObjectRelationship;
026    import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
027    import org.kuali.rice.krad.bo.PersistableBusinessObject;
028    import org.kuali.rice.krad.dao.BusinessObjectDao;
029    import org.kuali.rice.krad.exception.ObjectNotABusinessObjectRuntimeException;
030    import org.kuali.rice.krad.exception.ReferenceAttributeDoesntExistException;
031    import org.kuali.rice.krad.service.BusinessObjectService;
032    import org.kuali.rice.krad.service.DataObjectMetaDataService;
033    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
034    import org.kuali.rice.krad.service.ModuleService;
035    import org.kuali.rice.krad.service.PersistenceService;
036    import org.kuali.rice.krad.service.PersistenceStructureService;
037    import org.kuali.rice.krad.util.KRADConstants;
038    import org.kuali.rice.krad.util.ObjectUtils;
039    import org.springframework.transaction.annotation.Transactional;
040    
041    import java.beans.PropertyDescriptor;
042    import java.util.Collection;
043    import java.util.Collections;
044    import java.util.HashMap;
045    import java.util.HashSet;
046    import java.util.List;
047    import java.util.Map;
048    import java.util.Set;
049    
050    /**
051     * This class is the service implementation for the BusinessObjectService structure. This is the default implementation, that is
052     * delivered with Kuali.
053     */
054    
055    public class BusinessObjectServiceImpl implements BusinessObjectService {
056    
057        private PersistenceService persistenceService;
058        private PersistenceStructureService persistenceStructureService;
059        private BusinessObjectDao businessObjectDao;
060        private PersonService personService;
061        private DataObjectMetaDataService dataObjectMetaDataService;
062    
063        private boolean illegalBusinessObjectsForSaveInitialized;
064        private final Set<String> illegalBusinessObjectsForSave = new HashSet<String>();
065        
066        @Override
067        @Transactional
068        public <T extends PersistableBusinessObject> T save(T bo) {
069            validateBusinessObjectForSave(bo);
070            return (T) businessObjectDao.save(bo);
071        }
072    
073        @Override
074        @Transactional
075        public List<? extends PersistableBusinessObject> save(List<? extends PersistableBusinessObject> businessObjects) {
076            validateBusinessObjectForSave(businessObjects);
077            return businessObjectDao.save(businessObjects);
078        }
079    
080        @Override
081        @Transactional
082        public PersistableBusinessObject linkAndSave(PersistableBusinessObject bo) {
083            validateBusinessObjectForSave(bo);
084            persistenceService.linkObjects(bo);
085            return businessObjectDao.save(bo);
086        }
087    
088        @Override
089        @Transactional
090        public List<? extends PersistableBusinessObject> linkAndSave(List<? extends PersistableBusinessObject> businessObjects) {
091            validateBusinessObjectForSave(businessObjects);
092            return businessObjectDao.save(businessObjects);
093        }
094    
095        protected void validateBusinessObjectForSave(PersistableBusinessObject bo) {
096            if (bo == null) {
097                throw new IllegalArgumentException("Object passed in is null");
098            }
099            if (!isBusinessObjectAllowedForSave(bo)) {
100                    throw new IllegalArgumentException("Object passed in is a BusinessObject but has been restricted from save operations according to configuration parameter '" + KRADConstants.Config.ILLEGAL_BUSINESS_OBJECTS_FOR_SAVE);
101            }
102        }
103        
104        protected void validateBusinessObjectForSave(List<? extends PersistableBusinessObject> businessObjects) {
105            for (PersistableBusinessObject bo : businessObjects) {
106                     if (bo == null) {
107                     throw new IllegalArgumentException("One of the objects in the List is null.");
108                 }
109                     if (!isBusinessObjectAllowedForSave(bo)) {
110                             throw new IllegalArgumentException("One of the objects in the List is a BusinessObject but has been restricted from save operations according to configuration parameter '" + KRADConstants.Config.ILLEGAL_BUSINESS_OBJECTS_FOR_SAVE
111                                             + "  Passed in type was '" + bo.getClass().getName() + "'.");
112                     }
113            }
114        }
115        
116        
117        /**
118         * Returns true if the BusinessObjectService should be permitted to save instances of the given PersistableBusinessObject.
119         * Implementation checks a configuration parameter for class names of PersistableBusinessObjects that shouldn't be allowed
120         * to be saved.
121         */
122        protected boolean isBusinessObjectAllowedForSave(PersistableBusinessObject bo) {
123            if (!illegalBusinessObjectsForSaveInitialized) {
124                    synchronized (this) {
125                            boolean applyCheck = true;
126                            String applyCheckValue = ConfigContext.getCurrentContextConfig().getProperty(KRADConstants.Config.APPLY_ILLEGAL_BUSINESS_OBJECT_FOR_SAVE_CHECK);
127                            if (!StringUtils.isEmpty(applyCheckValue)) {
128                                    applyCheck = Boolean.valueOf(applyCheckValue);
129                            }
130                            if (applyCheck) {
131                                    String illegalBos = ConfigContext.getCurrentContextConfig().getProperty(KRADConstants.Config.ILLEGAL_BUSINESS_OBJECTS_FOR_SAVE);
132                                    if (!StringUtils.isEmpty(illegalBos)) {
133                                            String[] illegalBosSplit = illegalBos.split(",");
134                                            for (String illegalBo : illegalBosSplit) {
135                                                    illegalBusinessObjectsForSave.add(illegalBo.trim());
136                                            }
137                                    }
138                            }
139                    }
140                    illegalBusinessObjectsForSaveInitialized = true;
141            }
142            return !illegalBusinessObjectsForSave.contains(bo.getClass().getName());
143        }
144        
145    
146        @Override
147            public <T extends BusinessObject> T findBySinglePrimaryKey(Class<T> clazz, Object primaryKey) {
148                    return businessObjectDao.findBySinglePrimaryKey(clazz, primaryKey);
149            }
150        @Override
151        public <T extends BusinessObject> T findByPrimaryKey(Class<T> clazz, Map<String, ?> primaryKeys) {
152            return businessObjectDao.findByPrimaryKey(clazz, primaryKeys);
153        }
154    
155        @Override
156        public PersistableBusinessObject retrieve(PersistableBusinessObject object) {
157            return businessObjectDao.retrieve(object);
158        }
159    
160        @Override
161        public <T extends BusinessObject> Collection<T> findAll(Class<T> clazz) {
162            return businessObjectDao.findAll(clazz);
163        }
164        @Override
165        public <T extends BusinessObject> Collection<T> findAllOrderBy( Class<T> clazz, String sortField, boolean sortAscending ) {
166            final Map<String, ?> emptyParameters = Collections.emptyMap();
167            return businessObjectDao.findMatchingOrderBy(clazz, emptyParameters, sortField, sortAscending );
168        }
169        
170        @Override
171        public <T extends BusinessObject> Collection<T> findMatching(Class<T> clazz, Map<String, ?> fieldValues) {
172            return businessObjectDao.findMatching(clazz, fieldValues);
173        }
174    
175        @Override
176        public int countMatching(Class clazz, Map<String, ?> fieldValues) {
177            return businessObjectDao.countMatching(clazz, fieldValues);
178        }
179    
180        @Override
181        public int countMatching(Class clazz, Map<String, ?> positiveFieldValues, Map<String, ?> negativeFieldValues) {
182            return businessObjectDao.countMatching(clazz, positiveFieldValues, negativeFieldValues);
183        }
184        @Override
185        public <T extends BusinessObject> Collection<T> findMatchingOrderBy(Class<T> clazz, Map<String, ?> fieldValues, String sortField, boolean sortAscending) {
186            return businessObjectDao.findMatchingOrderBy(clazz, fieldValues, sortField, sortAscending);
187        }
188        @Override
189        @Transactional
190        public void delete(PersistableBusinessObject bo) {
191            businessObjectDao.delete(bo);
192        }
193    
194        @Override
195        @Transactional
196        public void delete(List<? extends PersistableBusinessObject> boList) {
197            businessObjectDao.delete(boList);
198        }
199    
200        @Override
201        @Transactional
202        public void deleteMatching(Class clazz, Map<String, ?> fieldValues) {
203            businessObjectDao.deleteMatching(clazz, fieldValues);
204        }
205    
206        @Override
207        public BusinessObject getReferenceIfExists(BusinessObject bo, String referenceName) {
208            // if either argument is null, then we have nothing to do, complain and abort
209            if (ObjectUtils.isNull(bo)) {
210                throw new IllegalArgumentException("Passed in BusinessObject was null.  No processing can be done.");
211            }
212            if (StringUtils.isEmpty(referenceName)) {
213                throw new IllegalArgumentException("Passed in referenceName was empty or null.  No processing can be done.");
214            }
215    
216            // make sure the attribute exists at all, throw exception if not
217            PropertyDescriptor propertyDescriptor;
218            try {
219                propertyDescriptor = PropertyUtils.getPropertyDescriptor(bo, referenceName);
220            }
221            catch (Exception e) {
222                throw new RuntimeException(e);
223            }
224            if (propertyDescriptor == null) {
225                throw new ReferenceAttributeDoesntExistException("Requested attribute: '" + referenceName + "' does not exist " + "on class: '" + bo.getClass().getName() + "'. GFK");
226            }
227    
228            // get the class of the attribute name
229            Class referenceClass = ObjectUtils.getPropertyType( bo, referenceName, persistenceStructureService );
230            if ( referenceClass == null ) {
231                    referenceClass = propertyDescriptor.getPropertyType();
232            }
233    
234            /*
235             * check for Person or EBO references in which case we can just get the reference through propertyutils
236             */
237            if (ExternalizableBusinessObject.class.isAssignableFrom(referenceClass)) {
238                try {
239                    BusinessObject referenceBoExternalizable = (BusinessObject) PropertyUtils.getProperty(bo, referenceName);
240                    if (referenceBoExternalizable!=null) {
241                            return referenceBoExternalizable;
242                    }
243                } catch (Exception ex) {
244                    //throw new RuntimeException("Unable to get property " + referenceName + " from a BO of class: " + bo.getClass().getName(),ex);
245                    //Proceed further - get the BO relationship using responsible module service and proceed further
246                }
247            }
248    
249            // make sure the class of the attribute descends from BusinessObject,
250            // otherwise throw an exception
251            if (!ExternalizableBusinessObject.class.isAssignableFrom(referenceClass) && !PersistableBusinessObject.class.isAssignableFrom(referenceClass)) {
252                throw new ObjectNotABusinessObjectRuntimeException("Attribute requested (" + referenceName + ") is of class: " + "'" + referenceClass.getName() + "' and is not a " + "descendent of PersistableBusinessObject.  Only descendents of PersistableBusinessObject " + "can be used.");
253            }
254    
255            // get the list of foreign-keys for this reference. if the reference
256            // does not exist, or is not a reference-descriptor, an exception will
257            // be thrown here.
258            //DataObjectRelationship boRel = dataObjectMetaDataService.getBusinessObjectRelationship( bo, referenceName );
259            DataObjectRelationship boRel = dataObjectMetaDataService.getDataObjectRelationship(bo, bo.getClass(),
260                    referenceName, "", true, false, false);
261            final Map<String,String> fkMap = boRel != null ? boRel.getParentToChildReferences() : Collections.<String, String>emptyMap();
262    
263            boolean allFkeysHaveValues = true;
264            // walk through the foreign keys, testing each one to see if it has a value
265            Map<String,Object> pkMap = new HashMap<String,Object>();
266            for (Map.Entry<String, String> entry : fkMap.entrySet()) {
267                String fkFieldName = entry.getKey();
268                String pkFieldName = entry.getValue();
269    
270                // attempt to retrieve the value for the given field
271                Object fkFieldValue;
272                try {
273                    fkFieldValue = PropertyUtils.getProperty(bo, fkFieldName);
274                }
275                catch (Exception e) {
276                    throw new RuntimeException(e);
277                }
278    
279                // determine if there is a value for the field
280                if (ObjectUtils.isNull(fkFieldValue)) {
281                    allFkeysHaveValues = false;
282                    break; // no reason to continue processing the fkeys
283                }
284                else if (String.class.isAssignableFrom(fkFieldValue.getClass())) {
285                    if (StringUtils.isEmpty((String) fkFieldValue)) {
286                        allFkeysHaveValues = false;
287                        break;
288                    }
289                    else {
290                        pkMap.put(pkFieldName, fkFieldValue);
291                    }
292                }
293    
294                // if there is a value, grab it
295                else {
296                    pkMap.put(pkFieldName, fkFieldValue);
297                }
298            }
299    
300            BusinessObject referenceBo = null;
301            // only do the retrieval if all Foreign Keys have values
302            if (allFkeysHaveValues) {
303                    if (ExternalizableBusinessObject.class.isAssignableFrom(referenceClass)) {
304                            ModuleService responsibleModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(referenceClass);
305                                    if(responsibleModuleService!=null) {
306                                            return responsibleModuleService.<ExternalizableBusinessObject>getExternalizableBusinessObject(referenceClass, pkMap);
307                                    }
308                    } else
309                            referenceBo = this.<BusinessObject>findByPrimaryKey(referenceClass, pkMap);
310            }
311    
312            // return what we have, it'll be null if it was never retrieved
313            return referenceBo;
314        }
315        @Override
316        public void linkUserFields(PersistableBusinessObject bo) {
317            if (bo == null) {
318                throw new IllegalArgumentException("bo passed in was null");
319            }
320    
321            bo.linkEditableUserFields();
322           
323            linkUserFields( Collections.singletonList( bo ) );
324        }
325    
326        @Override
327        public void linkUserFields(List<PersistableBusinessObject> bos) {
328    
329            // do nothing if there's nothing to process
330            if (bos == null) {
331                throw new IllegalArgumentException("List of bos passed in was null");
332            }
333            else if (bos.isEmpty()) {
334                return;
335            }
336    
337    
338            Person person;
339            for (PersistableBusinessObject bo : bos) {
340                // get a list of the reference objects on the BO
341                List<DataObjectRelationship> relationships = dataObjectMetaDataService.getDataObjectRelationships(
342                        bo.getClass());
343                for ( DataObjectRelationship rel : relationships ) {
344                    if ( Person.class.isAssignableFrom( rel.getRelatedClass() ) ) {
345                        person = (Person) ObjectUtils.getPropertyValue(bo, rel.getParentAttributeName() );
346                        if (person != null) {
347                            // find the universal user ID relationship and link the field
348                            for ( Map.Entry<String,String> entry : rel.getParentToChildReferences().entrySet() ) {
349                                if ( "principalId".equals(entry.getValue())) {
350                                    linkUserReference(bo, person, rel.getParentAttributeName(), entry.getKey() );
351                                    break;
352                                }
353                            }
354                        }                    
355                    }
356                }
357                if ( persistenceStructureService.isPersistable(bo.getClass())) {
358                        Map<String, Class> references = persistenceStructureService.listReferenceObjectFields(bo);
359    
360                        // walk through the ref objects, only doing work if they are Person objects
361                        for ( Map.Entry<String, Class> entry : references.entrySet() ) {
362                            if (Person.class.isAssignableFrom(entry.getValue())) {
363                                person = (Person) ObjectUtils.getPropertyValue(bo, entry.getKey());
364                                if (person != null) {
365                                    String fkFieldName = persistenceStructureService.getForeignKeyFieldName(bo.getClass(), entry.getKey(), "principalId");
366                                    linkUserReference(bo, person, entry.getKey(), fkFieldName);
367                                }
368                            }
369                        }
370                }
371            }
372        }
373    
374        /**
375         * 
376         * This method links a single UniveralUser back to the parent BO based on the authoritative principalName.
377         * 
378         * @param bo
379         * @param refFieldName
380         */
381        private void linkUserReference(PersistableBusinessObject bo, Person user, String refFieldName, String fkFieldName) {
382    
383            // if the UserId field is blank, there's nothing we can do, so quit
384            if (StringUtils.isBlank(user.getPrincipalName())) {
385                return;
386            }
387    
388            // attempt to load the user from the user-name, exit quietly if the user isnt found
389            Person userFromService = getPersonService().getPersonByPrincipalName(user.getPrincipalName());
390            if (userFromService == null) {
391                return;
392            }
393    
394            // attempt to set the universalId on the parent BO
395            setBoField(bo, fkFieldName, userFromService.getPrincipalId());
396        }
397    
398        private void setBoField(PersistableBusinessObject bo, String fieldName, Object fieldValue) {
399            try {
400                ObjectUtils.setObjectProperty(bo, fieldName, fieldValue.getClass(), fieldValue);
401            }
402            catch (Exception e) {
403                throw new RuntimeException("Could not set field [" + fieldName + "] on BO to value: " + fieldValue.toString() + " (see nested exception for details).", e);
404            }
405        }
406    
407        @Override
408            public PersistableBusinessObject manageReadOnly(PersistableBusinessObject bo) {
409                    return getBusinessObjectDao().manageReadOnly(bo);
410            }
411    
412            /**
413         * Gets the businessObjectDao attribute.
414         * 
415         * @return Returns the businessObjectDao.
416         */
417        protected BusinessObjectDao getBusinessObjectDao() {
418            return businessObjectDao;
419        }
420    
421        /**
422         * Sets the businessObjectDao attribute value.
423         * 
424         * @param businessObjectDao The businessObjectDao to set.
425         */
426        public void setBusinessObjectDao(BusinessObjectDao businessObjectDao) {
427            this.businessObjectDao = businessObjectDao;
428        }
429    
430        /**
431         * Sets the persistenceStructureService attribute value.
432         * 
433         * @param persistenceStructureService The persistenceStructureService to set.
434         */
435        public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
436            this.persistenceStructureService = persistenceStructureService;
437        }
438    
439        /**
440         * Sets the kualiUserService attribute value.
441         */
442            public final void setPersonService(PersonService personService) {
443            this.personService = personService;
444        }
445    
446            protected PersonService getPersonService() {
447            return personService != null ? personService : (personService = KimApiServiceLocator.getPersonService());
448        }
449    
450        /**
451         * Sets the persistenceService attribute value.
452         * 
453         * @param persistenceService The persistenceService to set.
454         */
455        public final void setPersistenceService(PersistenceService persistenceService) {
456            this.persistenceService = persistenceService;
457        }
458    
459        protected DataObjectMetaDataService getDataObjectMetaDataService() {
460            return dataObjectMetaDataService;
461        }
462    
463        public void setDataObjectMetaDataService(DataObjectMetaDataService dataObjectMetadataService) {
464            this.dataObjectMetaDataService = dataObjectMetadataService;
465        }
466    
467    }