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}