View Javadoc

1   /*
2    * EL4J, the Extension Library for the J2EE, adds incremental enhancements to
3    * the spring framework, http://el4j.sf.net
4    * Copyright (C) 2005 by ELCA Informatique SA, Av. de la Harpe 22-24,
5    * 1000 Lausanne, Switzerland, http://www.elca.ch
6    *
7    * EL4J is published under the GNU Lesser General Public License (LGPL)
8    * Version 2.1. See http://www.gnu.org/licenses/
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13   * GNU Lesser General Public License for more details.
14   *
15   * For alternative licensing, please contact info@elca.ch
16   */
17  package ch.elca.el4j.services.persistence.jpa.util;
18  
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  
23  import javax.persistence.EntityManager;
24  import javax.persistence.NoResultException;
25  import javax.persistence.NonUniqueResultException;
26  import javax.persistence.Query;
27  
28  import org.hibernate.FetchMode;
29  import org.hibernate.Session;
30  import org.hibernate.criterion.CriteriaSpecification;
31  import org.hibernate.criterion.Criterion;
32  import org.hibernate.criterion.DetachedCriteria;
33  import org.hibernate.criterion.Restrictions;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import ch.elca.el4j.services.persistence.jpa.helper.JpaHelperImpl;
38  import ch.elca.el4j.services.persistence.jpa.util.QueryException;;
39  
40  /**
41   * Query object returned by DataService.
42   *	@param <T> The type parameter.
43   *
44   * @svnLink $Revision: 4112 $;$Date: 2010-08-05 11:00:36 +0200 (Do, 05. Aug 2010) $;$Author: swrelca $;$URL: https://el4j.svn.sourceforge.net/svnroot/el4j/branches/el4j_3_1/el4j/framework/modules/hibernate/src/main/java/ch/elca/el4j/services/persistence/jpa/util/JpaQuery.java $
45   *
46   * @author David Bernhard (dab)
47   */
48  public class JpaQuery<T> {
49  
50  	/** The logger. */
51  	private static final Logger s_log
52  		= LoggerFactory.getLogger(JpaQuery.class);
53  
54  	/** Order of a sort. */
55  	public enum Order {
56  		/** Ascending order. */
57  		ASCENDING,
58  		/** Descending order. */
59  		DESCENDING
60  	}
61  
62  	/** The possible relations (eq, neq ...). */
63  	public enum Relation {
64  		/** equals. */
65  		EQ,
66  		/** Not equals. */
67  		NE,
68  		/** Less or equal. */
69  		LE,
70  		/** Greater or equal. */
71  		GE,
72  		/** Less than. */
73  		LT,
74  		/** Greater than. */
75  		GT;
76  	};
77  
78  	/** The conditions (used for warning message if there is a failure). */
79  	private Map<String, Object> conditions;
80  
81  	/** The cirteria object for this query. */
82  	private DetachedCriteria criteria;
83  
84  	/**
85  	 * Flag to indicate an exception should be thrown if
86  	 * no data is returned.
87  	 */
88  	private boolean failOnNull;
89  
90  	/**
91  	 * Flag to indicate returned instances must be detached.
92  	 */
93  	private boolean detach;
94  
95  	/** The domain class this query is for. */
96  	private Class<T> domainClass;
97  
98  	/** The entity manager. */
99  	private EntityManager em;
100 
101 	/**
102 	 * Create the query object.
103 	 * @param cls The class to query for.
104 	 * @param context The context.
105 	 * @param ds The data service.
106 	 */
107 	public JpaQuery(Class<T> cls,  JpaHelperImpl ds) {
108 		domainClass = cls;
109 		criteria = DetachedCriteria.forClass(cls);
110 		criteria.setResultTransformer(
111 			CriteriaSpecification.DISTINCT_ROOT_ENTITY);
112 		failOnNull  = false;
113 		conditions = new HashMap<String, Object>();
114 		em = ds.getEntityManager();
115 	}
116 
117 	/**
118 	 * Add a restriction.
119 	 * @param key The property name.
120 	 * @param value The property value.
121 	 * @return this
122 	 */
123 	public JpaQuery<T> where(String key, Object value) {
124 		if (value == null) {
125 			return whereNull(key);
126 		}
127 		criteria.add(Restrictions.eq(key, value));
128 		conditions.put(key, value);
129 		return this;
130 	}
131 
132 	/**
133 	 * Add a hibernate criterion.
134 	 * @param c The criterion.
135 	 * @return this
136 	 */
137 	public JpaQuery<T> where(Criterion c) {
138 		criteria.add(c);
139 		return this;
140 	}
141 
142 	/**
143 	 * Add a restriction.
144 	 * @param key The property name.
145 	 * @param r The relation type.
146 	 * @param value The property value.
147 	 * @return this
148 	 */
149 	public JpaQuery<T> where(String key, Relation r, Object value) {
150 		switch (r) {
151 			case EQ:
152 				criteria.add(Restrictions.eq(key, value));
153 				break;
154 			case NE:
155 				criteria.add(Restrictions.ne(key, value));
156 				break;
157 			case LE:
158 				criteria.add(Restrictions.le(key, value));
159 				break;
160 			case LT:
161 				criteria.add(Restrictions.lt(key, value));
162 				break;
163 			case GE:
164 				criteria.add(Restrictions.ge(key, value));
165 				break;
166 			case GT:
167 				criteria.add(Restrictions.gt(key, value));
168 				break;
169 			default:
170 				// This should never happen as we're switching on an enum.
171 				throw new QueryException("Not yet implemented: " + r);
172 		}
173 		return this;
174 	}
175 
176 	/**
177 	 * Add an is-null restriction.
178 	 * @param key The property name.
179 	 * @return this
180 	 */
181 	public JpaQuery<T> whereNull(String key) {
182 		criteria.add(Restrictions.isNull(key));
183 		conditions.put(key, "(null)");
184 		return this;
185 	}
186 
187 	/**
188 	 * Add an is--not-null restriction.
189 	 * @param key The property name.
190 	 * @return this
191 	 */
192 	public JpaQuery<T> whereNotNull(String key) {
193 		criteria.add(Restrictions.isNotNull(key));
194 		conditions.put(key, "(not null)");
195 		return this;
196 	}
197 
198 	/**
199 	 * Add an order.
200 	 * @param key The property to order by.
201 	 * @param order The order to use.
202 	 * @return this
203 	 */
204 	public JpaQuery<T> order(String key, Order order) {
205 		if (order == Order.ASCENDING) {
206 			criteria.addOrder(org.hibernate.criterion.Order.asc(key));
207 		} else {
208 			criteria.addOrder(org.hibernate.criterion.Order.desc(key));
209 		}
210 		return this;
211 	}
212 
213 	/**
214 	 * Add a data extent (a property to be eagerly fetched).
215 	 * @param names The names of the properties to fetch.
216 	 * @return this
217 	 */
218 	public JpaQuery<T> extent(String... names) {
219 		for (String name : names) {
220 			criteria.setFetchMode(name, FetchMode.JOIN);
221 		}
222 		return this;
223 	}
224 
225 	/**
226 	 * Fail if no elements are returned.
227 	 * @return this.
228 	 */
229 	public JpaQuery<T> failOnNull() {
230 		failOnNull = true;
231 		return this;
232 	}
233 
234 	/**
235 	 * Detach all returned elements.
236 	 * @return this.
237 	 */
238 	public JpaQuery<T> detach() {
239 		detach = true;
240 		return this;
241 	}
242 
243 	/**
244 	 * Execute the query.
245 	 * @return The query result.
246 	 */
247 	public List<T> execute() {
248 		Session session = session();
249 
250 		// Cast required because return type is "List", ok because we know
251 		// what type of elements are in it.
252 		@SuppressWarnings("unchecked")
253 		List<T> list = criteria.getExecutableCriteria(session).list();
254 		if (failOnNull && list.isEmpty()) {
255 			throw new NoResultException("Empty list returned, fail on "
256 				+ "null is set.");
257 		}
258 
259 		if (detach) {
260 			for (T element : list) {
261 				session.evict(element);
262 			}
263 		}
264 
265 		return list;
266 	}
267 
268 	/**
269 	 * Execute the query, expecting a unique element.
270 	 * If none is found, return null. If several are found,
271 	 * throw an exception.
272 	 * @return The unique element matching the criteria.
273 	 */
274 	public T executeUnique() {
275 		Session session = session();
276 
277 		// Cast required because return type is "List", ok because we know
278 		// what type of elements are in it.
279 		@SuppressWarnings("unchecked")
280 		List<T> list = criteria.getExecutableCriteria(session).list();
281 
282 		if (list.size() == 0) {
283 			if (failOnNull) {
284 				throw new NoResultException("No element found");
285 			} else {
286 				return null;
287 			}
288 		} else if (list.size() == 1) {
289 			T element = list.get(0);
290 			if (detach) {
291 				session.evict(element);
292 			}
293 			return element;
294 		} else {
295 			StringBuilder builder = new StringBuilder();
296 			builder.append("Multiple matches (");
297 			builder.append(list.size());
298 			builder.append(") for query: ");
299 			builder.append("SELECT FROM ");
300 			builder.append(domainClass.getSimpleName());
301 			for (String key : conditions.keySet()) {
302 				builder.append(" WHERE ");
303 				builder.append(key);
304 				builder.append(" = '");
305 				builder.append(conditions.get(key));
306 				builder.append("'");
307 			}
308 			builder.append(";");
309 
310 			throw new NonUniqueResultException(builder.toString());
311 		}
312 	}
313 
314 	/**
315 	 * Check all elements of a list match a type and return it cast if so.
316 	 * Throw an exception if not.
317 	 * @param list The list to check.
318 	 * @param query The query, to use for the error message if we fail.
319 	 * @param params The parameters, to use for the error message if we fail.
320 	 * @return The list cast to the correct type.
321 	 */
322 	// Safe because we do a manual check.
323 	// Required because we get a raw list from spring.
324 	@SuppressWarnings({ "rawtypes", "unchecked" })
325 	private List<T> checkTypes(List list, String query, Object... params) {
326 		for (Object o : list) {
327 			if (!domainClass.isAssignableFrom(o.getClass())) {
328 				StringBuilder builder = new StringBuilder(
329 					"HQL query returned different type than expected. Wanted ");
330 				builder.append(domainClass.getName());
331 				builder.append(" Got ");
332 				builder.append(o.getClass().getName());
333 				builder.append(" Query ");
334 				builder.append(query);
335 				for (Object p : params) {
336 					builder.append(", ");
337 					builder.append(p);
338 				}
339 				throw new QueryException(builder.toString());
340 			}
341 		}
342 
343 		return (List<T>) list;
344 	}
345 
346 	/**
347 	 * Build a query.
348 	 * @param queryString The string in a query language.
349 	 * @param params The parameters. Indexing is numerical.
350 	 * @return The query object,
351 	 */
352 	private Query query(String queryString, Object... params) {
353 		Query query = em.createQuery(queryString);
354 		// Loop with index variable as we use it in setParameter.
355 		for (int i = 0; i < params.length; i++) {
356 			// Check that no null parameter was passed.
357 			// This is usually an error as "obj = null" as SQL/HQL will always
358 			// be false - correct is "is null" which doesn't take a parameter.
359 			if (params[i] == null) {
360 				throw new QueryException("Found a null parameter in a "
361 					+ "HQL query. This is almost certainly a mistake.");
362 			}
363 
364 			// Parameters in the query start at 1.
365 			query.setParameter(i + 1, params[i]);
366 		}
367 		return query;
368 	}
369 
370 	/**
371 	 * Detach an object from the session.
372 	 * @param object The object to detach.
373 	 */
374 	private void detach(Object object) {
375 		// Ok as long as we're using hibernate.
376 		Session session = (Session) em.getDelegate();
377 		session.evict(object);
378 	}
379 
380 	/**
381 	 * Execute a HQL query.
382 	 *
383 	 * Warning: You must pass "distinct" yourself if desired. Due to the
384 	 * nature of HQL, this is not fully typesafe - types are checked but we
385 	 * cannot prevent an invalid tpye in the "from" clause.
386 	 * @param query The query string.
387 	 * @param params The parameters.
388 	 * @return The single object, if present.
389 	 */
390 	public List<T> executeHQL(String query, Object... params) {
391 		Query q = query(query, params);
392 		List<T> list = checkTypes(q.getResultList(), query, params);
393 
394 		if (failOnNull && list.isEmpty()) {
395 			throw new NoResultException("Empty list returned, fail on "
396 				+ "null is set.");
397 		}
398 
399 		if (detach) {
400 			for (T element : list) {
401 				detach(element);
402 			}
403 		}
404 
405 		return list;
406 	}
407 
408 	/**
409 	 * Execute HQL and epxect a single item. Throw an exception if more than
410 	 * one is found.
411 	 * 
412 	 * Warning: You must pass "distinct" yourself if desired. Due to the
413 	 * nature of HQL, this is not fully typesafe - types are checked but we
414 	 * cannot prevent an invalid tpye in the "from" clause.
415 	 * @param query The query string.
416 	 * @param params The parameters.
417 	 * @return The single object, if present.
418 	 */
419 	public T executeHQLUnique(String query, Object... params) {
420 
421 		Query q = query(query, params);
422 		List<T> list = checkTypes(q.getResultList(), query, params);
423 
424 		if (list.size() == 0) {
425 			if (failOnNull) {
426 				throw new NoResultException("No element found");
427 			} else {
428 				return null;
429 			}
430 		} else if (list.size() == 1) {
431 			T element = list.get(0);
432 			if (detach) {
433 				detach(element);
434 			}
435 			return element;
436 		} else {
437 			StringBuilder builder = new StringBuilder(
438 				"Multiple matches for query ");
439 			builder.append(query);
440 			for (Object p : params) {
441 				builder.append(", ");
442 				builder.append(p);
443 			}
444 			throw new NonUniqueResultException(builder.toString());
445 		}
446 	}
447 
448 	/**
449 	 * Get a session.
450 	 * @return The session.
451 	 */
452 	private Session session() {
453 		// Works as long as we use hibernate.
454 		Session session = (Session) em.getDelegate();
455 		if (!session.isOpen()) {
456 			// This happens during testing when we don't have a web container.
457 			session = session.getSessionFactory().openSession();
458 		}
459 		return session;
460 	}
461 }