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.datasource.AbstractDataSourceManager.DataSourceDefinition;
070import org.ametys.core.datasource.LDAPDataSourceManager;
071import org.ametys.core.datasource.SQLDataSourceManager;
072import org.ametys.core.ui.Callable;
073import org.ametys.core.user.directory.UserDirectory;
074import org.ametys.core.user.directory.UserDirectoryFactory;
075import org.ametys.core.user.directory.UserDirectoryModel;
076import org.ametys.core.user.population.UserPopulation;
077import org.ametys.core.user.population.UserPopulationDAO;
078import org.ametys.plugins.contentio.synchronize.impl.DefaultSynchronizingContentOperator;
079import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory;
080import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory;
081import org.ametys.plugins.workflow.support.WorkflowProvider;
082import org.ametys.runtime.i18n.I18nizableText;
083import org.ametys.runtime.model.checker.ItemCheckerTestFailureException;
084import org.ametys.runtime.parameter.Errors;
085import org.ametys.runtime.parameter.Parameter;
086import org.ametys.runtime.parameter.ParameterHelper;
087import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
088import org.ametys.runtime.parameter.Validator;
089import org.ametys.runtime.plugin.component.AbstractLogEnabled;
090import org.ametys.runtime.plugin.component.LogEnabled;
091import org.ametys.runtime.util.AmetysHomeHelper;
092
093/**
094 * DAO for accessing {@link SynchronizableContentsCollection}
095 */
096public class SynchronizableContentsCollectionDAO extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable, Disposable
097{
098    /** Avalon Role */
099    public static final String ROLE = SynchronizableContentsCollectionDAO.class.getName();
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 UserPopulationDAO _userPopulationDAO;
109    private UserDirectoryFactory _userDirectoryFactory;
110    private WorkflowProvider _workflowProvider;
111    private SynchronizingContentOperatorExtensionPoint _synchronizingContentOperatorEP;
112    private LanguagesManager _languagesManager;
113    
114    private ServiceManager _smanager;
115    private Context _context;
116
117    private SQLDataSourceManager _sqlDataSourceManager;
118    private LDAPDataSourceManager _ldapDataSourceManager;
119
120
121    @Override
122    public void initialize() throws Exception
123    {
124        __CONFIGURATION_FILE = new File(AmetysHomeHelper.getAmetysHome(), "config" + File.separator + "synchronizable-collections.xml");
125        _synchronizableCollections = new HashMap<>();
126        _lastFileReading = 0;
127    }
128    
129    @Override
130    public void contextualize(Context context) throws ContextException
131    {
132        _context = context;
133    }
134
135    @Override
136    public void service(ServiceManager smanager) throws ServiceException
137    {
138        _smanager = smanager;
139        _syncCollectionModelEP = (SynchronizeContentsCollectionModelExtensionPoint) smanager.lookup(SynchronizeContentsCollectionModelExtensionPoint.ROLE);
140        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
141        _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE);
142        _userDirectoryFactory = (UserDirectoryFactory) smanager.lookup(UserDirectoryFactory.ROLE);
143        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
144        _synchronizingContentOperatorEP = (SynchronizingContentOperatorExtensionPoint) smanager.lookup(SynchronizingContentOperatorExtensionPoint.ROLE);
145        _languagesManager = (LanguagesManager) smanager.lookup(LanguagesManager.ROLE);
146    }
147    
148    private SQLDataSourceManager _getSQLDataSourceManager()
149    {
150        if (_sqlDataSourceManager == null)
151        {
152            try
153            {
154                _sqlDataSourceManager = (SQLDataSourceManager) _smanager.lookup(SQLDataSourceManager.ROLE);
155            }
156            catch (ServiceException e)
157            {
158                throw new RuntimeException(e);
159            }
160        }
161        
162        return _sqlDataSourceManager;
163    }
164    
165    private LDAPDataSourceManager _getLDAPDataSourceManager()
166    {
167        if (_ldapDataSourceManager == null)
168        {
169            try
170            {
171                _ldapDataSourceManager = (LDAPDataSourceManager) _smanager.lookup(LDAPDataSourceManager.ROLE);
172            }
173            catch (ServiceException e)
174            {
175                throw new RuntimeException(e);
176            }
177        }
178        
179        return _ldapDataSourceManager;
180    }
181    
182    /**
183     * Gets a synchronizable contents collection to JSON format
184     * @param collectionId The id of the synchronizable contents collection to get
185     * @return An object representing a {@link SynchronizableContentsCollection}
186     */
187    @Callable
188    public Map<String, Object> getSynchronizableContentsCollectionAsJson(String collectionId)
189    {
190        return getSynchronizableContentsCollectionAsJson(getSynchronizableContentsCollection(collectionId));
191    }
192    
193    /**
194     * Gets a synchronizable contents collection to JSON format
195     * @param collection The synchronizable contents collection to get
196     * @return An object representing a {@link SynchronizableContentsCollection}
197     */
198    public Map<String, Object> getSynchronizableContentsCollectionAsJson(SynchronizableContentsCollection collection)
199    {
200        Map<String, Object> result = new LinkedHashMap<>();
201        result.put("id", collection.getId());
202        result.put("label", collection.getLabel());
203        
204        String cTypeId = collection.getContentType();
205        result.put("contentTypeId", cTypeId);
206        result.put("contentType", _contentTypeEP.getExtension(cTypeId).getLabel());
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.newInstance();
317            }
318            catch (InstantiationException | IllegalAccessException  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
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 HashMap<>();
360            modelMap.put("id", modelId);
361            modelMap.put("label", model.getLabel());
362            modelMap.put("description", model.getDescription());
363            
364            Map<String, Object> params = new LinkedHashMap<>();
365            for (String paramId : model.getParameters().keySet())
366            {
367                // prefix in case of two parameters from two different models have the same id which can lead to some errors in client-side
368                params.put(modelId + "$" + paramId, ParameterHelper.toJSON(model.getParameters().get(paramId)));
369            }
370            modelMap.put("parameters", params);
371            
372            collectionModels.add(modelMap);
373        }
374        result.put("models", collectionModels);
375        
376        // CONTENT TYPES
377        List<Object> contentTypes = new ArrayList<>();
378        for (String contentTypeId : _contentTypeEP.getExtensionsIds())
379        {
380            ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
381            if (_isValidContentType(contentType))
382            {
383                Map<String, Object> contentTypeMap = new HashMap<>();
384                contentTypeMap.put("value",  contentType.getId());
385                contentTypeMap.put("label", contentType.getLabel());
386                
387                contentTypes.add(contentTypeMap);
388            }
389        }
390        result.put("contentTypes", contentTypes);
391        
392        // LANGUAGES
393        List<Object> languages = new ArrayList<>();
394        Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages();
395        for (String lang : availableLanguages.keySet())
396        {
397            Language language = availableLanguages.get(lang);
398            Map<String, Object> languageMap = new HashMap<>();
399            languageMap.put("value", lang);
400            languageMap.put("label", language.getLabel());
401            languages.add(languageMap);
402        }
403        result.put("languages", languages);
404        
405        // WORKFLOWS
406        List<Object> workflows = new ArrayList<>();
407        String[] workflowNames = _workflowProvider.getAmetysObjectWorkflow().getWorkflowNames();
408        for (String workflowName : workflowNames)
409        {
410            Map<String, Object> workflowMap = new HashMap<>();
411            workflowMap.put("value", workflowName);
412            workflowMap.put("label", new I18nizableText("application", "WORKFLOW_" + workflowName));
413            workflows.add(workflowMap);
414        }
415        result.put("workflows", workflows);
416        
417        // SYNCHRONIZING CONTENT OPERATORS
418        List<Object> operators = new ArrayList<>();
419        for (String operatorId : _synchronizingContentOperatorEP.getExtensionsIds())
420        {
421            Map<String, Object> operatorMap = new HashMap<>();
422            operatorMap.put("value", operatorId);
423            operatorMap.put("label", _synchronizingContentOperatorEP.getExtension(operatorId).getLabel());
424            operators.add(operatorMap);
425        }
426        result.put("contentOperators", operators);
427        result.put("defaultContentOperator", DefaultSynchronizingContentOperator.class.getName());
428        
429        return result;
430    }
431    
432    private boolean _isValidContentType (ContentType cType)
433    {
434        return !cType.isReferenceTable() && !cType.isAbstract() && !cType.isMixin();
435    }
436    
437    /**
438     * Gets the values of the parameters of the given collection
439     * @param collectionId The id of the collection
440     * @return The values of the parameters
441     */
442    @Callable
443    public Map<String, Object> getCollectionParameterValues(String collectionId)
444    {
445        Map<String, Object> result = new LinkedHashMap<>();
446        
447        SynchronizableContentsCollection collection = getSynchronizableContentsCollection(collectionId);
448        if (collection == null)
449        {
450            getLogger().error("The collection of id '{}' does not exist.", collectionId);
451            result.put("error", "unknown");
452            return result;
453        }
454        
455        result.put("id", collectionId);
456        result.put("label", collection.getLabel());
457        String modelId = collection.getSynchronizeCollectionModelId();
458        result.put("modelId", modelId);
459        
460        result.put("contentType", collection.getContentType());
461        result.put("contentPrefix", collection.getContentPrefix());
462        result.put("restrictedField", collection.getRestrictedField());
463        result.put("synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly());
464        result.put("removalSync", collection.removalSync());
465        result.put("validateAfterImport", collection.validateAfterImport());
466        
467        result.put("workflowName", collection.getWorkflowName());
468        result.put("initialActionId", collection.getInitialActionId());
469        result.put("synchronizeActionId", collection.getSynchronizeActionId());
470        result.put("validateActionId", collection.getValidateActionId());
471        
472        result.put("contentOperator", collection.getSynchronizingContentOperator());
473        result.put("reportMails", collection.getReportMails());
474        
475        result.put("languages", collection.getLanguages());
476        
477        Map<String, Object> values = collection.getParameterValues();
478        for (String key : values.keySet())
479        {
480            result.put(modelId + "$" + key, values.get(key));
481        }
482        
483        return result;
484    }
485
486    /**
487     * Gets the supported user directories (i.e. user directories based on a datasource) of the population in a json map
488     * @param populationId The id of the user population
489     * @return the supported user directories (i.e. user directories based on a datasource) of the population in a json map 
490     */
491    @Callable
492    public List<Map<String, Object>> getSupportedUserDirectories(String populationId)
493    {
494        List<Map<String, Object>> result = new ArrayList<>();
495        
496        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId);
497        List<UserDirectory> userDirectories = userPopulation.getUserDirectories();
498        for (String udId : _getDatasourceBasedUserDirectories(userDirectories))
499        {
500            UserDirectory userDirectory = userPopulation.getUserDirectory(udId);
501            String udModelId = userDirectory.getUserDirectoryModelId();
502            UserDirectoryModel udModel = _userDirectoryFactory.getExtension(udModelId);
503            Map<String, Object> udMap = new HashMap<>();
504            
505            udMap.put("id", udId);
506            udMap.put("modelLabel", udModel.getLabel());
507            
508            if (userDirectory instanceof JdbcUserDirectory)
509            {
510                udMap.put("type", "SQL");
511            }
512            else if (userDirectories instanceof LdapUserDirectory)
513            {
514                udMap.put("type", "LDAP");
515            }
516            
517            result.add(udMap);
518        }
519        
520        return result;
521    }
522    
523    private List<String> _getDatasourceBasedUserDirectories(List<UserDirectory> userDirectories)
524    {
525        List<String> ids = new ArrayList<>();
526        for (UserDirectory userDirectory : userDirectories)
527        {
528            if (userDirectory instanceof JdbcUserDirectory || userDirectory instanceof LdapUserDirectory)
529            {
530                ids.add(userDirectory.getId());
531            }
532        }
533        
534        return ids;
535    }
536    
537    private boolean _writeFile()
538    {
539        File backup = _createBackup();
540        boolean errorOccured = false;
541        
542        // Do writing
543        try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE))
544        {
545            TransformerHandler th = _getTransformerHandler(os);
546
547            // sax the config
548            try
549            {
550                th.startDocument();
551                XMLUtils.startElement(th, "collections");
552                
553                _toSAX(th);
554                XMLUtils.endElement(th, "collections");
555                th.endDocument();
556            }
557            catch (Exception e)
558            {
559                getLogger().error("Error when saxing the collections", e);
560                errorOccured = true;
561            }
562        }
563        catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
564        {
565            if (getLogger().isErrorEnabled())
566            {
567                getLogger().error("Error when trying to modify the group directories with the configuration file {}", __CONFIGURATION_FILE, e);
568            }
569        }
570        
571        _restoreBackup(backup, errorOccured);
572        
573        return errorOccured;
574    }
575    
576    private File _createBackup()
577    {
578        File backup = new File(__CONFIGURATION_FILE.getPath() + ".tmp");
579        
580        // Create a backup file
581        try
582        {
583            Files.copy(__CONFIGURATION_FILE.toPath(), backup.toPath());
584        }
585        catch (IOException e)
586        {
587            getLogger().error("Error when creating backup '{}' file", __CONFIGURATION_FILE.toPath(), e);
588        }
589        
590        return backup;
591    }
592    
593    private void _restoreBackup(File backup, boolean errorOccured)
594    {
595        // Restore the file if an error previously occured
596        try
597        {
598            if (errorOccured)
599            {
600                // An error occured, restore the original
601                Files.copy(backup.toPath(), __CONFIGURATION_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING);
602                // Force to reread the file
603                _readFile(true);
604            }
605            Files.deleteIfExists(backup.toPath());
606        }
607        catch (IOException e)
608        {
609            if (getLogger().isErrorEnabled())
610            {
611                getLogger().error("Error when restoring backup '{}' file", __CONFIGURATION_FILE, e);
612            }
613        }
614    }
615    
616    /**
617     * Add a new {@link SynchronizableContentsCollection}
618     * @param values The parameters' values
619     * @return The id of new created collection or null in case of error
620     * @throws ProcessingException if creation failed
621     */
622    @Callable
623    public String addCollection (Map<String, Object> values) throws ProcessingException
624    {
625        getLogger().debug("Add new Collection with values '{}'", values);
626        _readFile(false);
627        
628        String id = _generateUniqueId((String) values.get("label"));
629        
630        try
631        {
632            _addCollection(id, values);
633            return id;
634        }
635        catch (Exception e)
636        {
637            throw new ProcessingException("Failed to add new collection'" + id + "'", e);
638        }
639    }
640    
641    /**
642     * Edit a {@link SynchronizableContentsCollection}
643     * @param id The id of collection to edit
644     * @param values The parameters' values
645     * @return The id of new created collection or null in case of error
646     * @throws ProcessingException if edition failed
647     */
648    @Callable
649    public Map<String, Object> editCollection (String id, Map<String, Object> values) throws ProcessingException
650    {
651        getLogger().debug("Edit Collection with id '{}' and values '{}'", id, values);
652        Map<String, Object> result = new LinkedHashMap<>();
653        
654        SynchronizableContentsCollection collection = _synchronizableCollections.get(id);
655        if (collection == null)
656        {
657            getLogger().error("The collection with id '{}' does not exist, it cannot be edited.", id);
658            result.put("error", "unknown");
659            return result;
660        }
661        else
662        {
663            _synchronizableCollections.remove(id);
664        }
665        
666        try
667        {
668            _addCollection(id, values);
669            result.put("id", id);
670            return result;
671        }
672        catch (Exception e)
673        {
674            throw new ProcessingException("Failed to edit collection of id '" + id + "'", e);
675        }
676    }
677    
678    private boolean _isValid(SynchronizableContentsCollection collection)
679    {
680        // Check validation of a data source on its parameters
681        
682        SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(collection.getSynchronizeCollectionModelId());
683        if (model != null)
684        {
685            for (Parameter<ParameterType> param : model.getParameters().values())
686            {
687                if (!_validateParameter(param, collection))
688                {
689                    // At least one parameter is invalid
690                    return false;
691                }
692                
693                if (param.getType() == ParameterType.DATASOURCE)
694                {
695                    String dataSourceId = (String) collection.getParameterValues().get(param.getId());
696                    
697                    if (!_checkDataSource(dataSourceId))
698                    {
699                        // At least one data source is not valid
700                        return false;
701                    }
702                    
703                }
704            }
705            
706            return true;
707        }
708        
709        return false; // no model found
710    }
711    
712    private boolean _validateParameter(Parameter param, SynchronizableContentsCollection collection)
713    {
714        Validator validator = param.getValidator();
715        if (validator != null)
716        {
717            Object value = collection.getParameterValues().get(param.getId());
718            
719            Errors errors = new Errors();
720            validator.validate(value, errors);
721            
722            return !errors.hasErrors();
723        }
724        
725        return true;
726    }
727    
728    private boolean _checkDataSource(String dataSourceId)
729    {
730        if (dataSourceId != null)
731        {
732            try
733            {
734                DataSourceDefinition def = _getSQLDataSourceManager().getDataSourceDefinition(dataSourceId);
735                
736                if (def != null)
737                {
738                    _getSQLDataSourceManager().checkParameters(def.getParameters());
739                }
740                else
741                {
742                    def = _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId);
743                    if (def != null)
744                    {
745                        _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId);
746                    }
747                    else
748                    {
749                        // The data source was not found
750                        return false;
751                    }
752                }
753            }
754            catch (ItemCheckerTestFailureException e)
755            {
756                // Connection to the SQL data source failed
757                return false;
758            }
759        }
760        
761        return true;
762    }
763    
764    private boolean _addCollection(String id, Map<String, Object> values) throws FileNotFoundException, IOException, TransformerConfigurationException, SAXException
765    {
766        File backup = _createBackup();
767        boolean success = false;
768        
769        // Do writing
770        try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE))
771        {
772            TransformerHandler th = _getTransformerHandler(os);
773
774            // sax the config
775            th.startDocument();
776            XMLUtils.startElement(th, "collections");
777            
778            // SAX already existing collections
779            _toSAX(th);
780            
781            // SAX the new collection
782            _saxCollection(th, id, values);
783            
784            XMLUtils.endElement(th, "collections");
785            th.endDocument();
786            
787            success = true;
788        }
789        
790        _restoreBackup(backup, !success);
791        
792        _readFile(false);
793        
794        return success;
795    }
796    
797    private TransformerHandler _getTransformerHandler(OutputStream os) throws TransformerConfigurationException
798    {
799        // create a transformer for saving sax into a file
800        TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
801        
802        StreamResult result = new StreamResult(os);
803        th.setResult(result);
804
805        // create the format of result
806        Properties format = new Properties();
807        format.put(OutputKeys.METHOD, "xml");
808        format.put(OutputKeys.INDENT, "yes");
809        format.put(OutputKeys.ENCODING, "UTF-8");
810        format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
811        th.getTransformer().setOutputProperties(format);
812        
813        return th;
814    }
815    
816    /**
817     * Removes the given collection
818     * @param id The id of the collection to remove
819     * @return A map containing the id of the removed collection, or an error
820     */
821    @Callable
822    public Map<String, Object> removeCollection(String id)
823    {
824        getLogger().debug("Remove Collection with id '{}'", id);
825        Map<String, Object> result = new LinkedHashMap<>();
826        
827        _readFile(false);
828        if (_synchronizableCollections.remove(id) == null)
829        {
830            getLogger().error("The synchronizable collection with id '{}' does not exist, it cannot be removed.", id);
831            result.put("error", "unknown");
832            return result;
833        }
834        
835        if (_writeFile())
836        {
837            return null;
838        }
839        
840        result.put("id", id);
841        return result;
842    }
843    
844    private String _generateUniqueId(String label)
845    {
846        // Id generated from name lowercased, trimmed, and spaces and underscores replaced by dashes
847        String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", "");
848        int i = 2;
849        String suffixedValue = value;
850        while (_synchronizableCollections.get(suffixedValue) != null)
851        {
852            suffixedValue = value + i;
853            i++;
854        }
855        
856        return suffixedValue;
857    }
858    
859    private void _toSAX(TransformerHandler handler) throws SAXException
860    {
861        for (SynchronizableContentsCollection collection : _synchronizableCollections.values())
862        {
863            _saxCollection(handler, collection);
864        }
865    }
866    
867    private void _saxCollection(ContentHandler handler, String id, Map<String, Object> parameters) throws SAXException
868    {
869        AttributesImpl atts = new AttributesImpl();
870        atts.addCDATAAttribute("id", id);
871        
872        XMLUtils.startElement(handler, "collection", atts);
873        
874        String label = (String) parameters.get("label");
875        if (label != null)
876        {
877            new I18nizableText(label).toSAX(handler, "label");
878        }
879        
880        _saxNonNullValue(handler, "contentType", parameters.get("contentType"));
881        _saxNonNullValue(handler, "contentPrefix", parameters.get("contentPrefix"));
882        _saxNonNullValue(handler, "restrictedField", parameters.get("restrictedField"));
883        _saxNonNullValue(handler, "synchronizeExistingContentsOnly", parameters.get("synchronizeExistingContentsOnly"));
884        _saxNonNullValue(handler, "removalSync", parameters.get("removalSync"));
885        
886        _saxNonNullValue(handler, "workflowName", parameters.get("workflowName"));
887        _saxNonNullValue(handler, "initialActionId", parameters.get("initialActionId"));
888        _saxNonNullValue(handler, "synchronizeActionId", parameters.get("synchronizeActionId"));
889        _saxNonNullValue(handler, "validateActionId", parameters.get("validateActionId"));
890        _saxNonNullValue(handler, "validateAfterImport", parameters.get("validateAfterImport"));
891        
892        _saxNonNullValue(handler, "reportMails", parameters.get("reportMails"));
893        _saxNonNullValue(handler, "contentOperator", parameters.get("contentOperator"));
894        
895        _saxLanguagesValue(handler, parameters.get("languages"));
896        
897        parameters.remove("id");
898        parameters.remove("label");
899        parameters.remove("contentType");
900        parameters.remove("synchronizeExistingContentsOnly");
901        parameters.remove("removalSync");
902        parameters.remove("workflowName");
903        parameters.remove("initialActionId");
904        parameters.remove("synchronizeActionId");
905        parameters.remove("validateActionId");
906        parameters.remove("contentPrefix");
907        parameters.remove("validateAfterImport");
908        parameters.remove("reportMails");
909        parameters.remove("contentOperator");
910        parameters.remove("restrictedField");
911        
912        String modelId = (String) parameters.get("modelId");
913        parameters.remove("modelId");
914        
915        _saxModel(handler, modelId, parameters, true);
916        
917        XMLUtils.endElement(handler, "collection");
918    }
919    
920    @SuppressWarnings("unchecked")
921    private void _saxLanguagesValue(ContentHandler handler, Object languages) throws SAXException
922    {
923        if (languages != null)
924        {
925            XMLUtils.startElement(handler, "languages");
926            for (String lang : (List<String>) languages)
927            {
928                XMLUtils.createElement(handler, "value", lang);
929            }
930            XMLUtils.endElement(handler, "languages");
931        }
932    }
933
934    private void _saxCollection(ContentHandler handler, SynchronizableContentsCollection collection) throws SAXException
935    {
936        AttributesImpl atts = new AttributesImpl();
937        atts.addCDATAAttribute("id", collection.getId());
938        XMLUtils.startElement(handler, "collection", atts);
939        
940        collection.getLabel().toSAX(handler, "label");
941        
942        _saxNonNullValue(handler, "contentType", collection.getContentType());
943        _saxNonNullValue(handler, "contentPrefix", collection.getContentPrefix());
944        _saxNonNullValue(handler, "restrictedField", collection.getRestrictedField());
945        
946        _saxNonNullValue(handler, "workflowName", collection.getWorkflowName());
947        _saxNonNullValue(handler, "initialActionId", collection.getInitialActionId());
948        _saxNonNullValue(handler, "synchronizeActionId", collection.getSynchronizeActionId());
949        _saxNonNullValue(handler, "validateActionId", collection.getValidateActionId());
950        _saxNonNullValue(handler, "validateAfterImport", collection.validateAfterImport());
951
952        _saxNonNullValue(handler, "reportMails", collection.getReportMails());
953        _saxNonNullValue(handler, "contentOperator", collection.getSynchronizingContentOperator());
954        _saxNonNullValue(handler, "synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly());
955        _saxNonNullValue(handler, "removalSync", collection.removalSync());
956        
957        _saxLanguagesValue(handler, collection.getLanguages());
958        
959        _saxModel(handler, collection.getSynchronizeCollectionModelId(), collection.getParameterValues(), false);
960        
961        XMLUtils.endElement(handler, "collection");
962    }
963    
964    private void _saxNonNullValue (ContentHandler handler, String tagName, Object value) throws SAXException
965    {
966        if (value != null)
967        {
968            XMLUtils.createElement(handler, tagName, ParameterHelper.valueToString(value));
969        }
970    }
971    
972    private void _saxModel(ContentHandler handler, String modelId, Map<String, Object> paramValues, boolean withPrefix) throws SAXException
973    {
974        AttributesImpl atts = new AttributesImpl();
975        atts.addCDATAAttribute("id", modelId);
976        XMLUtils.startElement(handler, "model", atts);
977        
978        for (String paramName : paramValues.keySet())
979        {
980            if (!withPrefix || paramName.startsWith(modelId))
981            {
982                Object value = paramValues.get(paramName);
983                if (value != null)
984                {
985                    atts.clear();
986                    atts.addCDATAAttribute("name", withPrefix ? paramName.substring(modelId.length() + 1) : paramName);
987                    XMLUtils.createElement(handler, "param", atts, ParameterHelper.valueToString(value));
988                }
989            }
990        }
991        
992        XMLUtils.endElement(handler, "model");
993    }
994    
995    @Override
996    public void dispose()
997    {
998        _synchronizableCollections.clear();
999        _lastFileReading = 0;
1000    }
1001}