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.FileOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.Date;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Properties;
030import java.util.Set;
031
032import javax.xml.transform.OutputKeys;
033import javax.xml.transform.TransformerConfigurationException;
034import javax.xml.transform.TransformerFactory;
035import javax.xml.transform.sax.SAXTransformerFactory;
036import javax.xml.transform.sax.TransformerHandler;
037import javax.xml.transform.stream.StreamResult;
038
039import org.apache.avalon.framework.activity.Initializable;
040import org.apache.avalon.framework.component.Component;
041import org.apache.avalon.framework.configuration.Configuration;
042import org.apache.avalon.framework.configuration.ConfigurationException;
043import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
044import org.apache.avalon.framework.service.ServiceException;
045import org.apache.avalon.framework.service.ServiceManager;
046import org.apache.avalon.framework.service.Serviceable;
047import org.apache.cocoon.xml.AttributesImpl;
048import org.apache.cocoon.xml.XMLUtils;
049import org.xml.sax.ContentHandler;
050import org.xml.sax.SAXException;
051
052import org.ametys.core.ObservationConstants;
053import org.ametys.core.observation.Event;
054import org.ametys.core.observation.ObservationManager;
055import org.ametys.core.user.CurrentUserProvider;
056import org.ametys.core.util.StringUtils;
057import org.ametys.runtime.i18n.I18nizableText;
058import org.ametys.runtime.parameter.ParameterCheckerTestFailureException;
059import org.ametys.runtime.parameter.ParameterHelper;
060import org.ametys.runtime.plugin.PluginsManager;
061import org.ametys.runtime.plugin.component.AbstractLogEnabled;
062
063/**
064 * Abstract component to handle data source
065 */
066public abstract class AbstractDataSourceManager extends AbstractLogEnabled implements Component, Initializable, Serviceable
067{
068    /** The suffix of any default data source */
069    public static final String DEFAULT_DATASOURCE_SUFFIX = "default-datasource";
070    
071    /** The observation manager */
072    protected ObservationManager _observationManager;
073    /** The current user provider */
074    protected CurrentUserProvider _currentUserProvider;
075    
076    /** The data source definitions */
077    protected Map<String, DataSourceDefinition> _dataSourcesDef;
078
079    private long _lastUpdate;
080
081    private DataSourceConsumerExtensionPoint _dataSourceConsumerEP;
082    
083    @Override
084    public void service(ServiceManager serviceManager) throws ServiceException
085    {
086        _dataSourceConsumerEP = (DataSourceConsumerExtensionPoint) serviceManager.lookup(DataSourceConsumerExtensionPoint.ROLE);
087        try
088        {
089            _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
090        }
091        catch (ServiceException e)
092        {
093            // Not a safe component... ignore it
094        }
095        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
096    }
097    
098    @Override
099    public void initialize() throws Exception
100    {
101        _dataSourcesDef = new HashMap<>();
102        
103        readConfiguration();
104        
105        // Check parameters and create data source
106        for (DataSourceDefinition def : _dataSourcesDef.values())
107        {
108            // Validate the used data sources if not already in safe mode
109            boolean isInUse = _dataSourceConsumerEP.isInUse(def.getId()) || (def.isDefault() && _dataSourceConsumerEP.isInUse(getDefaultDataSourceId()));
110            if (!PluginsManager.getInstance().isSafeMode() && isInUse)
111            {
112                checkParameters (def.getParameters());
113            }
114            
115            createDataSource (def);
116        }
117
118        if (getDefaultDataSourceDefinition() == null)
119        {
120            // Force a default data source at start-up if not present
121            internalSetDefaultDataSource();
122        }
123        
124        checkDataSources();
125    }
126    
127    /**
128     * Get the file configuration of data sources
129     * @return the file
130     */
131    public abstract File getFileConfiguration();
132    
133    /**
134     * Get the prefix for data source identifier
135     * @return the id prefix
136     */
137    protected abstract String getDataSourcePrefixId();
138    
139    /**
140     * Checks the parameters of a data source
141     * @param rawParameters the parameters of the data source
142     * @throws ParameterCheckerTestFailureException if the test failed
143     */
144    public abstract void checkParameters(Map<String, String> rawParameters) throws ParameterCheckerTestFailureException;
145    
146    /**
147     * Creates a data source from its configuration
148     * @param dataSource the data source configuration
149     */
150    protected abstract void createDataSource(DataSourceDefinition dataSource);
151    
152    /**
153     * Edit a data source from its configuration
154     * @param dataSource the data source configuration
155     */
156    protected abstract void editDataSource(DataSourceDefinition dataSource);
157    
158    /**
159     * Deletes a data source
160     * @param dataSource the data source configuration
161     */
162    protected abstract void deleteDataSource(DataSourceDefinition dataSource);
163    
164    /**
165     * Set a default data source internally
166     */
167    protected abstract void internalSetDefaultDataSource();
168    
169    /**
170     * Get the data source definitions 
171     * @param includePrivate true to include private data sources
172     * @param includeInternal true to include internal data sources. Not used by default.
173     * @param includeDefault true to include an additional data source definition for each default data source
174     * @return the data source definitions
175     */
176    public Map<String, DataSourceDefinition> getDataSourceDefinitions(boolean includePrivate, boolean includeInternal, boolean includeDefault)
177    {
178        readConfiguration();
179        
180        Map<String, DataSourceDefinition> dataSourceDefinitions = new HashMap<> ();
181        if (includeDefault)
182        {
183            DataSourceDefinition defaultDataSourceDefinition = getDefaultDataSourceDefinition();
184            if (defaultDataSourceDefinition != null)
185            {
186                dataSourceDefinitions.put(getDefaultDataSourceId(), defaultDataSourceDefinition);
187            }
188        }
189        
190        if (includePrivate)
191        {
192            dataSourceDefinitions.putAll(_dataSourcesDef);
193            return dataSourceDefinitions;
194        }
195        else
196        {
197            Map<String, DataSourceDefinition> publicDatasources = new HashMap<>();
198            for (DataSourceDefinition definition : _dataSourcesDef.values())
199            {
200                if (!definition.isPrivate())
201                {
202                    publicDatasources.put(definition.getId(), definition);
203                }
204            }
205            
206            dataSourceDefinitions.putAll(publicDatasources);
207            return dataSourceDefinitions;
208        }
209    }
210    
211    /**
212     * Get the data source definition or null if not found
213     * @param id the id of data source
214     * @return the data source definition or null if not found
215     */
216    public DataSourceDefinition getDataSourceDefinition(String id)
217    {
218        readConfiguration();
219        
220        if (getDefaultDataSourceId().equals(id))
221        {
222            return getDefaultDataSourceDefinition();
223        }
224        
225        return _dataSourcesDef.get(id);
226    }
227    
228    /**
229     * Add a data source
230     * @param name the name
231     * @param description the description 
232     * @param parameters the parameters
233     * @param isPrivate true if private
234     * @return the created data source definition
235     */
236    public DataSourceDefinition add(I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate)
237    {
238        readConfiguration();
239        
240        Map<String, String> rawParameters = new HashMap<>();
241        for (String paramName : parameters.keySet())
242        {
243            rawParameters.put(paramName, ParameterHelper.valueToString(parameters.get(paramName)));
244        }
245        
246        String id = getDataSourcePrefixId() + StringUtils.generateKey();
247        DataSourceDefinition ds = new DataSourceDefinition(id, name, description, rawParameters, isPrivate, false);
248        _dataSourcesDef.put(id, ds);
249        
250        saveConfiguration();
251        
252        createDataSource(ds);
253        
254        if (getDataSourceDefinitions(true, true, false).size() == 1)
255        {
256            internalSetDefaultDataSource();
257        }
258        
259        if (_observationManager != null)
260        {
261            Map<String, Object> eventParams = new HashMap<>();
262            eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, Collections.singletonList(ds.getId()));
263            _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_ADDED, _currentUserProvider.getUser(), eventParams));
264        }
265        
266        return ds;
267    }
268    
269    /**
270     * Edit a data source
271     * @param id the id
272     * @param name the name
273     * @param description the description 
274     * @param parameters the parameters
275     * @param isPrivate true if private
276     * @return the edited data source definition
277     */
278    public DataSourceDefinition edit(String id, I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate)
279    {
280        readConfiguration();
281        
282        if (_dataSourcesDef.containsKey(id))
283        {
284            Map<String, String> rawParameters = new HashMap<>();
285            for (String paramName : parameters.keySet())
286            {
287                rawParameters.put(paramName, ParameterHelper.valueToString(parameters.get(paramName)));
288            }
289            
290            boolean isDefault = _dataSourcesDef.get(id).isDefault();
291            DataSourceDefinition ds = new DataSourceDefinition(id, name, description, rawParameters, isPrivate, isDefault);
292            _dataSourcesDef.put(id, ds);
293            
294            saveConfiguration();
295            
296            editDataSource(ds);
297            
298            if (_observationManager != null)
299            {
300                Map<String, Object> eventParams = new HashMap<>();
301                eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, Collections.singletonList(ds.getId()));
302                _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_UPDATED, _currentUserProvider.getUser(), eventParams));
303            }
304            
305            return ds;
306        }
307        
308        throw new RuntimeException("The data source with id '" + id + "' was not found. Unable to edit it.");
309    }
310    
311    /**
312     * Delete data sources
313     * @param dataSourceIds the ids of the data sources to delete
314     * @param forceDeletion Force the remove event the datasource seems to be in use
315     */
316    public void delete(Collection<String> dataSourceIds, boolean forceDeletion)
317    {
318        readConfiguration();
319        
320        for (String id : dataSourceIds)
321        {
322            DataSourceDefinition dataSourceDef = _dataSourcesDef.get(id);
323            if (!forceDeletion && (_dataSourceConsumerEP.isInUse(id) || (dataSourceDef.isDefault() && _dataSourceConsumerEP.isInUse(getDefaultDataSourceId()))))
324            {
325                throw new IllegalStateException("The data source '" + id + "' is currently in use. The deletion process has been aborted.");
326            }
327            
328            if (id.equals(SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID))
329            {
330                throw new IllegalStateException("The data source '" + id + "' is an internal data source. The deletion process has been aborted.");
331            }
332            
333            deleteDataSource (dataSourceDef);
334            _dataSourcesDef.remove(id);
335        }
336        
337        saveConfiguration();
338        
339        if (getDataSourceDefinitions(true, true, false).size() == 1)
340        {
341            internalSetDefaultDataSource();
342        }
343        
344        if (_observationManager != null)
345        {
346            Map<String, Object> eventParams = new HashMap<>();
347            eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, dataSourceIds);
348            _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_DELETED, _currentUserProvider.getUser(), eventParams));
349        }
350    }
351    
352    /**
353     * Set the data source with the given id as the default data source
354     * @param id the id of the data source
355     * @return the {@link DataSourceDefinition} of the data source set as default 
356     */
357    public DataSourceDefinition setDefaultDataSource(String id)
358    {
359        readConfiguration();
360        
361        if (!id.startsWith(getDataSourcePrefixId()))
362        {
363            throw new RuntimeException("The data source with id '" + id + "' is not of the appropriate type to set is as default.");
364        }
365
366        // Remove the default attribute from the previous default data source (if any)
367        DataSourceDefinition oldDefaultDataSource = getDefaultDataSourceDefinition();
368        if (oldDefaultDataSource != null)
369        {
370            oldDefaultDataSource.setDefault(false);
371            _dataSourcesDef.put(oldDefaultDataSource.getId(), oldDefaultDataSource);
372            
373            saveConfiguration();
374            editDataSource(oldDefaultDataSource);
375        } 
376        
377        if (_dataSourcesDef.containsKey(id))
378        {
379            // Set the data source as the default one
380            DataSourceDefinition newDefaultDataSource = getDataSourceDefinition(id);
381            newDefaultDataSource.setDefault(true);
382            _dataSourcesDef.put(id, newDefaultDataSource);
383            
384            saveConfiguration();
385            editDataSource(newDefaultDataSource);
386            
387            return newDefaultDataSource;
388        }
389        
390        throw new RuntimeException("The data source with id '" + id + "' was not found. Unable to set it as the default data source.");
391    }
392    
393    /**
394     * Get the default data source for this type
395     * @return the definition object of the default data source. Can return null if no datasource is defined. 
396     */
397    public DataSourceDefinition getDefaultDataSourceDefinition()
398    {
399        List<DataSourceDefinition> defaultDataSourceDefinitions = new ArrayList<> ();
400        for (DataSourceDefinition definition : _dataSourcesDef.values())
401        {
402            if (definition.getId().startsWith(getDataSourcePrefixId()) && definition.isDefault())
403            {
404                defaultDataSourceDefinitions.add(definition);
405            }
406        }
407        
408        if (defaultDataSourceDefinitions.isEmpty())
409        {
410            return null;
411        }
412        else if (defaultDataSourceDefinitions.size() > 1)
413        {
414            throw new IllegalStateException("Found more than one default data source definition.");
415        }
416        else
417        {
418            return defaultDataSourceDefinitions.get(0);
419        }
420    }
421    
422    /**
423     * Get the id of the default data source 
424     * @return the id of the default data source
425     */
426    public String getDefaultDataSourceId()
427    {
428        return getDataSourcePrefixId() + DEFAULT_DATASOURCE_SUFFIX;
429    }
430    
431    /**
432     * Read and update the data sources configuration
433     */
434    protected void readConfiguration()
435    {
436        File file = getFileConfiguration();
437        if (file.exists() && file.lastModified() > _lastUpdate)
438        {
439            _lastUpdate = new Date().getTime();
440            _dataSourcesDef = readDataSourceDefinition(file);
441        }
442    }
443    
444    /**
445     * Read the read source definitions 
446     * @param file The configuration file
447     * @return the data source definitions
448     */
449    public static Map<String, DataSourceDefinition> readDataSourceDefinition (File file)
450    {
451        Map<String, DataSourceDefinition> definitions = new HashMap<>();
452        
453        try
454        {
455            if (file.exists())
456            {
457                Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(file);
458                for (Configuration dsConfig : configuration.getChildren("datasource"))
459                {
460                    String id = dsConfig.getAttribute("id");
461                    
462                    I18nizableText name = I18nizableText.parseI18nizableText(dsConfig.getChild("name"), "plugin.core");
463                    I18nizableText description = I18nizableText.parseI18nizableText(dsConfig.getChild("description"), "plugin.core", "");
464                    
465                    boolean isPrivate = dsConfig.getAttributeAsBoolean("private", false);
466                    boolean isDefault = dsConfig.getAttributeAsBoolean("default", false);
467                    
468                    Map<String, String> parameters = new HashMap<>();
469                    
470                    Configuration[] paramsConfig = dsConfig.getChild("parameters").getChildren();
471                    for (Configuration paramConfig : paramsConfig)
472                    {
473                        String value = paramConfig.getValue("");
474                        parameters.put(paramConfig.getName(), value);
475                    }
476                    
477                    DataSourceDefinition dataSource = new DataSourceDefinition(id, name, description, parameters, isPrivate, isDefault);
478                    definitions.put(id, dataSource);
479                }
480            }
481            
482            return definitions;
483        }
484        catch (IOException | ConfigurationException | SAXException e)
485        {
486            throw new RuntimeException("Unable to parse datasource configuration file.", e);
487        }
488    }
489    
490    /**
491     * Save the configured data sources 
492     */
493    protected void saveConfiguration()
494    {
495        File file = getFileConfiguration();
496        try
497        {
498            // Create file if it does not already exist
499            if (!file.exists())
500            {
501                file.getParentFile().mkdirs();
502                file.createNewFile();
503            }
504            
505            // create a transformer for saving sax into a file
506            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
507
508            // create the result where to write
509            try (OutputStream os = new FileOutputStream(file))
510            {
511                StreamResult sResult = new StreamResult(os);
512                th.setResult(sResult);
513    
514                // create the format of result
515                Properties format = new Properties();
516                format.put(OutputKeys.METHOD, "xml");
517                format.put(OutputKeys.INDENT, "yes");
518                format.put(OutputKeys.ENCODING, "UTF-8");
519                th.getTransformer().setOutputProperties(format);
520    
521                // Send SAX events
522                th.startDocument();
523                XMLUtils.startElement(th, "datasources");
524                
525                for (DataSourceDefinition datasource : _dataSourcesDef.values())
526                {
527                    saxDataSource(th, datasource);
528                }
529                
530                XMLUtils.endElement(th, "datasources");
531                th.endDocument();
532            }
533        }
534        catch (SAXException | IOException | TransformerConfigurationException e)
535        {
536            throw new RuntimeException("Unable to save the configuration of data sources", e);
537        }
538    }
539    
540    /**
541     * SAX an instance of data source
542     * @param handler the content handler to sax into
543     * @param dataSource the data source
544     * @throws SAXException if an error occurred while SAXing
545     */
546    protected void saxDataSource(ContentHandler handler, DataSourceDefinition dataSource) throws SAXException
547    {
548        AttributesImpl attrs = new AttributesImpl();
549        
550        attrs.addCDATAAttribute("id", dataSource.getId());
551        attrs.addCDATAAttribute("private", String.valueOf(dataSource.isPrivate()));
552        attrs.addCDATAAttribute("default", String.valueOf(dataSource.isDefault()));
553        
554        XMLUtils.startElement(handler, "datasource", attrs);
555        
556        dataSource.getName().toSAX(handler, "name");
557        dataSource.getDescription().toSAX(handler, "description");
558        
559        XMLUtils.startElement(handler, "parameters");
560        Map<String, String> parameters = dataSource.getParameters();
561        for (String paramName : parameters.keySet())
562        {
563            String value = parameters.get(paramName);
564            XMLUtils.createElement(handler, paramName, value != null ? value : "");
565        }
566        XMLUtils.endElement(handler, "parameters");
567        
568        XMLUtils.endElement(handler, "datasource");
569    }
570    
571    /**
572     * Check that the used data sources are indeed available 
573     */
574    protected void checkDataSources()
575    {
576        Set<String> usedDataSourceIds = _dataSourceConsumerEP.getUsedDataSourceIds();
577        for (String dataSourceId : usedDataSourceIds)
578        {
579            if (dataSourceId != null && dataSourceId.startsWith(getDataSourcePrefixId()) && getDataSourceDefinition(dataSourceId) == null  && !PluginsManager.getInstance().isSafeMode())
580            {
581                throw new UnknownDataSourceException("The data source '" + dataSourceId + "' was not found in the available data sources.");                
582            }
583        }
584    }
585    
586    /**
587     * This class represents the definition of a data source
588     */
589    public static class DataSourceDefinition implements Cloneable
590    {
591        private String _id;
592        private I18nizableText _name;
593        private I18nizableText _description;
594        private Map<String, String> _parameters;
595        private boolean _isPrivate;
596        private boolean _isDefault;
597        
598        /**
599         * Constructor
600         * @param id the id
601         * @param name the name
602         * @param description the description
603         * @param parameters the parameters
604         * @param isPrivate true if the data source is a private data source
605         * @param isDefault true if the data source is a default data source
606         */
607        public DataSourceDefinition(String id, I18nizableText name, I18nizableText description, Map<String, String> parameters, boolean isPrivate, boolean isDefault)
608        {
609            _id = id;
610            _name = name;
611            _description = description;
612            _parameters = parameters;
613            _isPrivate = isPrivate;
614            _isDefault = isDefault;
615        }
616        
617        /**
618         * The id of the data source
619         * @return the id of the data source
620         */
621        public String getId()
622        {
623            return _id;
624        }
625        
626        /**
627         * Get the name of the data source
628         * @return the name of the data source
629         */
630        public I18nizableText getName()
631        {
632            return _name;
633        }
634        
635        /**
636         * Get the description of the data source
637         * @return the description of the data source
638         */
639        public I18nizableText getDescription()
640        {
641            return _description;
642        }
643        
644        /**
645         * Returns true if this data source instance is private
646         * @return true if is private
647         */
648        public boolean isPrivate()
649        {
650            return _isPrivate;
651        }
652        
653        /**
654         * Returns true if this is a default data source
655         * @return true if this is a default data source
656         */
657        public boolean isDefault()
658        {
659            return _isDefault;
660        }
661        
662        /**
663         * Set default or not this data source 
664         * @param isDefault true to set this data source as the default one, false otherwise 
665         */
666        public void setDefault(boolean isDefault)
667        {
668            _isDefault = isDefault;
669        }
670        
671        /**
672         * Get the parameters of the data source definition
673         * @return the parameters
674         */
675        public Map<String, String> getParameters()
676        {
677            return _parameters;
678        }
679        
680        /**
681         * Duplicate the object
682         * @return The duplicated object
683         */
684        public DataSourceDefinition duplicate()
685        {
686            return new DataSourceDefinition(_id, _name, _description, new HashMap<>(_parameters), _isPrivate, _isDefault);
687        }
688        
689        @Override
690        protected Object clone() throws CloneNotSupportedException
691        {
692            // TODO Auto-generated method stub
693            return super.clone();
694        }
695    }
696}