View Javadoc

    1   /* Copyright 2004, 2005 Acegi Technology Pty Limited
    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
    16  package org.acegisecurity.domain.hibernate;
    17
    18  import java.io.Serializable;
    19  import java.lang.reflect.Method;
    20  import java.util.Collection;
    21  import java.util.List;
    22
    23  import org.acegisecurity.domain.PersistableEntity;
    24  import org.acegisecurity.domain.dao.Dao;
    25  import org.acegisecurity.domain.dao.DetachmentContextHolder;
    26  import org.acegisecurity.domain.dao.EvictionCapable;
    27  import org.acegisecurity.domain.dao.InitializationCapable;
    28  import org.acegisecurity.domain.dao.PaginatedList;
    29  import org.acegisecurity.domain.util.GenericsUtils;
    30  import org.acegisecurity.domain.validation.ValidationManager;
    31
    32  import org.hibernate.Criteria;
    33  import org.hibernate.EntityMode;
    34  import org.hibernate.FetchMode;
    35  import org.hibernate.Hibernate;
    36  import org.hibernate.HibernateException;
    37  import org.hibernate.Session;
    38  import org.hibernate.criterion.Expression;
    39  import org.hibernate.criterion.MatchMode;
    40  import org.hibernate.criterion.Order;
    41  import org.hibernate.metadata.ClassMetadata;
    42  import org.hibernate.type.Type;
    43  import org.springframework.dao.DataIntegrityViolationException;
    44  import org.springframework.orm.hibernate3.HibernateCallback;
    45  import org.springframework.orm.hibernate3.HibernateTemplate;
    46  import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
    47  import org.springframework.util.Assert;
    48  import org.springframework.validation.BindException;
    49
    50
    51  /***
    52   * Generics supporting {@link Dao} implementation that uses Hibernate 3 for persistence.
    53   *
    54   * @author Ben Alex
    55   * @author Matthew Porter
    56   * @version $Id: DaoHibernate.java,v 1.11 2005/11/17 00:55:49 benalex Exp $
    57   */
    58  public class DaoHibernate<E extends PersistableEntity> extends HibernateDaoSupport implements Dao<E>,
    59      EvictionCapable, InitializationCapable {
    60      //~ Instance fields ========================================================
    61
    62      /*** The class that this instance provides services for */
    63      private Class supportsClass;
    64
    65  	/*** Enables mutator methods to validate an object prior to persistence */
    66  	private ValidationManager validationManager;
    67
    68  	public DaoHibernate() {
    69  		this.supportsClass = GenericsUtils.getGeneric(getClass());
    70  		if (this.supportsClass == null) {
    71  			if (logger.isWarnEnabled()) {
    72  				logger.warn("Could not determine the generics type - you will need to set manually");
    73  			}
    74  		}
    75  	}
    76
    77      //~ Methods ================================================================
    78
    79  	/***
    80  	 * Obtains a <code>HibernateTemplate</code> that uses the appropriate <code>Session</code>
    81  	 * based on the value of {@link DetachmentContextHolder}.
    82  	 *
    83  	 * <p>Specifically, if <code>DetachmentContextHolder</code> requires detached instances,
    84  	 * the method will build a new <code>Session</code> (ignore the current thread-bound
    85  	 * <code>Session</code>) and use that new <code>Session</code> in the <code>HibernateTemplate</code>.
    86  	 * If <code>DetachmentContextHolder</code> is at its fault <code>false</code> value, the
    87  	 * returned <code>HibernateTemplate</code> will simply use the <code>Session</code> obtained
    88  	 * from the superclass, which is generally the same <code>Session</code> as used for the
    89  	 * transaction.
    90  	 *
    91  	 * @return the template, containing the correct <code>Session</code> based on the
    92  	 * <code>DetachmentContactHolder</code> request
    93  	 */
    94  	protected HibernateTemplate doGetHibernateTemplate() {
    95  		if (DetachmentContextHolder.isForceReturnOfDetachedInstances()) {
    96  			HibernateTemplate hibernateTemplate = new HibernateTemplate(getSessionFactory());
    97  			hibernateTemplate.setAlwaysUseNewSession(true);
    98  			return hibernateTemplate;
    99  		} else {
    100 			return super.getHibernateTemplate();
    101 		}
    102 	}
    103
    104     public void setSupportsClass(Class supportClass) {
    105         this.supportsClass = supportClass;
    106     }
    107
    108     public Class getSupportsClass() {
    109         return supportsClass;
    110     }
    111
    112     public ValidationManager getValidationManager() {
    113 		return validationManager;
    114 	}
    115
    116 	public void setValidationManager(ValidationManager validationManager) {
    117 		this.validationManager = validationManager;
    118 	}
    119
    120 	public E create(E value) {
    121         Assert.notNull(value);
    122 		validate(value);
    123         doGetHibernateTemplate().save(value);
    124
    125         return readId(value.getInternalId());
    126     }
    127
    128 	protected void validate(E value) throws DataIntegrityViolationException {
    129 		try {
    130 			validationManager.validate(value);
    131 		} catch (BindException bindException) {
    132 			throw new DataIntegrityViolationException("Entity state is invalid", bindException);
    133 		}
    134 	}
    135
    136     public void delete(E value) {
    137         Assert.notNull(value);
    138 		validate(value);
    139         doGetHibernateTemplate().delete(value);
    140     }
    141
    142     public void evict(PersistableEntity entity) {
    143         Assert.notNull(entity);
    144         doGetHibernateTemplate().evict(entity);
    145     }
    146
    147     public List<E> findAll() {
    148         return doGetHibernateTemplate().loadAll(supportsClass);
    149     }
    150
    151     public List<E> findId(Collection<Serializable> ids) {
    152         Assert.notNull(ids, "Collection of IDs cannot be null");
    153         Assert.notEmpty(ids, "There must be some values in the Collection list");
    154
    155         return (List) doGetHibernateTemplate().execute(getFindByIdCallback(ids));
    156     }
    157
    158     private E readId(final Serializable id, final boolean populate) {
    159         Assert.notNull(id);
    160 		return (E) doGetHibernateTemplate().execute(new HibernateCallback() {
    161 			public Object doInHibernate(Session session) throws HibernateException {
    162 				E obj = (E) session.get(supportsClass, id);
    163 				if (populate) {
    164 					initializeAllZeroArgumentGetters(obj);
    165 				}
    166 				return obj;
    167 			}
    168 		}, true);
    169     }
    170
    171     public E readId(Serializable id) {
    172         Assert.notNull(id);
    173 		return readId(id, false);
    174     }
    175
    176 	public E readPopulatedId(Serializable id) {
    177         Assert.notNull(id);
    178         return readId(id, true);
    179 	}
    180
    181 	/***
    182 	 * Locates every <code>get*()</code> method against the passed entity
    183 	 * and calls it. This method does not nest its initialization beyond
    184 	 * the immediately passed entity.
    185 	 *
    186 	 * <p>For example, a Foo object might provide a getBar() method.
    187 	 * Passing the Foo instance to this method will guarantee getBar() is
    188 	 * available to the services layer. However, if getBar() returned a Bar
    189 	 * which in turn provided a getCar() method, there is NO GUARANTEE
    190 	 * the getCar() method will be initialized.
    191 	 *
    192 	 * @param entity for which its immediate getters should be initialized
    193 	 */
    194 	protected void initializeAllZeroArgumentGetters(E entity) {
    195 		Method[] methods = entity.getClass().getMethods();
    196 		for (int i = 0; i < methods.length; i++) {
    197 			if (methods[i].getName().startsWith("get") && methods[i].getParameterTypes().length == 0) {
    198 				try {
    199 					Hibernate.initialize(methods[i].invoke(entity, new Object[] {}));
    200 				} catch (Exception ignored) {}
    201 			}
    202 		}
    203 	}
    204
    205     public PaginatedList<E> scroll(E value, int firstElement,
    206         int maxElements, String orderByAsc) {
    207 		validateScrollMethod(value, firstElement, maxElements, orderByAsc);
    208         return (PaginatedList) doGetHibernateTemplate().execute(getFindByValueCallback(
    209                 value.getClass(), false, value, firstElement, maxElements, Order.asc(orderByAsc)));
    210     }
    211
    212     public PaginatedList<E> scrollWithSubclasses(E value, int firstElement,
    213 	        int maxElements, String orderByAsc) {
    214 			validateScrollMethod(value, firstElement, maxElements, orderByAsc);
    215 	        return (PaginatedList) doGetHibernateTemplate().execute(getFindByValueCallback(
    216 	                this.supportsClass, false, value, firstElement, maxElements, Order.asc(orderByAsc)));
    217 	    }
    218
    219     public PaginatedList<E> scrollPopulated(E value, int firstElement,
    220 	        int maxElements, String orderByAsc) {
    221 			validateScrollMethod(value, firstElement, maxElements, orderByAsc);
    222 	        return (PaginatedList) doGetHibernateTemplate().execute(getFindByValueCallback(
    223 	                value.getClass(), true, value, firstElement, maxElements, Order.asc(orderByAsc)));
    224 	    }
    225
    226 	public PaginatedList<E> scrollPopulatedWithSubclasses(E value, int firstElement,
    227 		        int maxElements, String orderByAsc) {
    228 				validateScrollMethod(value, firstElement, maxElements, orderByAsc);
    229 		        return (PaginatedList) doGetHibernateTemplate().execute(getFindByValueCallback(
    230 		                this.supportsClass, true, value, firstElement, maxElements, Order.asc(orderByAsc)));
    231 		    }
    232
    233 	private void validateScrollMethod(E value, int firstElement, int MaxElements, String orderByAsc) {
    234         Assert.notNull(value);
    235         Assert.hasText(orderByAsc,
    236             "An orderByAsc is required (why not use your identity property?)");
    237 		Assert.isInstanceOf(this.supportsClass, value, "Can only scroll with values this DAO supports");
    238 	}
    239
    240 	public boolean supports(Class clazz) {
    241         Assert.notNull(clazz);
    242
    243         return this.supportsClass.equals(clazz);
    244     }
    245
    246     public E update(E value) {
    247         Assert.notNull(value);
    248 		validate(value);
    249         doGetHibernateTemplate().update(value);
    250
    251         return readId(value.getInternalId());
    252     }
    253
    254     /***
    255      * Custom initialization behavior. Called by superclass.
    256      *
    257      * @throws Exception if initialization fails
    258      */
    259     protected final void initDao() throws Exception {
    260         Assert.notNull(supportsClass, "supportClass is required");
    261 		Assert.notNull(validationManager, "validationManager is required");
    262         Assert.isTrue(PersistableEntity.class.isAssignableFrom(supportsClass), "supportClass is not an implementation of PersistableEntity");
    263         initHibernateDao();
    264     }
    265
    266     /***
    267      * Allows subclasses to provide custom initialization behaviour. Called
    268      * during {@link #initDao()}.
    269      *
    270      * @throws Exception
    271      */
    272     protected void initHibernateDao() throws Exception {}
    273
    274     public void initialize(Object entity) {
    275 		Hibernate.initialize(entity);
    276 	}
    277
    278 	public boolean isInitialized(Object entity) {
    279 		return Hibernate.isInitialized(entity);
    280 	}
    281
    282 	/***
    283      * Provides a <code>HibernateCallback</code> that will load a list of
    284      * objects by a <code>Collection</code> of identities.
    285      *
    286      * @param ids collection of identities to be loaded
    287      *
    288      * @return a <code>List</code> containing the matching objects
    289      */
    290     private HibernateCallback getFindByIdCallback(final Collection<Serializable> ids) {
    291         return new HibernateCallback() {
    292                 public Object doInHibernate(Session session)
    293                     throws HibernateException {
    294                     Criteria criteria = session.createCriteria(supportsClass);
    295
    296                     ClassMetadata classMetadata = getSessionFactory()
    297                                                       .getClassMetadata(supportsClass);
    298
    299                     String idPropertyName = classMetadata
    300                         .getIdentifierPropertyName();
    301                     criteria.add(Expression.in(idPropertyName, ids));
    302
    303                     return criteria.list();
    304                 }
    305             };
    306     }
    307
    308     /***
    309      * Get a new <code>HibernateCallback</code> for finding objects by a bean
    310      * property values, paginating the results. Properties with null values
    311      * and collections and empty Strings are ignored, as is any property with
    312      * the "version" name. If the property is mapped as String find a partial
    313      * match, otherwise find by exact match.
    314      *
    315      * @param whichClass the class (and subclasses) which results will be limited to including
    316      * @param initializeAllProperties indicates whether lazy initialized properties
    317      *        should be initialized in the returned results
    318      * @param bean bean with the values of the parameters
    319      * @param firstElement the first result, numbered from 0
    320      * @param count the maximum number of results
    321      * @param order DOCUMENT ME!
    322      *
    323      * @return a PaginatedList containing the requested objects
    324      */
    325     private HibernateCallback getFindByValueCallback(final Class whichClass, final boolean initializeAllProperties, final Object bean,
    326         final int firstElement, final int count, final Order order) {
    327         return new HibernateCallback() {
    328                 public Object doInHibernate(Session session)
    329                     throws HibernateException {
    330                     Criteria criteria = session.createCriteria(whichClass);
    331
    332                     criteria.addOrder(order);
    333
    334                     ClassMetadata classMetadata = getSessionFactory()
    335                                                       .getClassMetadata(bean
    336                             .getClass());
    337
    338 					Assert.notNull(classMetadata, "ClassMetadata for " + bean.getClass() + " unavailable from Hibernate - have you mapped this class against the SessionFactory?");
    339
    340                     /* get persistent properties */
    341                     Type[] propertyTypes = classMetadata.getPropertyTypes();
    342                     String[] propertyNames = classMetadata.getPropertyNames();
    343
    344                     /* for each persistent property of the bean */
    345                     for (int i = 0; i < propertyNames.length; i++) {
    346                         String name = propertyNames[i];
    347
    348 						// Indicate preferred fetching here
    349 						if (initializeAllProperties) {
    350 							criteria.setFetchMode(name, FetchMode.JOIN);
    351 						}
    352
    353                         // TODO: Check if EntityMode.POJO appropriate
    354                         Object value = classMetadata.getPropertyValue(bean, name, EntityMode.POJO);
    355
    356                         if (value == null) {
    357                             continue;
    358                         }
    359
    360                         // ignore empty Strings
    361                         if (value instanceof String) {
    362                             String string = (String) value;
    363
    364                             if ("".equals(string)) {
    365                                 continue;
    366                             }
    367                         }
    368
    369                         // ignore any collections
    370                         if (propertyTypes[i].isCollectionType()) {
    371                             continue;
    372                         }
    373
    374                         Type type = classMetadata.getPropertyType(name);
    375
    376                         if (name.equals("version")) {
    377                             continue;
    378                         }
    379
    380                         if (name.equals("lastUpdatedUsername") || name.equals("lastUpdatedDate")) {
    381                         	continue;
    382                         }
    383
    384                         if (type.equals(Hibernate.STRING)) {
    385                             // if the property is mapped as String, find partial match
    386                             criteria.add(Expression.ilike(name,
    387                                     value.toString(), MatchMode.ANYWHERE));
    388                         } else {
    389                             // find exact match
    390                             criteria.add(Expression.eq(name, value));
    391                         }
    392                     }
    393
    394                     /*
    395                      * TODO Use Criteria.count() when available in next Hibernate
    396                      * versions
    397                      */
    398                     int size = criteria.list().size();
    399
    400                     List<E> list = criteria.setFirstResult(firstElement).setMaxResults(count).list();
    401
    402                     return new PaginatedList<E>(list, firstElement, count, size);
    403                 }
    404             };
    405     }
    406 }