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.dao.impl;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023
024import javax.persistence.EntityManager;
025import javax.persistence.EntityNotFoundException;
026import javax.persistence.PersistenceContext;
027import javax.persistence.PersistenceException;
028
029import org.hibernate.FlushMode;
030import org.hibernate.Session;
031import org.hibernate.ejb.HibernateEntityManager;
032import org.hibernate.proxy.HibernateProxy;
033import org.kuali.rice.core.framework.persistence.jpa.OrmUtils;
034import org.kuali.rice.core.framework.persistence.jpa.criteria.Criteria;
035import org.kuali.rice.core.framework.persistence.jpa.criteria.QueryByCriteria;
036import org.kuali.rice.core.framework.persistence.jpa.criteria.QueryByCriteria.QueryByCriteriaType;
037import org.kuali.rice.core.framework.persistence.jpa.metadata.MetadataManager;
038import org.kuali.rice.krad.bo.BusinessObject;
039import org.kuali.rice.krad.bo.PersistableBusinessObject;
040import org.kuali.rice.krad.bo.PersistableBusinessObjectExtension;
041import org.kuali.rice.krad.dao.BusinessObjectDao;
042import org.kuali.rice.krad.service.PersistenceStructureService;
043import org.kuali.rice.krad.util.KRADPropertyConstants;
044import org.kuali.rice.krad.util.OjbCollectionHelper;
045import org.springframework.dao.DataAccessException;
046
047/**
048 * This class is the JPA implementation of the BusinessObjectDao interface.
049 */
050@SuppressWarnings("unchecked")
051public class BusinessObjectDaoJpa implements BusinessObjectDao {
052
053        @PersistenceContext
054        private EntityManager entityManager;
055
056        private PersistenceStructureService persistenceStructureService;
057
058        private OjbCollectionHelper ojbCollectionHelper;
059
060        public BusinessObjectDaoJpa(EntityManager entityManager, PersistenceStructureService persistenceStructureService) {
061                this.entityManager = entityManager;
062                this.persistenceStructureService = persistenceStructureService;
063        }
064
065        /**
066         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findBySinglePrimaryKey(java.lang.Class, java.lang.Object)
067         */
068        public <T extends BusinessObject> T findBySinglePrimaryKey(Class<T> clazz, Object primaryKey) {
069                return (T) entityManager.find(clazz, primaryKey);
070        }
071
072        /**
073         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findByPrimaryKey(java.lang.Class,
074         *      java.util.Map)
075         */
076        public <T extends BusinessObject> T findByPrimaryKey(Class<T> clazz, Map<String, ?> primaryKeys) { 
077                if (primaryKeys == null || primaryKeys.isEmpty()) {
078                        return null;
079                }
080                T bo = null;
081                try {
082                        bo = (T) new QueryByCriteria(entityManager, buildJpaCriteria(clazz, primaryKeys)).toQuery().getSingleResult();
083                } catch (PersistenceException e) {}
084                return bo;
085        }
086        
087        /**
088         * Retrieves an object, based on its PK object
089         * 
090         * @param clazz the class of the object to retrieve
091         * @param pkObject the value of the primary key
092         * @return the retrieved PersistableBusinessObject
093         */
094        public <T extends BusinessObject> T findByPrimaryKeyUsingKeyObject(Class<T> clazz, Object pkObject) { 
095                if (pkObject == null) {
096                        return null;
097                }
098                T bo = null;
099                try {
100                        bo = (T) entityManager.find(clazz, pkObject);
101                } catch (PersistenceException e) {}
102                return bo;
103        }
104
105        /**
106         * Retrieves all of the records for a given class name.
107         * 
108         * @param clazz -
109         *            the name of the object being used, either KualiCodeBase or a
110         *            subclass
111         * @return Collection
112         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findAll(java.lang.Class)
113         */
114        public <T extends BusinessObject> Collection<T> findAll(Class<T> clazz) {
115                return (Collection<T>) new QueryByCriteria(entityManager, new Criteria(clazz.getName())).toQuery().getResultList();
116        }
117
118        /**
119         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findAllOrderBy(java.lang.Class,
120         *      java.lang.String, boolean)
121         */
122        public <T extends BusinessObject> Collection<T> findAllOrderBy(Class<T> clazz, String sortField, boolean sortAscending) {
123                Criteria criteria = new Criteria(clazz.getName());
124                criteria.orderBy(sortField, sortAscending);
125                return new QueryByCriteria(entityManager, criteria).toQuery().getResultList();
126        }
127
128        /**
129         * This is the default impl that comes with Kuali - uses OJB.
130         * 
131         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findMatching(java.lang.Class,
132         *      java.util.Map)
133         */
134        public <T extends BusinessObject> Collection<T> findMatching(Class<T> clazz, Map<String, ?> fieldValues) {
135                return (Collection<T>)new QueryByCriteria(entityManager, buildJpaCriteria(clazz, fieldValues)).toQuery().getResultList();
136        }
137
138        /**
139         * Uses the passed query to form a Rice QueryByCriteria, which then translates to a JPA query and retrieves results
140         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findMatching(org.kuali.rice.core.framework.persistence.jpa.criteria.Criteria)
141         */
142        public <T extends BusinessObject> Collection<T> findMatching(Criteria criteria) {
143                return (List<T>)new QueryByCriteria(entityManager, criteria).toQuery().getResultList();
144        }
145
146        /**
147         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findAllActive(java.lang.Class)
148         */
149        public <T extends BusinessObject> Collection<T> findAllActive(Class<T> clazz) {
150                return (Collection<T>)new QueryByCriteria(entityManager, buildActiveJpaCriteria(clazz)).toQuery().getResultList();
151        }
152
153        /**
154         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findAllActive(java.lang.Class)
155         */
156        public <T extends BusinessObject> Collection<T> findAllInactive(Class<T> clazz) {
157                return (Collection<T>)new QueryByCriteria(entityManager, buildInactiveJpaCriteria(clazz)).toQuery().getResultList();
158        }
159        
160        /**
161         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findAllActiveOrderBy(java.lang.Class,
162         *      java.lang.String, boolean)
163         */
164        public <T extends BusinessObject> Collection<T> findAllActiveOrderBy(Class<T> clazz, String sortField, boolean sortAscending) {
165                Criteria criteria = buildActiveJpaCriteria(clazz);
166                criteria.orderBy(sortField, sortAscending);
167                return (Collection<T>)new QueryByCriteria(entityManager, criteria).toQuery().getResultList();
168        }
169
170        /**
171         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findMatchingActive(java.lang.Class,
172         *      java.util.Map)
173         */
174        public <T extends BusinessObject> Collection<T> findMatchingActive(Class<T> clazz, Map<String, ?> fieldValues) {
175                Criteria criteria = buildJpaCriteria(clazz, fieldValues);
176                criteria.and(buildActiveJpaCriteria(clazz));
177                return (Collection<T>)new QueryByCriteria(entityManager, criteria).toQuery().getResultList();
178        }
179
180        /**
181         * This is the default impl that comes with Kuali - uses OJB.
182         * 
183         * @see org.kuali.rice.krad.dao.BusinessObjectDao#countMatching(java.lang.Class,
184         *      java.util.Map)
185         */
186        public int countMatching(Class clazz, Map<String, ?> fieldValues) {
187                return ((Long) new QueryByCriteria(entityManager, buildJpaCriteria(clazz, fieldValues)).toCountQuery().getSingleResult()).intValue();
188        }
189
190        /**
191         * This is the default impl that comes with Kuali - uses OJB.
192         * 
193         * @see org.kuali.rice.krad.dao.BusinessObjectDao#countMatching(java.lang.Class,
194         *      java.util.Map, java.util.Map)
195         */
196        public int countMatching(Class clazz, Map<String, ?> positiveFieldValues, Map<String, ?> negativeFieldValues) {
197                Criteria criteria = buildJpaCriteria(clazz, positiveFieldValues);
198                criteria.and(buildNegativeJpaCriteria(clazz, negativeFieldValues));
199                return ((Long) new QueryByCriteria(entityManager, criteria).toCountQuery().getSingleResult()).intValue();
200        }
201
202        /**
203         * This is the default impl that comes with Kuali - uses OJB.
204         * 
205         * @see org.kuali.rice.krad.dao.BusinessObjectDao#findMatching(java.lang.Class,
206         *      java.util.Map)
207         */
208        public <T extends BusinessObject> Collection<T> findMatchingOrderBy(Class<T> clazz, Map<String, ?> fieldValues, String sortField, boolean sortAscending) {
209                Criteria criteria = buildJpaCriteria(clazz, fieldValues);
210                criteria.orderBy(sortField, sortAscending);
211                return (Collection<T>)new QueryByCriteria(entityManager, criteria).toQuery().getResultList();
212        }
213
214        /**
215         * Saves a business object.
216         * 
217         * @see org.kuali.rice.krad.dao.BusinessObjectDao#save(org.kuali.rice.krad.bo.PersistableBusinessObject)
218         */
219        public PersistableBusinessObject save(PersistableBusinessObject bo) throws DataAccessException {
220                /* KC determined this is not needed for JPA
221                // if collections exist on the BO, create a copy and use to process the
222                // collections to ensure
223                // that removed elements are deleted from the database
224                Set<String> boCollections = getPersistenceStructureService().listCollectionObjectTypes(bo.getClass()).keySet();
225                PersistableBusinessObject savedBo = null;
226                if (!boCollections.isEmpty()) {
227                        // refresh bo to get db copy of collections
228                        savedBo = (PersistableBusinessObject) ObjectUtils.deepCopy(bo);
229                        for (String boCollection : boCollections) {
230                                if (getPersistenceStructureService().isCollectionUpdatable(savedBo.getClass(), boCollection)) {
231                                        savedBo.refreshReferenceObject(boCollection);
232                                }
233                        }
234                }
235                */
236                if (entityManager.contains(bo) && ((HibernateEntityManager)entityManager).getSession().isReadOnly(bo)) {
237                        ((HibernateEntityManager)entityManager).getSession().setReadOnly(bo, false); // are we read only?  turn that off...
238                }
239                return reattachAndSave(bo);
240        }
241        
242                
243
244        /**
245         * Saves a business object.
246         * 
247         * @see org.kuali.rice.krad.dao.BusinessObjectDao#save(org.kuali.rice.krad.bo.PersistableBusinessObject)
248         */
249        public List<? extends PersistableBusinessObject> save(List businessObjects) throws DataAccessException {
250                List<PersistableBusinessObject> savedBOs = new ArrayList<PersistableBusinessObject>();
251                for (Iterator i = businessObjects.iterator(); i.hasNext();) {
252                        Object bo = i.next();
253                        final PersistableBusinessObject savedBusinessObject = reattachAndSave((PersistableBusinessObject) bo);
254                        savedBOs.add(savedBusinessObject);
255                }
256                return savedBOs;
257        }
258
259        /**
260         * Deletes the business object passed in.
261         * 
262         * @param bo
263         * @throws DataAccessException
264         * @see org.kuali.rice.krad.dao.BusinessObjectDao#delete(org.kuali.rice.krad.bo.PersistableBusinessObject)
265         */
266        public void delete(PersistableBusinessObject bo) {
267                final PersistableBusinessObject realPBO = materialize(bo);
268                if (realPBO != null) {
269                        if (realPBO.getExtension() != null) {
270                                delete(realPBO.getExtension());
271                        }
272                        if (entityManager.contains(realPBO)) {
273                                entityManager.remove(realPBO);
274                        } else {
275                                final PersistableBusinessObject foundBO = (PersistableBusinessObject)entityManager.find(realPBO.getClass(), MetadataManager.getEntityPrimaryKeyObject(realPBO));
276                                if (foundBO != null) {
277                                        entityManager.remove(foundBO);
278                                }
279                        }
280                }
281        }
282        
283        /**
284         * If the object is a proxy, materializes it
285         * 
286         * @param bo the business object, which may be a sneaky proxy
287         * @return the materialized non-proxied business object
288         */
289        protected PersistableBusinessObject materialize(PersistableBusinessObject bo) {
290                try {
291                        if (bo instanceof HibernateProxy) {
292                                return (PersistableBusinessObject)((HibernateProxy)bo).getHibernateLazyInitializer().getImplementation();
293                        }
294                        return bo;
295                } catch (EntityNotFoundException enfe) {
296                        return null;  // could not find the entity - just return null
297                }
298        }
299
300        /**
301         * @see org.kuali.rice.krad.dao.BusinessObjectDao#delete(java.util.List)
302         */
303        public void delete(List<? extends PersistableBusinessObject> boList) {
304                for (PersistableBusinessObject bo : boList) {
305                        delete(bo);
306                }
307        }
308
309        /**
310         * @see org.kuali.rice.krad.dao.BusinessObjectDao#deleteMatching(java.lang.Class,
311         *      java.util.Map)
312         */
313        public void deleteMatching(Class clazz, Map<String, ?> fieldValues) {
314                // Rice JPA MetadataManager
315                new QueryByCriteria(entityManager, buildJpaCriteria(clazz, fieldValues), QueryByCriteriaType.DELETE).toQuery().executeUpdate();
316        }
317
318        /**
319         * @see org.kuali.rice.krad.dao.BusinessObjectDao#retrieve(org.kuali.rice.krad.bo.PersistableBusinessObject)
320         */
321        public PersistableBusinessObject retrieve(PersistableBusinessObject object) {
322                PersistableBusinessObject pbo = null;
323                Object pkObject = MetadataManager.getEntityPrimaryKeyObject(object);
324                if (pkObject != null) {
325                        pbo = (PersistableBusinessObject) entityManager.find(object.getClass(), pkObject);
326                        if (pbo != null && pbo.getExtension() != null) {
327                                pbo.setExtension((PersistableBusinessObjectExtension) entityManager.find(pbo.getExtension().getClass(), MetadataManager.getPersistableBusinessObjectPrimaryKeyObjectWithValuesForExtension(pbo, pbo.getExtension())));
328                        }
329                }
330                return pbo;
331        }
332
333        private Criteria buildJpaCriteria(Class clazz, Map<String, ?> fieldValues) {
334                Criteria criteria = new Criteria(clazz.getName());
335                for (Iterator i = fieldValues.entrySet().iterator(); i.hasNext();) {
336                        Map.Entry<String, ?> e = (Map.Entry<String, ?>) i.next();
337
338                        String key = e.getKey();
339                        Object value = e.getValue();
340                        String alias = "";
341                        String[] keySplit = key.split("\\.");
342                        if (keySplit.length > 1) {
343                                alias = keySplit[keySplit.length-2];
344                                String variableKey = keySplit[keySplit.length-1];
345                                for (int j = 0; j < keySplit.length - 1; j++)  {
346                                        if (criteria.getAliasIndex(keySplit[j]) == -1) {
347                                        criteria.join(keySplit[j], keySplit[j], false, true);
348                                }
349                                }
350                                key = "__JPA_ALIAS[['" + alias + "']]__." + variableKey;
351                        }
352                        if (value == null) {
353                                continue;
354                        } else if (value instanceof Collection) {
355                                criteria.in(key, (Collection)value);
356                        } else {
357                                criteria.eq(key, value);
358                        }
359                }
360                return criteria;
361        }
362
363        private Criteria buildActiveJpaCriteria(Class clazz) {
364                Criteria criteria = new Criteria(clazz.getName());
365                criteria.eq(KRADPropertyConstants.ACTIVE, true);
366                return criteria;
367        }
368
369        private Criteria buildInactiveJpaCriteria(Class clazz) {
370                Criteria criteria = new Criteria(clazz.getName());
371                criteria.eq(KRADPropertyConstants.ACTIVE, false);
372                return criteria;
373        }
374
375        private Criteria buildNegativeJpaCriteria(Class clazz, Map negativeFieldValues) {
376                Criteria criteria = new Criteria(clazz.getName());
377                for (Iterator i = negativeFieldValues.entrySet().iterator(); i.hasNext();) {
378                        Map.Entry e = (Map.Entry) i.next();
379
380                        String key = (String) e.getKey();
381                        Object value = e.getValue();
382                        if (value instanceof Collection) {
383                                criteria.notIn(key, (List) value);
384                        } else {
385                                criteria.ne(key, value);
386                        }
387                }
388
389                return criteria;
390        }
391
392        /**
393         * @see org.kuali.rice.krad.dao.BusinessObjectDao#manageReadOnly(org.kuali.rice.krad.bo.PersistableBusinessObject)
394         */
395        public PersistableBusinessObject manageReadOnly(PersistableBusinessObject bo) {
396                Session session = ((HibernateEntityManager)entityManager).getSession();
397                FlushMode currentFlushMode = session.getFlushMode();
398                session.setFlushMode(FlushMode.MANUAL); // make sure the merge doesn't flush what we're trying to make read only
399                PersistableBusinessObject managedBO = entityManager.merge(bo);
400                session.setReadOnly(managedBO, true);
401                session.setFlushMode(currentFlushMode);
402                return managedBO;
403        }
404
405        /**
406         * Gets the persistenceStructureService attribute.
407         * 
408         * @return Returns the persistenceStructureService.
409         */
410        protected PersistenceStructureService getPersistenceStructureService() {
411                return persistenceStructureService;
412        }
413
414        /**
415         * Sets the persistenceStructureService attribute value.
416         * 
417         * @param persistenceStructureService
418         *            The persistenceStructureService to set.
419         */
420        public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
421                this.persistenceStructureService = persistenceStructureService;
422        }
423
424        private PersistableBusinessObject reattachAndSave(PersistableBusinessObject bo) {
425                PersistableBusinessObject attachedBo = findByPrimaryKey(bo.getClass(), MetadataManager.getEntityPrimaryKeyValuePairs(bo));
426                PersistableBusinessObject newBo = attachedBo;
427                if (attachedBo == null) {
428                        newBo = entityManager.merge(bo);
429                        if (bo.getExtension() != null) {
430                                entityManager.merge(bo.getExtension());
431                        }
432                } else {
433                        /*if (bo.getExtension() != null) {
434                                PersistableBusinessObject attachedBoe = findByPrimaryKey(bo.getExtension().getClass(), MetadataManager.getEntityPrimaryKeyValuePairs(bo.getExtension()));
435                                OrmUtils.reattach(bo.getExtension(),attachedBoe);
436                                attachedBo.setExtension((PersistableBusinessObjectExtension) attachedBoe);
437                                entityManager.merge(attachedBoe);
438                        }*/
439                        OrmUtils.reattach(bo, attachedBo);
440                        newBo = entityManager.merge(attachedBo);
441                }
442                return newBo;
443        }
444
445    /**
446     * @return the entityManager
447     */
448    public EntityManager getEntityManager() {
449        return this.entityManager;
450    }
451
452    /**
453     * @param entityManager the entityManager to set
454     */
455    public void setEntityManager(EntityManager entityManager) {
456        this.entityManager = entityManager;
457    }
458        
459}