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.datasource.DataSourceConsumer.TypeOfUse;
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.model.checker.ItemCheckerTestFailureException;
059import org.ametys.runtime.plugin.PluginsManager;
060import org.ametys.runtime.plugin.component.AbstractLogEnabled;
061
062/**
063 * Abstract component to handle data source
064 */
065public abstract class AbstractDataSourceManager extends AbstractLogEnabled implements Component, Initializable, Serviceable, Disposable
066{
067    /** The suffix of any default data source */
068    public static final String DEFAULT_DATASOURCE_SUFFIX = "default-datasource";
069    
070    /** The observation manager */
071    protected ObservationManager _observationManager;
072    /** The current user provider */
073    protected CurrentUserProvider _currentUserProvider;
074    
075    /** The data source definitions */
076    protected Map<String, DataSourceDefinition> _dataSourcesDef;
077
078    private long _lastUpdate;
079
080    private DataSourceConsumerExtensionPoint _dataSourceConsumerEP;
081    
082    @Override
083    public void service(ServiceManager serviceManager) throws ServiceException
084    {
085        _dataSourceConsumerEP = (DataSourceConsumerExtensionPoint) serviceManager.lookup(DataSourceConsumerExtensionPoint.ROLE);
086        try
087        {
088            _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
089        }
090        catch (ServiceException e)
091        {
092            // Not a safe component... ignore it
093        }
094        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
095    }
096    
097    @Override
098    public void initialize() throws Exception
099    {
100        _dataSourcesDef = new HashMap<>();
101        
102        readConfiguration();
103        
104        // Check parameters and create data source
105        for (DataSourceDefinition def : _dataSourcesDef.values())
106        {
107            try
108            {
109                // Validate the data source
110                checkParameters(def.getParameters());
111            }
112            catch (ItemCheckerTestFailureException e)
113            {
114                TypeOfUse typeOfUse = TypeOfUse.merge(_dataSourceConsumerEP.isInUse(def.getId()), def.isDefault() ? _dataSourceConsumerEP.isInUse(getDefaultDataSourceId()) : TypeOfUse.NOT_USED);
115                if (!PluginsManager.getInstance().isSafeMode() && typeOfUse == TypeOfUse.BLOCKING)
116                {
117                    // If not already in safe mode, go to safe mode for blocking use
118                    throw e;
119                }
120                else
121                {
122                    getLogger().warn("The data source '{}' is currently invalid", def.getId(), e);
123                }
124            }
125            
126            // Create the data source
127            createDataSource (def);
128        }
129
130        if (getDefaultDataSourceDefinition() == null)
131        {
132            // Force a default data source at start-up if not present
133            internalSetDefaultDataSource();
134        }
135        
136        checkDataSources();
137    }
138    
139    /**
140     * Get the file configuration of data sources
141     * @return the file
142     */
143    public abstract File getFileConfiguration();
144    
145    /**
146     * Get the prefix for data source identifier
147     * @return the id prefix
148     */
149    protected abstract String getDataSourcePrefixId();
150    
151    /**
152     * Checks the parameters of a data source
153     * @param parameters the parameters of the data source
154     * @throws ItemCheckerTestFailureException if the test failed
155     */
156    public abstract void checkParameters(Map<String, Object> parameters) throws ItemCheckerTestFailureException;
157    
158    /**
159     * Creates a data source from its configuration
160     * @param dataSource the data source configuration
161     */
162    protected abstract void createDataSource(DataSourceDefinition dataSource);
163    
164    /**
165     * Edit a data source from its configuration
166     * @param dataSource the data source configuration
167     */
168    protected abstract void editDataSource(DataSourceDefinition dataSource);
169    
170    /**
171     * Deletes a data source
172     * @param dataSource the data source configuration
173     */
174    protected abstract void deleteDataSource(DataSourceDefinition dataSource);
175    
176    /**
177     * Set a default data source internally
178     */
179    protected abstract void internalSetDefaultDataSource();
180    
181    /**
182     * Get the data source definitions 
183     * @param includePrivate true to include private data sources
184     * @param includeInternal true to include internal data sources. Not used by default.
185     * @param includeDefault true to include an additional data source definition for each default data source
186     * @return the data source definitions
187     */
188    public Map<String, DataSourceDefinition> getDataSourceDefinitions(boolean includePrivate, boolean includeInternal, boolean includeDefault)
189    {
190        readConfiguration();
191        
192        Map<String, DataSourceDefinition> dataSourceDefinitions = new HashMap<> ();
193        if (includeDefault)
194        {
195            DataSourceDefinition defaultDataSourceDefinition = getDefaultDataSourceDefinition();
196            if (defaultDataSourceDefinition != null)
197            {
198                dataSourceDefinitions.put(getDefaultDataSourceId(), defaultDataSourceDefinition);
199            }
200        }
201        
202        if (includePrivate)
203        {
204            dataSourceDefinitions.putAll(_dataSourcesDef);
205            return dataSourceDefinitions;
206        }
207        else
208        {
209            Map<String, DataSourceDefinition> publicDatasources = new HashMap<>();
210            for (DataSourceDefinition definition : _dataSourcesDef.values())
211            {
212                if (!definition.isPrivate())
213                {
214                    publicDatasources.put(definition.getId(), definition);
215                }
216            }
217            
218            dataSourceDefinitions.putAll(publicDatasources);
219            return dataSourceDefinitions;
220        }
221    }
222    
223    /**
224     * Get the data source definition or null if not found
225     * @param id the id of data source
226     * @return the data source definition or null if not found
227     */
228    public DataSourceDefinition getDataSourceDefinition(String id)
229    {
230        readConfiguration();
231        
232        if (getDefaultDataSourceId().equals(id))
233        {
234            return getDefaultDataSourceDefinition();
235        }
236        
237        return _dataSourcesDef.get(id);
238    }
239    
240    /**
241     * Add a data source
242     * @param name the name
243     * @param description the description 
244     * @param parameters the parameters
245     * @param isPrivate true if private
246     * @return the created data source definition
247     */
248    public DataSourceDefinition add(I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate)
249    {
250        readConfiguration();
251        
252        String id = getDataSourcePrefixId() + StringUtils.generateKey();
253        DataSourceDefinition ds = new DataSourceDefinition(id, name, description, parameters, isPrivate, false);
254        _dataSourcesDef.put(id, ds);
255        
256        saveConfiguration();
257        
258        createDataSource(ds);
259        
260        if (getDataSourceDefinitions(true, true, false).size() == 1)
261        {
262            internalSetDefaultDataSource();
263        }
264        
265        if (_observationManager != null)
266        {
267            Map<String, Object> eventParams = new HashMap<>();
268            eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, Collections.singletonList(ds.getId()));
269            _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_ADDED, _currentUserProvider.getUser(), eventParams));
270        }
271        
272        return ds;
273    }
274    
275    /**
276     * Edit a data source
277     * @param id the id
278     * @param name the name
279     * @param description the description 
280     * @param parameters the parameters
281     * @param isPrivate true if private
282     * @return the edited data source definition
283     */
284    public DataSourceDefinition edit(String id, I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate)
285    {
286        readConfiguration();
287        
288        if (_dataSourcesDef.containsKey(id))
289        {
290            boolean isDefault = _dataSourcesDef.get(id).isDefault();
291            DataSourceDefinition ds = new DataSourceDefinition(id, name, description, parameters, 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 && TypeOfUse.merge(_dataSourceConsumerEP.isInUse(id), dataSourceDef.isDefault() ? _dataSourceConsumerEP.isInUse(getDefaultDataSourceId()) : TypeOfUse.NOT_USED) != TypeOfUse.NOT_USED)
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 final void readConfiguration()
435    {
436        File file = getFileConfiguration();
437        if (file.exists() && file.lastModified() > _lastUpdate)
438        {
439            _lastUpdate = new Date().getTime();
440            _dataSourcesDef = doReadConfiguration(file);
441        }
442    }
443    
444    /**
445     * Actually read configuration.
446     * @param file the definitions file.
447     * @return all definitions.
448     */
449    protected abstract Map<String, DataSourceDefinition> doReadConfiguration(File file);
450    
451    /**
452     * Save the configured data sources 
453     */
454    protected void saveConfiguration()
455    {
456        File file = getFileConfiguration();
457        try
458        {
459            // Create file if it does not already exist
460            if (!file.exists())
461            {
462                file.getParentFile().mkdirs();
463                file.createNewFile();
464            }
465            
466            // create a transformer for saving sax into a file
467            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
468
469            // create the result where to write
470            try (OutputStream os = new FileOutputStream(file))
471            {
472                StreamResult sResult = new StreamResult(os);
473                th.setResult(sResult);
474    
475                // create the format of result
476                Properties format = new Properties();
477                format.put(OutputKeys.METHOD, "xml");
478                format.put(OutputKeys.INDENT, "yes");
479                format.put(OutputKeys.ENCODING, "UTF-8");
480                format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
481                th.getTransformer().setOutputProperties(format);
482    
483                // Send SAX events
484                th.startDocument();
485                XMLUtils.startElement(th, "datasources");
486                
487                for (DataSourceDefinition datasource : _dataSourcesDef.values())
488                {
489                    saxDataSource(th, datasource);
490                }
491                
492                XMLUtils.endElement(th, "datasources");
493                th.endDocument();
494            }
495        }
496        catch (SAXException | IOException | TransformerConfigurationException e)
497        {
498            throw new RuntimeException("Unable to save the configuration of data sources", e);
499        }
500    }
501    
502    /**
503     * SAX an instance of data source
504     * @param handler the content handler to sax into
505     * @param dataSource the data source
506     * @throws SAXException if an error occurred while SAXing
507     */
508    protected void saxDataSource(ContentHandler handler, DataSourceDefinition dataSource) throws SAXException
509    {
510        AttributesImpl attrs = new AttributesImpl();
511        
512        attrs.addCDATAAttribute("id", dataSource.getId());
513        attrs.addCDATAAttribute("private", String.valueOf(dataSource.isPrivate()));
514        attrs.addCDATAAttribute("default", String.valueOf(dataSource.isDefault()));
515        
516        XMLUtils.startElement(handler, "datasource", attrs);
517        
518        dataSource.getName().toSAX(handler, "name");
519        dataSource.getDescription().toSAX(handler, "description");
520        
521        XMLUtils.startElement(handler, "parameters");
522        Map<String, Object> parameters = dataSource.getParameters();
523        for (String paramName : parameters.keySet())
524        {
525            Object value = parameters.get(paramName);
526            XMLUtils.createElement(handler, paramName, value != null ? value.toString() : "");
527        }
528        XMLUtils.endElement(handler, "parameters");
529        
530        XMLUtils.endElement(handler, "datasource");
531    }
532    
533    /**
534     * Check that the used data sources are indeed available 
535     */
536    protected void checkDataSources()
537    {
538        Set<String> usedDataSourceIds = _dataSourceConsumerEP.getUsedDataSourceIds().keySet();
539        for (String dataSourceId : usedDataSourceIds)
540        {
541            if (dataSourceId != null && dataSourceId.startsWith(getDataSourcePrefixId()) && getDataSourceDefinition(dataSourceId) == null  && !PluginsManager.getInstance().isSafeMode())
542            {
543                throw new UnknownDataSourceException("The data source '" + dataSourceId + "' was not found in the available data sources.");                
544            }
545        }
546    }
547    
548    public void dispose()
549    {
550        for (DataSourceDefinition ds : _dataSourcesDef.values())
551        {
552            deleteDataSource(ds);
553        }
554        
555        _dataSourcesDef.clear();
556        _lastUpdate = 0;
557    }
558    
559    /**
560     * This class represents the definition of a data source
561     */
562    public static class DataSourceDefinition implements Cloneable
563    {
564        private String _id;
565        private I18nizableText _name;
566        private I18nizableText _description;
567        private Map<String, Object> _parameters;
568        private boolean _isPrivate;
569        private boolean _isDefault;
570        
571        /**
572         * Constructor
573         * @param id the id
574         * @param name the name
575         * @param description the description
576         * @param parameters the parameters
577         * @param isPrivate true if the data source is a private data source
578         * @param isDefault true if the data source is a default data source
579         */
580        public DataSourceDefinition(String id, I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate, boolean isDefault)
581        {
582            _id = id;
583            _name = name;
584            _description = description;
585            _parameters = parameters;
586            _isPrivate = isPrivate;
587            _isDefault = isDefault;
588        }
589        
590        /**
591         * The id of the data source
592         * @return the id of the data source
593         */
594        public String getId()
595        {
596            return _id;
597        }
598        
599        /**
600         * Get the name of the data source
601         * @return the name of the data source
602         */
603        public I18nizableText getName()
604        {
605            return _name;
606        }
607        
608        /**
609         * Get the description of the data source
610         * @return the description of the data source
611         */
612        public I18nizableText getDescription()
613        {
614            return _description;
615        }
616        
617        /**
618         * Returns true if this data source instance is private
619         * @return true if is private
620         */
621        public boolean isPrivate()
622        {
623            return _isPrivate;
624        }
625        
626        /**
627         * Returns true if this is a default data source
628         * @return true if this is a default data source
629         */
630        public boolean isDefault()
631        {
632            return _isDefault;
633        }
634        
635        /**
636         * Set default or not this data source 
637         * @param isDefault true to set this data source as the default one, false otherwise 
638         */
639        public void setDefault(boolean isDefault)
640        {
641            _isDefault = isDefault;
642        }
643        
644        /**
645         * Get the parameters of the data source definition
646         * @return the parameters
647         */
648        public Map<String, Object> getParameters()
649        {
650            return _parameters;
651        }
652        
653        /**
654         * Duplicate the object
655         * @return The duplicated object
656         */
657        public DataSourceDefinition duplicate()
658        {
659            return new DataSourceDefinition(_id, _name, _description, new HashMap<>(_parameters), _isPrivate, _isDefault);
660        }
661    }
662}