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.plugins.contentio.synchronize;
017
018import java.io.File;
019import java.io.FileNotFoundException;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.OutputStream;
023import java.nio.file.Files;
024import java.nio.file.StandardCopyOption;
025import java.time.Instant;
026import java.time.temporal.ChronoUnit;
027import java.util.ArrayList;
028import java.util.HashMap;
029import java.util.LinkedHashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Properties;
033
034import javax.xml.transform.OutputKeys;
035import javax.xml.transform.TransformerConfigurationException;
036import javax.xml.transform.TransformerFactory;
037import javax.xml.transform.TransformerFactoryConfigurationError;
038import javax.xml.transform.sax.SAXTransformerFactory;
039import javax.xml.transform.sax.TransformerHandler;
040import javax.xml.transform.stream.StreamResult;
041
042import org.apache.avalon.framework.activity.Disposable;
043import org.apache.avalon.framework.activity.Initializable;
044import org.apache.avalon.framework.component.Component;
045import org.apache.avalon.framework.configuration.Configuration;
046import org.apache.avalon.framework.configuration.ConfigurationException;
047import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
048import org.apache.avalon.framework.context.Context;
049import org.apache.avalon.framework.context.ContextException;
050import org.apache.avalon.framework.context.Contextualizable;
051import org.apache.avalon.framework.service.ServiceException;
052import org.apache.avalon.framework.service.ServiceManager;
053import org.apache.avalon.framework.service.Serviceable;
054import org.apache.cocoon.ProcessingException;
055import org.apache.cocoon.components.LifecycleHelper;
056import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
057import org.apache.cocoon.xml.AttributesImpl;
058import org.apache.cocoon.xml.XMLUtils;
059import org.apache.commons.lang3.StringUtils;
060import org.apache.xml.serializer.OutputPropertiesFactory;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063import org.xml.sax.ContentHandler;
064import org.xml.sax.SAXException;
065
066import org.ametys.cms.contenttype.ContentType;
067import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
068import org.ametys.cms.languages.Language;
069import org.ametys.cms.languages.LanguagesManager;
070import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition;
071import org.ametys.core.datasource.LDAPDataSourceManager;
072import org.ametys.core.datasource.SQLDataSourceManager;
073import org.ametys.core.ui.Callable;
074import org.ametys.core.user.directory.UserDirectory;
075import org.ametys.core.user.directory.UserDirectoryFactory;
076import org.ametys.core.user.directory.UserDirectoryModel;
077import org.ametys.core.user.population.UserPopulation;
078import org.ametys.core.user.population.UserPopulationDAO;
079import org.ametys.plugins.contentio.synchronize.impl.DefaultSynchronizingContentOperator;
080import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory;
081import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory;
082import org.ametys.plugins.workflow.support.WorkflowProvider;
083import org.ametys.runtime.i18n.I18nizableText;
084import org.ametys.runtime.model.DefinitionContext;
085import org.ametys.runtime.model.ElementDefinition;
086import org.ametys.runtime.model.ModelItem;
087import org.ametys.runtime.model.checker.ItemCheckerTestFailureException;
088import org.ametys.runtime.model.type.ModelItemTypeConstants;
089import org.ametys.runtime.parameter.Errors;
090import org.ametys.runtime.parameter.Validator;
091import org.ametys.runtime.plugin.component.AbstractLogEnabled;
092import org.ametys.runtime.plugin.component.LogEnabled;
093import org.ametys.runtime.util.AmetysHomeHelper;
094
095/**
096 * DAO for accessing {@link SynchronizableContentsCollection}
097 */
098public class SynchronizableContentsCollectionDAO extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable, Disposable
099{
100    /** Avalon Role */
101    public static final String ROLE = SynchronizableContentsCollectionDAO.class.getName();
102    
103    /** Separator for parameters with model id as prefix */
104    public static final String SCC_PARAMETERS_SEPARATOR = "$";
105    
106    private static File __CONFIGURATION_FILE;
107    
108    private Map<String, SynchronizableContentsCollection> _synchronizableCollections;
109    private long _lastFileReading;
110
111    private SynchronizeContentsCollectionModelExtensionPoint _syncCollectionModelEP;
112    private ContentTypeExtensionPoint _contentTypeEP;
113    private UserPopulationDAO _userPopulationDAO;
114    private UserDirectoryFactory _userDirectoryFactory;
115    private WorkflowProvider _workflowProvider;
116    private SynchronizingContentOperatorExtensionPoint _synchronizingContentOperatorEP;
117    private LanguagesManager _languagesManager;
118    
119    private ServiceManager _smanager;
120    private Context _context;
121
122    private SQLDataSourceManager _sqlDataSourceManager;
123    private LDAPDataSourceManager _ldapDataSourceManager;
124
125
126    @Override
127    public void initialize() throws Exception
128    {
129        __CONFIGURATION_FILE = new File(AmetysHomeHelper.getAmetysHome(), "config" + File.separator + "synchronizable-collections.xml");
130        _synchronizableCollections = new HashMap<>();
131        _lastFileReading = 0;
132    }
133    
134    @Override
135    public void contextualize(Context context) throws ContextException
136    {
137        _context = context;
138    }
139
140    @Override
141    public void service(ServiceManager smanager) throws ServiceException
142    {
143        _smanager = smanager;
144        _syncCollectionModelEP = (SynchronizeContentsCollectionModelExtensionPoint) smanager.lookup(SynchronizeContentsCollectionModelExtensionPoint.ROLE);
145        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
146        _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE);
147        _userDirectoryFactory = (UserDirectoryFactory) smanager.lookup(UserDirectoryFactory.ROLE);
148        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
149        _synchronizingContentOperatorEP = (SynchronizingContentOperatorExtensionPoint) smanager.lookup(SynchronizingContentOperatorExtensionPoint.ROLE);
150        _languagesManager = (LanguagesManager) smanager.lookup(LanguagesManager.ROLE);
151    }
152    
153    private SQLDataSourceManager _getSQLDataSourceManager()
154    {
155        if (_sqlDataSourceManager == null)
156        {
157            try
158            {
159                _sqlDataSourceManager = (SQLDataSourceManager) _smanager.lookup(SQLDataSourceManager.ROLE);
160            }
161            catch (ServiceException e)
162            {
163                throw new RuntimeException(e);
164            }
165        }
166        
167        return _sqlDataSourceManager;
168    }
169    
170    private LDAPDataSourceManager _getLDAPDataSourceManager()
171    {
172        if (_ldapDataSourceManager == null)
173        {
174            try
175            {
176                _ldapDataSourceManager = (LDAPDataSourceManager) _smanager.lookup(LDAPDataSourceManager.ROLE);
177            }
178            catch (ServiceException e)
179            {
180                throw new RuntimeException(e);
181            }
182        }
183        
184        return _ldapDataSourceManager;
185    }
186    
187    /**
188     * Gets a synchronizable contents collection to JSON format
189     * @param collectionId The id of the synchronizable contents collection to get
190     * @return An object representing a {@link SynchronizableContentsCollection}
191     */
192    @Callable
193    public Map<String, Object> getSynchronizableContentsCollectionAsJson(String collectionId)
194    {
195        return getSynchronizableContentsCollectionAsJson(getSynchronizableContentsCollection(collectionId));
196    }
197    
198    /**
199     * Gets a synchronizable contents collection to JSON format
200     * @param collection The synchronizable contents collection to get
201     * @return An object representing a {@link SynchronizableContentsCollection}
202     */
203    public Map<String, Object> getSynchronizableContentsCollectionAsJson(SynchronizableContentsCollection collection)
204    {
205        Map<String, Object> result = new LinkedHashMap<>();
206        result.put("id", collection.getId());
207        result.put("label", collection.getLabel());
208        
209        String cTypeId = collection.getContentType();
210        result.put("contentTypeId", cTypeId);
211        result.put("contentType", _contentTypeEP.getExtension(cTypeId).getLabel());
212        
213        String modelId = collection.getSynchronizeCollectionModelId();
214        result.put("modelId", modelId);
215        SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId);
216        result.put("model", model.getLabel());
217        
218        result.put("isValid", _isValid(collection));
219        
220        return result;
221    }
222    
223    /**
224     * Get the synchronizable contents collections
225     * @return the synchronizable contents collections
226     */
227    public List<SynchronizableContentsCollection> getSynchronizableContentsCollections()
228    {
229        getLogger().debug("Calling #getSynchronizableContentsCollections()");
230        _readFile(false);
231        ArrayList<SynchronizableContentsCollection> cols = new ArrayList<>(_synchronizableCollections.values());
232        getLogger().debug("#getSynchronizableContentsCollections() returns '{}'", cols);
233        return cols;
234    }
235    
236    /**
237     * Get a synchronizable contents collection by its id
238     * @param collectionId The id of collection
239     * @return the synchronizable contents collection or <code>null</code> if not found
240     */
241    public SynchronizableContentsCollection getSynchronizableContentsCollection(String collectionId)
242    {
243        getLogger().debug("Calling #getSynchronizableContentsCollection(String collectionId) with collectionId '{}'", collectionId);
244        _readFile(false);
245        SynchronizableContentsCollection col = _synchronizableCollections.get(collectionId);
246        getLogger().debug("#getSynchronizableContentsCollection(String collectionId) with collectionId '{}' returns '{}'", collectionId, col);
247        return col;
248    }
249    
250    private void _readFile(boolean forceRead)
251    {
252        try
253        {
254            if (!__CONFIGURATION_FILE.exists())
255            {
256                getLogger().debug("=> SCC file does not exist, it will be created.");
257                _createFile(__CONFIGURATION_FILE);
258            }
259            else
260            {
261                // In Linux file systems, the precision of java.io.File.lastModified() is the second, so we need here to always have
262                // this (bad!) precision by doing the truncation to second precision (/1000 * 1000) on the millis time value.
263                // Therefore, the boolean outdated is computed with '>=' operator, and not '>', which will lead to sometimes (but rarely) unnecessarily re-read the file.
264                long cfgFileLastModified = (__CONFIGURATION_FILE.lastModified() / 1000) * 1000;
265                boolean outdated = cfgFileLastModified >= _lastFileReading;
266                getLogger().debug("=> forceRead: {}", forceRead);
267                getLogger().debug("=> The configuration was last modified in (long value): {}", cfgFileLastModified);
268                getLogger().debug("=> The '_lastFileReading' fields is equal to (long value): {}", _lastFileReading);
269                if (forceRead || outdated)
270                {
271                    getLogger().debug(forceRead ? "=> SCC file will be read (force)" : "=> SCC file was (most likely) updated since the last time it was read ({} >= {}). It will be re-read...", cfgFileLastModified, _lastFileReading);
272                    getLogger().debug("==> '_synchronizableCollections' map before calling #_readFile(): '{}'", _synchronizableCollections);
273                    _lastFileReading = Instant.now().truncatedTo(ChronoUnit.SECONDS).toEpochMilli();
274                    _synchronizableCollections = new LinkedHashMap<>();
275                    
276                    Configuration cfg = new DefaultConfigurationBuilder().buildFromFile(__CONFIGURATION_FILE);
277                    for (Configuration collectionConfig : cfg.getChildren("collection"))
278                    {
279                        SynchronizableContentsCollection syncCollection = _createSynchronizableCollection(collectionConfig);
280                        _synchronizableCollections.put(syncCollection.getId(), syncCollection);
281                    }
282                    getLogger().debug("==> '_synchronizableCollections' map after calling #_readFile(): '{}'", _synchronizableCollections);
283                }
284                else
285                {
286                    getLogger().debug("=> SCC file will not be re-read, the internal representation is up-to-date.");
287                }
288            }
289        }
290        catch (Exception e)
291        {
292            getLogger().error("Failed to retrieve synchronizable contents collections from the configuration file {}", __CONFIGURATION_FILE, e);
293        }
294    }
295    
296    private void _createFile(File file) throws IOException, TransformerConfigurationException, SAXException
297    {
298        file.createNewFile();
299        try (OutputStream os = new FileOutputStream(file))
300        {
301            TransformerHandler th = _getTransformerHandler(os);
302            
303            th.startDocument();
304            XMLUtils.createElement(th, "collections");
305            th.endDocument();
306        }
307    }
308    
309    private SynchronizableContentsCollection _createSynchronizableCollection(Configuration collectionConfig) throws ConfigurationException
310    {
311        String modelId = collectionConfig.getChild("model").getAttribute("id");
312        
313        if (_syncCollectionModelEP.hasExtension(modelId))
314        {
315            SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId);
316            Class<SynchronizableContentsCollection> synchronizableCollectionClass = model.getSynchronizableCollectionClass();
317            
318            SynchronizableContentsCollection synchronizableCollection = null;
319            try
320            {
321                synchronizableCollection = synchronizableCollectionClass.getDeclaredConstructor().newInstance();
322            }
323            catch (Exception e)
324            {
325                throw new IllegalArgumentException("Cannot instanciate the class " + synchronizableCollectionClass.getCanonicalName() + ". Check that there is a public constructor with no arguments.");
326            }
327            
328            Logger logger = LoggerFactory.getLogger(synchronizableCollectionClass);
329            try
330            {
331                if (synchronizableCollection instanceof LogEnabled)
332                {
333                    ((LogEnabled) synchronizableCollection).setLogger(logger);
334                }
335                
336                LifecycleHelper.setupComponent(synchronizableCollection, new SLF4JLoggerAdapter(logger), _context, _smanager, collectionConfig);
337            }
338            catch (Exception e)
339            {
340                throw new ConfigurationException("The model id '" + modelId + "' is not a valid", e);
341            }
342            
343            return synchronizableCollection;
344        }
345        
346        throw new ConfigurationException("The model id '" + modelId + "' is not a valid model for collection '" + collectionConfig.getChild("id") + "'", collectionConfig);
347    }
348    
349    /**
350     * Gets the configuration for creating/editing a collection of synchronizable contents.
351     * @return A map containing information about what is needed to create/edit a collection of synchronizable contents
352     * @throws Exception If an error occurs.
353     */
354    @Callable
355    public Map<String, Object> getEditionConfiguration() throws Exception
356    {
357        Map<String, Object> result = new HashMap<>();
358        
359        // MODELS
360        List<Object> collectionModels = new ArrayList<>();
361        for (String modelId : _syncCollectionModelEP.getExtensionsIds())
362        {
363            SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId);
364            Map<String, Object> modelMap = new HashMap<>();
365            modelMap.put("id", modelId);
366            modelMap.put("label", model.getLabel());
367            modelMap.put("description", model.getDescription());
368            
369            Map<String, Object> params = new LinkedHashMap<>();
370            for (ModelItem param : model.getModelItems())
371            {
372                // prefix in case of two parameters from two different models have the same id which can lead to some errors in client-side
373                params.put(modelId + SCC_PARAMETERS_SEPARATOR + param.getPath(), param.toJSON(DefinitionContext.newInstance().withEdition(true)));
374            }
375            modelMap.put("parameters", params);
376            
377            collectionModels.add(modelMap);
378        }
379        result.put("models", collectionModels);
380        
381        // CONTENT TYPES
382        List<Object> contentTypes = new ArrayList<>();
383        for (String contentTypeId : _contentTypeEP.getExtensionsIds())
384        {
385            ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
386            if (_isValidContentType(contentType))
387            {
388                Map<String, Object> contentTypeMap = new HashMap<>();
389                contentTypeMap.put("value",  contentType.getId());
390                contentTypeMap.put("label", contentType.getLabel());
391                
392                contentTypes.add(contentTypeMap);
393            }
394        }
395        result.put("contentTypes", contentTypes);
396        
397        // LANGUAGES
398        List<Object> languages = new ArrayList<>();
399        Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages();
400        for (String lang : availableLanguages.keySet())
401        {
402            Language language = availableLanguages.get(lang);
403            Map<String, Object> languageMap = new HashMap<>();
404            languageMap.put("value", lang);
405            languageMap.put("label", language.getLabel());
406            languages.add(languageMap);
407        }
408        result.put("languages", languages);
409        
410        // WORKFLOWS
411        List<Object> workflows = new ArrayList<>();
412        String[] workflowNames = _workflowProvider.getAmetysObjectWorkflow().getWorkflowNames();
413        for (String workflowName : workflowNames)
414        {
415            Map<String, Object> workflowMap = new HashMap<>();
416            workflowMap.put("value", workflowName);
417            workflowMap.put("label", new I18nizableText("application", "WORKFLOW_" + workflowName));
418            workflows.add(workflowMap);
419        }
420        result.put("workflows", workflows);
421        
422        // SYNCHRONIZING CONTENT OPERATORS
423        List<Object> operators = new ArrayList<>();
424        for (String operatorId : _synchronizingContentOperatorEP.getExtensionsIds())
425        {
426            Map<String, Object> operatorMap = new HashMap<>();
427            operatorMap.put("value", operatorId);
428            operatorMap.put("label", _synchronizingContentOperatorEP.getExtension(operatorId).getLabel());
429            operators.add(operatorMap);
430        }
431        result.put("contentOperators", operators);
432        result.put("defaultContentOperator", DefaultSynchronizingContentOperator.class.getName());
433        
434        return result;
435    }
436    
437    private boolean _isValidContentType (ContentType cType)
438    {
439        return !cType.isReferenceTable() && !cType.isAbstract() && !cType.isMixin();
440    }
441    
442    /**
443     * Gets the values of the parameters of the given collection
444     * @param collectionId The id of the collection
445     * @return The values of the parameters
446     */
447    @Callable
448    public Map<String, Object> getCollectionParameterValues(String collectionId)
449    {
450        Map<String, Object> result = new LinkedHashMap<>();
451        
452        SynchronizableContentsCollection collection = getSynchronizableContentsCollection(collectionId);
453        if (collection == null)
454        {
455            getLogger().error("The collection of id '{}' does not exist.", collectionId);
456            result.put("error", "unknown");
457            return result;
458        }
459        
460        result.put("id", collectionId);
461        result.put("label", collection.getLabel());
462        String modelId = collection.getSynchronizeCollectionModelId();
463        result.put("modelId", modelId);
464        
465        result.put("contentType", collection.getContentType());
466        result.put("contentPrefix", collection.getContentPrefix());
467        result.put("restrictedField", collection.getRestrictedField());
468        result.put("synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly());
469        result.put("removalSync", collection.removalSync());
470        result.put("validateAfterImport", collection.validateAfterImport());
471        
472        result.put("workflowName", collection.getWorkflowName());
473        result.put("initialActionId", collection.getInitialActionId());
474        result.put("synchronizeActionId", collection.getSynchronizeActionId());
475        result.put("validateActionId", collection.getValidateActionId());
476        
477        result.put("contentOperator", collection.getSynchronizingContentOperator());
478        result.put("reportMails", collection.getReportMails());
479        
480        result.put("languages", collection.getLanguages());
481        
482        Map<String, Object> values = collection.getParameterValues();
483        for (String key : values.keySet())
484        {
485            result.put(modelId + SCC_PARAMETERS_SEPARATOR + key, values.get(key));
486        }
487        
488        return result;
489    }
490
491    /**
492     * Gets the supported user directories (i.e. user directories based on a datasource) of the population in a json map
493     * @param populationId The id of the user population
494     * @return the supported user directories (i.e. user directories based on a datasource) of the population in a json map 
495     */
496    @Callable
497    public List<Map<String, Object>> getSupportedUserDirectories(String populationId)
498    {
499        List<Map<String, Object>> result = new ArrayList<>();
500        
501        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId);
502        List<UserDirectory> userDirectories = userPopulation.getUserDirectories();
503        for (String udId : _getDatasourceBasedUserDirectories(userDirectories))
504        {
505            UserDirectory userDirectory = userPopulation.getUserDirectory(udId);
506            String udModelId = userDirectory.getUserDirectoryModelId();
507            UserDirectoryModel udModel = _userDirectoryFactory.getExtension(udModelId);
508            Map<String, Object> udMap = new HashMap<>();
509            
510            udMap.put("id", udId);
511            udMap.put("modelLabel", udModel.getLabel());
512            
513            if (userDirectory instanceof JdbcUserDirectory)
514            {
515                udMap.put("type", "SQL");
516            }
517            else if (userDirectories instanceof LdapUserDirectory)
518            {
519                udMap.put("type", "LDAP");
520            }
521            
522            result.add(udMap);
523        }
524        
525        return result;
526    }
527    
528    private List<String> _getDatasourceBasedUserDirectories(List<UserDirectory> userDirectories)
529    {
530        List<String> ids = new ArrayList<>();
531        for (UserDirectory userDirectory : userDirectories)
532        {
533            if (userDirectory instanceof JdbcUserDirectory || userDirectory instanceof LdapUserDirectory)
534            {
535                ids.add(userDirectory.getId());
536            }
537        }
538        
539        return ids;
540    }
541    
542    private boolean _writeFile()
543    {
544        File backup = _createBackup();
545        boolean errorOccured = false;
546        
547        // Do writing
548        try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE))
549        {
550            TransformerHandler th = _getTransformerHandler(os);
551
552            // sax the config
553            try
554            {
555                th.startDocument();
556                XMLUtils.startElement(th, "collections");
557                
558                _toSAX(th);
559                XMLUtils.endElement(th, "collections");
560                th.endDocument();
561            }
562            catch (Exception e)
563            {
564                getLogger().error("Error when saxing the collections", e);
565                errorOccured = true;
566            }
567        }
568        catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
569        {
570            if (getLogger().isErrorEnabled())
571            {
572                getLogger().error("Error when trying to modify the group directories with the configuration file {}", __CONFIGURATION_FILE, e);
573            }
574        }
575        
576        _restoreBackup(backup, errorOccured);
577        
578        return errorOccured;
579    }
580    
581    private File _createBackup()
582    {
583        File backup = new File(__CONFIGURATION_FILE.getPath() + ".tmp");
584        
585        // Create a backup file
586        try
587        {
588            Files.copy(__CONFIGURATION_FILE.toPath(), backup.toPath());
589        }
590        catch (IOException e)
591        {
592            getLogger().error("Error when creating backup '{}' file", __CONFIGURATION_FILE.toPath(), e);
593        }
594        
595        return backup;
596    }
597    
598    private void _restoreBackup(File backup, boolean errorOccured)
599    {
600        // Restore the file if an error previously occured
601        try
602        {
603            if (errorOccured)
604            {
605                // An error occured, restore the original
606                Files.copy(backup.toPath(), __CONFIGURATION_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING);
607                // Force to reread the file
608                _readFile(true);
609            }
610            Files.deleteIfExists(backup.toPath());
611        }
612        catch (IOException e)
613        {
614            if (getLogger().isErrorEnabled())
615            {
616                getLogger().error("Error when restoring backup '{}' file", __CONFIGURATION_FILE, e);
617            }
618        }
619    }
620    
621    /**
622     * Add a new {@link SynchronizableContentsCollection}
623     * @param values The parameters' values
624     * @return The id of new created collection or null in case of error
625     * @throws ProcessingException if creation failed
626     */
627    @Callable
628    public String addCollection (Map<String, Object> values) throws ProcessingException
629    {
630        getLogger().debug("Add new Collection with values '{}'", values);
631        _readFile(false);
632        
633        String id = _generateUniqueId((String) values.get("label"));
634        
635        try
636        {
637            _addCollection(id, values);
638            return id;
639        }
640        catch (Exception e)
641        {
642            throw new ProcessingException("Failed to add new collection'" + id + "'", e);
643        }
644    }
645    
646    /**
647     * Edit a {@link SynchronizableContentsCollection}
648     * @param id The id of collection to edit
649     * @param values The parameters' values
650     * @return The id of new created collection or null in case of error
651     * @throws ProcessingException if edition failed
652     */
653    @Callable
654    public Map<String, Object> editCollection (String id, Map<String, Object> values) throws ProcessingException
655    {
656        getLogger().debug("Edit Collection with id '{}' and values '{}'", id, values);
657        Map<String, Object> result = new LinkedHashMap<>();
658        
659        SynchronizableContentsCollection collection = _synchronizableCollections.get(id);
660        if (collection == null)
661        {
662            getLogger().error("The collection with id '{}' does not exist, it cannot be edited.", id);
663            result.put("error", "unknown");
664            return result;
665        }
666        else
667        {
668            _synchronizableCollections.remove(id);
669        }
670        
671        try
672        {
673            _addCollection(id, values);
674            result.put("id", id);
675            return result;
676        }
677        catch (Exception e)
678        {
679            throw new ProcessingException("Failed to edit collection of id '" + id + "'", e);
680        }
681    }
682    
683    private boolean _isValid(SynchronizableContentsCollection collection)
684    {
685        // Check validation of a data source on its parameters
686        
687        SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(collection.getSynchronizeCollectionModelId());
688        if (model != null)
689        {
690            for (ModelItem param : model.getModelItems())
691            {
692                if (!_validateParameter(param, collection))
693                {
694                    // At least one parameter is invalid
695                    return false;
696                }
697                
698                if (ModelItemTypeConstants.DATASOURCE_ELEMENT_TYPE_ID.equals(param.getType().getId()))
699                {
700                    String dataSourceId = (String) collection.getParameterValues().get(param.getPath());
701                    
702                    if (!_checkDataSource(dataSourceId))
703                    {
704                        // At least one data source is not valid
705                        return false;
706                    }
707                    
708                }
709            }
710            
711            return true;
712        }
713        
714        return false; // no model found
715    }
716    
717    private boolean _validateParameter(ModelItem modelItem, SynchronizableContentsCollection collection)
718    {
719        if (modelItem instanceof ElementDefinition)
720        {
721            ElementDefinition param = (ElementDefinition) modelItem;
722            Validator validator = param.getValidator();
723            if (validator != null)
724            {
725                Object value = collection.getParameterValues().get(param.getPath());
726                
727                Errors errors = new Errors();
728                validator.validate(value, errors);
729                
730                return !errors.hasErrors();
731            }
732        }
733        
734        return true;
735    }
736    
737    private boolean _checkDataSource(String dataSourceId)
738    {
739        if (dataSourceId != null)
740        {
741            try
742            {
743                DataSourceDefinition def = _getSQLDataSourceManager().getDataSourceDefinition(dataSourceId);
744                
745                if (def != null)
746                {
747                    _getSQLDataSourceManager().checkParameters(def.getParameters());
748                }
749                else
750                {
751                    def = _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId);
752                    if (def != null)
753                    {
754                        _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId);
755                    }
756                    else
757                    {
758                        // The data source was not found
759                        return false;
760                    }
761                }
762            }
763            catch (ItemCheckerTestFailureException e)
764            {
765                // Connection to the SQL data source failed
766                return false;
767            }
768        }
769        
770        return true;
771    }
772    
773    private boolean _addCollection(String id, Map<String, Object> values) throws FileNotFoundException, IOException, TransformerConfigurationException, SAXException
774    {
775        File backup = _createBackup();
776        boolean success = false;
777        
778        // Do writing
779        try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE))
780        {
781            TransformerHandler th = _getTransformerHandler(os);
782
783            // sax the config
784            th.startDocument();
785            XMLUtils.startElement(th, "collections");
786            
787            // SAX already existing collections
788            _toSAX(th);
789            
790            // SAX the new collection
791            _saxCollection(th, id, values);
792            
793            XMLUtils.endElement(th, "collections");
794            th.endDocument();
795            
796            success = true;
797        }
798        
799        _restoreBackup(backup, !success);
800        
801        _readFile(false);
802        
803        return success;
804    }
805    
806    private TransformerHandler _getTransformerHandler(OutputStream os) throws TransformerConfigurationException
807    {
808        // create a transformer for saving sax into a file
809        TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
810        
811        StreamResult result = new StreamResult(os);
812        th.setResult(result);
813
814        // create the format of result
815        Properties format = new Properties();
816        format.put(OutputKeys.METHOD, "xml");
817        format.put(OutputKeys.INDENT, "yes");
818        format.put(OutputKeys.ENCODING, "UTF-8");
819        format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
820        th.getTransformer().setOutputProperties(format);
821        
822        return th;
823    }
824    
825    /**
826     * Removes the given collection
827     * @param id The id of the collection to remove
828     * @return A map containing the id of the removed collection, or an error
829     */
830    @Callable
831    public Map<String, Object> removeCollection(String id)
832    {
833        getLogger().debug("Remove Collection with id '{}'", id);
834        Map<String, Object> result = new LinkedHashMap<>();
835        
836        _readFile(false);
837        if (_synchronizableCollections.remove(id) == null)
838        {
839            getLogger().error("The synchronizable collection with id '{}' does not exist, it cannot be removed.", id);
840            result.put("error", "unknown");
841            return result;
842        }
843        
844        if (_writeFile())
845        {
846            return null;
847        }
848        
849        result.put("id", id);
850        return result;
851    }
852    
853    private String _generateUniqueId(String label)
854    {
855        // Id generated from name lowercased, trimmed, and spaces and underscores replaced by dashes
856        String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", "");
857        int i = 2;
858        String suffixedValue = value;
859        while (_synchronizableCollections.get(suffixedValue) != null)
860        {
861            suffixedValue = value + i;
862            i++;
863        }
864        
865        return suffixedValue;
866    }
867    
868    private void _toSAX(TransformerHandler handler) throws SAXException
869    {
870        for (SynchronizableContentsCollection collection : _synchronizableCollections.values())
871        {
872            _saxCollection(handler, collection);
873        }
874    }
875    
876    private void _saxCollection(ContentHandler handler, String id, Map<String, Object> parameters) throws SAXException
877    {
878        AttributesImpl atts = new AttributesImpl();
879        atts.addCDATAAttribute("id", id);
880        
881        XMLUtils.startElement(handler, "collection", atts);
882        
883        String label = (String) parameters.get("label");
884        if (label != null)
885        {
886            new I18nizableText(label).toSAX(handler, "label");
887        }
888        
889        _saxNonNullValue(handler, "contentType", parameters.get("contentType"));
890        _saxNonNullValue(handler, "contentPrefix", parameters.get("contentPrefix"));
891        _saxNonNullValue(handler, "restrictedField", parameters.get("restrictedField"));
892        _saxNonNullValue(handler, "synchronizeExistingContentsOnly", parameters.get("synchronizeExistingContentsOnly"));
893        _saxNonNullValue(handler, "removalSync", parameters.get("removalSync"));
894        
895        _saxNonNullValue(handler, "workflowName", parameters.get("workflowName"));
896        _saxNonNullValue(handler, "initialActionId", parameters.get("initialActionId"));
897        _saxNonNullValue(handler, "synchronizeActionId", parameters.get("synchronizeActionId"));
898        _saxNonNullValue(handler, "validateActionId", parameters.get("validateActionId"));
899        _saxNonNullValue(handler, "validateAfterImport", parameters.get("validateAfterImport"));
900        
901        _saxNonNullValue(handler, "reportMails", parameters.get("reportMails"));
902        _saxNonNullValue(handler, "contentOperator", parameters.get("contentOperator"));
903        
904        _saxLanguagesValue(handler, parameters.get("languages"));
905        
906        parameters.remove("id");
907        parameters.remove("label");
908        parameters.remove("contentType");
909        parameters.remove("synchronizeExistingContentsOnly");
910        parameters.remove("removalSync");
911        parameters.remove("workflowName");
912        parameters.remove("initialActionId");
913        parameters.remove("synchronizeActionId");
914        parameters.remove("validateActionId");
915        parameters.remove("contentPrefix");
916        parameters.remove("validateAfterImport");
917        parameters.remove("reportMails");
918        parameters.remove("contentOperator");
919        parameters.remove("restrictedField");
920        
921        String modelId = (String) parameters.get("modelId");
922        parameters.remove("modelId");
923        
924        _saxModel(handler, modelId, parameters, true);
925        
926        XMLUtils.endElement(handler, "collection");
927    }
928    
929    @SuppressWarnings("unchecked")
930    private void _saxLanguagesValue(ContentHandler handler, Object languages) throws SAXException
931    {
932        if (languages != null)
933        {
934            XMLUtils.startElement(handler, "languages");
935            for (String lang : (List<String>) languages)
936            {
937                XMLUtils.createElement(handler, "value", lang);
938            }
939            XMLUtils.endElement(handler, "languages");
940        }
941    }
942
943    private void _saxCollection(ContentHandler handler, SynchronizableContentsCollection collection) throws SAXException
944    {
945        AttributesImpl atts = new AttributesImpl();
946        atts.addCDATAAttribute("id", collection.getId());
947        XMLUtils.startElement(handler, "collection", atts);
948        
949        collection.getLabel().toSAX(handler, "label");
950        
951        _saxNonNullValue(handler, "contentType", collection.getContentType());
952        _saxNonNullValue(handler, "contentPrefix", collection.getContentPrefix());
953        _saxNonNullValue(handler, "restrictedField", collection.getRestrictedField());
954        
955        _saxNonNullValue(handler, "workflowName", collection.getWorkflowName());
956        _saxNonNullValue(handler, "initialActionId", collection.getInitialActionId());
957        _saxNonNullValue(handler, "synchronizeActionId", collection.getSynchronizeActionId());
958        _saxNonNullValue(handler, "validateActionId", collection.getValidateActionId());
959        _saxNonNullValue(handler, "validateAfterImport", collection.validateAfterImport());
960
961        _saxNonNullValue(handler, "reportMails", collection.getReportMails());
962        _saxNonNullValue(handler, "contentOperator", collection.getSynchronizingContentOperator());
963        _saxNonNullValue(handler, "synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly());
964        _saxNonNullValue(handler, "removalSync", collection.removalSync());
965        
966        _saxLanguagesValue(handler, collection.getLanguages());
967        
968        _saxModel(handler, collection.getSynchronizeCollectionModelId(), collection.getParameterValues(), false);
969        
970        XMLUtils.endElement(handler, "collection");
971    }
972    
973    private void _saxNonNullValue (ContentHandler handler, String tagName, Object value) throws SAXException
974    {
975        if (value != null)
976        {
977            XMLUtils.createElement(handler, tagName, value.toString());
978        }
979    }
980    
981    private void _saxModel(ContentHandler handler, String modelId, Map<String, Object> paramValues, boolean withPrefix) throws SAXException
982    {
983        AttributesImpl atts = new AttributesImpl();
984        atts.addCDATAAttribute("id", modelId);
985        XMLUtils.startElement(handler, "model", atts);
986        
987        SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId);
988        String prefix = withPrefix ? modelId + SCC_PARAMETERS_SEPARATOR : StringUtils.EMPTY;
989        for (ModelItem parameter : model.getModelItems())
990        {
991            String paramFieldName = prefix + parameter.getPath();
992            Object value = paramValues.get(paramFieldName);
993            if (value != null)
994            {
995                atts.clear();
996                atts.addCDATAAttribute("name", parameter.getPath());
997                XMLUtils.createElement(handler, "param", atts, ((ElementDefinition) parameter).getType().toString(value));
998            }
999        }
1000        
1001        XMLUtils.endElement(handler, "model");
1002    }
1003    
1004    @Override
1005    public void dispose()
1006    {
1007        _synchronizableCollections.clear();
1008        _lastFileReading = 0;
1009    }
1010}