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