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