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    }