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