001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.datasource; 017 018import java.io.File; 019import java.io.IOException; 020import java.sql.Connection; 021import java.sql.SQLException; 022import java.util.HashMap; 023import java.util.LinkedHashMap; 024import java.util.Map; 025 026import javax.sql.DataSource; 027 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.dbcp2.ConnectionFactory; 034import org.apache.commons.dbcp2.DriverManagerConnectionFactory; 035import org.apache.commons.dbcp2.PoolableConnection; 036import org.apache.commons.dbcp2.PoolableConnectionFactory; 037import org.apache.commons.dbcp2.PoolingDataSource; 038import org.apache.commons.pool2.ObjectPool; 039import org.apache.commons.pool2.impl.GenericObjectPool; 040import org.xml.sax.SAXException; 041 042import org.ametys.core.datasource.dbtype.SQLDatabaseType; 043import org.ametys.core.datasource.dbtype.SQLDatabaseTypeExtensionPoint; 044import org.ametys.plugins.core.impl.checker.SQLConnectionChecker; 045import org.ametys.plugins.core.impl.datasource.DerbySQLDatabaseType; 046import org.ametys.runtime.i18n.I18nizableText; 047import org.ametys.runtime.model.checker.ItemCheckerTestFailureException; 048import org.ametys.runtime.servlet.RuntimeConfig; 049import org.ametys.runtime.util.AmetysHomeHelper; 050 051/** 052 * This component handles SQL data sources. 053 * It is associated with the configuration file $AMETYS_HOME/config/datasources-sql.xml 054 */ 055public class SQLDataSourceManager extends AbstractDataSourceManager 056{ 057 /** Avalon Role */ 058 public static final String ROLE = SQLDataSourceManager.class.getName(); 059 060 /** Name of parameter for database type */ 061 public static final String PARAM_DATABASE_TYPE = "dbtype"; 062 /** Name of parameter for database url */ 063 public static final String PARAM_DATABASE_URL = "url"; 064 /** Name of parameter for user's login */ 065 public static final String PARAM_DATABASE_USER = "user"; 066 /** Name of parameter for user's password */ 067 public static final String PARAM_DATABASE_PASSWORD = "password"; 068 069 /** The id of the internal DataSource */ 070 public static final String SQL_DATASOURCE_PREFIX = "SQL-"; 071 072 /** The id of the internal DataSource */ 073 public static final String AMETYS_INTERNAL_DATASOURCE_ID = "SQL-ametys-internal"; 074 private static final I18nizableText __AMETYS_INTERNAL_DATASOURCE_NAME = new I18nizableText("plugin.core", "PLUGINS_CORE_INTERNAL_DATASOURCE_LABEL"); 075 private static final I18nizableText __AMETYS_INTERNAL_DATASOURCE_DESCRIPTION = new I18nizableText("plugin.core", "PLUGINS_CORE_INTERNAL_DATASOURCE_DESCRIPTION"); 076 077 private static String __filename; 078 079 private Map<String, DataSource> _sqlDataSources; 080 private Map<String, ObjectPool> _pools; 081 082 private DataSourceDefinition _internalDataSource; 083 084 private SQLDatabaseTypeExtensionPoint _sqlDatabaseTypeEP; 085 086 private ServiceManager _manager; 087 088 @Override 089 public void service(ServiceManager serviceManager) throws ServiceException 090 { 091 _manager = serviceManager; 092 super.service(serviceManager); 093 } 094 095 /** 096 * Read the read source definitions 097 * @param file The configuration file 098 * @return the data source definitions 099 */ 100 public static Map<String, DataSourceDefinition> readDataSourceDefinition (File file) 101 { 102 Map<String, DataSourceDefinition> definitions = new HashMap<>(); 103 104 try 105 { 106 if (file.exists()) 107 { 108 Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(file); 109 for (Configuration dsConfig : configuration.getChildren("datasource")) 110 { 111 String id = dsConfig.getAttribute("id"); 112 113 I18nizableText name = I18nizableText.parseI18nizableText(dsConfig.getChild("name"), "plugin.core"); 114 I18nizableText description = I18nizableText.parseI18nizableText(dsConfig.getChild("description"), "plugin.core", ""); 115 116 boolean isPrivate = dsConfig.getAttributeAsBoolean("private", false); 117 boolean isDefault = dsConfig.getAttributeAsBoolean("default", false); 118 119 Map<String, Object> parameters = new HashMap<>(); 120 121 Configuration[] paramsConfig = dsConfig.getChild("parameters").getChildren(); 122 for (Configuration paramConfig : paramsConfig) 123 { 124 String value = paramConfig.getValue(""); 125 parameters.put(paramConfig.getName(), value); 126 } 127 128 DataSourceDefinition dataSource = new DataSourceDefinition(id, name, description, parameters, isPrivate, isDefault); 129 definitions.put(id, dataSource); 130 } 131 } 132 133 return definitions; 134 } 135 catch (IOException | ConfigurationException | SAXException e) 136 { 137 throw new RuntimeException("Unable to parse datasource configuration file.", e); 138 } 139 } 140 141 @Override 142 protected Map<String, DataSourceDefinition> doReadConfiguration(File file) 143 { 144 return readDataSourceDefinition(file); 145 } 146 147 private SQLDatabaseTypeExtensionPoint getSQLDatabaseTypeEP() 148 { 149 if (_sqlDatabaseTypeEP == null) 150 { 151 try 152 { 153 _sqlDatabaseTypeEP = (SQLDatabaseTypeExtensionPoint) _manager.lookup(SQLDatabaseTypeExtensionPoint.ROLE); 154 } 155 catch (ServiceException e) 156 { 157 throw new RuntimeException(e); 158 } 159 } 160 return _sqlDatabaseTypeEP; 161 } 162 163 /** 164 * Set the config filename. Only use for tests. 165 * @param filename Name with path of the config file 166 */ 167 public static void setFilename(String filename) 168 { 169 __filename = filename; 170 } 171 172 /** 173 * Get the configuration file for SQL data sources 174 * @return the configuration file 175 */ 176 public static File getStaticFileConfiguration () 177 { 178 if (__filename != null) 179 { 180 return new File(__filename); 181 } 182 183 return new File(AmetysHomeHelper.getAmetysHomeConfig(), "datasources-sql.xml"); 184 } 185 186 @Override 187 public File getFileConfiguration() 188 { 189 return getStaticFileConfiguration(); 190 } 191 192 /** 193 * Initializes connections. 194 * @param preserveInternalDB also initialize internal db connection, or not. 195 * @throws Exception if an error occurs 196 */ 197 198 public void initialize(boolean preserveInternalDB) throws Exception 199 { 200 if (preserveInternalDB && _internalDataSource == null) 201 { 202 throw new IllegalStateException("Internal dataSource should not be null to be preserved."); 203 } 204 205 String internalId = null; 206 DataSource internalDataSource = null; 207 ObjectPool internalPool = null; 208 209 if (preserveInternalDB) 210 { 211 // keep internal DB pool 212 internalId = _internalDataSource.getId(); 213 internalDataSource = _sqlDataSources.get(internalId); 214 internalPool = _pools.get(internalId); 215 } 216 217 _sqlDataSources = new HashMap<>(); 218 _pools = new HashMap<>(); 219 220 if (preserveInternalDB) 221 { 222 _sqlDataSources.put(internalId, internalDataSource); 223 _pools.put(internalId, internalPool); 224 } 225 else 226 { 227 _internalDataSource = getInternalDataSourceDefinition(); 228 } 229 230 super.initialize(); 231 232 if (!preserveInternalDB) 233 { 234 // Add the internal and not editable DB 235 createDataSource(_internalDataSource); 236 } 237 } 238 239 @Override 240 public void initialize() throws Exception 241 { 242 initialize(false); 243 } 244 245 @Override 246 public DataSourceDefinition getDataSourceDefinition(String id) 247 { 248 readConfiguration(); 249 250 if (AMETYS_INTERNAL_DATASOURCE_ID.equals(id)) 251 { 252 return _internalDataSource; 253 } 254 255 return super.getDataSourceDefinition(id); 256 } 257 258 @Override 259 public Map<String, DataSourceDefinition> getDataSourceDefinitions(boolean includePrivate, boolean includeInternal, boolean includeDefault) 260 { 261 readConfiguration(); 262 263 Map<String, DataSourceDefinition> datasources = new LinkedHashMap<>(); 264 265 if (includePrivate && includeInternal) 266 { 267 // Include the internal db 268 datasources.put(AMETYS_INTERNAL_DATASOURCE_ID, _internalDataSource); 269 } 270 271 datasources.putAll(super.getDataSourceDefinitions(includePrivate, includeInternal, includeDefault)); 272 return datasources; 273 } 274 275 @Override 276 protected String getDataSourcePrefixId() 277 { 278 return SQL_DATASOURCE_PREFIX; 279 } 280 281 /** 282 * Get the datasource definition for internal database 283 * @return The datasource definition 284 */ 285 public static DataSourceDefinition getInternalDataSourceDefinition () 286 { 287 Map<String, Object> parameters = new HashMap<>(); 288 parameters.put (PARAM_DATABASE_TYPE, ConnectionHelper.DATABASE_DERBY); 289 290 File dbFile = new File (RuntimeConfig.getInstance().getAmetysHome(), "data" + File.separator + "internal-db"); 291 parameters.put (PARAM_DATABASE_URL, "jdbc:derby:" + dbFile.getAbsolutePath() + ";create=true"); 292 parameters.put (PARAM_DATABASE_USER, ""); 293 parameters.put (PARAM_DATABASE_PASSWORD, ""); 294 295 return new DataSourceDefinition(AMETYS_INTERNAL_DATASOURCE_ID, __AMETYS_INTERNAL_DATASOURCE_NAME, __AMETYS_INTERNAL_DATASOURCE_DESCRIPTION, parameters, true, false); 296 } 297 298 /** 299 * Get a connection object the internal data source 300 * @return the connection object to Ametys' internal SQL data source 301 */ 302 public Connection getInternalSQLDataSourceConnection() 303 { 304 DataSource dataSource; 305 Connection connection = null; 306 try 307 { 308 dataSource = getSQLDataSource(AMETYS_INTERNAL_DATASOURCE_ID); 309 connection = dataSource.getConnection(); 310 } 311 catch (SQLException e) 312 { 313 throw new RuntimeException("Unable to get the connection to the internal ametys SQL data source", e); 314 } 315 316 return connection; 317 } 318 319 /** 320 * Get the existing SQL data sources 321 * @return the SQL data sources 322 */ 323 public Map<String, DataSource> getSQLDataSources () 324 { 325 return _sqlDataSources; 326 } 327 328 /** 329 * Get the SQL data source by its identifier 330 * @param id The id of data source 331 * @return the SQL data source 332 */ 333 public DataSource getSQLDataSource (String id) 334 { 335 DataSource dataSource = _sqlDataSources.get(id); 336 if (getDefaultDataSourceId().equals(id)) 337 { 338 String actualDataSourceId = getDefaultDataSourceDefinition().getId(); 339 dataSource = _sqlDataSources.get(actualDataSourceId); 340 } 341 342 if (dataSource == null) 343 { 344 throw new UnknownDataSourceException("The data source of id '" + id + "' was not found."); 345 } 346 347 return dataSource; 348 } 349 350 351 @Override 352 public DataSourceDefinition setDefaultDataSource(String id) 353 { 354 readConfiguration(); 355 356 // If the default data source was the internal data source, it is no longer the case 357 DataSourceDefinition oldDefaultDataSource = getDefaultDataSourceDefinition(); 358 if (oldDefaultDataSource != null && oldDefaultDataSource.getId().equals(AMETYS_INTERNAL_DATASOURCE_ID)) 359 { 360 _internalDataSource.setDefault(false); 361 } 362 363 try 364 { 365 return super.setDefaultDataSource(id); 366 } 367 catch (RuntimeException e) 368 { 369 if (id.equals(AMETYS_INTERNAL_DATASOURCE_ID)) 370 { 371 _internalDataSource.setDefault(true); 372 return _internalDataSource; 373 } 374 else 375 { 376 throw new RuntimeException(e); 377 } 378 } 379 } 380 381 @Override 382 public DataSourceDefinition getDefaultDataSourceDefinition() 383 { 384 if (_internalDataSource.isDefault()) 385 { 386 return _internalDataSource; 387 } 388 else 389 { 390 return super.getDefaultDataSourceDefinition(); 391 } 392 } 393 394 @Override 395 protected void internalSetDefaultDataSource() 396 { 397 _internalDataSource.setDefault(true); 398 } 399 400 @Override 401 public void checkParameters(Map<String, Object> parameters) throws ItemCheckerTestFailureException 402 { 403 SQLConnectionChecker.check((String) parameters.get(PARAM_DATABASE_URL), (String) parameters.get(PARAM_DATABASE_USER), (String) parameters.get(PARAM_DATABASE_PASSWORD), _manager); 404 } 405 406 @Override 407 protected void editDataSource(DataSourceDefinition dataSource) 408 { 409 deleteDataSource(dataSource); 410 createDataSource(dataSource); 411 } 412 413 @Override 414 protected void createDataSource(DataSourceDefinition dataSourceDef) 415 { 416 Map<String, Object> parameters = dataSourceDef.getParameters(); 417 418 String url = (String) parameters.get(PARAM_DATABASE_URL); 419 String user = (String) parameters.get(PARAM_DATABASE_USER); 420 String password = (String) parameters.get(PARAM_DATABASE_PASSWORD); 421 422 String dbtype = (String) parameters.get(PARAM_DATABASE_TYPE); 423 if (!getSQLDatabaseTypeEP().hasExtension(dbtype)) 424 { 425 throw new IllegalArgumentException("Database of type '" + dbtype + "' is not supported"); 426 } 427 SQLDatabaseType sqlDbType = getSQLDatabaseTypeEP().getExtension(dbtype); 428 429 ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(url, user, password); 430 PoolableConnectionFactory poolableConnectionFactory = new PoolableConnectionFactory(connectionFactory, null); 431 432 GenericObjectPool<PoolableConnection> connectionPool = new GenericObjectPool<>(poolableConnectionFactory); 433 connectionPool.setMaxTotal(-1); 434 connectionPool.setMaxIdle(10); 435 connectionPool.setMinIdle(2); 436 connectionPool.setTestOnBorrow(true); 437 connectionPool.setTestOnReturn(false); 438 connectionPool.setTestWhileIdle(true); 439 connectionPool.setTimeBetweenEvictionRunsMillis(1000 * 60 * 30); 440 441 poolableConnectionFactory.setPool(connectionPool); 442 poolableConnectionFactory.setValidationQuery(sqlDbType.getValidationQuery()); 443 poolableConnectionFactory.setDefaultAutoCommit(true); 444 poolableConnectionFactory.setDefaultReadOnly(false); 445 446 PoolingDataSource<PoolableConnection> dataSource = new PoolingDataSource<>(connectionPool); 447 448 String id = dataSourceDef.getId(); 449 450 // make sure the previous connection has been disposed 451 if (_pools.containsKey(id)) 452 { 453 _disposePool(id); 454 } 455 456 // Store the connection pools and the data sources 457 _pools.put(id, connectionPool); 458 _sqlDataSources.put(id, dataSource); 459 } 460 461 @Override 462 protected void deleteDataSource(DataSourceDefinition dataSource) 463 { 464 _sqlDataSources.remove(dataSource.getId()); 465 _disposePool(dataSource.getId()); 466 } 467 468 /** 469 * Disposes connections. 470 * @param preserveInternalDB also remove internal db connection, or not. 471 */ 472 public void dispose(boolean preserveInternalDB) 473 { 474 if (preserveInternalDB && _internalDataSource == null) 475 { 476 throw new IllegalStateException("Internal dataSource should not be null to be preserved."); 477 } 478 479 String internalId = null; 480 ObjectPool internalPool = null; 481 482 if (preserveInternalDB) 483 { 484 // keep internal DB pool 485 internalId = _internalDataSource.getId(); 486 internalPool = _pools.get(internalId); 487 } 488 489 for (String id : _pools.keySet()) 490 { 491 if (!id.equals(_internalDataSource.getId()) || !preserveInternalDB) 492 { 493 // Not calling _disposePool(id); to avoid 494 // ConcurrentModificationException RUNTIME-1633 495 _closePool(id); 496 } 497 } 498 499 _pools.clear(); 500 _pools.put(internalId, internalPool); 501 } 502 503 @Override 504 public void dispose() 505 { 506 dispose(false); 507 } 508 509 /** 510 * Dispose of a connection pool 511 * @param id the id of the connection pool to dispose of 512 */ 513 private void _disposePool(String id) 514 { 515 _closePool(id); 516 _pools.remove(id); 517 } 518 519 private void _closePool(String id) 520 { 521 try 522 { 523 _pools.get(id).close(); 524 } 525 catch (Exception e) 526 { 527 getLogger().warn("Unable to close the connection pool '{}'", id, e); 528 } 529 530 try 531 { 532 DataSourceDefinition definition = getDataSourceDefinition(id); 533 Map<String, Object> parameters = definition.getParameters(); 534 String dbtype = (String) parameters.get(PARAM_DATABASE_TYPE); 535 536 if (ConnectionHelper.DATABASE_DERBY.equals(dbtype)) 537 { 538 DerbySQLDatabaseType.shutDown(definition); 539 } 540 } 541 catch (Exception e) 542 { 543 getLogger().warn("Unable to shutdown a Derby database '{}'", id, e); 544 } 545 } 546}