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.sql.Connection; 020import java.sql.SQLException; 021import java.util.HashMap; 022import java.util.LinkedHashMap; 023import java.util.Map; 024 025import javax.sql.DataSource; 026 027import org.apache.avalon.framework.activity.Disposable; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.commons.dbcp2.ConnectionFactory; 031import org.apache.commons.dbcp2.DriverManagerConnectionFactory; 032import org.apache.commons.dbcp2.PoolableConnection; 033import org.apache.commons.dbcp2.PoolableConnectionFactory; 034import org.apache.commons.dbcp2.PoolingDataSource; 035import org.apache.commons.pool2.ObjectPool; 036import org.apache.commons.pool2.impl.GenericObjectPool; 037 038import org.ametys.core.datasource.dbtype.SQLDatabaseType; 039import org.ametys.core.datasource.dbtype.SQLDatabaseTypeExtensionPoint; 040import org.ametys.plugins.core.impl.checker.SQLConnectionChecker; 041import org.ametys.runtime.i18n.I18nizableText; 042import org.ametys.runtime.parameter.ParameterCheckerTestFailureException; 043import org.ametys.runtime.servlet.RuntimeConfig; 044import org.ametys.runtime.util.AmetysHomeHelper; 045 046/** 047 * This component handles SQL data sources. 048 * It is associated with the configuration file $AMETYS_HOME/config/datasources-sql.xml 049 */ 050public class SQLDataSourceManager extends AbstractDataSourceManager implements Disposable 051{ 052 /** Avalon Role */ 053 public static final String ROLE = SQLDataSourceManager.class.getName(); 054 055 /** Name of parameter for database type */ 056 public static final String PARAM_DATABASE_TYPE = "dbtype"; 057 /** Name of parameter for database url */ 058 public static final String PARAM_DATABASE_URL = "url"; 059 /** Name of parameter for user's login */ 060 public static final String PARAM_DATABASE_USER = "user"; 061 /** Name of parameter for user's password */ 062 public static final String PARAM_DATABASE_PASSWORD = "password"; 063 064 /** The id of the internal DataSource */ 065 public static final String SQL_DATASOURCE_PREFIX = "SQL-"; 066 067 /** The id of the internal DataSource */ 068 public static final String AMETYS_INTERNAL_DATASOURCE_ID = "SQL-ametys-internal"; 069 private static final I18nizableText __AMETYS_INTERNAL_DATASOURCE_NAME = new I18nizableText("plugin.core", "PLUGINS_CORE_INTERNAL_DATASOURCE_LABEL"); 070 private static final I18nizableText __AMETYS_INTERNAL_DATASOURCE_DESCRIPTION = new I18nizableText("plugin.core", "PLUGINS_CORE_INTERNAL_DATASOURCE_LABEL"); 071 072 073 private static String __filename; 074 075 private Map<String, DataSource> _sqlDataSources; 076 private Map<String, ObjectPool> _pools; 077 078 private DataSourceDefinition _internalDataSource; 079 080 private SQLDatabaseTypeExtensionPoint _sqlDatabaseTypeEP; 081 082 private ServiceManager _manager; 083 084 @Override 085 public void service(ServiceManager serviceManager) throws ServiceException 086 { 087 _manager = serviceManager; 088 super.service(serviceManager); 089 } 090 091 private SQLDatabaseTypeExtensionPoint getSQLDatabaseTypeEP() 092 { 093 if (_sqlDatabaseTypeEP == null) 094 { 095 try 096 { 097 _sqlDatabaseTypeEP = (SQLDatabaseTypeExtensionPoint) _manager.lookup(SQLDatabaseTypeExtensionPoint.ROLE); 098 } 099 catch (ServiceException e) 100 { 101 throw new RuntimeException(e); 102 } 103 } 104 return _sqlDatabaseTypeEP; 105 } 106 107 /** 108 * Set the config filename. Only use for tests. 109 * @param filename Name with path of the config file 110 */ 111 public static void setFilename(String filename) 112 { 113 __filename = filename; 114 } 115 116 /** 117 * Get the configuration file for SQL data sources 118 * @return the configuration file 119 */ 120 public static File getStaticFileConfiguration () 121 { 122 if (__filename != null) 123 { 124 return new File(__filename); 125 } 126 127 return new File(AmetysHomeHelper.getAmetysHomeConfig(), "datasources-sql.xml"); 128 } 129 130 @Override 131 public File getFileConfiguration() 132 { 133 return getStaticFileConfiguration(); 134 } 135 136 137 @Override 138 public void initialize() throws Exception 139 { 140 _sqlDataSources = new HashMap<>(); 141 _pools = new HashMap<>(); 142 143 // Add the internal and not editable DB 144 _internalDataSource = getInternalDataSourceDefinition(); 145 146 super.initialize(); 147 148 createDataSource(_internalDataSource); 149 } 150 151 @Override 152 public DataSourceDefinition getDataSourceDefinition(String id) 153 { 154 readConfiguration(); 155 156 if (AMETYS_INTERNAL_DATASOURCE_ID.equals(id)) 157 { 158 return _internalDataSource; 159 } 160 161 return super.getDataSourceDefinition(id); 162 } 163 164 @Override 165 public Map<String, DataSourceDefinition> getDataSourceDefinitions(boolean includePrivate, boolean includeInternal, boolean includeDefault) 166 { 167 readConfiguration(); 168 169 Map<String, DataSourceDefinition> datasources = new LinkedHashMap<>(); 170 171 if (includePrivate && includeInternal) 172 { 173 // Include the internal db 174 datasources.put(AMETYS_INTERNAL_DATASOURCE_ID, _internalDataSource); 175 } 176 177 datasources.putAll(super.getDataSourceDefinitions(includePrivate, includeInternal, includeDefault)); 178 return datasources; 179 } 180 181 @Override 182 protected String getDataSourcePrefixId() 183 { 184 return SQL_DATASOURCE_PREFIX; 185 } 186 187 /** 188 * Get the datasource definition for internal database 189 * @return The datasource definition 190 */ 191 public static DataSourceDefinition getInternalDataSourceDefinition () 192 { 193 Map<String, String> parameters = new HashMap<>(); 194 parameters.put (PARAM_DATABASE_TYPE, ConnectionHelper.DATABASE_DERBY); 195 196 File dbFile = new File (RuntimeConfig.getInstance().getAmetysHome(), "data" + File.separator + "internal-db"); 197 parameters.put (PARAM_DATABASE_URL, "jdbc:derby:" + dbFile.getAbsolutePath() + ";create=true"); 198 parameters.put (PARAM_DATABASE_USER, ""); 199 parameters.put (PARAM_DATABASE_PASSWORD, ""); 200 201 return new DataSourceDefinition(AMETYS_INTERNAL_DATASOURCE_ID, __AMETYS_INTERNAL_DATASOURCE_NAME, __AMETYS_INTERNAL_DATASOURCE_DESCRIPTION, parameters, true, false); 202 } 203 204 /** 205 * Get a connection object the internal data source 206 * @return the connection object to Ametys' internal SQL data source 207 */ 208 public Connection getInternalSQLDataSourceConnection() 209 { 210 DataSource dataSource; 211 Connection connection = null; 212 try 213 { 214 dataSource = getSQLDataSource(AMETYS_INTERNAL_DATASOURCE_ID); 215 connection = dataSource.getConnection(); 216 } 217 catch (SQLException e) 218 { 219 throw new RuntimeException("Unable to get the connection to the internal ametys SQL data source", e); 220 } 221 222 return connection; 223 } 224 225 /** 226 * Get the existing SQL data sources 227 * @return the SQL data sources 228 */ 229 public Map<String, DataSource> getSQLDataSources () 230 { 231 return _sqlDataSources; 232 } 233 234 /** 235 * Get the SQL data source by its identifier 236 * @param id The id of data source 237 * @return the SQL data source 238 */ 239 public DataSource getSQLDataSource (String id) 240 { 241 DataSource dataSource = _sqlDataSources.get(id); 242 if (getDefaultDataSourceId().equals(id)) 243 { 244 String actualDataSourceId = getDefaultDataSourceDefinition().getId(); 245 dataSource = _sqlDataSources.get(actualDataSourceId); 246 } 247 248 if (dataSource == null) 249 { 250 throw new UnknownDataSourceException("The data source of id '" + id + "' was not found."); 251 } 252 253 return dataSource; 254 } 255 256 257 @Override 258 public DataSourceDefinition setDefaultDataSource(String id) 259 { 260 readConfiguration(); 261 262 // If the default data source was the internal data source, it is no longer the case 263 DataSourceDefinition oldDefaultDataSource = getDefaultDataSourceDefinition(); 264 if (oldDefaultDataSource != null && oldDefaultDataSource.getId().equals(AMETYS_INTERNAL_DATASOURCE_ID)) 265 { 266 _internalDataSource.setDefault(false); 267 } 268 269 try 270 { 271 return super.setDefaultDataSource(id); 272 } 273 catch (RuntimeException e) 274 { 275 if (id.equals(AMETYS_INTERNAL_DATASOURCE_ID)) 276 { 277 _internalDataSource.setDefault(true); 278 return _internalDataSource; 279 } 280 else 281 { 282 throw new RuntimeException(e); 283 } 284 } 285 } 286 287 @Override 288 public DataSourceDefinition getDefaultDataSourceDefinition() 289 { 290 if (_internalDataSource.isDefault()) 291 { 292 return _internalDataSource; 293 } 294 else 295 { 296 return super.getDefaultDataSourceDefinition(); 297 } 298 } 299 300 @Override 301 protected void internalSetDefaultDataSource() 302 { 303 _internalDataSource.setDefault(true); 304 } 305 306 @Override 307 public void checkParameters(Map<String, String> rawParameters) throws ParameterCheckerTestFailureException 308 { 309 SQLConnectionChecker.check(rawParameters.get(PARAM_DATABASE_URL), rawParameters.get(PARAM_DATABASE_USER), rawParameters.get(PARAM_DATABASE_PASSWORD), _manager); 310 } 311 312 @Override 313 protected void editDataSource(DataSourceDefinition dataSource) 314 { 315 deleteDataSource(dataSource); 316 createDataSource(dataSource); 317 } 318 319 @Override 320 protected void createDataSource(DataSourceDefinition dataSourceDef) 321 { 322 Map<String, String> parameters = dataSourceDef.getParameters(); 323 324 String url = parameters.get(PARAM_DATABASE_URL); 325 String user = parameters.get(PARAM_DATABASE_USER); 326 String password = parameters.get(PARAM_DATABASE_PASSWORD); 327 328 String dbtype = parameters.get(PARAM_DATABASE_TYPE); 329 if (!getSQLDatabaseTypeEP().hasExtension(dbtype)) 330 { 331 throw new IllegalArgumentException("Database of type '" + dbtype + "' is not supported"); 332 } 333 SQLDatabaseType sqlDbType = getSQLDatabaseTypeEP().getExtension(dbtype); 334 335 ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(url, user, password); 336 PoolableConnectionFactory poolableConnectionFactory = new PoolableConnectionFactory(connectionFactory, null); 337 338 GenericObjectPool<PoolableConnection> connectionPool = new GenericObjectPool<>(poolableConnectionFactory); 339 connectionPool.setMaxTotal(-1); 340 connectionPool.setMaxIdle(10); 341 connectionPool.setMinIdle(2); 342 connectionPool.setTestOnBorrow(true); 343 connectionPool.setTestOnReturn(false); 344 connectionPool.setTestWhileIdle(true); 345 connectionPool.setTimeBetweenEvictionRunsMillis(1000 * 60 * 30); 346 347 poolableConnectionFactory.setPool(connectionPool); 348 poolableConnectionFactory.setValidationQuery(sqlDbType.getValidationQuery()); 349 poolableConnectionFactory.setDefaultAutoCommit(true); 350 poolableConnectionFactory.setDefaultReadOnly(false); 351 352 PoolingDataSource<PoolableConnection> dataSource = new PoolingDataSource<>(connectionPool); 353 354 String id = dataSourceDef.getId(); 355 356 // make sure the previous connection has been disposed 357 if (_pools.containsKey(id)) 358 { 359 _disposePool(id); 360 } 361 362 // Store the connection pools and the data sources 363 _pools.put(id, connectionPool); 364 _sqlDataSources.put(id, dataSource); 365 } 366 367 @Override 368 protected void deleteDataSource(DataSourceDefinition dataSource) 369 { 370 _sqlDataSources.remove(dataSource.getId()); 371 _disposePool(dataSource.getId()); 372 } 373 374 @Override 375 public void dispose() 376 { 377 for (String id : _pools.keySet()) 378 { 379 // Not calling _disposePool(id); to avoid 380 // ConcurrentModificationException RUNTIME-1633 381 _pools.get(id).close(); 382 } 383 384 _pools = null; 385 } 386 387 /** 388 * Dispose of a connection pool 389 * @param id the id of the connection pool to dispose of 390 */ 391 private void _disposePool(String id) 392 { 393 try 394 { 395 _pools.get(id).close(); 396 } 397 catch (Exception e) 398 { 399 getLogger().warn("Unable to close the edited connection pool", e); 400 } 401 402 _pools.remove(id); 403 } 404}