001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2009 SonarSource SA
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.api.database;
021    
022    import java.sql.Connection;
023    import java.sql.SQLException;
024    import java.util.Iterator;
025    import java.util.Map;
026    import java.util.Properties;
027    
028    import javax.persistence.EntityManager;
029    import javax.persistence.EntityManagerFactory;
030    import javax.persistence.Persistence;
031    
032    import org.apache.commons.configuration.Configuration;
033    import org.apache.commons.lang.StringUtils;
034    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    import org.sonar.api.database.dialect.Dialect;
037    import org.sonar.api.database.dialect.DialectRepository;
038    
039    public abstract class AbstractDatabaseConnector implements DatabaseConnector {
040      protected static final Logger LOG_SQL = LoggerFactory.getLogger("org.hibernate.SQL");
041      protected static final Logger LOG = LoggerFactory.getLogger(AbstractDatabaseConnector.class);
042    
043      private Configuration configuration = null;
044      private EntityManagerFactory factory = null;
045      private int databaseVersion = SchemaMigration.VERSION_UNKNOWN;
046      private boolean operational = false;
047      private boolean started = false;
048      private boolean startsFailIfSchemaOutdated;
049      private Integer transactionIsolation = null;
050      private Dialect dialect = null;
051    
052      protected AbstractDatabaseConnector(Configuration configuration, boolean startsFailIfSchemaOutdated) {
053        this.configuration = configuration;
054        this.startsFailIfSchemaOutdated = startsFailIfSchemaOutdated;
055      }
056    
057      protected AbstractDatabaseConnector() {
058      }
059    
060      public Configuration getConfiguration() {
061        return configuration;
062      }
063    
064      public void setConfiguration(Configuration configuration) {
065        this.configuration = configuration;
066      }
067    
068      /**
069       * Indicates if the connector is operational : database connection OK and schema version OK
070       */
071      public boolean isOperational() {
072        return operational;
073      }
074    
075      /**
076       * Indicates if the connector is started : database connection OK and schema version OK or KO
077       */
078      protected boolean isStarted() {
079        return started;
080      }
081    
082      /**
083       * Get the JDBC transaction isolation defined by the configuration
084       *
085       * @return JDBC transaction isolation
086       */
087      public Integer getTransactionIsolation() {
088        return transactionIsolation;
089      }
090    
091      public void start() {
092        if (!started) {
093          transactionIsolation = configuration.getInteger(DatabaseProperties.PROP_ISOLATION, null /* use driver default setting */);
094          String jdbcConnectionUrl = testConnection();
095          dialect = DialectRepository.find(configuration.getString("sonar.jdbc.dialect"), jdbcConnectionUrl);
096          LoggerFactory.getLogger("org.sonar.INFO").info("Database dialect class " + dialect.getClass().getName());
097          started = true;
098        }
099        if (!operational) {
100          boolean upToDate = upToDateSchemaVersion();
101          if (!upToDate && startsFailIfSchemaOutdated) {
102            throw new DatabaseException(databaseVersion, SchemaMigration.LAST_VERSION);
103          }
104          if (upToDate) {
105            factory = createEntityManagerFactory();
106            operational = true;
107          }
108        }
109      }
110    
111      public void stop() {
112        if (factory != null && factory.isOpen()) {
113          factory.close();
114          factory = null;
115        }
116        operational = false;
117        started = false;
118      }
119    
120      public abstract void setupEntityManagerFactory(Properties factoryProps);
121    
122      public EntityManagerFactory getEntityManagerFactory() {
123        return factory;
124      }
125    
126      protected void setEntityManagerFactory(EntityManagerFactory factory) {
127        this.factory = factory;
128      }
129    
130      protected EntityManagerFactory createEntityManagerFactory() {
131        // other settings are stored into /META-INF/persistence.xml
132        Properties props = getHibernateProperties();
133        logHibernateSettings(props);
134        return Persistence.createEntityManagerFactory("sonar", props);
135      }
136    
137      private void logHibernateSettings(Properties props) {
138        if (LOG.isDebugEnabled()) {
139          for (Map.Entry<Object, Object> entry : props.entrySet()) {
140            LOG.debug(entry.getKey() + ": " + entry.getValue());
141          }
142        }
143      }
144    
145      protected Properties getHibernateProperties() {
146        Properties props = new Properties();
147        if (transactionIsolation != null) {
148          props.put("hibernate.connection.isolation", Integer.toString(transactionIsolation));
149        }
150        props.put("hibernate.hbm2ddl.auto", getConfiguration().getString(DatabaseProperties.PROP_HIBERNATE_HBM2DLL, "validate"));
151        props.put("hibernate.dialect", getDialectClass());
152    
153        props.put("hibernate.generate_statistics", getConfiguration().getBoolean(DatabaseProperties.PROP_HIBERNATE_GENERATE_STATISTICS, false));
154        props.put("hibernate.show_sql", Boolean.valueOf(LOG_SQL.isInfoEnabled()).toString());
155    
156        Configuration subset = getConfiguration().subset("sonar.hibernate");
157        for (Iterator keys = subset.getKeys(); keys.hasNext();) {
158          String key = (String) keys.next();
159          if (StringUtils.isNotBlank((String)subset.getProperty(key))) {
160            props.put("hibernate." + key, subset.getProperty(key));
161          }
162        }
163    
164        // custom impl setup
165        setupEntityManagerFactory(props);
166    
167    
168        return props;
169      }
170    
171      public EntityManager createEntityManager() {
172        return factory.createEntityManager();
173      }
174    
175      private String testConnection() throws DatabaseException {
176        Connection connection = null;
177        try {
178          connection = getConnection();
179          return connection.getMetaData().getURL();
180    
181        } catch (SQLException e) {
182          throw new DatabaseException("Cannot open connection to database: " + e.getMessage(), e);
183    
184        } finally {
185          close(connection);
186        }
187      }
188    
189      protected int loadVersion() {
190        Connection connection = null;
191        try {
192          connection = getConnection();
193          return SchemaMigration.getCurrentVersion(connection);
194    
195        } catch (SQLException e) {
196          // schema not created
197          return 0;
198        } finally {
199          close(connection);
200        }
201      }
202    
203      private void close(Connection connection) {
204        if (connection != null) {
205          try {
206            connection.close();
207          } catch (SQLException e) {
208            // why does close() throw a checked-exception ???
209          }
210        }
211      }
212    
213      protected boolean upToDateSchemaVersion() {
214        if (databaseVersion == SchemaMigration.LAST_VERSION) {
215          return true;
216        }
217        databaseVersion = loadVersion();
218        return databaseVersion == SchemaMigration.LAST_VERSION;
219      }
220    
221      protected int getDatabaseVersion() {
222        return databaseVersion;
223      }
224    
225      public Dialect getDialect() {
226        return dialect;
227      }
228    
229      public String getDialectClass() {
230        String dialectClass = configuration.getString(DatabaseProperties.PROP_DIALECT_CLASS);
231        if (dialectClass == null && dialect != null) {
232          dialectClass = dialect.getHibernateDialectClass().getName();
233        }
234        return dialectClass;
235      }
236    
237    }