ref #6628 harmonizing cdm home folder utils and providing a default subfolder suggest...
[cdmlib.git] / cdmlib-remote / src / main / java / eu / etaxonomy / cdm / opt / config / DataSourceConfigurer.java
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.xml.XmlBeanFactory;
30 import org.springframework.context.annotation.Bean;
31 import org.springframework.context.annotation.Configuration;
32 import org.springframework.core.io.FileSystemResource;
33 import org.springframework.jndi.JndiObjectFactoryBean;
34
35 import com.mchange.v2.c3p0.ComboPooledDataSource;
36
37 import eu.etaxonomy.cdm.common.CdmUtils;
38 import eu.etaxonomy.cdm.database.WrappedCdmDataSource;
39 import eu.etaxonomy.cdm.database.update.CdmUpdater;
40 import eu.etaxonomy.cdm.model.metadata.CdmMetaData;
41 import eu.etaxonomy.cdm.model.metadata.CdmMetaData.MetaDataPropertyName;
42 import eu.etaxonomy.cdm.remote.config.AbstractWebApplicationConfigurer;
43
44 /**
45 * The <code>DataSourceConfigurer</code> can be used as a replacement for a xml configuration in the application context.
46 * Enter the following in your application context configuration in order to enable the <code>DataSourceConfigurer</code>:
47 *
48 <pre>
49 &lt;!-- enable processing of annotations such as @Autowired and @Configuration --&gt;
50 &lt;context:annotation-config/&gt;
51
52 &lt;bean class="eu.etaxonomy.cdm.remote.config.DataSourceConfigurer" &gt;
53 &lt;/bean&gt;
54 </pre>
55 * The <code>DataSourceConfigurer</code> allows alternative ways to specify a data source:
56 *
57 * <ol>
58 * <li>Specify the data source bean to use in the Java environment properties:
59 * <code>-Dcdm.datasource={dataSourceName}</code> ({@link #ATTRIBUTE_DATASOURCE_NAME}).
60 * The data source bean with the given name will then be loaded from the <code>cdm.beanDefinitionFile</code>
61 * ({@link #CDM_BEAN_DEFINITION_FILE}), which must be a valid Spring bean definition file.
62 * </li>
63 * <li>
64 * Use a JDBC data source which is bound into the JNDI context. In this case the JNDI name is specified
65 * via the {@link #ATTRIBUTE_JDBC_JNDI_NAME} as attribute to the ServletContext.
66 * This scenario usually being used by the cdm-server application.
67 * </li>
68 * </ol>
69 * The attributes used in (1) and (2) are in a first step being searched in the ServletContext
70 * if not found search in a second step in the environment variables of the OS, see:{@link #findProperty(String, boolean)}.
71 *
72 * @author a.kohlbecker
73 * @date 04.02.2011
74 *
75 */
76 @Configuration
77 public class DataSourceConfigurer extends AbstractWebApplicationConfigurer {
78
79 public static final Logger logger = Logger.getLogger(DataSourceConfigurer.class);
80
81 protected static final String HIBERNATE_DIALECT = "hibernate.dialect";
82 protected static final String HIBERNATE_SEARCH_DEFAULT_INDEX_BASE = "hibernate.search.default.indexBase";
83 protected static final String CDM_BEAN_DEFINITION_FILE = "cdm.beanDefinitionFile";
84
85 /**
86 * Attribute to configure the name of the data source as set as bean name in the datasources.xml.
87 * This name usually is used as the prefix for the webapplication root path.
88 * <br>
89 * <b>This is a required attribute!</b>
90 *
91 * @see AbstractWebApplicationConfigurer#findProperty(String, boolean)
92 *
93 * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes</code>
94 *
95 */
96 protected static final String ATTRIBUTE_DATASOURCE_NAME = "cdm.datasource";
97 /**
98 * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes</code>
99 */
100 public static final String ATTRIBUTE_JDBC_JNDI_NAME = "cdm.jdbcJndiName";
101
102 /**
103 * Force a schema update when the cdmlib-remote-webapp instance is starting up
104 * see also <code>eu.etaxonomy.cdm.server.instance.SharedAttributes.ATTRIBUTE_FORCE_SCHEMA_UPDATE</code>
105 */
106 public static final String ATTRIBUTE_FORCE_SCHEMA_UPDATE = "cdm.forceSchemaUpdate";
107
108 protected static final String DATASOURCE_BEANDEF_DEFAULT = CdmUtils.getCdmHomeDir().getPath() + File.separator + "datasources.xml";
109
110 protected static String beanDefinitionFile = DATASOURCE_BEANDEF_DEFAULT;
111
112
113 private String cmdServerInstanceName = null;
114
115 /**
116 * The file to load the {@link DataSource} beans from.
117 * This file is usually {@code ./.cdmLibrary/datasources.xml}
118 *
119 * @param filename
120 */
121 public void setBeanDefinitionFile(String filename){
122 beanDefinitionFile = filename;
123 }
124
125
126 private String dataSourceId = null;
127
128 private DataSource dataSource;
129
130 private DataSourceProperties dataSourceProperties;
131
132 private Properties getHibernateProperties() {
133 Properties hibernateProperties = webApplicationContext.getBean("jndiHibernateProperties", Properties.class);
134 return hibernateProperties;
135 }
136
137
138
139 @Bean
140 public DataSource dataSource() {
141
142 String beanName = findProperty(ATTRIBUTE_DATASOURCE_NAME, true);
143 String jndiName = null;
144 if(this.dataSource == null){
145 jndiName = findProperty(ATTRIBUTE_JDBC_JNDI_NAME, false);
146
147 if(jndiName != null){
148 dataSource = useJndiDataSource(jndiName);
149 dataSourceId = FilenameUtils.getName(jndiName);
150 } else {
151 dataSource = loadDataSourceBean(beanName);
152 dataSourceId = beanName;
153 }
154 }
155
156 if(dataSource == null){
157 return null;
158 }
159
160 // validate correct schema version
161 try {
162
163 Connection connection = dataSource.getConnection();
164 String metadataTableName = "CdmMetaData";
165 if(inferHibernateDialectName(dataSource).equals(H2CorrectedDialect.class.getName())){
166 metadataTableName = metadataTableName.toUpperCase();
167 }
168 ResultSet tables = connection.getMetaData().getTables(connection.getCatalog(), null, metadataTableName, null);
169 if(tables.first()){
170 ResultSet resultSet = connection.createStatement().executeQuery(MetaDataPropertyName.DB_SCHEMA_VERSION.getSqlQuery());
171 String version = null;
172 if(resultSet.next()){
173 version = resultSet.getString(1);
174 } else {
175 throw new RuntimeException("Unable to retrieve version info from data source " + dataSource.toString());
176 }
177
178 connection.close();
179
180 if(!CdmMetaData.isDbSchemaVersionCompatible(version)){
181 /*
182 * any exception thrown here would be nested into a spring
183 * BeanException which can not be caught in the servlet
184 * container, so we post the information into the
185 * ServletContext
186 */
187 String errorMessage = "Incompatible version [" + (beanName != null ? beanName : jndiName) + "] expected version: " + CdmMetaData.getDbSchemaVersion() + ", data base version " + version;
188 addErrorMessageToServletContextAttributes(errorMessage);
189 }
190 } else {
191 // throw new RuntimeException("database " + dataSource.toString() + " is empty or not a cdm database");
192 logger.error("database " + dataSource.toString() + " is empty or not a cdm database");
193 }
194
195
196 } catch (SQLException e) {
197 RuntimeException re = new RuntimeException("Unable to connect or to retrieve version info from data source " + dataSource.toString() , e);
198 addErrorMessageToServletContextAttributes(re.getMessage());
199 throw re;
200
201 }
202
203
204 String forceSchemaUpdate = findProperty(ATTRIBUTE_FORCE_SCHEMA_UPDATE, false);
205 if(forceSchemaUpdate != null){
206 logger.info("Update of data source requested by property '" + ATTRIBUTE_FORCE_SCHEMA_UPDATE + "'");
207
208 CdmUpdater updater = CdmUpdater.NewInstance();
209 WrappedCdmDataSource cdmDataSource = new WrappedCdmDataSource(dataSource);
210 updater.updateToCurrentVersion(cdmDataSource, null);
211 }
212
213 return dataSource;
214 }
215
216 @Bean
217 public DataSourceProperties dataSourceProperties(){
218 if(this.dataSourceProperties == null){
219 dataSourceProperties = loadDataSourceProperties();
220 if(dataSourceId == null){
221 dataSource();
222 }
223 dataSourceProperties.setCurrentDataSourceId(dataSourceId);
224 }
225 return dataSourceProperties;
226 }
227
228
229 private DataSource useJndiDataSource(String jndiName) {
230 logger.info("using jndi datasource '" + jndiName + "'");
231
232 JndiObjectFactoryBean jndiFactory = new JndiObjectFactoryBean();
233 /*
234 JndiTemplate jndiTemplate = new JndiTemplate();
235 jndiFactory.setJndiTemplate(jndiTemplate); no need to use a JndiTemplate
236 if I try using JndiTemplate I get an org.hibernate.AnnotationException: "Unknown Id.generator: system-increment"
237 when running multiple instances via the Bootloader
238 */
239 jndiFactory.setResourceRef(true);
240 jndiFactory.setJndiName(jndiName);
241 try {
242 jndiFactory.afterPropertiesSet();
243 } catch (IllegalArgumentException e) {
244 logger.error(e, e);
245 } catch (NamingException e) {
246 logger.error(e, e);
247 }
248 Object obj = jndiFactory.getObject();
249 return (DataSource)obj;
250 }
251
252 /**
253 * Loads the {@link DataSource} bean from the cdm bean definition file.
254 * This file is usually {@code ./.cdmLibrary/datasources.xml}
255 *
256 * @param beanName
257 * @return
258 */
259 private DataSource loadDataSourceBean(String beanName) {
260
261 File f = new File("./");
262 System.err.println(f.getAbsolutePath());
263
264 String beanDefinitionFileFromProperty = findProperty(CDM_BEAN_DEFINITION_FILE, false);
265 String path = (beanDefinitionFileFromProperty != null ? beanDefinitionFileFromProperty : beanDefinitionFile);
266 logger.info("loading DataSourceBean '" + beanName + "' from: " + path);
267 FileSystemResource file = new FileSystemResource(path);
268 XmlBeanFactory beanFactory = new XmlBeanFactory(file);
269 DataSource dataSource = beanFactory.getBean(beanName, DataSource.class);
270 if(dataSource instanceof ComboPooledDataSource){
271 logger.info("DataSourceBean '" + beanName + "' is a ComboPooledDataSource [URL:" + ((ComboPooledDataSource)dataSource).getJdbcUrl()+ "]");
272 } else {
273 logger.error("DataSourceBean '" + beanName + "' IS NOT a ComboPooledDataSource");
274 }
275 return dataSource;
276 }
277
278
279 /**
280 * Loads the <code>dataSourceProperties</code> bean from the cdm bean
281 * definition file.
282 * This file is usually {@code ./.cdmLibrary/datasources.xml}
283 *
284 * @return the DataSourceProperties bean or an empty instance if the bean is not found
285 */
286 private DataSourceProperties loadDataSourceProperties() {
287
288 String beanDefinitionFileFromProperty = findProperty(CDM_BEAN_DEFINITION_FILE, false);
289 String path = (beanDefinitionFileFromProperty != null ? beanDefinitionFileFromProperty : beanDefinitionFile);
290 logger.info("loading dataSourceProperties from: " + path);
291 FileSystemResource file = new FileSystemResource(path);
292 XmlBeanFactory beanFactory = new XmlBeanFactory(file);
293 DataSourceProperties properties = null;
294 try {
295 properties = beanFactory.getBean("dataSourceProperties", DataSourceProperties.class);
296 } catch (BeansException e) {
297 logger.warn("bean 'dataSourceProperties' not found");
298 properties = new DataSourceProperties();
299 }
300 return properties;
301 }
302
303 @Bean
304 public Properties hibernateProperties(){
305 Properties props = getHibernateProperties();
306 props.setProperty(HIBERNATE_DIALECT, inferHibernateDialectName());
307 props.setProperty(HIBERNATE_SEARCH_DEFAULT_INDEX_BASE, CdmUtils.getCdmHomeSubDir(CdmUtils.SUBFOLDER_WEBAPP).getPath() + "/index/".replace("/", File.separator) + findProperty(ATTRIBUTE_DATASOURCE_NAME, true));
308 logger.debug("hibernateProperties: " + props.toString());
309 return props;
310 }
311
312 /**
313 * Returns the full class name of the according {@link org.hibernate.dialect.Dialect} implementation
314 *
315 * @param ds the DataSource
316 * @return the name
317 */
318 public String inferHibernateDialectName() {
319 DataSource ds = dataSource();
320 return inferHibernateDialectName(ds);
321 }
322
323
324
325 /**
326 * Returns the full class name of the according {@link org.hibernate.dialect.Dialect} implementation
327 *
328 * @param ds the DataSource
329 * @return the name
330 */
331 public String inferHibernateDialectName(DataSource ds) {
332 String url = "<SEE PRIOR REFLECTION ERROR>";
333 Method m = null;
334 try {
335 m = ds.getClass().getMethod("getUrl");
336 } catch (SecurityException e) {
337 logger.error(e);
338 } catch (NoSuchMethodException e) {
339 try {
340 m = ds.getClass().getMethod("getJdbcUrl");
341 } catch (SecurityException e2) {
342 logger.error(e2);
343 } catch (NoSuchMethodException e2) {
344 logger.error(e2);
345 }
346 }
347 try {
348 url = (String)m.invoke(ds);
349 } catch (IllegalArgumentException e) {
350 logger.error(e);
351 } catch (IllegalAccessException e) {
352 logger.error(e);
353 } catch (InvocationTargetException e) {
354 logger.error(e);
355 } catch (SecurityException e) {
356 logger.error(e);
357 }
358
359 if(url != null){
360 if(url.contains(":mysql:")){
361 // TODO we should switch all databases to InnoDB !
362 // TODO open jdbc connection to check engine and choose between
363 // MySQL5MyISAMUtf8Dialect and MySQL5MyISAMUtf8Dialect
364 // see #3371 (switch cdm to MySQL InnoDB)
365 return MySQL5MyISAMUtf8Dialect.class.getName();
366 }
367 if(url.contains(":h2:")){
368 return H2CorrectedDialect.class.getName();
369 }
370 if(url.contains(":postgresql:")){
371 return PostgreSQL82Dialect.class.getName();
372 }
373 }
374
375 logger.error("hibernate dialect mapping for "+url+ " not yet implemented or unavailable");
376 return null;
377 }
378
379 }