Project

General

Profile

Download (19.5 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.Objects;
19
import java.util.Properties;
20

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

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

    
43
import com.mchange.v2.c3p0.ComboPooledDataSource;
44

    
45
import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys;
46
import eu.etaxonomy.cdm.config.ConfigFileUtil;
47
import eu.etaxonomy.cdm.database.CdmDatabaseException;
48
import eu.etaxonomy.cdm.database.WrappedCdmDataSource;
49
import eu.etaxonomy.cdm.database.update.CdmUpdater;
50
import eu.etaxonomy.cdm.model.metadata.CdmMetaData;
51
import eu.etaxonomy.cdm.model.metadata.CdmMetaDataPropertyName;
52
import eu.etaxonomy.cdm.remote.config.AbstractWebApplicationConfigurer;
53

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

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

    
95
    public static final Logger logger = LogManager.getLogger(DataSourceConfigurer.class);
96

    
97
    protected static final String HIBERNATE_DIALECT = "hibernate.dialect";
98
    protected static final String HIBERNATE_SEARCH_DEFAULT_INDEX_BASE = "hibernate.search.default.indexBase";
99
    protected static final String CDM_BEAN_DEFINITION_FILE = "cdm.beanDefinitionFile";
100

    
101
    @Autowired // Important!!!!
102
    private ConfigFileUtil configFileUtil;
103

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

    
122
    /**
123
     * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes</code>
124
     */
125
    public static final String ATTRIBUTE_JDBC_JNDI_NAME = "cdm.jdbcJndiName";
126

    
127
    /**
128
     * Force a schema update when the cdmlib-remote-webapp instance is starting up
129
     * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes.ATTRIBUTE_FORCE_SCHEMA_UPDATE</code>
130
     */
131
    public static final String ATTRIBUTE_FORCE_SCHEMA_UPDATE = "cdm.forceSchemaUpdate";
132

    
133
    /**
134
     * <b>WARNING!!!!!!!!!!!!!!!</b> Using this option will delete the existing data base followed by database creation.
135
     * <p>
136
     * Force a schema creation when the cdmlib-remote-webapp instance is starting up. Will set the hibernate property
137
     * {@code hibernate.hbm2ddl.auto} to {@code create}
138
     * <p>
139
     * This option will inactivate {@link #ATTRIBUTE_FORCE_SCHEMA_UPDATE} even if this is set.
140
     * <p>
141
     * This attribute has no equivalent in <code>eu.etaxonomy.cdm.server.instance.SharedAttributes</code> and never must
142
     * have any since this setting this attribute for any database listed for a server is far too dangerous. It must only
143
     * be possible to set this option per data base individually.
144
     */
145
    public static final String ATTRIBUTE_FORCE_SCHEMA_CREATE = "cdm.forceSchemaCreate";
146

    
147
    private static String beanDefinitionFile = null;
148

    
149

    
150
    /**
151
     * The file to load the {@link DataSource} beans from.
152
     * This file is usually {@code ${user.home}/.cdmLibrary/datasources.xml}
153
     * The variable <code>${user.home}</code>is determined by {@link ConfigFileUtil.getCdmHomeDir()}
154
     *
155
     *
156
     * @param filename
157
     */
158
    public void setBeanDefinitionFile(String filename){
159
        beanDefinitionFile = filename;
160
    }
161

    
162
    public String getBeanDefinitionFile(){
163
        if(beanDefinitionFile == null){
164
            beanDefinitionFile = configFileUtil.getCdmHomeDir().getPath() + File.separator + "datasources.xml";
165
        }
166
        return beanDefinitionFile;
167
    }
168

    
169
    private String dataSourceId = null;
170

    
171
    private DataSource dataSource;
172

    
173
    private DataSourceProperties dataSourceProperties;
174

    
175
    private Properties getHibernateProperties() {
176
        Properties hibernateProperties = webApplicationContext.getBean("jndiHibernateProperties", Properties.class);
177
        return hibernateProperties;
178
    }
179

    
180
    @Bean
181
    @Order(Ordered.HIGHEST_PRECEDENCE)
182
    public DataSource dataSource() {
183

    
184
        String beanName = findProperty(ATTRIBUTE_DATASOURCE_NAME, true);
185
        String jndiName = null;
186
        if(this.dataSource == null){
187
            jndiName = findProperty(ATTRIBUTE_JDBC_JNDI_NAME, false);
188

    
189
            if(jndiName != null){
190
                try {
191
                    dataSource = useJndiDataSource(jndiName);
192
                    dataSourceId = FilenameUtils.getName(jndiName);
193
                } catch (NamingException e) {
194
                   throw new DataSourceException("JNDI data source (" + jndiName + ") not found. Jdbc URI correct? Does the database exist?", e);
195
                }
196
            } else {
197
                dataSource = loadDataSourceBean(beanName);
198
                dataSourceId = beanName;
199
            }
200
        }
201

    
202
        if(dataSource == null){
203
            return null;
204
        }
205

    
206
        MutablePropertySources sources = env.getPropertySources();
207
        Properties props = new Properties();
208
        props.setProperty(CdmConfigurationKeys.CDM_DATA_SOURCE_ID, dataSourceId);
209
        sources.addFirst(new PropertiesPropertySource("cdm-datasource",  props));
210

    
211
        if(!isForceSchemaCreate()) {
212
            // validate correct schema version
213
            Connection connection  = null;
214
            try {
215
                connection = dataSource.getConnection();
216
                String metadataTableName = "CdmMetaData";
217
                if(inferHibernateDialectName(dataSource).equals(H2CorrectedDialect.class.getName())){
218
                    metadataTableName = metadataTableName.toUpperCase();
219
                }
220
                ResultSet tables = connection.getMetaData().getTables(connection.getCatalog(), null, metadataTableName, null);
221
                if(tables.first()){
222
                    ResultSet resultSet;
223
                    try {
224
                        resultSet = connection.createStatement().executeQuery(CdmMetaDataPropertyName.DB_SCHEMA_VERSION.getSqlQuery());
225
                    } catch (Exception e) {
226
                        try {
227
                            resultSet = connection.createStatement().executeQuery(CdmMetaDataPropertyName.DB_SCHEMA_VERSION.getSqlQueryOld());
228
                        } catch (Exception e1) {
229
                            throw e1;
230
                        }
231
                    }
232
                    String version = null;
233
                    if(resultSet.next()){
234
                        version = resultSet.getString(1);
235
                    } else {
236
                        CdmDatabaseException cde = new CdmDatabaseException("Unable to retrieve version info from data source " + dataSource.toString()
237
                        + " -  the database may have been corrupted or is not a cdm database");
238
                        addErrorMessageToServletContextAttributes(cde.getMessage());
239
                        throw cde;
240
                    }
241

    
242
                    connection.close();
243

    
244
                    if(!CdmMetaData.isDbSchemaVersionCompatible(version)){
245
                        /*
246
                         * any exception thrown here would be nested into a spring
247
                         * BeanException which can not be caught in the servlet
248
                         * container, so we post the information into the
249
                         * ServletContext
250
                         */
251
                        String errorMessage = "Incompatible version [" + (beanName != null ? beanName : jndiName) + "] expected version: " + CdmMetaData.getDbSchemaVersion() + ",  data base version  " + version;
252
                        addErrorMessageToServletContextAttributes(errorMessage);
253
                    }
254
                } else {
255
                    CdmDatabaseException cde = new CdmDatabaseException("database " + dataSource.toString() + " is empty or not a cdm database");
256
                    logger.error(cde.getMessage());
257
                    // throw cde; // TODO: No exception was thrown here before. Is this correct behavior or
258
                }
259
            } catch (SQLException e) {
260
                CdmDatabaseException re = new CdmDatabaseException("Unable to connect or to retrieve version info from data source " + dataSource.toString() , e);
261
                addErrorMessageToServletContextAttributes(re.getMessage());
262
                throw re;
263
            } finally {
264
                if(connection != null){
265
                    try {
266
                        connection.close();
267
                    } catch (SQLException e) {
268
                        // IGNORE //
269
                    }
270
                }
271
            } // END // validate correct schema version
272
        } // END !isForceSchemaCreate()
273

    
274

    
275
        String forceSchemaUpdate = findProperty(ATTRIBUTE_FORCE_SCHEMA_UPDATE, false);
276
        if(forceSchemaUpdate != null){
277
            if(!isForceSchemaCreate()) {
278
                logger.info("Update of data source requested by property '" + ATTRIBUTE_FORCE_SCHEMA_UPDATE + "'");
279
                CdmUpdater updater = CdmUpdater.NewInstance();
280
                WrappedCdmDataSource cdmDataSource = new WrappedCdmDataSource(dataSource);
281
                updater.updateToCurrentVersion(cdmDataSource, null);
282
                cdmDataSource.closeOpenConnections();
283
            } else {
284
                logger.info("Update of data source requested by property '" + ATTRIBUTE_FORCE_SCHEMA_UPDATE + "' but overwritten by " + ATTRIBUTE_FORCE_SCHEMA_CREATE);
285
            }
286
        }
287

    
288
        return dataSource;
289
    }
290

    
291
    @Bean
292
    public DataSourceProperties dataSourceProperties(){
293
        if(this.dataSourceProperties == null){
294
            dataSourceProperties = loadDataSourceProperties();
295
            if(dataSourceId == null){
296
                dataSource();
297
            }
298
            dataSourceProperties.setCurrentDataSourceId(dataSourceId);
299
        }
300
        return dataSourceProperties;
301
    }
302

    
303

    
304
    private DataSource useJndiDataSource(String jndiName) throws NamingException {
305
        logger.info("using jndi datasource '" + jndiName + "'");
306

    
307
        JndiObjectFactoryBean jndiFactory = new JndiObjectFactoryBean();
308
        /*
309
        JndiTemplate jndiTemplate = new JndiTemplate();
310
        jndiFactory.setJndiTemplate(jndiTemplate); no need to use a JndiTemplate
311
        if I try using JndiTemplate I get an org.hibernate.AnnotationException: "Unknown Id.generator: system-increment"
312
        when running multiple instances via the Bootloader
313
        */
314
        jndiFactory.setResourceRef(true);
315
        jndiFactory.setJndiName(jndiName);
316
        try {
317
            jndiFactory.afterPropertiesSet();
318
        } catch (IllegalArgumentException e) {
319
            logger.error(e, e);
320
        } catch (NamingException e) {
321
            logger.error(e, e);
322
            throw e;
323
        }
324
        Object obj = jndiFactory.getObject();
325
        return (DataSource)obj;
326
    }
327

    
328
    /**
329
     * Loads the {@link DataSource} bean from the cdm bean definition file.
330
     * This file is usually {@code ./.cdmLibrary/datasources.xml}
331
     *
332
     * @param beanName
333
     * @return
334
     */
335
    private DataSource loadDataSourceBean(String beanName) {
336

    
337
        String beanDefinitionFileFromProperty = findProperty(CDM_BEAN_DEFINITION_FILE, false);
338
        String path = (beanDefinitionFileFromProperty != null ? beanDefinitionFileFromProperty : getBeanDefinitionFile());
339
        logger.info("loading DataSourceBean '" + beanName + "' from: " + path);
340
        FileSystemResource file = new FileSystemResource(path);
341
        XmlBeanFactory beanFactory  = new XmlBeanFactory(file);
342
        DataSource dataSource = beanFactory.getBean(beanName, DataSource.class);
343
        if(dataSource instanceof ComboPooledDataSource){
344
            logger.info("DataSourceBean '" + beanName + "' is a ComboPooledDataSource [URL:" + ((ComboPooledDataSource)dataSource).getJdbcUrl()+ "]");
345
        } else {
346
            logger.error("DataSourceBean '" + beanName + "' IS NOT a ComboPooledDataSource");
347
        }
348
        return dataSource;
349
    }
350

    
351

    
352
    /**
353
     * Loads the <code>dataSourceProperties</code> bean from the cdm bean
354
     * definition file.
355
     * This file is usually {@code ./.cdmLibrary/datasources.xml}
356
     *
357
     * @return the DataSourceProperties bean or an empty instance if the bean is not found
358
     */
359
    private DataSourceProperties loadDataSourceProperties() {
360

    
361
        String beanDefinitionFileFromProperty = findProperty(CDM_BEAN_DEFINITION_FILE, false);
362
        String path = (beanDefinitionFileFromProperty != null ? beanDefinitionFileFromProperty : getBeanDefinitionFile());
363
        logger.info("loading dataSourceProperties from: " + path);
364
        FileSystemResource file = new FileSystemResource(path);
365
        XmlBeanFactory beanFactory  = new XmlBeanFactory(file);
366
        DataSourceProperties properties = null;
367
        try {
368
            properties = beanFactory.getBean("dataSourceProperties", DataSourceProperties.class);
369
        } catch (BeansException e) {
370
            logger.warn("bean 'dataSourceProperties' not found");
371
            properties = new DataSourceProperties();
372
        }
373
        return properties;
374
    }
375

    
376
    @Bean
377
    public Properties hibernateProperties(){
378
        Properties props = getHibernateProperties();
379
        props.setProperty(HIBERNATE_DIALECT, inferHibernateDialectName());
380
        String searchPath = configFileUtil.getCdmHomeSubDir(ConfigFileUtil.SUBFOLDER_WEBAPP).getPath();
381
        props.setProperty(HIBERNATE_SEARCH_DEFAULT_INDEX_BASE,
382
                searchPath +
383
                "/index/".replace("/", File.separator) +
384
                findProperty(ATTRIBUTE_DATASOURCE_NAME, true));
385
        if(isForceSchemaCreate()) {
386
            props.setProperty("hibernate.hbm2ddl.auto", "create");
387
        }
388
        logger.debug("hibernateProperties: " + props.toString());
389
        return props;
390
    }
391

    
392
    private boolean isForceSchemaCreate() {
393
        String propVal = findProperty(ATTRIBUTE_FORCE_SCHEMA_CREATE, false);
394
        logger.debug("System property " + ATTRIBUTE_FORCE_SCHEMA_CREATE +  " = " + Objects.toString(propVal, "[NULL]"));
395
        return propVal != null && !(propVal.toLowerCase().equals("false") || propVal.equals("0"));
396
    }
397

    
398
    /**
399
     * Returns the full class name of the according {@link org.hibernate.dialect.Dialect} implementation
400
     *
401
     * @param ds the DataSource
402
     * @return the name
403
     */
404
    public String inferHibernateDialectName() {
405
        DataSource ds = dataSource();
406
        return inferHibernateDialectName(ds);
407
    }
408

    
409

    
410

    
411
    /**
412
     * Returns the full class name of the according {@link org.hibernate.dialect.Dialect} implementation
413
     *
414
     * @param ds the DataSource
415
     * @return the name
416
     */
417
    public String inferHibernateDialectName(DataSource ds) {
418
        String url = "<SEE PRIOR REFLECTION ERROR>";
419
        Method m = null;
420
        try {
421
            m = ds.getClass().getMethod("getUrl");
422
        } catch (SecurityException e) {
423
            logger.error(e);
424
        } catch (NoSuchMethodException e) {
425
            try {
426
                m = ds.getClass().getMethod("getJdbcUrl");
427
            } catch (SecurityException e2) {
428
                logger.error(e2);
429
            } catch (NoSuchMethodException e2) {
430
                logger.error(e2);
431
            }
432
        }
433
        try {
434
            url = (String)m.invoke(ds);
435
        } catch (IllegalArgumentException e) {
436
            logger.error(e);
437
        } catch (IllegalAccessException e) {
438
            logger.error(e);
439
        } catch (InvocationTargetException e) {
440
            logger.error(e);
441
        } catch (SecurityException e) {
442
            logger.error(e);
443
        }
444

    
445
        if(url != null){
446
            if(url.contains(":mysql:")){
447
                // TODO we should switch all databases to InnoDB !
448
                // TODO open jdbc connection to check engine and choose between
449
                // MySQL5MyISAMUtf8Dialect and MySQL5MyISAMUtf8Dialect
450
                // see #3371 (switch cdm to MySQL InnoDB)
451
                return MySQL5MyISAMUtf8Dialect.class.getName();
452
            }
453
            if(url.contains(":h2:")){
454
                return H2CorrectedDialect.class.getName();
455
            }
456
            if(url.contains(":postgresql:")){
457
                return PostgreSQL82Dialect.class.getName();
458
            }
459
        }
460

    
461
        logger.error("hibernate dialect mapping for "+url+ " not yet implemented or unavailable");
462
        return null;
463
    }
464

    
465
}
(1-1/6)