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