001 /*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2011 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * Sonar is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * Sonar is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public
017 * License along with Sonar; if not, write to the Free Software
018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
019 */
020 package org.sonar.jpa.session;
021
022 import org.apache.commons.lang.StringUtils;
023 import org.sonar.api.database.DatabaseSession;
024
025 import java.util.*;
026
027 import javax.persistence.EntityManager;
028 import javax.persistence.NonUniqueResultException;
029 import javax.persistence.PersistenceException;
030 import javax.persistence.Query;
031
032 public class JpaDatabaseSession extends DatabaseSession {
033
034 private final DatabaseConnector connector;
035 private EntityManager entityManager = null;
036 private int index = 0;
037 private boolean inTransaction = false;
038
039 public JpaDatabaseSession(DatabaseConnector connector) {
040 this.connector = connector;
041 }
042
043 /**
044 * Note that usage of this method is discouraged, because it allows to construct and execute queries without additional exception handling,
045 * which done in methods of this class.
046 */
047 public EntityManager getEntityManager() {
048 return entityManager;
049 }
050
051 public void start() {
052 entityManager = connector.createEntityManager();
053 index = 0;
054 }
055
056 public void stop() {
057 commit();
058 if (entityManager != null && entityManager.isOpen()) {
059 entityManager.close();
060 entityManager = null;
061 }
062 }
063
064 public void commit() {
065 if (entityManager != null && inTransaction) {
066 if (entityManager.isOpen()) {
067 if (entityManager.getTransaction().getRollbackOnly()) {
068 entityManager.getTransaction().rollback();
069 } else {
070 entityManager.getTransaction().commit();
071 }
072 entityManager.clear();
073 index = 0;
074 }
075 inTransaction = false;
076 }
077 }
078
079 public void rollback() {
080 if (entityManager != null && inTransaction) {
081 entityManager.getTransaction().rollback();
082 inTransaction = false;
083 }
084 }
085
086 public <T> T save(T model) {
087 startTransaction();
088 internalSave(model, true);
089 return model;
090 }
091
092 public Object saveWithoutFlush(Object model) {
093 startTransaction();
094 internalSave(model, false);
095 return model;
096 }
097
098 public boolean contains(Object model) {
099 startTransaction();
100 return entityManager.contains(model);
101 }
102
103 public void save(Object... models) {
104 startTransaction();
105 for (Object model : models) {
106 save(model);
107 }
108 }
109
110 private void internalSave(Object model, boolean flushIfNeeded) {
111 try {
112 entityManager.persist(model);
113 } catch (PersistenceException e) {
114 /*
115 * See http://jira.codehaus.org/browse/SONAR-2234
116 * In some cases Hibernate can throw exceptions without meaningful information about context, so we improve them here.
117 */
118 throw new PersistenceException("Unable to persist : " + model, e);
119 }
120 if (flushIfNeeded && (++index % BATCH_SIZE == 0)) {
121 commit();
122 }
123 }
124
125 public Object merge(Object model) {
126 startTransaction();
127 return entityManager.merge(model);
128 }
129
130 public void remove(Object model) {
131 startTransaction();
132 entityManager.remove(model);
133 if (++index % BATCH_SIZE == 0) {
134 commit();
135 }
136 }
137
138 public void removeWithoutFlush(Object model) {
139 startTransaction();
140 entityManager.remove(model);
141 }
142
143 public <T> T reattach(Class<T> entityClass, Object primaryKey) {
144 startTransaction();
145 return entityManager.getReference(entityClass, primaryKey);
146 }
147
148 private void startTransaction() {
149 if (!inTransaction) {
150 entityManager.getTransaction().begin();
151 inTransaction = true;
152 }
153 }
154
155 /**
156 * Note that not recommended to directly execute {@link Query#getSingleResult()}, because it will bypass exception handling,
157 * which done in {@link #getSingleResult(Query, Object)}.
158 */
159 public Query createQuery(String hql) {
160 startTransaction();
161 return entityManager.createQuery(hql);
162 }
163
164 @Override
165 public Query createNativeQuery(String sql) {
166 startTransaction();
167 return entityManager.createNativeQuery(sql);
168 }
169
170 /**
171 * @return the result or <code>defaultValue</code>, if not found
172 * @throws NonUniqueResultException if more than one result
173 */
174 public <T> T getSingleResult(Query query, T defaultValue) {
175 /*
176 * See http://jira.codehaus.org/browse/SONAR-2225
177 * By default Hibernate throws NonUniqueResultException without meaningful information about context,
178 * so we improve it here by adding all results in error message.
179 * Note that in some rare situations we can receive too many results, which may lead to OOME,
180 * but actually it will mean that database is corrupted as we don't expect more than one result
181 * and in fact org.hibernate.ejb.QueryImpl#getSingleResult() anyway does loading of several results under the hood.
182 */
183 List<T> result = query.getResultList();
184
185 if (result.size() == 1) {
186 return result.get(0);
187
188 } else if (result.isEmpty()) {
189 return defaultValue;
190
191 } else {
192 Set<T> uniqueResult = new HashSet<T>(result);
193 if (uniqueResult.size() > 1) {
194 throw new NonUniqueResultException("Expected single result, but got : " + result.toString());
195 } else {
196 return uniqueResult.iterator().next();
197 }
198 }
199 }
200
201 public <T> T getEntity(Class<T> entityClass, Object id) {
202 startTransaction();
203 return getEntityManager().find(entityClass, id);
204 }
205
206 /**
207 * @return the result or <code>null</code>, if not found
208 * @throws NonUniqueResultException if more than one result
209 */
210 public <T> T getSingleResult(Class<T> entityClass, Object... criterias) {
211 try {
212 return getSingleResult(getQueryForCriterias(entityClass, true, criterias), (T) null);
213
214 } catch (NonUniqueResultException ex) {
215 NonUniqueResultException e = new NonUniqueResultException("Expected single result for entitiy " + entityClass.getSimpleName()
216 + " with criterias : " + StringUtils.join(criterias, ","));
217 throw (NonUniqueResultException) e.initCause(ex);
218 }
219 }
220
221 public <T> List<T> getResults(Class<T> entityClass, Object... criterias) {
222 return getQueryForCriterias(entityClass, true, criterias).getResultList();
223 }
224
225 public <T> List<T> getResults(Class<T> entityClass) {
226 return getQueryForCriterias(entityClass, false, null).getResultList();
227 }
228
229 private Query getQueryForCriterias(Class<?> entityClass, boolean raiseError, Object... criterias) {
230 if (criterias == null && raiseError) {
231 throw new IllegalStateException("criterias parameter must be provided");
232 }
233 startTransaction();
234 StringBuilder hql = new StringBuilder("SELECT o FROM ").append(entityClass.getSimpleName()).append(" o");
235 if (criterias != null) {
236 hql.append(" WHERE ");
237 Map<String, Object> mappedCriterias = new HashMap<String, Object>();
238 for (int i = 0; i < criterias.length; i += 2) {
239 mappedCriterias.put((String) criterias[i], criterias[i + 1]);
240 }
241 buildCriteriasHQL(hql, mappedCriterias);
242 Query query = getEntityManager().createQuery(hql.toString());
243
244 for (Map.Entry<String, Object> entry : mappedCriterias.entrySet()) {
245 query.setParameter(entry.getKey(), entry.getValue());
246 }
247 return query;
248 }
249 return getEntityManager().createQuery(hql.toString());
250 }
251
252 private void buildCriteriasHQL(StringBuilder hql, Map<String, Object> mappedCriterias) {
253 for (Iterator<String> i = mappedCriterias.keySet().iterator(); i.hasNext();) {
254 String criteria = i.next();
255 hql.append("o.").append(criteria).append("=:").append(criteria);
256 if (i.hasNext()) {
257 hql.append(" AND ");
258 }
259 }
260 }
261
262 }