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