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}