001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krad.service.impl;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.config.property.ConfigContext;
021import org.kuali.rice.kim.api.identity.Person;
022import org.kuali.rice.kim.api.identity.PersonService;
023import org.kuali.rice.kim.api.services.KimApiServiceLocator;
024import org.kuali.rice.krad.bo.BusinessObject;
025import org.kuali.rice.krad.bo.DataObjectRelationship;
026import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
027import org.kuali.rice.krad.bo.PersistableBusinessObject;
028import org.kuali.rice.krad.dao.BusinessObjectDao;
029import org.kuali.rice.krad.exception.ObjectNotABusinessObjectRuntimeException;
030import org.kuali.rice.krad.exception.ReferenceAttributeDoesntExistException;
031import org.kuali.rice.krad.service.BusinessObjectService;
032import org.kuali.rice.krad.service.DataObjectMetaDataService;
033import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
034import org.kuali.rice.krad.service.ModuleService;
035import org.kuali.rice.krad.service.PersistenceService;
036import org.kuali.rice.krad.service.PersistenceStructureService;
037import org.kuali.rice.krad.util.KRADConstants;
038import org.kuali.rice.krad.util.ObjectUtils;
039import org.springframework.transaction.annotation.Transactional;
040
041import java.beans.PropertyDescriptor;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.HashMap;
045import java.util.HashSet;
046import java.util.List;
047import java.util.Map;
048import 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
055public 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 = null;
230        if(bo instanceof PersistableBusinessObject) {
231            referenceClass = persistenceStructureService.getBusinessObjectAttributeClass(((PersistableBusinessObject)bo).getClass(), referenceName);
232        }
233        if(referenceClass == null) {
234            referenceClass = ObjectUtils.getPropertyType( bo, referenceName, persistenceStructureService );
235        }
236        if ( referenceClass == null ) {
237                referenceClass = propertyDescriptor.getPropertyType();
238        }
239
240        /*
241         * check for Person or EBO references in which case we can just get the reference through propertyutils
242         */
243        if (ExternalizableBusinessObject.class.isAssignableFrom(referenceClass)) {
244            try {
245                BusinessObject referenceBoExternalizable = (BusinessObject) PropertyUtils.getProperty(bo, referenceName);
246                if (referenceBoExternalizable!=null) {
247                        return referenceBoExternalizable;
248                }
249            } catch (Exception ex) {
250                //throw new RuntimeException("Unable to get property " + referenceName + " from a BO of class: " + bo.getClass().getName(),ex);
251                //Proceed further - get the BO relationship using responsible module service and proceed further
252            }
253        }
254
255        // make sure the class of the attribute descends from BusinessObject,
256        // otherwise throw an exception
257        if (!ExternalizableBusinessObject.class.isAssignableFrom(referenceClass) && !PersistableBusinessObject.class.isAssignableFrom(referenceClass)) {
258            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.");
259        }
260
261        // get the list of foreign-keys for this reference. if the reference
262        // does not exist, or is not a reference-descriptor, an exception will
263        // be thrown here.
264        //DataObjectRelationship boRel = dataObjectMetaDataService.getBusinessObjectRelationship( bo, referenceName );
265        DataObjectRelationship boRel = dataObjectMetaDataService.getDataObjectRelationship(bo, bo.getClass(),
266                referenceName, "", true, false, false);
267        final Map<String,String> fkMap = boRel != null ? boRel.getParentToChildReferences() : Collections.<String, String>emptyMap();
268
269        boolean allFkeysHaveValues = true;
270        // walk through the foreign keys, testing each one to see if it has a value
271        Map<String,Object> pkMap = new HashMap<String,Object>();
272        for (Map.Entry<String, String> entry : fkMap.entrySet()) {
273            String fkFieldName = entry.getKey();
274            String pkFieldName = entry.getValue();
275
276            // attempt to retrieve the value for the given field
277            Object fkFieldValue;
278            try {
279                fkFieldValue = PropertyUtils.getProperty(bo, fkFieldName);
280            }
281            catch (Exception e) {
282                throw new RuntimeException(e);
283            }
284
285            // determine if there is a value for the field
286            if (ObjectUtils.isNull(fkFieldValue)) {
287                allFkeysHaveValues = false;
288                break; // no reason to continue processing the fkeys
289            }
290            else if (String.class.isAssignableFrom(fkFieldValue.getClass())) {
291                if (StringUtils.isEmpty((String) fkFieldValue)) {
292                    allFkeysHaveValues = false;
293                    break;
294                }
295                else {
296                    pkMap.put(pkFieldName, fkFieldValue);
297                }
298            }
299
300            // if there is a value, grab it
301            else {
302                pkMap.put(pkFieldName, fkFieldValue);
303            }
304        }
305
306        BusinessObject referenceBo = null;
307        // only do the retrieval if all Foreign Keys have values
308        if (allFkeysHaveValues) {
309                if (ExternalizableBusinessObject.class.isAssignableFrom(referenceClass)) {
310                        ModuleService responsibleModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(referenceClass);
311                                if(responsibleModuleService!=null) {
312                                        return responsibleModuleService.<ExternalizableBusinessObject>getExternalizableBusinessObject(referenceClass, pkMap);
313                                }
314                } else
315                        referenceBo = this.<BusinessObject>findByPrimaryKey(referenceClass, pkMap);
316        }
317
318        // return what we have, it'll be null if it was never retrieved
319        return referenceBo;
320    }
321    @Override
322    public void linkUserFields(PersistableBusinessObject bo) {
323        if (bo == null) {
324            throw new IllegalArgumentException("bo passed in was null");
325        }
326
327        bo.linkEditableUserFields();
328       
329        linkUserFields( Collections.singletonList( bo ) );
330    }
331
332    @Override
333    public void linkUserFields(List<PersistableBusinessObject> bos) {
334
335        // do nothing if there's nothing to process
336        if (bos == null) {
337            throw new IllegalArgumentException("List of bos passed in was null");
338        }
339        else if (bos.isEmpty()) {
340            return;
341        }
342
343
344        Person person;
345        for (PersistableBusinessObject bo : bos) {
346            // get a list of the reference objects on the BO
347            List<DataObjectRelationship> relationships = dataObjectMetaDataService.getDataObjectRelationships(
348                    bo.getClass());
349            for ( DataObjectRelationship rel : relationships ) {
350                if ( Person.class.isAssignableFrom( rel.getRelatedClass() ) ) {
351                    person = (Person) ObjectUtils.getPropertyValue(bo, rel.getParentAttributeName() );
352                    if (person != null) {
353                        // find the universal user ID relationship and link the field
354                        for ( Map.Entry<String,String> entry : rel.getParentToChildReferences().entrySet() ) {
355                            if ( "principalId".equals(entry.getValue())) {
356                                linkUserReference(bo, person, rel.getParentAttributeName(), entry.getKey() );
357                                break;
358                            }
359                        }
360                    }                    
361                }
362            }
363            if ( persistenceStructureService.isPersistable(bo.getClass())) {
364                    Map<String, Class> references = persistenceStructureService.listReferenceObjectFields(bo);
365
366                    // walk through the ref objects, only doing work if they are Person objects
367                    for ( Map.Entry<String, Class> entry : references.entrySet() ) {
368                        if (Person.class.isAssignableFrom(entry.getValue())) {
369                            person = (Person) ObjectUtils.getPropertyValue(bo, entry.getKey());
370                            if (person != null) {
371                                String fkFieldName = persistenceStructureService.getForeignKeyFieldName(bo.getClass(), entry.getKey(), "principalId");
372                                linkUserReference(bo, person, entry.getKey(), fkFieldName);
373                            }
374                        }
375                    }
376            }
377        }
378    }
379
380    /**
381     * 
382     * This method links a single UniveralUser back to the parent BO based on the authoritative principalName.
383     * 
384     * @param bo
385     * @param refFieldName
386     */
387    private void linkUserReference(PersistableBusinessObject bo, Person user, String refFieldName, String fkFieldName) {
388
389        // if the UserId field is blank, there's nothing we can do, so quit
390        if (StringUtils.isBlank(user.getPrincipalName())) {
391            return;
392        }
393
394        // attempt to load the user from the user-name, exit quietly if the user isnt found
395        Person userFromService = getPersonService().getPersonByPrincipalName(user.getPrincipalName());
396        if (userFromService == null) {
397            return;
398        }
399
400        // attempt to set the universalId on the parent BO
401        setBoField(bo, fkFieldName, userFromService.getPrincipalId());
402    }
403
404    private void setBoField(PersistableBusinessObject bo, String fieldName, Object fieldValue) {
405        try {
406            ObjectUtils.setObjectProperty(bo, fieldName, fieldValue.getClass(), fieldValue);
407        }
408        catch (Exception e) {
409            throw new RuntimeException("Could not set field [" + fieldName + "] on BO to value: " + fieldValue.toString() + " (see nested exception for details).", e);
410        }
411    }
412
413    @Override
414        public PersistableBusinessObject manageReadOnly(PersistableBusinessObject bo) {
415                return getBusinessObjectDao().manageReadOnly(bo);
416        }
417
418        /**
419     * Gets the businessObjectDao attribute.
420     * 
421     * @return Returns the businessObjectDao.
422     */
423    protected BusinessObjectDao getBusinessObjectDao() {
424        return businessObjectDao;
425    }
426
427    /**
428     * Sets the businessObjectDao attribute value.
429     * 
430     * @param businessObjectDao The businessObjectDao to set.
431     */
432    public void setBusinessObjectDao(BusinessObjectDao businessObjectDao) {
433        this.businessObjectDao = businessObjectDao;
434    }
435
436    /**
437     * Sets the persistenceStructureService attribute value.
438     * 
439     * @param persistenceStructureService The persistenceStructureService to set.
440     */
441    public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
442        this.persistenceStructureService = persistenceStructureService;
443    }
444
445    /**
446     * Sets the kualiUserService attribute value.
447     */
448        public final void setPersonService(PersonService personService) {
449        this.personService = personService;
450    }
451
452        protected PersonService getPersonService() {
453        return personService != null ? personService : (personService = KimApiServiceLocator.getPersonService());
454    }
455
456    /**
457     * Sets the persistenceService attribute value.
458     * 
459     * @param persistenceService The persistenceService to set.
460     */
461    public final void setPersistenceService(PersistenceService persistenceService) {
462        this.persistenceService = persistenceService;
463    }
464
465    protected DataObjectMetaDataService getDataObjectMetaDataService() {
466        return dataObjectMetaDataService;
467    }
468
469    public void setDataObjectMetaDataService(DataObjectMetaDataService dataObjectMetadataService) {
470        this.dataObjectMetaDataService = dataObjectMetadataService;
471    }
472
473}