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