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.IOException;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.configuration.ConfigurationException;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.cocoon.ProcessingException;
030import org.apache.commons.lang3.StringUtils;
031import org.xml.sax.SAXException;
032
033import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition;
034import org.ametys.core.datasource.DataSourceConsumer.TypeOfUse;
035import org.ametys.core.ui.Callable;
036import org.ametys.core.util.I18nUtils;
037import org.ametys.runtime.i18n.I18nizableText;
038import org.ametys.runtime.model.checker.ItemCheckerTestFailureException;
039import org.ametys.runtime.plugin.component.AbstractLogEnabled;
040
041/**
042 * Component gathering manipulation methods for SQL and LDAP data sources 
043 */
044public class DataSourceClientInteraction extends AbstractLogEnabled implements Component, Serviceable
045{
046    /** The Avalon role */
047    public static final String ROLE = DataSourceClientInteraction.class.getName();
048    
049    /** The SQL data source manager */
050    private SQLDataSourceManager _sqlDataSourceManager;
051    
052    /** The LDAP data source manager */
053    private LDAPDataSourceManager _ldapDataSourceManager;
054    
055    /** The extension for data source clients */
056    private DataSourceConsumerExtensionPoint _dataSourceConsumerEP;
057    
058    /** Component gathering utility method allowing to handle internationalizable text */
059    private I18nUtils _i18nUtils;
060    
061    /**
062     * Enum for data source types
063     */
064    public enum DataSourceType 
065    {
066        /** SQL */
067        SQL,
068        /** LDAP */
069        LDAP
070    }
071    
072    @Override
073    public void service(ServiceManager serviceManager) throws ServiceException
074    {
075        _sqlDataSourceManager = (SQLDataSourceManager) serviceManager.lookup(SQLDataSourceManager.ROLE);
076        _ldapDataSourceManager = (LDAPDataSourceManager) serviceManager.lookup(LDAPDataSourceManager.ROLE);
077        _dataSourceConsumerEP = (DataSourceConsumerExtensionPoint) serviceManager.lookup(DataSourceConsumerExtensionPoint.ROLE);
078        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
079    }
080    
081    /**
082     * Get the existing data sources
083     * @param dataSourceType the data source type. Can be empty or null to get all data sources
084     * @param includePrivate true to include private data sources
085     * @param includeInternal true to include internal data sources
086     * @param includeDefault true to include the default data sources
087     * @param allowedTypes The sub-types of datasource allowed. Can be null. For now, this parameter is only used for sql data sources. The types are the type of databases (mysql, oarcle, ...)
088     * @return the existing data sources
089     * @throws Exception if an error occurred
090     */
091    public List<Map<String, Object>> getDataSources (DataSourceType dataSourceType, boolean includePrivate, boolean includeInternal, boolean includeDefault, List<String> allowedTypes) throws Exception
092    {
093        List<Map<String, Object>> datasources = new ArrayList<>();
094        
095        switch (dataSourceType)
096        {
097            case SQL:
098                Map<String, DataSourceDefinition> sqlDataSources = _sqlDataSourceManager.getDataSourceDefinitions(includePrivate, includeInternal, includeDefault);
099                for (String id : sqlDataSources.keySet())
100                {
101                    if (allowedTypes == null || allowedTypes.contains(sqlDataSources.get(id).getParameters().get(SQLDataSourceManager.PARAM_DATABASE_TYPE)))
102                    {
103                        datasources.add(getSQLDataSource(id));
104                    }
105                }
106                break;
107
108            case LDAP:
109                Map<String, DataSourceDefinition> ldapDataSources = _ldapDataSourceManager.getDataSourceDefinitions(includePrivate, includeInternal, includeDefault);
110                for (String id : ldapDataSources.keySet())
111                {
112                    datasources.add(getLDAPDataSource(id));
113                }
114                break;
115                
116            default:
117                break;
118        }
119        
120        return datasources;
121    }
122    
123    /**
124     * Get the existing data sources
125     * @param dataSourceType the data source type. Can be empty or null to get all data sources
126     * @param includePrivate true to include private data sources
127     * @param includeInternal true to include internal data sources
128     * @param includeDefault true to include the default data sources
129     * @param allowedTypes The sub-types of datasource allowed. Can be null. For now, this parameter is only used for sql data sources. The types are the type of databases (mysql, oarcle, ...)
130     * @return the existing data sources
131     * @throws Exception if an error occurred
132     */
133    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
134    public List<Map<String, Object>> getDataSources (String dataSourceType, boolean includePrivate, boolean includeInternal, boolean includeDefault, List<String> allowedTypes) throws Exception
135    {
136        List<Map<String, Object>> datasources = new ArrayList<>();
137        
138        if (StringUtils.isEmpty(dataSourceType) || dataSourceType.equals(DataSourceType.SQL.toString()))
139        {
140            datasources.addAll(getDataSources(DataSourceType.SQL, includePrivate, includeInternal, includeDefault, allowedTypes));
141        }
142        
143        if (StringUtils.isEmpty(dataSourceType) || dataSourceType.equals(DataSourceType.LDAP.toString()))
144        {
145            datasources.addAll(getDataSources(DataSourceType.LDAP, includePrivate, includeInternal, includeDefault, allowedTypes));
146        }
147        
148        return datasources;
149    }
150    
151    /**
152     * Get the existing data sources regardless of their type
153     * @param includePrivate true to include private data sources
154     * @param includeInternal true to include internal data sources
155     * @param includeDefault true to include default data sources
156     * @return the existing data sources
157     * @throws Exception if an error occurred
158     */
159    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
160    public List<Map<String, Object>> getDataSources (boolean includePrivate, boolean includeInternal, boolean includeDefault) throws Exception
161    {
162        return getDataSources((String) null, includePrivate, includeInternal, includeDefault, null);
163    }
164    
165    /**
166     * Get the ldap data source information
167     * @param type the data source's type
168     * @param id The id the data source
169     * @return The data source's information
170     * @throws Exception if an error occurred
171     */
172    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
173    public Map<String, Object> getDataSource (String type, String id) throws Exception
174    {
175        DataSourceType dsType = DataSourceType.valueOf(type);
176        switch (dsType)
177        {
178            case SQL:
179                return getSQLDataSource(id);
180
181            case LDAP:
182                return getLDAPDataSource(id);
183            default:
184                getLogger().error("Unable to get data source: unknown data source type '" + type + "'.");
185                return null;
186        }
187    }
188    
189    
190    /**
191     * Get the ldap data source information
192     * @param id The id the data source
193     * @return The data source's information
194     * @throws Exception if an error occurred
195     */
196    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
197    public Map<String, Object> getLDAPDataSource (String id) throws Exception
198    {
199        DataSourceDefinition ldapDefinition = _ldapDataSourceManager.getDataSourceDefinition(id);
200        Map<String, Object> def2json = _dataSourceDefinition2Json(DataSourceType.LDAP.toString(), ldapDefinition);
201        if (ldapDefinition == null)
202        {
203            getLogger().error("Unable to find the data source definition for the id '" + id + "'.");
204        }
205        else
206        {
207            def2json.put("id", id); // Keep the 'LDAP-default-datasource' id
208            def2json.put("type", "LDAP");
209            
210            Map<String, Object> parameters = new HashMap<>();
211            parameters.putAll(ldapDefinition.getParameters());
212            // We do not want password to travel in clear
213            if (StringUtils.isNotBlank((String) parameters.get("adminPassword")))
214            {
215                parameters.put("adminPassword", "PASSWORD");
216            }
217            def2json.putAll(parameters);
218            
219            // The configuration data source consumer refers to the stored values of the configuration
220            // For the default data source, it is "LDAP-default-datasource"
221            boolean isInUse = TypeOfUse.merge(_dataSourceConsumerEP.isInUse(ldapDefinition.getId()), ldapDefinition.isDefault() ? _dataSourceConsumerEP.isInUse(_ldapDataSourceManager.getDefaultDataSourceId()) : TypeOfUse.NOT_USED) != TypeOfUse.NOT_USED;
222            def2json.put("isInUse", isInUse);
223            
224            if ((_ldapDataSourceManager.getDataSourcePrefixId() + AbstractDataSourceManager.DEFAULT_DATASOURCE_SUFFIX).equals(id))
225            {
226                _setDefaultDataSourceName(def2json);
227            }
228        }
229        
230        return def2json;
231    }
232    
233    /**
234     * Get the sql data source information
235     * @param id The id the data source
236     * @return The data source's information
237     * @throws Exception if an error occurred
238     */
239    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
240    public Map<String, Object> getSQLDataSource (String id) throws Exception
241    {
242        DataSourceDefinition sqlDefinition = _sqlDataSourceManager.getDataSourceDefinition(id);
243        
244        Map<String, Object> def2json = _dataSourceDefinition2Json(DataSourceType.SQL.toString(), sqlDefinition);
245        if (sqlDefinition == null)
246        {
247            getLogger().error("Unable to find the data source definition for the id '" + id + "'.");
248        }
249        else
250        {
251            def2json.put("id", id); // Keep the 'SQL-default-datasource' id
252            def2json.put("type", "SQL");
253            
254            Map<String, Object> parameters = new HashMap<>();
255            parameters.putAll(sqlDefinition.getParameters());
256            // We do not want password to travel in clear
257            if (StringUtils.isNotBlank((String) parameters.get("password")))
258            {
259                parameters.put("password", "PASSWORD");
260            }
261            def2json.putAll(parameters);
262            
263            // The configuration data source consumer refers to the stored values of the configuration
264            // For the default data source, it is "SQL-default-datasource"
265            boolean isInUse = TypeOfUse.merge(_dataSourceConsumerEP.isInUse(sqlDefinition.getId()), sqlDefinition.isDefault() ? _dataSourceConsumerEP.isInUse(_sqlDataSourceManager.getDefaultDataSourceId()) : TypeOfUse.NOT_USED) != TypeOfUse.NOT_USED;
266            def2json.put("isInUse", isInUse);
267            
268            if (_sqlDataSourceManager.getDefaultDataSourceId().equals(id))
269            {
270                _setDefaultDataSourceName(def2json);
271            }
272        }
273        
274        return def2json;
275    }
276    
277    /**
278     * Add a data source 
279     * @param type The type of data source
280     * @param parameters The parameters of the data source to create
281     * @return the created data source as JSON object
282     * @throws IOException if an error occurred 
283     * @throws SAXException if an error occurred 
284     * @throws ConfigurationException if an error occurred  
285     * @throws ProcessingException if an error occurred  
286     */
287    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
288    public Map<String, Object> addDataSource (String type, Map<String, Object> parameters) throws ProcessingException, ConfigurationException, SAXException, IOException
289    {
290        String name = (String) parameters.get("name");
291        String description = (String) parameters.get("description");
292        boolean isPrivate = (boolean) parameters.get("private");
293        
294        parameters.remove("id");
295        parameters.remove("name");
296        parameters.remove("description");
297        parameters.remove("private");
298        parameters.remove("type");
299        
300        DataSourceDefinition def = null;
301        if (type.equals(DataSourceType.SQL.toString()))
302        {
303            def = _sqlDataSourceManager.add(new I18nizableText(name), new I18nizableText(description), parameters, isPrivate);
304        }
305        else if (type.equals(DataSourceType.LDAP.toString()))
306        {
307            def = _ldapDataSourceManager.add(new I18nizableText(name), new I18nizableText(description), parameters, isPrivate);
308        }
309        else
310        {
311            throw new IllegalArgumentException("Unable to add data source: unknown data source type '" + type + "'.");
312        }
313        
314        return _dataSourceDefinition2Json(type, def);
315    }
316
317    /**
318     * Edit a data source 
319     * @param type The type of data source
320     * @param parameters The parameters of the data source to edit
321     * @return the edited data source as JSON object
322     * @throws IOException if an error occurred  
323     * @throws SAXException if an error occurred  
324     * @throws ConfigurationException if an error occurred  
325     * @throws ProcessingException if an error occurred  
326     */
327    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
328    public Map<String, Object> editDataSource (String type, Map<String, Object> parameters) throws ProcessingException, ConfigurationException, SAXException, IOException
329    {
330        String id = (String) parameters.get("id");
331        String name = (String) parameters.get("name");
332        String description = (String) parameters.get("description");
333        boolean isPrivate = (boolean) parameters.get("private");
334        
335        parameters.remove("id");
336        parameters.remove("name");
337        parameters.remove("description");
338        parameters.remove("private");
339        parameters.remove("type");
340        
341        DataSourceDefinition def = null;
342        if (type.equals(DataSourceType.SQL.toString()))
343        {
344            DataSourceDefinition previousdataSourceDefinition = _sqlDataSourceManager.getDataSourceDefinition(id); 
345            if (previousdataSourceDefinition != null)
346            {
347                // Inject the recorded password before overriding the existing data source (saved passwords are not sent)
348                String previousPassword = (String) previousdataSourceDefinition.getParameters().get("password");
349                if (parameters.get("password") == null && StringUtils.isNotEmpty(previousPassword))
350                {
351                    parameters.put("password", previousPassword);
352                }
353            }
354            else
355            {
356                getLogger().error("The data source of id '" + id + "' was not found. Unable to get the previous password.");
357            }
358
359            def = _sqlDataSourceManager.edit(id, new I18nizableText(name), new I18nizableText(description), parameters, isPrivate);
360        }
361        else if (type.equals(DataSourceType.LDAP.toString()))
362        {
363            DataSourceDefinition previousdataSourceDefinition = _ldapDataSourceManager.getDataSourceDefinition(id);
364            if (previousdataSourceDefinition != null)
365            {
366                // Inject the recorded password before overriding the existing data source (saved passwords are not sent)
367                String previousPassword = (String) previousdataSourceDefinition.getParameters().get(LDAPDataSourceManager.PARAM_ADMIN_PASSWORD);
368                if (parameters.get(LDAPDataSourceManager.PARAM_ADMIN_PASSWORD) == null && StringUtils.isNotEmpty(previousPassword))
369                {
370                    parameters.put(LDAPDataSourceManager.PARAM_ADMIN_PASSWORD, previousPassword);
371                }
372            }
373            else
374            {
375                getLogger().error("The data source of id '" + id + "' was not found. Unable to get the previous password.");
376            }
377            
378            def = _ldapDataSourceManager.edit(id, new I18nizableText(name), new I18nizableText(description), parameters, isPrivate);
379        }
380        else
381        {
382            throw new IllegalArgumentException("Unable to edit data source: unknown data source type '" + type + "'.");
383        }
384        
385        return _dataSourceDefinition2Json(type, def);
386    }
387    
388    /**
389     * Remove one or several data sources 
390     * @param type The type of data source
391     * @param ids the ids of the data sources to remove
392     * @throws IOException if an error occurred while reading configuration file
393     * @throws SAXException if an error occurred while parsing configuration file
394     * @throws ConfigurationException if an error occurred while parsing configuration reading file
395     * @throws ProcessingException if an error occurred while saving changes
396     */
397    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
398    public void removeDataSource (String type, List<String> ids) throws ConfigurationException, SAXException, IOException, ProcessingException
399    {
400        if (type.equals(DataSourceType.SQL.toString()))
401        {
402            _sqlDataSourceManager.delete(ids, false);
403        }
404        else if (type.equals(DataSourceType.LDAP.toString()))
405        {
406            _ldapDataSourceManager.delete(ids, false);
407        }
408        else
409        {
410            throw new IllegalArgumentException("Unable to delete data sources: unknown data source type '" + type + "'.");
411        }
412    }
413    
414    /**
415     * Set the data source of the given id as the default data source for the given type
416     * @param type the type of the data source
417     * @param id the id of the data source
418     * @return the {@link DataSourceDefinition} of data source set as default in JSON
419     */
420    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
421    public Map<String, Object> setDefaultDataSource(String type, String id)
422    {
423        DataSourceDefinition def = null;
424        if (type.equals(DataSourceType.SQL.toString()))
425        {
426            def = _sqlDataSourceManager.setDefaultDataSource(id);
427        }
428        else if (type.equals(DataSourceType.LDAP.toString()))
429        {
430            def = _ldapDataSourceManager.setDefaultDataSource(id);
431        }
432        else
433        {
434            throw new IllegalArgumentException("Unable set to default the data source: unknown data source type '" + type + "'.");
435        }
436        
437        return _dataSourceDefinition2Json(type, def);
438    }
439    
440    private void _setDefaultDataSourceName(Map<String, Object> dataSourceAsJSON)
441    {
442        String defaultDataSourceName = _i18nUtils.translate(new I18nizableText("plugin.core", "PLUGINS_CORE_DEFAULT_DATASOURCE_NAME_PREFIX"));
443        defaultDataSourceName += _i18nUtils.translate((I18nizableText) dataSourceAsJSON.get("name"));
444        defaultDataSourceName += _i18nUtils.translate(new I18nizableText("plugin.core", "PLUGINS_CORE_DEFAULT_DATASOURCE_NAME_SUFFIX"));
445        
446        dataSourceAsJSON.put("name", defaultDataSourceName);
447    }
448    
449    private Map<String, Object> _dataSourceDefinition2Json (String type, DataSourceDefinition dataSourceDef)
450    {
451        Map<String, Object> infos = new HashMap<>();
452        if (dataSourceDef != null)
453        {
454            infos.put("id", dataSourceDef.getId());
455            infos.put("name", dataSourceDef.getName());
456            infos.put("description", dataSourceDef.getDescription());
457            infos.put("private", dataSourceDef.isPrivate());
458            infos.put("isDefault", dataSourceDef.isDefault());
459            
460            boolean isInUse = TypeOfUse.merge(_dataSourceConsumerEP.isInUse(dataSourceDef.getId()), dataSourceDef.isDefault() ? _dataSourceConsumerEP.isInUse(_ldapDataSourceManager.getDefaultDataSourceId()) : TypeOfUse.NOT_USED) != TypeOfUse.NOT_USED;
461            infos.put("isInUse", isInUse);
462
463            Map<String, Object> parameters = dataSourceDef.getParameters();
464            for (String paramName : parameters.keySet())
465            {
466                infos.put(paramName, parameters.get(paramName));
467            }
468            
469            // Is the data source valid ?
470            infos.put("isValid", _isValid(type, parameters));
471        }
472        
473        return infos;
474    }
475
476    private boolean _isValid(String type, Map<String, Object> parameters)
477    {
478        boolean isValid = true;
479        if (type.equals(DataSourceType.SQL.toString()))
480        {
481            try
482            {
483                _sqlDataSourceManager.checkParameters(parameters);
484            }
485            catch (ItemCheckerTestFailureException e)
486            {
487                isValid = false;
488            }
489        }
490        else if (type.equals(DataSourceType.LDAP.toString()))
491        {
492            try
493            {
494                _ldapDataSourceManager.checkParameters(parameters);
495            }
496            catch (ItemCheckerTestFailureException e)
497            {
498                isValid = false;
499            }
500        }
501        else
502        {
503            throw new IllegalArgumentException("Unable to convert a data source definition to JSON : unknown data source type '" + type + "'.");
504        }
505        
506        return isValid;
507    }
508}