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