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