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