/*
* Databinder: a simple bridge from Wicket to Hibernate
* Copyright (C) 2006 Nathan Hamblen nathan@technically.us
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.databinder.models.hib;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.persistence.Version;
import net.databinder.hib.Databinder;
import net.databinder.models.BindingModel;
import net.databinder.models.LoadableWritableModel;
import org.apache.wicket.WicketRuntimeException;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.proxy.HibernateProxyHelper;
/**
* Model loaded and persisted by Hibernate. This central Databinder class can be initialized with an
* entity ID, different types of queries, or an existing persistent object. As a writable Wicket model,
* the object it contains may be swapped at any time for a different persistent object, a Serializable
* object, or null.
* @author Nathan Hamblen
*/
public class HibernateObjectModel<T> extends LoadableWritableModel<T> implements BindingModel<T> {
private Class objectClass;
private Serializable objectId;
private QueryBuilder queryBuilder;
private CriteriaBuilder criteriaBuilder;
/** May store unsaved objects between requests. */
private T retainedObject;
/** Enable retaining unsaved objects between requests. */
private boolean retainUnsaved = true;
private Object factoryKey;
/**
* Create a model bound to the given class and entity id. If nothing matches
* the id the model object will be null.
* @param objectClass class to be loaded and stored by Hibernate
* @param entityId id of the persistent object
*/
public HibernateObjectModel(Class objectClass, Serializable entityId) {
this.objectClass = objectClass;
this.objectId = entityId;
}
/**
* Constructor for a model with no existing persistent object. This class should be
* Serializable so that the new object can be stored in the session until it is persisted.
* If serialization is impossible, call setRetainUnsaved(false) and the object will be discarded
* and recreated with each request.
* @param objectClass class to be loaded and stored by Hibernate
*/
public HibernateObjectModel(Class objectClass) {
this.objectClass = objectClass;
}
/**
* Construct with an entity.
* @param persistentObject should be previously persisted or Serializable for temp storage.
*/
public HibernateObjectModel(T persistentObject) {
setObject(persistentObject);
}
/**
* Construct with a query and binder that return exactly one result. Use this for fetch
* instructions, scalar results, or if the persistent object ID is not available.
* Queries that return more than one result will produce exceptions. Queries that return
* no result will produce a null object.
* @param queryString query returning one result
* @param queryBinder bind id or other parameters
*/
public HibernateObjectModel(String queryString, QueryBinder queryBinder) {
this(new QueryBinderBuilder(queryString, queryBinder));
}
/**
* Construct with a class and criteria binder that return exactly one result. Use this for fetch
* instructions, scalar results, or if the persistent object ID is not available. Criteria that
* return more than one result will produce exceptions. Criteria that return no result
* will produce a null object.
* @param objectClass class of object for root criteria
* @param criteriaBuilder builder to apply criteria restrictions
*/
public HibernateObjectModel(Class objectClass, CriteriaBuilder criteriaBuilder) {
this.objectClass = objectClass;
this.criteriaBuilder = criteriaBuilder;
}
/**
* Construct with a query builder that returns exactly one result, used for custom query
* objects. Queries that return more than one result will produce exceptions. Queries that
* return no result will produce a null object.
* @param queryBuilder builder to create and bind query object
*/
public HibernateObjectModel(QueryBuilder queryBuilder) {
this.queryBuilder = queryBuilder;
}
/**
* Construct with no object. Will return null for getObject().
*/
public HibernateObjectModel() {
}
/** @return session factory key, or null for the default factory */
public Object getFactoryKey() {
return factoryKey;
}
/**
* Set a factory key other than the default (null).
* @param key session factory key
* @return this, for chaining
*/
public HibernateObjectModel setFactoryKey(Object key) {
this.factoryKey = key;
return this;
}
/**
* Change the persistent object contained in this model.
* Because this method establishes a persistent object ID, queries and binders
* are removed if present.
* @param object must be an entity contained in the current Hibernate session, or Serializable, or null
*/
public void setObject(T object) {
unbind(); // clear everything but class, name
objectClass = null;
if (object != null) {
objectClass = HibernateProxyHelper.getClassWithoutInitializingProxy(object);
Session sess = Databinder.getHibernateSession(factoryKey);
if (sess.contains(object))
objectId = sess.getIdentifier(object);
else if (retainUnsaved)
retainedObject = (T) object;
setTempModelObject(object); // skip calling load later
}
}
public Serializable getIdentifier() {
return Databinder.getHibernateSession(factoryKey).getIdentifier(getObject());
}
/**
* Load the object through Hibernate, contruct a new instance if it is not
* bound to an id, or use unsaved retained object. Returns null if no
* criteria needed to load or construct an object are available.
*/
@SuppressWarnings("unchecked")
@Override
protected T load() {
if (objectClass == null && queryBuilder == null)
return null; // can't load without one of these
try {
if (!isBound()) {
if (retainUnsaved && retainedObject != null)
return retainedObject;
else if (retainUnsaved) try {
return retainedObject = (T) objectClass.newInstance();
} catch (ClassCastException e) {
throw new WicketRuntimeException("Unsaved entity must be Serializable or retainUnsaved set to false; see HibernateObjectModel javadocs.");
}
else
return (T) objectClass.newInstance();
}
} catch (ClassCastException e) {
throw new RuntimeException("Retaining unsaved model objects requires that they be Serializable.", e);
} catch (Throwable e) {
throw new RuntimeException("Unable to instantiate object. Does it have a default constructor?", e);
}
Session sess = Databinder.getHibernateSession(factoryKey);
if (objectId != null) {
return (T) sess.get(objectClass, objectId);
}
if(criteriaBuilder != null) {
Criteria criteria = sess.createCriteria(objectClass);
criteriaBuilder.build(criteria);
return (T) criteria.uniqueResult();
}
return (T) queryBuilder.build(sess).uniqueResult();
}
/**
* Checks if the model is retaining an object this has since become a
* persistent entity. If so, the ID is fetched and the reference discarded.
*/
public void checkBinding() {
if (!isBound() && retainedObject != null) {
Session sess = Databinder.getHibernateSession(factoryKey);
if (sess.contains(retainedObject)) {
objectId = sess.getIdentifier(retainedObject);
retainedObject = null;
}
}
}
/**
* Uses version annotation to find version for this Model's object.
* @return Persistent storage version number if available, null otherwise
*/
public Serializable getVersion() {
Object o = getObject();
if (o != null) {
Class c = Hibernate.getClass(o);
try {
for (Method m : c.getMethods())
if (m.isAnnotationPresent(Version.class)
&& m.getParameterTypes().length == 0
&& m.getReturnType() instanceof Serializable)
return (Serializable) m.invoke(o, new Object[] {});
for (Field f : c.getDeclaredFields())
if (f.isAnnotationPresent(Version.class)
&& f.getType() instanceof Serializable) {
f.setAccessible(true);
return (Serializable) f.get(o);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return null;
}
/** Compares contained objects if present, otherwise calls super-implementation.*/
@Override
public boolean equals(Object obj) {
Object target = getObject();
if (target != null && obj instanceof HibernateObjectModel)
return target.equals(((HibernateObjectModel)obj).getObject());
return super.equals(obj);
}
/** @return hash of contained object if present, otherwise from super-implementation.*/
@Override
public int hashCode() {
Object target = getObject();
if (target == null)
return super.hashCode();
return target.hashCode();
}
/**
* Disassociates this object from any persistent object, but retains the class
* for constructing a blank copy if requested.
* @see HibernateObjectModel#HibernateObjectModel(Class objectClass)
* @see #isBound()
*/
public void unbind() {
objectId = null;
queryBuilder = null;
criteriaBuilder = null;
retainedObject = null;
detach();
}
/**
* "bound" models are those that can be loaded from persistent storage by a known id or
* query. When bound, this model discards its temporary model object at the end of every
* request cycle and reloads it via Hiberanate when needed again. When unbound, its
* behavior is dictated by the value of retanUnsaved.
* @return true if information needed to load from Hibernate (identifier, query, or criteria) is present
*/
public boolean isBound() {
return objectId != null || criteriaBuilder != null || queryBuilder != null;
}
/**
* When retainUnsaved is true (the default) and the model is not bound,
* the model object must be Serializable as it is retained in the Web session between
* requests. See isBound() for more information.
* @return true if unsaved objects should be retained between requests.
*/
public boolean getRetainUnsaved() {
return retainUnsaved;
}
/**
* Unsaved Serializable objects can be retained between requests.
* @param retainUnsaved set to true to retain unsaved objects
*/
public void setRetainUnsaved(boolean retainUnsaved) {
this.retainUnsaved = retainUnsaved;
}
}