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