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