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    }