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