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