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