Project

General

Profile

Download (16.6 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
 * Copyright (C) 2009 EDIT
3
 * European Distributed Institute of Taxonomy
4
 * http://www.e-taxonomy.eu
5
 *
6
 * The contents of this file are subject to the Mozilla Public License Version 1.1
7
 * See LICENSE.TXT at the top of this package for the full license terms.
8
 */
9

    
10
package eu.etaxonomy.cdm.opt.config;
11

    
12
import java.io.File;
13
import java.lang.reflect.InvocationTargetException;
14
import java.lang.reflect.Method;
15
import java.sql.Connection;
16
import java.sql.ResultSet;
17
import java.sql.SQLException;
18
import java.util.Properties;
19

    
20
import javax.naming.NamingException;
21
import javax.sql.DataSource;
22

    
23
import org.apache.commons.io.FilenameUtils;
24
import org.apache.log4j.Logger;
25
import org.hibernate.dialect.H2CorrectedDialect;
26
import org.hibernate.dialect.MySQL5MyISAMUtf8Dialect;
27
import org.hibernate.dialect.PostgreSQL82Dialect;
28
import org.springframework.beans.BeansException;
29
import org.springframework.beans.factory.annotation.Autowired;
30
import org.springframework.beans.factory.xml.XmlBeanFactory;
31
import org.springframework.context.annotation.Bean;
32
import org.springframework.context.annotation.Configuration;
33
import org.springframework.core.Ordered;
34
import org.springframework.core.annotation.Order;
35
import org.springframework.core.env.ConfigurableEnvironment;
36
import org.springframework.core.env.MutablePropertySources;
37
import org.springframework.core.env.PropertiesPropertySource;
38
import org.springframework.core.io.FileSystemResource;
39
import org.springframework.jndi.JndiObjectFactoryBean;
40

    
41
import com.mchange.v2.c3p0.ComboPooledDataSource;
42

    
43
import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys;
44
import eu.etaxonomy.cdm.common.CdmUtils;
45
import eu.etaxonomy.cdm.database.WrappedCdmDataSource;
46
import eu.etaxonomy.cdm.database.update.CdmUpdater;
47
import eu.etaxonomy.cdm.model.metadata.CdmMetaData;
48
import eu.etaxonomy.cdm.model.metadata.CdmMetaDataPropertyName;
49
import eu.etaxonomy.cdm.remote.config.AbstractWebApplicationConfigurer;
50

    
51
/**
52
 * The <code>DataSourceConfigurer</code> can be used as a replacement for a xml configuration in the application context.
53
 * <p>
54
 * The id of the loaded data source bean aka the <b>cdm instance name</b> is put into the <b>Spring environment</b> from where it can be retrieved using the
55
 * key {@link CDM_DATA_SOURCE_ID}.
56
 * <p>
57
 * Enter the following in your application context configuration in order to enable the <code>DataSourceConfigurer</code>:
58
 *
59
<pre>
60
&lt;!-- enable processing of annotations such as @Autowired and @Configuration --&gt;
61
&lt;context:annotation-config/&gt;
62

    
63
&lt;bean class="eu.etaxonomy.cdm.remote.config.DataSourceConfigurer" &gt;
64
&lt;/bean&gt;
65
</pre>
66
 * The <code>DataSourceConfigurer</code> allows alternative ways to specify a data source:
67
 *
68
 * <ol>
69
 * <li>Specify the data source bean to use in the Java environment properties:
70
 * <code>-Dcdm.datasource={dataSourceName}</code> ({@link #ATTRIBUTE_DATASOURCE_NAME}).
71
 * The data source bean with the given name will then be loaded from the <code>cdm.beanDefinitionFile</code>
72
 * ({@link #CDM_BEAN_DEFINITION_FILE}), which must be a valid Spring bean definition file.
73
 * </li>
74
 * <li>
75
 * Use a JDBC data source which is bound into the JNDI context. In this case the JNDI name is specified
76
 * via the {@link #ATTRIBUTE_JDBC_JNDI_NAME} as attribute to the ServletContext.
77
 * This scenario usually being used by the cdm-server application.
78
 * </li>
79
 * </ol>
80
 * The attributes used in (1) and (2) are in a first step being searched in the ServletContext
81
 * if not found search in a second step in the environment variables of the OS, see:{@link #findProperty(String, boolean)}.
82
 *
83
 * @author a.kohlbecker
84
 * @since 04.02.2011
85
 *
86
 */
87
@Configuration
88
public class DataSourceConfigurer extends AbstractWebApplicationConfigurer {
89

    
90
    public static final Logger logger = Logger.getLogger(DataSourceConfigurer.class);
91

    
92
    protected static final String HIBERNATE_DIALECT = "hibernate.dialect";
93
    protected static final String HIBERNATE_SEARCH_DEFAULT_INDEX_BASE = "hibernate.search.default.indexBase";
94
    protected static final String CDM_BEAN_DEFINITION_FILE = "cdm.beanDefinitionFile";
95

    
96
    /**
97
     * key for the spring environment to the datasource bean id aka instance name
98
     * @deprecated use CdmConfigurationKeys.CDM_DATA_SOURCE_ID instead
99
     */
100
    @Deprecated
101
    public static final String CDM_DATA_SOURCE_ID = CdmConfigurationKeys.CDM_DATA_SOURCE_ID;
102

    
103
    /**
104
     * Attribute to configure the name of the data source as set as bean name in the datasources.xml.
105
     * This name usually is used as the prefix for the webapplication root path.
106
     * <br>
107
     * <b>This is a required attribute!</b>
108
     *
109
     * @see AbstractWebApplicationConfigurer#findProperty(String, boolean)
110
     *
111
     * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes</code>
112
     *
113
     */
114
    protected static final String ATTRIBUTE_DATASOURCE_NAME = "cdm.datasource";
115
    /**
116
     * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes</code>
117
     */
118
    public static final String ATTRIBUTE_JDBC_JNDI_NAME = "cdm.jdbcJndiName";
119

    
120
    /**
121
     * Force a schema update when the cdmlib-remote-webapp instance is starting up
122
     * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes.ATTRIBUTE_FORCE_SCHEMA_UPDATE</code>
123
     */
124
    public static final String ATTRIBUTE_FORCE_SCHEMA_UPDATE = "cdm.forceSchemaUpdate";
125

    
126
    protected static final String DATASOURCE_BEANDEF_DEFAULT = CdmUtils.getCdmHomeDir().getPath() + File.separator + "datasources.xml";
127

    
128
    protected static String beanDefinitionFile = DATASOURCE_BEANDEF_DEFAULT;
129

    
130

    
131
    /**
132
     * The file to load the {@link DataSource} beans from.
133
     * This file is usually {@code ./.cdmLibrary/datasources.xml}
134
     *
135
     * @param filename
136
     */
137
    public void setBeanDefinitionFile(String filename){
138
        beanDefinitionFile = filename;
139
    }
140

    
141
    @Autowired
142
    private ConfigurableEnvironment env;
143

    
144
    private String dataSourceId = null;
145

    
146
    private DataSource dataSource;
147

    
148
    private DataSourceProperties dataSourceProperties;
149

    
150
    private Properties getHibernateProperties() {
151
        Properties hibernateProperties = webApplicationContext.getBean("jndiHibernateProperties", Properties.class);
152
        return hibernateProperties;
153
    }
154

    
155

    
156

    
157
    @Bean
158
    @Order(Ordered.HIGHEST_PRECEDENCE)
159
    public DataSource dataSource() {
160

    
161
        String beanName = findProperty(ATTRIBUTE_DATASOURCE_NAME, true);
162
        String jndiName = null;
163
        if(this.dataSource == null){
164
            jndiName = findProperty(ATTRIBUTE_JDBC_JNDI_NAME, false);
165

    
166
            if(jndiName != null){
167
                dataSource = useJndiDataSource(jndiName);
168
                dataSourceId = FilenameUtils.getName(jndiName);
169
            } else {
170
                dataSource = loadDataSourceBean(beanName);
171
                dataSourceId = beanName;
172
            }
173
        }
174

    
175
        if(dataSource == null){
176
            return null;
177
        }
178

    
179
        MutablePropertySources sources = env.getPropertySources();
180
        Properties props = new Properties();
181
        props.setProperty(CDM_DATA_SOURCE_ID, dataSourceId);
182
        sources.addFirst(new PropertiesPropertySource("cdm-datasource",  props));
183

    
184
        // validate correct schema version
185
        Connection connection  = null;
186
        try {
187

    
188
            connection = dataSource.getConnection();
189
            String metadataTableName = "CdmMetaData";
190
            if(inferHibernateDialectName(dataSource).equals(H2CorrectedDialect.class.getName())){
191
                metadataTableName = metadataTableName.toUpperCase();
192
            }
193
            ResultSet tables = connection.getMetaData().getTables(connection.getCatalog(), null, metadataTableName, null);
194
            if(tables.first()){
195
                ResultSet resultSet;
196
                try {
197
                    resultSet = connection.createStatement().executeQuery(CdmMetaDataPropertyName.DB_SCHEMA_VERSION.getSqlQuery());
198
                } catch (Exception e) {
199
                    try {
200
                        resultSet = connection.createStatement().executeQuery(CdmMetaDataPropertyName.DB_SCHEMA_VERSION.getSqlQueryOld());
201
                    } catch (Exception e1) {
202
                        throw e1;
203
                    }
204
                }
205
                String version = null;
206
                if(resultSet.next()){
207
                    version = resultSet.getString(1);
208
                } else {
209
                    throw new RuntimeException("Unable to retrieve version info from data source " + dataSource.toString());
210
                }
211

    
212
                connection.close();
213

    
214
                if(!CdmMetaData.isDbSchemaVersionCompatible(version)){
215
                    /*
216
                     * any exception thrown here would be nested into a spring
217
                     * BeanException which can not be caught in the servlet
218
                     * container, so we post the information into the
219
                     * ServletContext
220
                     */
221
                    String errorMessage = "Incompatible version [" + (beanName != null ? beanName : jndiName) + "] expected version: " + CdmMetaData.getDbSchemaVersion() + ",  data base version  " + version;
222
                    addErrorMessageToServletContextAttributes(errorMessage);
223
                }
224
            } else {
225
//            	throw new RuntimeException("database " + dataSource.toString() + " is empty or not a cdm database");
226
                logger.error("database " + dataSource.toString() + " is empty or not a cdm database");
227
            }
228

    
229

    
230
        } catch (SQLException e) {
231
            RuntimeException re =   new RuntimeException("Unable to connect or to retrieve version info from data source " + dataSource.toString() , e);
232
            addErrorMessageToServletContextAttributes(re.getMessage());
233
            throw re;
234
        } finally {
235
            if(connection != null){
236
                try {
237
                    connection.close();
238
                } catch (SQLException e) {
239
                    // IGNORE //
240
                }
241
            }
242
        }
243

    
244

    
245
        String forceSchemaUpdate = findProperty(ATTRIBUTE_FORCE_SCHEMA_UPDATE, false);
246
        if(forceSchemaUpdate != null){
247
            logger.info("Update of data source requested by property '" + ATTRIBUTE_FORCE_SCHEMA_UPDATE + "'");
248

    
249
            CdmUpdater updater = CdmUpdater.NewInstance();
250
            WrappedCdmDataSource cdmDataSource = new WrappedCdmDataSource(dataSource);
251
            updater.updateToCurrentVersion(cdmDataSource, null);
252
            cdmDataSource.closeOpenConnections();
253
        }
254

    
255
        return dataSource;
256
    }
257

    
258
    @Bean
259
    public DataSourceProperties dataSourceProperties(){
260
        if(this.dataSourceProperties == null){
261
            dataSourceProperties = loadDataSourceProperties();
262
            if(dataSourceId == null){
263
                dataSource();
264
            }
265
            dataSourceProperties.setCurrentDataSourceId(dataSourceId);
266
        }
267
        return dataSourceProperties;
268
    }
269

    
270

    
271
    private DataSource useJndiDataSource(String jndiName) {
272
        logger.info("using jndi datasource '" + jndiName + "'");
273

    
274
        JndiObjectFactoryBean jndiFactory = new JndiObjectFactoryBean();
275
        /*
276
        JndiTemplate jndiTemplate = new JndiTemplate();
277
        jndiFactory.setJndiTemplate(jndiTemplate); no need to use a JndiTemplate
278
        if I try using JndiTemplate I get an org.hibernate.AnnotationException: "Unknown Id.generator: system-increment"
279
        when running multiple instances via the Bootloader
280
        */
281
        jndiFactory.setResourceRef(true);
282
        jndiFactory.setJndiName(jndiName);
283
        try {
284
            jndiFactory.afterPropertiesSet();
285
        } catch (IllegalArgumentException e) {
286
            logger.error(e, e);
287
        } catch (NamingException e) {
288
            logger.error(e, e);
289
        }
290
        Object obj = jndiFactory.getObject();
291
        return (DataSource)obj;
292
    }
293

    
294
    /**
295
     * Loads the {@link DataSource} bean from the cdm bean definition file.
296
     * This file is usually {@code ./.cdmLibrary/datasources.xml}
297
     *
298
     * @param beanName
299
     * @return
300
     */
301
    private DataSource loadDataSourceBean(String beanName) {
302

    
303
        File f = new File("./");
304
        System.err.println(f.getAbsolutePath());
305

    
306
        String beanDefinitionFileFromProperty = findProperty(CDM_BEAN_DEFINITION_FILE, false);
307
        String path = (beanDefinitionFileFromProperty != null ? beanDefinitionFileFromProperty : beanDefinitionFile);
308
        logger.info("loading DataSourceBean '" + beanName + "' from: " + path);
309
        FileSystemResource file = new FileSystemResource(path);
310
        XmlBeanFactory beanFactory  = new XmlBeanFactory(file);
311
        DataSource dataSource = beanFactory.getBean(beanName, DataSource.class);
312
        if(dataSource instanceof ComboPooledDataSource){
313
            logger.info("DataSourceBean '" + beanName + "' is a ComboPooledDataSource [URL:" + ((ComboPooledDataSource)dataSource).getJdbcUrl()+ "]");
314
        } else {
315
            logger.error("DataSourceBean '" + beanName + "' IS NOT a ComboPooledDataSource");
316
        }
317
        return dataSource;
318
    }
319

    
320

    
321
    /**
322
     * Loads the <code>dataSourceProperties</code> bean from the cdm bean
323
     * definition file.
324
     * This file is usually {@code ./.cdmLibrary/datasources.xml}
325
     *
326
     * @return the DataSourceProperties bean or an empty instance if the bean is not found
327
     */
328
    private DataSourceProperties loadDataSourceProperties() {
329

    
330
        String beanDefinitionFileFromProperty = findProperty(CDM_BEAN_DEFINITION_FILE, false);
331
        String path = (beanDefinitionFileFromProperty != null ? beanDefinitionFileFromProperty : beanDefinitionFile);
332
        logger.info("loading dataSourceProperties from: " + path);
333
        FileSystemResource file = new FileSystemResource(path);
334
        XmlBeanFactory beanFactory  = new XmlBeanFactory(file);
335
        DataSourceProperties properties = null;
336
        try {
337
            properties = beanFactory.getBean("dataSourceProperties", DataSourceProperties.class);
338
        } catch (BeansException e) {
339
            logger.warn("bean 'dataSourceProperties' not found");
340
            properties = new DataSourceProperties();
341
        }
342
        return properties;
343
    }
344

    
345
    @Bean
346
    public Properties hibernateProperties(){
347
        Properties props = getHibernateProperties();
348
        props.setProperty(HIBERNATE_DIALECT, inferHibernateDialectName());
349
        String searchPath = CdmUtils.getCdmHomeSubDir(CdmUtils.SUBFOLDER_WEBAPP).getPath();
350
        props.setProperty(HIBERNATE_SEARCH_DEFAULT_INDEX_BASE,
351
                searchPath +
352
                "/index/".replace("/", File.separator) +
353
                findProperty(ATTRIBUTE_DATASOURCE_NAME, true));
354
        logger.debug("hibernateProperties: " + props.toString());
355
        return props;
356
    }
357

    
358
    /**
359
     * Returns the full class name of the according {@link org.hibernate.dialect.Dialect} implementation
360
     *
361
     * @param ds the DataSource
362
     * @return the name
363
     */
364
    public String inferHibernateDialectName() {
365
        DataSource ds = dataSource();
366
        return inferHibernateDialectName(ds);
367
    }
368

    
369

    
370

    
371
    /**
372
     * Returns the full class name of the according {@link org.hibernate.dialect.Dialect} implementation
373
     *
374
     * @param ds the DataSource
375
     * @return the name
376
     */
377
    public String inferHibernateDialectName(DataSource ds) {
378
        String url = "<SEE PRIOR REFLECTION ERROR>";
379
        Method m = null;
380
        try {
381
            m = ds.getClass().getMethod("getUrl");
382
        } catch (SecurityException e) {
383
            logger.error(e);
384
        } catch (NoSuchMethodException e) {
385
            try {
386
                m = ds.getClass().getMethod("getJdbcUrl");
387
            } catch (SecurityException e2) {
388
                logger.error(e2);
389
            } catch (NoSuchMethodException e2) {
390
                logger.error(e2);
391
            }
392
        }
393
        try {
394
            url = (String)m.invoke(ds);
395
        } catch (IllegalArgumentException e) {
396
            logger.error(e);
397
        } catch (IllegalAccessException e) {
398
            logger.error(e);
399
        } catch (InvocationTargetException e) {
400
            logger.error(e);
401
        } catch (SecurityException e) {
402
            logger.error(e);
403
        }
404

    
405
        if(url != null){
406
            if(url.contains(":mysql:")){
407
                // TODO we should switch all databases to InnoDB !
408
                // TODO open jdbc connection to check engine and choose between
409
                // MySQL5MyISAMUtf8Dialect and MySQL5MyISAMUtf8Dialect
410
                // see #3371 (switch cdm to MySQL InnoDB)
411
                return MySQL5MyISAMUtf8Dialect.class.getName();
412
            }
413
            if(url.contains(":h2:")){
414
                return H2CorrectedDialect.class.getName();
415
            }
416
            if(url.contains(":postgresql:")){
417
                return PostgreSQL82Dialect.class.getName();
418
            }
419
        }
420

    
421
        logger.error("hibernate dialect mapping for "+url+ " not yet implemented or unavailable");
422
        return null;
423
    }
424

    
425
}
(1-1/5)