001 /*
002 * Databinder: a simple bridge from Wicket to Hibernate
003 * Copyright (C) 2006 Nathan Hamblen nathan@technically.us
004
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either
008 * version 2.1 of the License, or (at your option) any later version.
009 *
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013 * Lesser General Public License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public
016 * License along with this library; if not, write to the Free Software
017 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
018 */
019
020 package net.databinder.models.hib;
021
022 import java.io.Serializable;
023 import java.lang.reflect.Field;
024 import java.lang.reflect.Method;
025
026 import javax.persistence.Version;
027
028 import net.databinder.hib.Databinder;
029 import net.databinder.models.BindingModel;
030 import net.databinder.models.LoadableWritableModel;
031
032 import org.apache.wicket.WicketRuntimeException;
033 import org.hibernate.Criteria;
034 import org.hibernate.Hibernate;
035 import org.hibernate.Session;
036 import org.hibernate.proxy.HibernateProxyHelper;
037
038 /**
039 * Model loaded and persisted by Hibernate. This central Databinder class can be initialized with an
040 * entity ID, different types of queries, or an existing persistent object. As a writable Wicket model,
041 * the object it contains may be swapped at any time for a different persistent object, a Serializable
042 * object, or null.
043 * @author Nathan Hamblen
044 */
045 public class HibernateObjectModel extends LoadableWritableModel implements BindingModel {
046 private Class objectClass;
047 private Serializable objectId;
048 private QueryBuilder queryBuilder;
049 private CriteriaBuilder criteriaBuilder;
050 /** May store unsaved objects between requests. */
051 private Serializable retainedObject;
052 /** Enable retaining unsaved objects between requests. */
053 private boolean retainUnsaved = true;
054
055 private Object factoryKey;
056
057 /**
058 * Create a model bound to the given class and entity id. If nothing matches
059 * the id the model object will be null.
060 * @param objectClass class to be loaded and stored by Hibernate
061 * @param entityId id of the persistent object
062 */
063 public HibernateObjectModel(Class objectClass, Serializable entityId) {
064 this.objectClass = objectClass;
065 this.objectId = entityId;
066 }
067
068 /**
069 * Constructor for a model with no existing persistent object. This class should be
070 * Serializable so that the new object can be stored in the session until it is persisted.
071 * If serialization is impossible, call setRetainUnsaved(false) and the object will be discarded
072 * and recreated with each request.
073 * @param objectClass class to be loaded and stored by Hibernate
074 */
075 public HibernateObjectModel(Class objectClass) {
076 this.objectClass = objectClass;
077 }
078
079 /**
080 * Construct with an entity.
081 * @param persistentObject should be previously persisted or Serializable for temp storage.
082 */
083 public HibernateObjectModel(Object persistentObject) {
084 setObject(persistentObject);
085 }
086
087 /**
088 * Construct with a query and binder that return exactly one result. Use this for fetch
089 * instructions, scalar results, or if the persistent object ID is not available.
090 * Queries that return more than one result will produce exceptions. Queries that return
091 * no result will produce a null object.
092 * @param queryString query returning one result
093 * @param queryBinder bind id or other parameters
094 */
095 public HibernateObjectModel(String queryString, QueryBinder queryBinder) {
096 this(new QueryBinderBuilder(queryString, queryBinder));
097 }
098
099 /**
100 * Construct with a class and criteria binder that return exactly one result. Use this for fetch
101 * instructions, scalar results, or if the persistent object ID is not available. Criteria that
102 * return more than one result will produce exceptions. Criteria that return no result
103 * will produce a null object.
104 * @param objectClass class of object for root criteria
105 * @param criteriaBuilder builder to apply criteria restrictions
106 */
107 public HibernateObjectModel(Class objectClass, CriteriaBuilder criteriaBuilder) {
108 this.objectClass = objectClass;
109 this.criteriaBuilder = criteriaBuilder;
110 }
111
112 /**
113 * Construct with a query builder that returns exactly one result, used for custom query
114 * objects. Queries that return more than one result will produce exceptions. Queries that
115 * return no result will produce a null object.
116 * @param queryBuilder builder to create and bind query object
117 */
118 public HibernateObjectModel(QueryBuilder queryBuilder) {
119 this.queryBuilder = queryBuilder;
120 }
121
122 /**
123 * Construct with no object. Will return null for getObject().
124 */
125 public HibernateObjectModel() {
126 }
127
128 /** @return session factory key, or null for the default factory */
129 public Object getFactoryKey() {
130 return factoryKey;
131 }
132
133 /**
134 * Set a factory key other than the default (null).
135 * @param key session factory key
136 * @return this, for chaining
137 */
138 public HibernateObjectModel setFactoryKey(Object key) {
139 this.factoryKey = key;
140 return this;
141 }
142
143 /**
144 * Change the persistent object contained in this model.
145 * Because this method establishes a persistent object ID, queries and binders
146 * are removed if present.
147 * @param object must be an entity contained in the current Hibernate session, or Serializable, or null
148 */
149 public void setObject(Object object) {
150 unbind(); // clear everything but class, name
151 objectClass = null;
152
153 if (object != null) {
154 objectClass = HibernateProxyHelper.getClassWithoutInitializingProxy(object);
155
156 Session sess = Databinder.getHibernateSession(factoryKey);
157 if (sess.contains(object))
158 objectId = sess.getIdentifier(object);
159 else if (retainUnsaved)
160 retainedObject = (Serializable) object;
161 setTempModelObject(object); // skip calling load later
162 }
163 }
164
165 public Serializable getIdentifier() {
166 return Databinder.getHibernateSession(factoryKey).getIdentifier(getObject());
167 }
168
169 /**
170 * @deprecated use {@link #unbind()}
171 */
172 public void clearPersistentObject() {
173 unbind();
174 }
175
176 /**
177 * Load the object through Hibernate, contruct a new instance if it is not
178 * bound to an id, or use unsaved retained object. Returns null if no
179 * criteria needed to load or construct an object are available.
180 */
181 @Override
182 protected Object load() {
183 if (objectClass == null && queryBuilder == null)
184 return null; // can't load without one of these
185 try {
186 if (!isBound()) {
187 if (retainUnsaved && retainedObject != null)
188 return retainedObject;
189 else if (retainUnsaved) try {
190 return retainedObject = (Serializable) objectClass.newInstance();
191 } catch (ClassCastException e) {
192 throw new WicketRuntimeException("Unsaved entity must be Serializable or retainUnsaved set to false; see HibernateObjectModel javadocs.");
193 }
194 else
195 return objectClass.newInstance();
196 }
197 } catch (ClassCastException e) {
198 throw new RuntimeException("Retaining unsaved model objects requires that they be Serializable.", e);
199 } catch (Throwable e) {
200 throw new RuntimeException("Unable to instantiate object. Does it have a default constructor?", e);
201 }
202 Session sess = Databinder.getHibernateSession(factoryKey);
203 if (objectId != null) {
204 return sess.get(objectClass, objectId);
205 }
206
207 if(criteriaBuilder != null) {
208 Criteria criteria = sess.createCriteria(objectClass);
209 criteriaBuilder.build(criteria);
210 return criteria.uniqueResult();
211 }
212
213 return queryBuilder.build(sess).uniqueResult();
214 }
215
216 /**
217 * Checks if the model is retaining an object this has since become a
218 * persistent entity. If so, the ID is fetched and the reference discarded.
219 */
220 public void checkBinding() {
221 if (!isBound() && retainedObject != null) {
222 Session sess = Databinder.getHibernateSession(factoryKey);
223
224 if (sess.contains(retainedObject)) {
225 objectId = sess.getIdentifier(retainedObject);
226 retainedObject = null;
227 }
228 }
229 }
230
231 /**
232 * Uses version annotation to find version for this Model's object.
233 * @return Persistent storage version number if available, null otherwise
234 */
235 public Serializable getVersion() {
236 Object o = getObject();
237
238 if (o != null) {
239 Class c = Hibernate.getClass(o);
240 try {
241 for (Method m : c.getMethods())
242 if (m.isAnnotationPresent(Version.class)
243 && m.getParameterTypes().length == 0
244 && m.getReturnType() instanceof Serializable)
245 return (Serializable) m.invoke(o, new Object[] {});
246 for (Field f : c.getDeclaredFields())
247 if (f.isAnnotationPresent(Version.class)
248 && f.getType() instanceof Serializable) {
249 f.setAccessible(true);
250 return (Serializable) f.get(o);
251 }
252 } catch (Exception e) {
253 throw new RuntimeException(e);
254 }
255 }
256 return null;
257 }
258
259 /** Compares contained objects if present, otherwise calls super-implementation.*/
260 @Override
261 public boolean equals(Object obj) {
262 Object target = getObject();
263 if (target != null && obj instanceof HibernateObjectModel)
264 return target.equals(((HibernateObjectModel)obj).getObject());
265 return super.equals(obj);
266 }
267
268 /** @return hash of contained object if present, otherwise from super-implementation.*/
269 @Override
270 public int hashCode() {
271 Object target = getObject();
272 if (target == null)
273 return super.hashCode();
274 return target.hashCode();
275 }
276
277
278 /**
279 * Disassociates this object from any persistent object, but retains the class
280 * for constructing a blank copy if requested.
281 * @see HibernateObjectModel#HibernateObjectModel(Class objectClass)
282 */
283 public void unbind() {
284 objectId = null;
285 queryBuilder = null;
286 criteriaBuilder = null;
287 retainedObject = null;
288 detach();
289 }
290
291 /**
292 * "bound" models are those that can be loaded from persistent storage by a known id or
293 * query. When bound, this model discards its temporary model object at the end of every
294 * request cycle and reloads it via Hiberanate when needed again. When unbound, its
295 * behavior is dictated by the value of retanUnsaved.
296 * @return true if information needed to load from Hibernate (identifier, query, or criteria) is present
297 */
298 public boolean isBound() {
299 return objectId != null || criteriaBuilder != null || queryBuilder != null;
300 }
301
302 /**
303 * When retainUnsaved is true (the default) and the model is not bound,
304 * the model object must be Serializable as it is retained in the Web session between
305 * requests. See isBound() for more information.
306 * @return true if unsaved objects should be retained between requests.
307 */
308 public boolean getRetainUnsaved() {
309 return retainUnsaved;
310 }
311
312 /**
313 * Unsaved Serializable objects can be retained between requests.
314 * @param retainUnsaved set to true to retain unsaved objects
315 */
316 public void setRetainUnsaved(boolean retainUnsaved) {
317 this.retainUnsaved = retainUnsaved;
318 }
319 }