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.WorkflowHelper;
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 WorkflowHelper _workflowHelper;
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        _workflowHelper = (WorkflowHelper) smanager.lookup(WorkflowHelper.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 = _workflowHelper.getWorkflowNames();
413        for (String workflowName : workflowNames)
414        {
415            Map<String, Object> workflowMap = new HashMap<>();
416            workflowMap.put("value", workflowName);
417            workflowMap.put("label", _workflowHelper.getWorkflowLabel(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("ignoreRestrictions", collection.ignoreRestrictions());
471        result.put("validateAfterImport", collection.validateAfterImport());
472        
473        result.put("workflowName", collection.getWorkflowName());
474        result.put("initialActionId", collection.getInitialActionId());
475        result.put("synchronizeActionId", collection.getSynchronizeActionId());
476        result.put("validateActionId", collection.getValidateActionId());
477        
478        result.put("contentOperator", collection.getSynchronizingContentOperator());
479        result.put("reportMails", collection.getReportMails());
480        
481        result.put("languages", collection.getLanguages());
482        
483        Map<String, Object> values = collection.getParameterValues();
484        for (String key : values.keySet())
485        {
486            result.put(modelId + SCC_PARAMETERS_SEPARATOR + key, values.get(key));
487        }
488        
489        return result;
490    }
491
492    /**
493     * Gets the supported user directories (i.e. user directories based on a datasource) of the population in a json map
494     * @param populationId The id of the user population
495     * @return the supported user directories (i.e. user directories based on a datasource) of the population in a json map 
496     */
497    @Callable
498    public List<Map<String, Object>> getSupportedUserDirectories(String populationId)
499    {
500        List<Map<String, Object>> result = new ArrayList<>();
501        
502        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId);
503        List<UserDirectory> userDirectories = userPopulation.getUserDirectories();
504        for (String udId : _getDatasourceBasedUserDirectories(userDirectories))
505        {
506            UserDirectory userDirectory = userPopulation.getUserDirectory(udId);
507            String udModelId = userDirectory.getUserDirectoryModelId();
508            UserDirectoryModel udModel = _userDirectoryFactory.getExtension(udModelId);
509            Map<String, Object> udMap = new HashMap<>();
510            
511            udMap.put("id", udId);
512            udMap.put("modelLabel", udModel.getLabel());
513            
514            if (userDirectory instanceof JdbcUserDirectory)
515            {
516                udMap.put("type", "SQL");
517            }
518            else if (userDirectories instanceof LdapUserDirectory)
519            {
520                udMap.put("type", "LDAP");
521            }
522            
523            result.add(udMap);
524        }
525        
526        return result;
527    }
528    
529    private List<String> _getDatasourceBasedUserDirectories(List<UserDirectory> userDirectories)
530    {
531        List<String> ids = new ArrayList<>();
532        for (UserDirectory userDirectory : userDirectories)
533        {
534            if (userDirectory instanceof JdbcUserDirectory || userDirectory instanceof LdapUserDirectory)
535            {
536                ids.add(userDirectory.getId());
537            }
538        }
539        
540        return ids;
541    }
542    
543    private boolean _writeFile()
544    {
545        File backup = _createBackup();
546        boolean errorOccured = false;
547        
548        // Do writing
549        try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE))
550        {
551            TransformerHandler th = _getTransformerHandler(os);
552
553            // sax the config
554            try
555            {
556                th.startDocument();
557                XMLUtils.startElement(th, "collections");
558                
559                _toSAX(th);
560                XMLUtils.endElement(th, "collections");
561                th.endDocument();
562            }
563            catch (Exception e)
564            {
565                getLogger().error("Error when saxing the collections", e);
566                errorOccured = true;
567            }
568        }
569        catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
570        {
571            if (getLogger().isErrorEnabled())
572            {
573                getLogger().error("Error when trying to modify the group directories with the configuration file {}", __CONFIGURATION_FILE, e);
574            }
575        }
576        
577        _restoreBackup(backup, errorOccured);
578        
579        return errorOccured;
580    }
581    
582    private File _createBackup()
583    {
584        File backup = new File(__CONFIGURATION_FILE.getPath() + ".tmp");
585        
586        // Create a backup file
587        try
588        {
589            Files.copy(__CONFIGURATION_FILE.toPath(), backup.toPath());
590        }
591        catch (IOException e)
592        {
593            getLogger().error("Error when creating backup '{}' file", __CONFIGURATION_FILE.toPath(), e);
594        }
595        
596        return backup;
597    }
598    
599    private void _restoreBackup(File backup, boolean errorOccured)
600    {
601        // Restore the file if an error previously occured
602        try
603        {
604            if (errorOccured)
605            {
606                // An error occured, restore the original
607                Files.copy(backup.toPath(), __CONFIGURATION_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING);
608                // Force to reread the file
609                _readFile(true);
610            }
611            Files.deleteIfExists(backup.toPath());
612        }
613        catch (IOException e)
614        {
615            if (getLogger().isErrorEnabled())
616            {
617                getLogger().error("Error when restoring backup '{}' file", __CONFIGURATION_FILE, e);
618            }
619        }
620    }
621    
622    /**
623     * Add a new {@link SynchronizableContentsCollection}
624     * @param values The parameters' values
625     * @return The id of new created collection or null in case of error
626     * @throws ProcessingException if creation failed
627     */
628    @Callable
629    public String addCollection (Map<String, Object> values) throws ProcessingException
630    {
631        getLogger().debug("Add new Collection with values '{}'", values);
632        _readFile(false);
633        
634        String id = _generateUniqueId((String) values.get("label"));
635        
636        try
637        {
638            _addCollection(id, values);
639            return id;
640        }
641        catch (Exception e)
642        {
643            throw new ProcessingException("Failed to add new collection'" + id + "'", e);
644        }
645    }
646    
647    /**
648     * Edit a {@link SynchronizableContentsCollection}
649     * @param id The id of collection to edit
650     * @param values The parameters' values
651     * @return The id of new created collection or null in case of error
652     * @throws ProcessingException if edition failed
653     */
654    @Callable
655    public Map<String, Object> editCollection (String id, Map<String, Object> values) throws ProcessingException
656    {
657        getLogger().debug("Edit Collection with id '{}' and values '{}'", id, values);
658        Map<String, Object> result = new LinkedHashMap<>();
659        
660        SynchronizableContentsCollection collection = _synchronizableCollections.get(id);
661        if (collection == null)
662        {
663            getLogger().error("The collection with id '{}' does not exist, it cannot be edited.", id);
664            result.put("error", "unknown");
665            return result;
666        }
667        else
668        {
669            _synchronizableCollections.remove(id);
670        }
671        
672        try
673        {
674            _addCollection(id, values);
675            result.put("id", id);
676            return result;
677        }
678        catch (Exception e)
679        {
680            throw new ProcessingException("Failed to edit collection of id '" + id + "'", e);
681        }
682    }
683    
684    private boolean _isValid(SynchronizableContentsCollection collection)
685    {
686        // Check validation of a data source on its parameters
687        
688        SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(collection.getSynchronizeCollectionModelId());
689        if (model != null)
690        {
691            for (ModelItem param : model.getModelItems())
692            {
693                if (!_validateParameter(param, collection))
694                {
695                    // At least one parameter is invalid
696                    return false;
697                }
698                
699                if (ModelItemTypeConstants.DATASOURCE_ELEMENT_TYPE_ID.equals(param.getType().getId()))
700                {
701                    String dataSourceId = (String) collection.getParameterValues().get(param.getPath());
702                    
703                    if (!_checkDataSource(dataSourceId))
704                    {
705                        // At least one data source is not valid
706                        return false;
707                    }
708                    
709                }
710            }
711            
712            return true;
713        }
714        
715        return false; // no model found
716    }
717    
718    private boolean _validateParameter(ModelItem modelItem, SynchronizableContentsCollection collection)
719    {
720        if (modelItem instanceof ElementDefinition)
721        {
722            ElementDefinition param = (ElementDefinition) modelItem;
723            Validator validator = param.getValidator();
724            if (validator != null)
725            {
726                Object value = collection.getParameterValues().get(param.getPath());
727                
728                Errors errors = new Errors();
729                validator.validate(value, errors);
730                
731                return !errors.hasErrors();
732            }
733        }
734        
735        return true;
736    }
737    
738    private boolean _checkDataSource(String dataSourceId)
739    {
740        if (dataSourceId != null)
741        {
742            try
743            {
744                DataSourceDefinition def = _getSQLDataSourceManager().getDataSourceDefinition(dataSourceId);
745                
746                if (def != null)
747                {
748                    _getSQLDataSourceManager().checkParameters(def.getParameters());
749                }
750                else
751                {
752                    def = _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId);
753                    if (def != null)
754                    {
755                        _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId);
756                    }
757                    else
758                    {
759                        // The data source was not found
760                        return false;
761                    }
762                }
763            }
764            catch (ItemCheckerTestFailureException e)
765            {
766                // Connection to the SQL data source failed
767                return false;
768            }
769        }
770        
771        return true;
772    }
773    
774    private boolean _addCollection(String id, Map<String, Object> values) throws FileNotFoundException, IOException, TransformerConfigurationException, SAXException
775    {
776        File backup = _createBackup();
777        boolean success = false;
778        
779        // Do writing
780        try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE))
781        {
782            TransformerHandler th = _getTransformerHandler(os);
783
784            // sax the config
785            th.startDocument();
786            XMLUtils.startElement(th, "collections");
787            
788            // SAX already existing collections
789            _toSAX(th);
790            
791            // SAX the new collection
792            _saxCollection(th, id, values);
793            
794            XMLUtils.endElement(th, "collections");
795            th.endDocument();
796            
797            success = true;
798        }
799        
800        _restoreBackup(backup, !success);
801        
802        _readFile(false);
803        
804        return success;
805    }
806    
807    private TransformerHandler _getTransformerHandler(OutputStream os) throws TransformerConfigurationException
808    {
809        // create a transformer for saving sax into a file
810        TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
811        
812        StreamResult result = new StreamResult(os);
813        th.setResult(result);
814
815        // create the format of result
816        Properties format = new Properties();
817        format.put(OutputKeys.METHOD, "xml");
818        format.put(OutputKeys.INDENT, "yes");
819        format.put(OutputKeys.ENCODING, "UTF-8");
820        format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
821        th.getTransformer().setOutputProperties(format);
822        
823        return th;
824    }
825    
826    /**
827     * Removes the given collection
828     * @param id The id of the collection to remove
829     * @return A map containing the id of the removed collection, or an error
830     */
831    @Callable
832    public Map<String, Object> removeCollection(String id)
833    {
834        getLogger().debug("Remove Collection with id '{}'", id);
835        Map<String, Object> result = new LinkedHashMap<>();
836        
837        _readFile(false);
838        if (_synchronizableCollections.remove(id) == null)
839        {
840            getLogger().error("The synchronizable collection with id '{}' does not exist, it cannot be removed.", id);
841            result.put("error", "unknown");
842            return result;
843        }
844        
845        if (_writeFile())
846        {
847            return null;
848        }
849        
850        result.put("id", id);
851        return result;
852    }
853    
854    private String _generateUniqueId(String label)
855    {
856        // Id generated from name lowercased, trimmed, and spaces and underscores replaced by dashes
857        String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", "");
858        int i = 2;
859        String suffixedValue = value;
860        while (_synchronizableCollections.get(suffixedValue) != null)
861        {
862            suffixedValue = value + i;
863            i++;
864        }
865        
866        return suffixedValue;
867    }
868    
869    private void _toSAX(TransformerHandler handler) throws SAXException
870    {
871        for (SynchronizableContentsCollection collection : _synchronizableCollections.values())
872        {
873            _saxCollection(handler, collection);
874        }
875    }
876    
877    private void _saxCollection(ContentHandler handler, String id, Map<String, Object> parameters) throws SAXException
878    {
879        AttributesImpl atts = new AttributesImpl();
880        atts.addCDATAAttribute("id", id);
881        
882        XMLUtils.startElement(handler, "collection", atts);
883        
884        String label = (String) parameters.get("label");
885        if (label != null)
886        {
887            new I18nizableText(label).toSAX(handler, "label");
888        }
889        
890        _saxNonNullValue(handler, "contentType", parameters.get("contentType"));
891        _saxNonNullValue(handler, "contentPrefix", parameters.get("contentPrefix"));
892        _saxNonNullValue(handler, "restrictedField", parameters.get("restrictedField"));
893        _saxNonNullValue(handler, "synchronizeExistingContentsOnly", parameters.get("synchronizeExistingContentsOnly"));
894        _saxNonNullValue(handler, "removalSync", parameters.get("removalSync"));
895        _saxNonNullValue(handler, "ignoreRestrictions", parameters.get("ignoreRestrictions"));
896        
897        _saxNonNullValue(handler, "workflowName", parameters.get("workflowName"));
898        _saxNonNullValue(handler, "initialActionId", parameters.get("initialActionId"));
899        _saxNonNullValue(handler, "synchronizeActionId", parameters.get("synchronizeActionId"));
900        _saxNonNullValue(handler, "validateActionId", parameters.get("validateActionId"));
901        _saxNonNullValue(handler, "validateAfterImport", parameters.get("validateAfterImport"));
902        
903        _saxNonNullValue(handler, "reportMails", parameters.get("reportMails"));
904        _saxNonNullValue(handler, "contentOperator", parameters.get("contentOperator"));
905        
906        _saxLanguagesValue(handler, parameters.get("languages"));
907        
908        parameters.remove("id");
909        parameters.remove("label");
910        parameters.remove("contentType");
911        parameters.remove("synchronizeExistingContentsOnly");
912        parameters.remove("removalSync");
913        parameters.remove("ignoreRestrictions");
914        parameters.remove("workflowName");
915        parameters.remove("initialActionId");
916        parameters.remove("synchronizeActionId");
917        parameters.remove("validateActionId");
918        parameters.remove("contentPrefix");
919        parameters.remove("validateAfterImport");
920        parameters.remove("reportMails");
921        parameters.remove("contentOperator");
922        parameters.remove("restrictedField");
923        
924        String modelId = (String) parameters.get("modelId");
925        parameters.remove("modelId");
926        
927        _saxModel(handler, modelId, parameters, true);
928        
929        XMLUtils.endElement(handler, "collection");
930    }
931    
932    @SuppressWarnings("unchecked")
933    private void _saxLanguagesValue(ContentHandler handler, Object languages) throws SAXException
934    {
935        if (languages != null)
936        {
937            XMLUtils.startElement(handler, "languages");
938            for (String lang : (List<String>) languages)
939            {
940                XMLUtils.createElement(handler, "value", lang);
941            }
942            XMLUtils.endElement(handler, "languages");
943        }
944    }
945
946    private void _saxCollection(ContentHandler handler, SynchronizableContentsCollection collection) throws SAXException
947    {
948        AttributesImpl atts = new AttributesImpl();
949        atts.addCDATAAttribute("id", collection.getId());
950        XMLUtils.startElement(handler, "collection", atts);
951        
952        collection.getLabel().toSAX(handler, "label");
953        
954        _saxNonNullValue(handler, "contentType", collection.getContentType());
955        _saxNonNullValue(handler, "contentPrefix", collection.getContentPrefix());
956        _saxNonNullValue(handler, "restrictedField", collection.getRestrictedField());
957        
958        _saxNonNullValue(handler, "workflowName", collection.getWorkflowName());
959        _saxNonNullValue(handler, "initialActionId", collection.getInitialActionId());
960        _saxNonNullValue(handler, "synchronizeActionId", collection.getSynchronizeActionId());
961        _saxNonNullValue(handler, "validateActionId", collection.getValidateActionId());
962        _saxNonNullValue(handler, "validateAfterImport", collection.validateAfterImport());
963
964        _saxNonNullValue(handler, "reportMails", collection.getReportMails());
965        _saxNonNullValue(handler, "contentOperator", collection.getSynchronizingContentOperator());
966        _saxNonNullValue(handler, "synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly());
967        _saxNonNullValue(handler, "removalSync", collection.removalSync());
968        _saxNonNullValue(handler, "ignoreRestrictions", collection.ignoreRestrictions());
969        
970        _saxLanguagesValue(handler, collection.getLanguages());
971        
972        _saxModel(handler, collection.getSynchronizeCollectionModelId(), collection.getParameterValues(), false);
973        
974        XMLUtils.endElement(handler, "collection");
975    }
976    
977    private void _saxNonNullValue (ContentHandler handler, String tagName, Object value) throws SAXException
978    {
979        if (value != null)
980        {
981            XMLUtils.createElement(handler, tagName, value.toString());
982        }
983    }
984    
985    private void _saxModel(ContentHandler handler, String modelId, Map<String, Object> paramValues, boolean withPrefix) throws SAXException
986    {
987        AttributesImpl atts = new AttributesImpl();
988        atts.addCDATAAttribute("id", modelId);
989        XMLUtils.startElement(handler, "model", atts);
990        
991        SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId);
992        String prefix = withPrefix ? modelId + SCC_PARAMETERS_SEPARATOR : StringUtils.EMPTY;
993        for (ModelItem parameter : model.getModelItems())
994        {
995            String paramFieldName = prefix + parameter.getPath();
996            Object value = paramValues.get(paramFieldName);
997            if (value != null)
998            {
999                atts.clear();
1000                atts.addCDATAAttribute("name", parameter.getPath());
1001                XMLUtils.createElement(handler, "param", atts, ((ElementDefinition) parameter).getType().toString(value));
1002            }
1003        }
1004        
1005        XMLUtils.endElement(handler, "model");
1006    }
1007    
1008    @Override
1009    public void dispose()
1010    {
1011        _synchronizableCollections.clear();
1012        _lastFileReading = 0;
1013    }
1014}