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