1 /* Copyright 2004-2005 Graeme Rocher
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 package org.codehaus.groovy.grails.orm.hibernate.cfg;
16 
17 import groovy.lang.GroovyObject;
18 import groovy.lang.GroovySystem;
19 import groovy.lang.MetaClass;
20 
21 import java.lang.reflect.Modifier;
22 import java.sql.SQLException;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Set;
28 
29 import org.apache.commons.lang.StringUtils;
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32 import org.codehaus.groovy.grails.commons.ApplicationHolder;
33 import org.codehaus.groovy.grails.commons.ArtefactHandler;
34 import org.codehaus.groovy.grails.commons.ConfigurationHolder;
35 import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler;
36 import org.codehaus.groovy.grails.commons.GrailsApplication;
37 import org.codehaus.groovy.grails.commons.GrailsClass;
38 import org.codehaus.groovy.grails.commons.GrailsClassUtils;
39 import org.codehaus.groovy.grails.commons.GrailsDomainClass;
40 import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty;
41 import org.codehaus.groovy.grails.orm.hibernate.GrailsHibernateDomainClass;
42 import org.codehaus.groovy.grails.orm.hibernate.proxy.GroovyAwareJavassistProxyFactory;
43 import org.codehaus.groovy.grails.orm.hibernate.proxy.HibernateProxyHandler;
44 import org.hibernate.Criteria;
45 import org.hibernate.EntityMode;
46 import org.hibernate.FetchMode;
47 import org.hibernate.FlushMode;
48 import org.hibernate.Hibernate;
49 import org.hibernate.HibernateException;
50 import org.hibernate.LockMode;
51 import org.hibernate.Session;
52 import org.hibernate.SessionFactory;
53 import org.hibernate.criterion.Order;
54 import org.hibernate.engine.EntityEntry;
55 import org.hibernate.engine.SessionImplementor;
56 import org.hibernate.engine.Status;
57 import org.hibernate.mapping.PersistentClass;
58 import org.hibernate.mapping.Property;
59 import org.hibernate.metadata.ClassMetadata;
60 import org.hibernate.property.Getter;
61 import org.hibernate.property.Setter;
62 import org.hibernate.proxy.HibernateProxy;
63 import org.hibernate.type.AbstractComponentType;
64 import org.springframework.beans.SimpleTypeConverter;
65 import org.springframework.orm.hibernate3.HibernateCallback;
66 import org.springframework.orm.hibernate3.HibernateTemplate;
67 
68 /**
69  * Utility methods for configuring Hibernate inside Grails.
70  *
71  * @author Graeme Rocher
72  * @since 0.4
73  */
74 public class GrailsHibernateUtil {
75     private static final Log LOG = LogFactory.getLog(GrailsHibernateUtil.class);
76 
77     private static final String DYNAMIC_FILTER_ENABLER = "dynamicFilterEnabler";
78 
79     public static SimpleTypeConverter converter = new SimpleTypeConverter();
80     public static final String ARGUMENT_MAX = "max";
81     public static final String ARGUMENT_OFFSET = "offset";
82     public static final String ARGUMENT_ORDER = "order";
83     public static final String ARGUMENT_SORT = "sort";
84     public static final String ORDER_DESC = "desc";
85     public static final String ORDER_ASC = "asc";
86     public static final String ARGUMENT_FETCH = "fetch";
87     public static final String ARGUMENT_IGNORE_CASE = "ignoreCase";
88     public static final String ARGUMENT_CACHE = "cache";
89     public static final String ARGUMENT_LOCK = "lock";
90     public static final String CONFIG_PROPERTY_CACHE_QUERIES="grails.hibernate.cache.queries";
91     public static final Class<?>[] EMPTY_CLASS_ARRAY=new Class<?>[0];
92 
93     private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler();
94 
enableDynamicFilterEnablerIfPresent(SessionFactory sessionFactory, Session session)95     public static void enableDynamicFilterEnablerIfPresent(SessionFactory sessionFactory, Session session) {
96     	if(sessionFactory != null && session != null) {
97             final Set definedFilterNames = sessionFactory.getDefinedFilterNames();
98     		if(definedFilterNames != null && definedFilterNames.contains(DYNAMIC_FILTER_ENABLER))
99             		session.enableFilter(DYNAMIC_FILTER_ENABLER); // work around for HHH-2624
100 
101     	}
102     }
103 
104     @SuppressWarnings("rawtypes")
configureHibernateDomainClasses(SessionFactory sessionFactory, GrailsApplication application)105     public static void configureHibernateDomainClasses(SessionFactory sessionFactory, GrailsApplication application) {
106         Map<String, GrailsDomainClass> hibernateDomainClassMap = new HashMap<String, GrailsDomainClass>();
107         ArtefactHandler artefactHandler = application.getArtefactHandler(DomainClassArtefactHandler.TYPE);
108         Map defaultContraints = Collections.emptyMap();
109         if (artefactHandler instanceof DomainClassArtefactHandler) {
110             final Map map = ((DomainClassArtefactHandler) artefactHandler).getDefaultConstraints();
111             if (map != null) {
112                 defaultContraints = map;
113             }
114         }
115         for (Object o : sessionFactory.getAllClassMetadata().values()) {
116             ClassMetadata classMetadata = (ClassMetadata) o;
117             configureDomainClass(sessionFactory, application, classMetadata,
118                     classMetadata.getMappedClass(EntityMode.POJO),
119                     hibernateDomainClassMap, defaultContraints);
120         }
121         configureInheritanceMappings(hibernateDomainClassMap);
122     }
123 
124     @SuppressWarnings("rawtypes")
configureInheritanceMappings(Map hibernateDomainClassMap)125     public static void configureInheritanceMappings(Map hibernateDomainClassMap) {
126         // now get through all domainclasses, and add all subclasses to root class
127         for (Object o : hibernateDomainClassMap.values()) {
128             GrailsDomainClass baseClass = (GrailsDomainClass) o;
129             if (!baseClass.isRoot()) {
130                 Class<?> superClass = baseClass.getClazz().getSuperclass();
131 
132                 while (!superClass.equals(Object.class) && !superClass.equals(GroovyObject.class)) {
133                     GrailsDomainClass gdc = (GrailsDomainClass) hibernateDomainClassMap.get(superClass.getName());
134 
135                     if (gdc == null || gdc.getSubClasses() == null) {
136                         LOG.debug("did not find superclass names when mapping inheritance....");
137                         break;
138                     }
139                     gdc.getSubClasses().add(baseClass);
140                     superClass = superClass.getSuperclass();
141                 }
142             }
143         }
144     }
145 
146     @SuppressWarnings({ "unchecked", "rawtypes" })
configureDomainClass(SessionFactory sessionFactory, GrailsApplication application, ClassMetadata cmd, Class<?> persistentClass, Map<String, GrailsDomainClass> hibernateDomainClassMap, Map defaultContraints)147     private static void configureDomainClass(SessionFactory sessionFactory, GrailsApplication application,
148             ClassMetadata cmd, Class<?> persistentClass, Map<String, GrailsDomainClass> hibernateDomainClassMap,
149             Map defaultContraints) {
150 
151         if (Modifier.isAbstract(persistentClass.getModifiers())) {
152             return;
153         }
154 
155         LOG.trace("Configuring domain class [" + persistentClass + "]");
156         GrailsDomainClass dc = (GrailsDomainClass) application.getArtefact(DomainClassArtefactHandler.TYPE, persistentClass.getName());
157         if (dc == null) {
158             // a patch to add inheritance to this system
159             GrailsHibernateDomainClass ghdc = new GrailsHibernateDomainClass(
160                     persistentClass, sessionFactory, application, cmd, defaultContraints);
161 
162             hibernateDomainClassMap.put(persistentClass.getName(), ghdc);
163             dc = (GrailsDomainClass) application.addArtefact(DomainClassArtefactHandler.TYPE, ghdc);
164         }
165     }
166 
167     @SuppressWarnings("rawtypes")
populateArgumentsForCriteria(Class<?> targetClass, Criteria c, Map argMap)168     public static void populateArgumentsForCriteria(Class<?> targetClass, Criteria c, Map argMap) {
169         Integer maxParam = null;
170         Integer offsetParam = null;
171         if (argMap.containsKey(ARGUMENT_MAX)) {
172             maxParam = converter.convertIfNecessary(argMap.get(ARGUMENT_MAX),Integer.class);
173         }
174         if (argMap.containsKey(ARGUMENT_OFFSET)) {
175             offsetParam = converter.convertIfNecessary(argMap.get(ARGUMENT_OFFSET),Integer.class);
176         }
177         String orderParam = (String)argMap.get(ARGUMENT_ORDER);
178         Object fetchObj = argMap.get(ARGUMENT_FETCH);
179         if (fetchObj instanceof Map) {
180             Map fetch = (Map)fetchObj;
181             for (Object o : fetch.keySet()) {
182                 String associationName = (String) o;
183                 c.setFetchMode(associationName, getFetchMode(fetch.get(associationName)));
184             }
185         }
186 
187         final String sort = (String)argMap.get(ARGUMENT_SORT);
188         final String order = ORDER_DESC.equalsIgnoreCase(orderParam) ? ORDER_DESC : ORDER_ASC;
189         final int max = maxParam == null ? -1 : maxParam;
190         final int offset = offsetParam == null ? -1 : offsetParam;
191         if (max > -1) {
192             c.setMaxResults(max);
193         }
194         if (offset > -1) {
195             c.setFirstResult(offset);
196         }
197         if (GrailsClassUtils.getBooleanFromMap(ARGUMENT_CACHE, argMap)) {
198             c.setCacheable(true);
199         }
200         if (GrailsClassUtils.getBooleanFromMap(ARGUMENT_LOCK, argMap)) {
201             c.setLockMode(LockMode.UPGRADE);
202         }
203         else {
204             if (argMap.get(ARGUMENT_CACHE) == null) {
205                 cacheCriteriaByMapping(targetClass, c);
206             }
207         }
208         if (sort != null) {
209             boolean ignoreCase = true;
210             Object caseArg = argMap.get(ARGUMENT_IGNORE_CASE);
211             if (caseArg instanceof Boolean) {
212                 ignoreCase = (Boolean) caseArg;
213             }
214             addOrderPossiblyNested(c, targetClass, sort, order, ignoreCase);
215         }
216         else {
217             Mapping m = GrailsDomainBinder.getMapping(targetClass);
218             if (m != null && !StringUtils.isBlank(m.getSort())) {
219                 if (ORDER_DESC.equalsIgnoreCase(m.getOrder())) {
220                     c.addOrder(Order.desc(m.getSort()));
221                 }
222                 else {
223                     c.addOrder(Order.asc(m.getSort()));
224                 }
225             }
226         }
227     }
228 
229     /**
230      * Add order to criteria, creating necessary subCriteria if nested sort property (ie. sort:'nested.property').
231      */
addOrderPossiblyNested(Criteria c, Class<?> targetClass, String sort, String order, boolean ignoreCase)232     private static void addOrderPossiblyNested(Criteria c, Class<?> targetClass, String sort, String order, boolean ignoreCase) {
233         int firstDotPos = sort.indexOf(".");
234         if (firstDotPos == -1) {
235             addOrder(c, sort, order, ignoreCase);
236         } else { // nested property
237             String sortHead = sort.substring(0,firstDotPos);
238             String sortTail = sort.substring(firstDotPos+1);
239             GrailsDomainClassProperty property = getGrailsDomainClassProperty(targetClass, sortHead);
240             if (property.isEmbedded()) {
241                 // embedded objects cannot reference entities (at time of writing), so no more recursion needed
242                 addOrder(c, sort, order, ignoreCase);
243             } else {
244                 Criteria subCriteria = c.createCriteria(sortHead);
245                 Class<?> propertyTargetClass = property.getReferencedDomainClass().getClazz();
246                 addOrderPossiblyNested(subCriteria, propertyTargetClass, sortTail, order, ignoreCase); // Recurse on nested sort
247             }
248         }
249     }
250 
251     /**
252      * Add order directly to criteria.
253      */
addOrder(Criteria c, String sort, String order, boolean ignoreCase)254     private static void addOrder(Criteria c, String sort, String order, boolean ignoreCase) {
255         if (ORDER_DESC.equals(order)) {
256             c.addOrder( ignoreCase ? Order.desc(sort).ignoreCase() : Order.desc(sort));
257         }
258         else {
259             c.addOrder( ignoreCase ? Order.asc(sort).ignoreCase() : Order.asc(sort) );
260         }
261     }
262 
263     /**
264      * Get hold of the GrailsDomainClassProperty represented by the targetClass' propertyName,
265      * assuming targetClass corresponds to a GrailsDomainClass.
266      */
getGrailsDomainClassProperty(Class<?> targetClass, String propertyName)267     private static GrailsDomainClassProperty getGrailsDomainClassProperty(Class<?> targetClass, String propertyName) {
268         GrailsClass grailsClass = ApplicationHolder.getApplication().getArtefact(DomainClassArtefactHandler.TYPE, targetClass.getName());
269         if (!(grailsClass instanceof GrailsDomainClass)) {
270             throw new IllegalArgumentException("Unexpected: class is not a domain class:"+targetClass.getName());
271         }
272         GrailsDomainClass domainClass = (GrailsDomainClass) grailsClass;
273         return domainClass.getPropertyByName(propertyName);
274     }
275 
276     /**
277      * Configures the criteria instance to cache based on the configured mapping.
278      *
279      * @param targetClass The target class
280      * @param criteria The criteria
281      */
cacheCriteriaByMapping(Class<?> targetClass, Criteria criteria)282     public static void cacheCriteriaByMapping(Class<?> targetClass, Criteria criteria) {
283         Mapping m = GrailsDomainBinder.getMapping(targetClass);
284         if (m != null && m.getCache() != null && m.getCache().getEnabled()) {
285             criteria.setCacheable(true);
286         }
287     }
288 
289     @SuppressWarnings("rawtypes")
populateArgumentsForCriteria(Criteria c, Map argMap)290     public static void populateArgumentsForCriteria(Criteria c, Map argMap) {
291         populateArgumentsForCriteria(null, c, argMap);
292     }
293 
294     /**
295      * Retrieves the fetch mode for the specified instance; otherwise returns the default FetchMode.
296      *
297      * @param object The object, converted to a string
298      * @return The FetchMode
299      */
getFetchMode(Object object)300     public static FetchMode getFetchMode(Object object) {
301         String name = object != null ? object.toString() : "default";
302         if (name.equalsIgnoreCase(FetchMode.JOIN.toString()) || name.equalsIgnoreCase("eager")) {
303             return FetchMode.JOIN;
304         }
305         if (name.equalsIgnoreCase(FetchMode.SELECT.toString()) || name.equalsIgnoreCase("lazy")) {
306             return FetchMode.SELECT;
307         }
308         return FetchMode.DEFAULT;
309     }
310 
311     /**
312      * Sets the target object to read-only using the given SessionFactory instance. This
313      * avoids Hibernate performing any dirty checking on the object
314      *
315      * @see #setObjectToReadWrite(Object, org.hibernate.SessionFactory)
316      *
317      * @param target The target object
318      * @param sessionFactory The SessionFactory instance
319      */
setObjectToReadyOnly(Object target, SessionFactory sessionFactory)320     public static void setObjectToReadyOnly(Object target, SessionFactory sessionFactory) {
321         Session session = sessionFactory.getCurrentSession();
322         if (canModifyReadWriteState(session, target)) {
323             if (target instanceof HibernateProxy) {
324                 target = ((HibernateProxy)target).getHibernateLazyInitializer().getImplementation();
325             }
326             session.setReadOnly(target, true);
327             session.setFlushMode(FlushMode.MANUAL);
328         }
329     }
330 
canModifyReadWriteState(Session session, Object target)331     private static boolean canModifyReadWriteState(Session session, Object target) {
332         return session.contains(target) && Hibernate.isInitialized(target);
333     }
334 
335     /**
336      * Sets the target object to read-write, allowing Hibernate to dirty check it and auto-flush changes.
337      *
338      * @see #setObjectToReadyOnly(Object, org.hibernate.SessionFactory)
339      *
340      * @param target The target object
341      * @param sessionFactory The SessionFactory instance
342      */
setObjectToReadWrite(final Object target, SessionFactory sessionFactory)343     public static void setObjectToReadWrite(final Object target, SessionFactory sessionFactory) {
344         HibernateTemplate template = new HibernateTemplate(sessionFactory);
345         template.setExposeNativeSession(true);
346         template.execute(new HibernateCallback<Void>() {
347             public Void doInHibernate(Session session) throws HibernateException, SQLException {
348                 if (canModifyReadWriteState(session, target)) {
349                     SessionImplementor sessionImpl = (SessionImplementor) session;
350                     EntityEntry ee = sessionImpl.getPersistenceContext().getEntry(target);
351 
352                     if (ee != null && ee.getStatus() == Status.READ_ONLY) {
353                         Object actualTarget = target;
354                         if (target instanceof HibernateProxy) {
355                             actualTarget = ((HibernateProxy)target).getHibernateLazyInitializer().getImplementation();
356                         }
357 
358                         session.setReadOnly(actualTarget, false);
359                         session.setFlushMode(FlushMode.AUTO);
360                         incrementVersion(target);
361                     }
362                 }
363                 return null;
364             }
365         });
366     }
367 
368     /**
369      * Increments the entities version number in order to force an update
370      * @param target The target entity
371      */
incrementVersion(Object target)372     public static void incrementVersion(Object target) {
373         MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(target.getClass());
374         if (metaClass.hasProperty(target, GrailsDomainClassProperty.VERSION)!=null) {
375             Object version = metaClass.getProperty(target, GrailsDomainClassProperty.VERSION);
376             if (version instanceof Long) {
377                 Long newVersion = (Long) version + 1;
378                 metaClass.setProperty(target, GrailsDomainClassProperty.VERSION, newVersion);
379             }
380         }
381     }
382 
383     /**
384      * Ensures the meta class is correct for a given class
385      *
386      * @param target The GroovyObject
387      * @param persistentClass The persistent class
388      */
ensureCorrectGroovyMetaClass(Object target, Class<?> persistentClass)389     public static void ensureCorrectGroovyMetaClass(Object target, Class<?> persistentClass) {
390         if (target instanceof GroovyObject) {
391             GroovyObject go = ((GroovyObject)target);
392             if (!go.getMetaClass().getTheClass().equals(persistentClass)) {
393                 go.setMetaClass(GroovySystem.getMetaClassRegistry().getMetaClass(persistentClass));
394             }
395         }
396     }
397 
398     /**
399      * Unwraps and initializes a HibernateProxy.
400      * @param proxy The proxy
401      * @return the unproxied instance
402      */
unwrapProxy(HibernateProxy proxy)403     public static Object unwrapProxy(HibernateProxy proxy) {
404         return proxyHandler.unwrapProxy(proxy);
405     }
406 
407     /**
408      * Returns the proxy for a given association or null if it is not proxied
409      *
410      * @param obj The object
411      * @param associationName The named assoication
412      * @return A proxy
413      */
getAssociationProxy(Object obj, String associationName)414     public static HibernateProxy getAssociationProxy(Object obj, String associationName) {
415         return proxyHandler.getAssociationProxy(obj, associationName);
416     }
417 
418     /**
419      * Checks whether an associated property is initialized and returns true if it is
420      *
421      * @param obj The name of the object
422      * @param associationName The name of the association
423      * @return True if is initialized
424      */
isInitialized(Object obj, String associationName)425     public static boolean isInitialized(Object obj, String associationName) {
426         return proxyHandler.isInitialized(obj, associationName);
427     }
428 
isCacheQueriesByDefault()429     public static boolean isCacheQueriesByDefault() {
430         Object o = ConfigurationHolder.getFlatConfig().get(CONFIG_PROPERTY_CACHE_QUERIES);
431         return (o != null && o instanceof Boolean)?((Boolean)o).booleanValue():false;
432     }
433 
434     @SuppressWarnings("serial")
buildProxyFactory(PersistentClass persistentClass)435     public static GroovyAwareJavassistProxyFactory buildProxyFactory(PersistentClass persistentClass) {
436         GroovyAwareJavassistProxyFactory proxyFactory = new GroovyAwareJavassistProxyFactory();
437 
438         Set<Class<?>> proxyInterfaces = new HashSet<Class<?>>() {{
439             add(HibernateProxy.class);
440         }};
441 
442         final Class<?> javaClass = persistentClass.getMappedClass();
443         final Property identifierProperty = persistentClass.getIdentifierProperty();
444         final Getter idGetter = identifierProperty!=null?  identifierProperty.getGetter(javaClass) : null;
445         final Setter idSetter =identifierProperty!=null? identifierProperty.getSetter(javaClass) : null;
446 
447         if (idGetter == null ||  idSetter==null) return null;
448 
449         try {
450             proxyFactory.postInstantiate(persistentClass.getEntityName(), javaClass, proxyInterfaces,
451                     idGetter.getMethod(), idSetter.getMethod(),
452                     persistentClass.hasEmbeddedIdentifier() ?
453                             (AbstractComponentType) persistentClass.getIdentifier().getType() :
454                             null);
455         }
456         catch (HibernateException e) {
457             LOG.warn("Cannot instantiate proxy factory: " + e.getMessage());
458             return null;
459         }
460 
461         return proxyFactory;
462     }
463 
unwrapIfProxy(Object instance)464     public static Object unwrapIfProxy(Object instance) {
465         return proxyHandler.unwrapIfProxy(instance);
466     }
467 }
468