001/*
002 *  Copyright 2018 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.runtime.config;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.OutputStream;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Properties;
030
031import javax.xml.transform.OutputKeys;
032import javax.xml.transform.TransformerFactory;
033import javax.xml.transform.TransformerFactoryConfigurationError;
034import javax.xml.transform.sax.SAXTransformerFactory;
035import javax.xml.transform.sax.TransformerHandler;
036import javax.xml.transform.stream.StreamResult;
037
038import org.apache.avalon.framework.activity.Initializable;
039import org.apache.avalon.framework.configuration.ConfigurationException;
040import org.apache.avalon.framework.context.Context;
041import org.apache.avalon.framework.context.Contextualizable;
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.avalon.framework.service.Serviceable;
045import org.apache.cocoon.xml.XMLUtils;
046import org.apache.commons.lang3.StringUtils;
047import org.apache.xml.serializer.OutputPropertiesFactory;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050import org.xml.sax.SAXException;
051
052import org.ametys.core.util.I18nUtils;
053import org.ametys.core.util.I18nizableTextComparator;
054import org.ametys.core.util.I18nizableTextKeyComparator;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.model.CategorizedElementDefinitionHelper;
057import org.ametys.runtime.model.DefinitionAndValue;
058import org.ametys.runtime.model.ElementDefinition;
059import org.ametys.runtime.model.Enumerator;
060import org.ametys.runtime.model.I18nizableTextModelItemComparator;
061import org.ametys.runtime.model.Model;
062import org.ametys.runtime.model.ModelHelper;
063import org.ametys.runtime.model.ModelItem;
064import org.ametys.runtime.model.ModelItemGroup;
065import org.ametys.runtime.model.View;
066import org.ametys.runtime.model.checker.ItemChecker;
067import org.ametys.runtime.model.disableconditions.DefaultDisableConditionsEvaluator;
068import org.ametys.runtime.model.disableconditions.DisableConditions;
069import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator;
070import org.ametys.runtime.model.exception.UndefinedItemPathException;
071import org.ametys.runtime.model.type.ElementType;
072import org.ametys.runtime.model.type.ModelItemTypeConstants;
073import org.ametys.runtime.model.type.ModelItemTypeExtensionPoint;
074import org.ametys.runtime.model.type.xml.XMLElementType;
075import org.ametys.runtime.parameter.ValidationResult;
076import org.ametys.runtime.parameter.Validator;
077import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
078
079/**
080 * This manager handle the parameters of the application that have to be stored by the plugins.
081 */
082public final class ConfigManager implements Model, Contextualizable, Serviceable, Initializable
083{
084    // shared instance
085    private static ConfigManager __manager;
086    
087    private static final String __GENERAL_INFORMATIONS_I18N_KEY = "PLUGINS_CORE_CONFIG_CATEGORY_GENERAL";
088 
089    // Logger for traces
090    private Logger _logger = LoggerFactory.getLogger(ConfigManager.class);
091    private Logger _threadSafeComponentLogger = LoggerFactory.getLogger("runtime.plugin.threadsafecomponent");
092
093    // Avalon stuff
094    private ServiceManager _serviceManager;
095    private Context _context;
096    
097    // ComponentManager for the disable conditions
098    private ThreadSafeComponentManager<DisableConditions> _disableConditionsManager;
099    // ComponentManager for the validators
100    private ThreadSafeComponentManager<Validator> _validatorManager;
101    // ComponentManager for the enumerators
102    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
103    // ComponentManager for the parameter checkers
104    private ThreadSafeComponentManager<ItemChecker> _parameterCheckerManager;
105    
106    private Map<String, ConfigParameterInfo> _declaredParams;
107    private Map<String, ConfigParameterInfo> _declaredParamCheckers;
108    private Collection<String> _usedParamIds;
109    private Map<String, ConfigParameterDefinitionWrapper> _configParameterWrappers;
110    private Map<String, ConfigParameterCheckerDescriptor> _parameterCheckers;
111    
112    // The configuration model items
113    private List<ModelItem> _categorizedDefinitions;
114    private Map<String, ElementDefinition> _flatDefinitions;
115    
116    // Determines if the extension point is initialized
117    private boolean _isInitialized;
118    // Determines if all parameters are valued
119    private boolean _isComplete;
120    
121    private ModelItemTypeExtensionPoint _configParameterTypeEP;
122    private DisableConditionsEvaluator _disableConditionsEvaluator;
123    
124    private ConfigManager()
125    {
126        // empty constructor
127    }
128
129    /**
130     * Returns the shared instance of the ConfigManager
131     * @return the shared instance of the ConfigManager
132     */
133    public static ConfigManager getInstance()
134    {
135        if (__manager == null)
136        {
137            __manager = new ConfigManager();
138        }
139
140        return __manager;
141    }
142
143    @Override
144    public void contextualize(Context context)
145    {
146        _context = context;
147    }
148    
149    @Override
150    public void service(ServiceManager manager) throws ServiceException
151    {
152        _serviceManager = manager;
153        _configParameterTypeEP = (ModelItemTypeExtensionPoint) _serviceManager.lookup(ModelItemTypeExtensionPoint.ROLE_CONFIG);
154        _disableConditionsEvaluator = (DisableConditionsEvaluator) _serviceManager.lookup(DefaultDisableConditionsEvaluator.ROLE);
155    }
156    
157    @Override
158    public void initialize()
159    {
160        _usedParamIds = new ArrayList<>();
161        _declaredParams = new HashMap<>();
162        _configParameterWrappers = new HashMap<>();
163        _declaredParamCheckers = new HashMap<>();
164        _parameterCheckers = new HashMap<>();
165        _flatDefinitions = new HashMap<>();
166        
167        _disableConditionsManager = new ThreadSafeComponentManager<>();
168        _disableConditionsManager.setLogger(_threadSafeComponentLogger);
169        _disableConditionsManager.contextualize(_context);
170        _disableConditionsManager.service(_serviceManager);
171        
172        _validatorManager = new ThreadSafeComponentManager<>();
173        _validatorManager.setLogger(_threadSafeComponentLogger);
174        _validatorManager.contextualize(_context);
175        _validatorManager.service(_serviceManager);
176        
177        _enumeratorManager = new ThreadSafeComponentManager<>();
178        _enumeratorManager.setLogger(_threadSafeComponentLogger);
179        _enumeratorManager.contextualize(_context);
180        _enumeratorManager.service(_serviceManager);
181        
182        _parameterCheckerManager = new ThreadSafeComponentManager<>();
183        _parameterCheckerManager.setLogger(_threadSafeComponentLogger);
184        _parameterCheckerManager.contextualize(_context);
185        _parameterCheckerManager.service(_serviceManager);
186    }
187    
188    /**
189     * Registers new available parameters.
190     * The addFeatureConfig() method allows to select which ones are actually useful.
191     * @param pluginName the name of the plugin defining the parameters
192     * @param parameters the configuration parameters definition
193     * @param parameterCheckers the parameters checkers definition
194     */
195    public void addPluginConfig(String pluginName, Map<String, ConfigParameterInfo> parameters, Map<String, ConfigParameterInfo> parameterCheckers)
196    {
197        _logger.debug("Adding parameters and parameters checkers for plugin {}.", pluginName);
198
199        // declare parameters and parameter checkers configured in the plugin
200        _declareParameters(parameters);
201        _declareParameterCheckers(parameterCheckers);
202    }
203
204    /**
205     * Registers a new parameter or references a globalConfig parameter.<br>
206     * @param featureId the id of the feature defining the parameters
207     * @param parameters the configuration parameters definition
208     * @param parameterCheckers the parameters checkers definition
209     * @param parameterReferences references to already defined parameters
210     */
211    public void addFeatureConfig(String featureId, Map<String, ConfigParameterInfo> parameters, Map<String, ConfigParameterInfo> parameterCheckers, Collection<String> parameterReferences)
212    {
213        _logger.debug("Selecting parameters for feature {}.", featureId);
214        
215        // declare parameters and parameter checkers configured in the feature
216        _declareParameters(parameters);
217        _declareParameterCheckers(parameterCheckers);
218
219        // Add parameters declared in feature to the list of used parameters
220        _usedParamIds.addAll(parameters.keySet());
221        
222        // Add referenced parameters to the list of used parameters
223        _usedParamIds.addAll(parameterReferences);
224    }
225    
226    private void _declareParameters(Map<String, ConfigParameterInfo> parameters)
227    {
228        for (String id : parameters.keySet())
229        {
230            ConfigParameterInfo info = parameters.get(id);
231            
232            // Check if the parameter is not already declared
233            if (_declaredParams.containsKey(id))
234            {
235                throw new IllegalArgumentException("The config parameter '" + id + "' is already declared. Parameters ids must be unique");
236            }
237
238            // Add the new parameter to the list of declared ones
239            _declaredParams.put(id, info);
240
241            _logger.debug("Parameter added: {}", id);
242        }
243        
244        _logger.debug("{} parameter(s) added", parameters.size());
245    }
246    
247    private void _declareParameterCheckers(Map<String, ConfigParameterInfo> parameterCheckers)
248    {
249        for (String id : parameterCheckers.keySet())
250        {
251            ConfigParameterInfo info = parameterCheckers.get(id);
252            
253            // Check if the parameter checker is not already declared
254            if (_declaredParamCheckers.containsKey(id))
255            {
256                throw new IllegalArgumentException("The config parameter checker '" + id + "' is already declared. Parameter checkers ids must be unique.");
257            }
258
259            // Add the new parameter checker to the list of declared ones
260            _declaredParamCheckers.put(id, info);
261
262            _logger.debug("Parameter checker added: {}", id);
263        }
264        
265        _logger.debug("{} parameter checker(s) added", parameterCheckers.size());
266    }
267
268    /**
269     * Ends the initialization of the config parameters, by checking against the
270     * already valued parameters.<br>
271     * If at least one parameter has no value, the application won't start.
272     */
273    public void parseAndValidate()
274    {
275        _logger.debug("Initialization");
276
277        _isInitialized = false;
278        _isComplete = true;
279
280        ConfigParameterDefinitionParser parser = new ConfigParameterDefinitionParser(_configParameterTypeEP, _disableConditionsManager, _enumeratorManager, _validatorManager);
281        _parseParameters(parser);
282        
283        ConfigParameterCheckerParser parameterCheckerParser = new ConfigParameterCheckerParser(_parameterCheckerManager);
284        _parseParameterCheckers(parameterCheckerParser);
285        
286        _categorizeParameters();
287
288        try
289        {
290            parser.lookupComponents();
291            parameterCheckerParser.lookupComponents();
292        }
293        catch (Exception e)
294        {
295            throw new RuntimeException("Unable to lookup parameter local components", e);
296        }
297        
298        _validateParametersForReading();
299
300        _usedParamIds.clear();
301        _declaredParams.clear();
302        _declaredParamCheckers.clear();
303        
304        _isInitialized = true;
305
306        Config.setInitialized(_isComplete);
307        
308        _logger.debug("Initialization ended");
309    }
310
311    private void _parseParameters(ConfigParameterDefinitionParser definitionParser)
312    {
313        ConfigParameterDefinitionWrapperParser parser = new ConfigParameterDefinitionWrapperParser(definitionParser);
314        for (String id : _usedParamIds)
315        {
316            // Check if the parameter is not already used
317            if (_configParameterWrappers.get(id) == null)
318            {
319                // Move the parameter from the unused list, to the used list
320                ConfigParameterInfo parameterInfo = _declaredParams.get(id);
321                
322                if (parameterInfo == null)
323                {
324                    throw new RuntimeException("The parameter '" + id + "' is used but not declared");
325                }
326                
327                ConfigParameterDefinitionWrapper configParameterWrapper = null;
328
329                try
330                {
331                    configParameterWrapper = parser.parse(_serviceManager, parameterInfo.getPluginName(), parameterInfo.getConfiguration(), getInstance(), null);
332                }
333                catch (ConfigurationException ex)
334                {
335                    throw new RuntimeException("Unable to configure the config parameter : " + id, ex);
336                }
337                
338                _configParameterWrappers.put(id, configParameterWrapper);
339            }
340        }
341    }
342
343    private void _parseParameterCheckers(ConfigParameterCheckerParser parameterCheckerParser)
344    {
345        for (String id : _declaredParamCheckers.keySet())
346        {
347            boolean invalidParameters = false;
348            
349            // Check if the parameter checker is not already used
350            if (_parameterCheckers.get(id) == null)
351            {
352                ConfigParameterInfo info = _declaredParamCheckers.get(id);
353                
354                ConfigParameterCheckerDescriptor parameterChecker = null;
355                try
356                {
357                    
358                    parameterChecker = parameterCheckerParser.parseParameterChecker(info.getPluginName(), info.getConfiguration());
359                }
360                catch (ConfigurationException ex)
361                {
362                    throw new RuntimeException("Unable to configure the parameter checker: " + id, ex);
363                }
364                
365                for (String linkedParameterPath : parameterChecker.getLinkedParamsPaths())
366                {
367                    ConfigParameterDefinitionWrapper linkedParameter = null;
368
369                    // Linked parameters can be declared with an absolute path, in which case they are prefixed with '/
370                    if (linkedParameterPath.startsWith(ModelItem.ITEM_PATH_SEPARATOR))
371                    {
372                        linkedParameter = _configParameterWrappers.get(linkedParameterPath.substring(ModelItem.ITEM_PATH_SEPARATOR.length()));
373                    }
374                    else
375                    {
376                        linkedParameter = _configParameterWrappers.get(linkedParameterPath);
377                    }
378                    
379                    // If at least one parameter used is invalid, the parameter checker is invalidated
380                    if (linkedParameter == null)
381                    {
382                        invalidParameters = true;
383                        break;
384                    }
385                }
386                
387                if (invalidParameters)
388                {
389                    _logger.debug("All the configuration parameters associated to the parameter checker '{}' are not used.\nThis parameter checker will be ignored.", parameterChecker.getName());
390                }
391                else
392                {
393                    _parameterCheckers.put(id, parameterChecker);
394                }
395            }
396        }
397    }
398    
399    private void _categorizeParameters()
400    {
401        Collection<ConfigParameterDefinitionWrapper> categorizedParameterProxiesValues = _configParameterWrappers.values();
402        _categorizedDefinitions = ConfigParameterDefinitionHelper.categorizeConfigParameters(categorizedParameterProxiesValues);
403        _flatDefinitions = ConfigParameterDefinitionHelper.getFlatDefinitions(categorizedParameterProxiesValues);
404        
405        // Add parameter checkers to categories, groups and element definitions
406        _addParameterCheckersToModelItems();
407    }
408
409    private void _addParameterCheckersToModelItems()
410    {
411        for (ConfigParameterCheckerDescriptor parameterChecker: _parameterCheckers.values())
412        {
413            I18nizableText uiCategory = parameterChecker.getUiRefCategory();
414            if (uiCategory != null)
415            {
416                ModelItemGroup category = _getModelItemGroup(_categorizedDefinitions, uiCategory);
417                if (category == null)
418                {
419                    _logger.warn("The category {} doesn't exist, thus the parameter checker {} will not be added.", uiCategory, parameterChecker.getName());
420                }
421                else
422                {
423                    I18nizableText uiGroup = parameterChecker.getUiRefGroup();
424                    if (uiGroup == null)
425                    {
426                        category.addItemChecker(parameterChecker);
427                    }
428                    else
429                    {
430                        ModelItemGroup group = _getModelItemGroup(category.getChildren(), uiGroup);
431                        if (group == null)
432                        {
433                            _logger.warn("The group {} doesn't exist,  thus the parameter checker {} will not be added.", uiGroup, parameterChecker.getName());
434                        }
435                        else
436                        {
437                            group.addItemChecker(parameterChecker);
438                        }
439                    }
440                }
441            }
442            else
443            {
444                String uiParameterId = parameterChecker.getUiRefParamId();
445                if (uiParameterId != null)
446                {
447                    ElementDefinition definition = _flatDefinitions.get(uiParameterId);
448                    if (definition == null)
449                    {
450                        _logger.warn("The parameter {} doesn't exist, thus the parameter checker {} will not be added.", uiParameterId, parameterChecker.getName());
451                    }
452                    else
453                    {
454                        definition.addItemChecker(parameterChecker);
455                    }
456                }
457            }
458        }
459    }
460
461    private ModelItemGroup _getModelItemGroup(List<ModelItem> items, I18nizableText itemGroupLabel)
462    {
463        for (ModelItem item : items)
464        {
465            if (itemGroupLabel.equals(item.getLabel()) && item instanceof ModelItemGroup)
466            {
467                return (ModelItemGroup) item;
468            }
469        }
470        
471        return null;
472    }
473    
474    private void _validateParametersForReading()
475    {
476        // Dispose potential previous parameters
477        Config.dispose();
478        
479        // Get configuration values
480        Map<String, Object> values = null;
481        try
482        {
483            Config.setModel(this);
484            Map<String, DefinitionAndValue> definitionAndValues = Config.__read();
485            values = Config.__extractValues(definitionAndValues);
486        }
487        catch (Exception e)
488        {
489            _logger.error("Cannot read the configuration file.", e);
490            _isComplete = false;
491        }
492        
493        if (_isComplete && values != null)
494        {
495            for (ElementDefinition definition : _flatDefinitions.values())
496            {
497                boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(definition, values);
498                boolean isDisabled = _disableConditionsEvaluator.evaluateDisableConditions(definition, definition.getName(), values);
499                
500                if (isGroupSwitchOn && !isDisabled)
501                {
502                    if (!values.containsKey(definition.getName()))
503                    {
504                        _logger.warn("The parameter '" + definition.getName() + "' is not valued. Configuration is not initialized.");
505                        _isComplete = false;
506                    }
507                    else
508                    {
509                        Object value = values.get(definition.getName());
510                        ValidationResult result = ModelHelper.validateValue(definition, value);
511                        if (result.hasErrors())
512                        {
513                            if (_logger.isWarnEnabled())
514                            {
515                                StringBuilder sb = new StringBuilder("The parameter '" + definition.getName() + "' is not valid with value '" + value + "' :");
516                                
517                                for (I18nizableText error : result.getErrors())
518                                {
519                                    sb.append("\n* " + error.toString());
520                                }
521                                sb.append("\nConfiguration is not initialized");
522                                
523                                _logger.warn(sb.toString());
524                            }
525                            
526                            _isComplete = false;
527                        }
528                    }
529                    
530                }
531            }
532            
533        }
534    }
535    
536    /**
537     * Update the configuration file with the given values<br>
538     * Values are untyped (all are of type String) and might be null.
539     * @param values A map (key, value).
540     * @param fileName the configuration file absolute path
541     * @return errors The fields in error
542     * @throws Exception If an error occurred while saving values
543     */
544    public Map<String, List<I18nizableText>> save(Map<String, Object> values, String fileName) throws Exception
545    {
546        // Retrieve the old values for password purposes
547        Map<String, Object> oldValues = getOldValues();
548        
549        // Resolve each value and associate it to its definition
550        Map<String, Object> resolvedValues = _resolveValues(values, oldValues);
551        
552        // Validate parameters
553        Map<String, List<I18nizableText>> errorFields = CategorizedElementDefinitionHelper.validateValuesForWriting(resolvedValues, _flatDefinitions.values(), _disableConditionsEvaluator, _logger)
554                                                                                          .getAllErrors();
555        if (!errorFields.isEmpty())
556        {
557            return errorFields;
558        }
559
560        // SAX
561        _saxConfigurationFile(fileName, resolvedValues);
562        
563        return Collections.EMPTY_MAP;
564    }
565
566    /**
567     * Retrieves old values if the configuration is not well initialized
568     * @return the old values
569     */
570    public Map<String, Object> getOldValues()
571    {
572        if (Config.getInstance() == null)
573        {
574            try
575            {
576                Map<String, DefinitionAndValue> oldDefinitionAndValues = Config.__read();
577                return Config.__extractValues(oldDefinitionAndValues);
578            }
579            catch (Exception e)
580            {
581                return new HashMap<>();
582            }
583        }
584        
585        return null;
586    }
587
588    private Map<String, Object> _resolveValues(Map<String, Object> values, Map<String, Object> oldValues)
589    {
590        Map<String, Object> resolvedValues = new HashMap<>();
591        for (Map.Entry<String, Object> value : values.entrySet())
592        {
593            ElementDefinition definition = _flatDefinitions.get(value.getKey());
594            if (definition != null)
595            {
596                Object resolvedValue = _resolveValue(definition, value.getValue(), oldValues);
597                resolvedValues.put(value.getKey(), resolvedValue);
598            }
599        }
600        return resolvedValues;
601    }
602    
603    /**
604     * Resolve the given value for the parameter check
605     * @param parameterName name of the parameter to resolve
606     * @param value the value to resolve
607     * @param oldValues the old values if the configuration is not initialized
608     * @return the resolved value as a String
609     */
610    public String resolveValueForParameterChecker(String parameterName, Object value, Map<String, Object> oldValues)
611    {
612        ElementDefinition definition = _flatDefinitions.get(parameterName);
613        
614        if (definition == null)
615        {
616            return null;
617        }
618        
619        Object resolvedValue = _resolveValue(definition, value, oldValues);
620        return definition.getType().toString(resolvedValue);
621    }
622    
623    private Object _resolveValue(ElementDefinition definition, Object value, Map<String, Object> oldValues)
624    {
625        String parameterId = definition.getName();
626        ElementType type = definition.getType();
627        Object resolvedValue = value;
628
629        // keeps the value of an empty password field
630        if (value == null && ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(type.getId()))
631        {
632            if (Config.getInstance() != null)
633            {
634                resolvedValue = Config.getInstance().getValue(parameterId);
635            }
636            else if (oldValues != null)
637            {
638                resolvedValue = oldValues.get(definition.getName());
639            }
640        }
641        
642        return resolvedValue;
643    }
644    
645    private void _saxConfigurationFile(String fileName, Map<String, Object> values) throws TransformerFactoryConfigurationError, Exception
646    {
647        // create the result where to write
648        File outputFile = new File(fileName);
649        outputFile.getParentFile().mkdirs();
650        
651        try (OutputStream os = new FileOutputStream(fileName))
652        {
653            // create a transformer for saving sax into a file
654            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
655            
656            StreamResult result = new StreamResult(os);
657            th.setResult(result);
658
659            // create the format of result
660            Properties format = new Properties();
661            format.put(OutputKeys.METHOD, "xml");
662            format.put(OutputKeys.INDENT, "yes");
663            format.put(OutputKeys.ENCODING, "UTF-8");
664            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
665            th.getTransformer().setOutputProperties(format);
666
667            // sax the configuration into the transformer
668            _saxParameters(th, values);
669        }
670        catch (Exception e)
671        {
672            throw new Exception("An error occured while saving the config values.", e);
673        }
674    }
675
676    private void _saxParameters(TransformerHandler handler, Map<String, Object> values) throws SAXException
677    {
678        handler.startDocument();
679        XMLUtils.startElement(handler, "config");
680        
681
682        // Iterate over categorized parameters
683        for (ModelItem category : _categorizedDefinitions)
684        {
685            if (!(category instanceof ModelItemGroup))
686            {
687                // Should not happen, categories are created in categorizeParameters method
688                continue;
689            }
690            
691            StringBuilder categoryLabel = new StringBuilder();
692            categoryLabel.append("+\n      | ");
693            categoryLabel.append(category.getLabel().toString());
694            categoryLabel.append("\n      +");
695            
696            // Comment on current category
697            XMLUtils.data(handler, "\n  ");
698            handler.comment(categoryLabel.toString().toCharArray(), 0, categoryLabel.length());
699            XMLUtils.data(handler, "\n");
700            XMLUtils.data(handler, "\n");
701            
702            Iterator<ModelItem> groups = ((ModelItemGroup) category).getChildren().iterator();
703            while (groups.hasNext())
704            {
705                ModelItem group = groups.next();
706                if (!(group instanceof ModelItemGroup))
707                {
708                    // Should not happen, groups are created in categorizeParameters method
709                    continue;
710                }
711                
712                StringBuilder groupLabel = new StringBuilder();
713                groupLabel.append(" ");
714                groupLabel.append(group.getLabel().toString());
715                groupLabel.append(" ");
716
717                // Comment on current group
718                XMLUtils.data(handler, "  ");
719                handler.comment(groupLabel.toString().toCharArray(), 0, groupLabel.length());
720                XMLUtils.data(handler, "\n  ");
721                
722                for (ModelItem definition : ((ModelItemGroup) group).getChildren())
723                {
724                    if (!(definition instanceof ElementDefinition))
725                    {
726                        // Should not happen, parameter references are created in categorizeParameters method
727                        continue;
728                    }
729                    
730                    Object value = values.get(definition.getName());
731                    ElementType<Object> type = ((ElementDefinition) definition).getType();
732                    
733                    if (!(type instanceof XMLElementType))
734                    {
735                        // Should not happen, configuration parameters only work with XML element types
736                        continue;
737                    }
738                    
739                    ((XMLElementType<Object>) type).write(handler, definition.getName(), value);
740                }
741                
742                if (groups.hasNext())
743                {
744                    XMLUtils.data(handler, "\n");
745                }
746            }
747            
748            XMLUtils.data(handler, "\n");
749        }
750
751        XMLUtils.endElement(handler, "config");
752        handler.endDocument();
753    }
754
755    /**
756     * Evaluate the {@link DisableConditions} of the given configuration parameter against the configuration values
757     * @param definition the configuration parameter
758     * @param config the {@link Config} instance
759     * @return <code>true</code> if the parameter is disabled, <code>false</code> otherwise
760     */
761    public boolean evaluateDisableConditions(ElementDefinition definition, Config config)
762    {
763        return _disableConditionsEvaluator.evaluateDisableConditions(definition, definition.getName(), config);
764    }
765
766    public ModelItem getModelItem(String itemPath) throws UndefinedItemPathException
767    {
768        ModelItem item = _flatDefinitions.get(itemPath);
769        if (item != null)
770        {
771            return item;
772        }
773        else
774        {
775            throw new UndefinedItemPathException("The parameter '" + itemPath + "' is not defined in the configuration.");
776        }
777    }
778    
779    /**
780     * Retrieves all configuration parameters
781     * @return all configuration parameters
782     */
783    public Collection<ElementDefinition> getConfigurationParameters()
784    {
785        return _flatDefinitions.values();
786    }
787    
788    public Collection<ModelItem> getModelItems()
789    {
790        return _categorizedDefinitions;
791    }
792    
793    /**
794     * Retrieves the view of Configuration
795     * @param i18nUtils the i18nUtils to use categories's sorting
796     * @return the configuration's view
797     */
798    public View getView(I18nUtils i18nUtils)
799    {
800        Comparator<ModelItem> categoriesComparator = new I18nizableTextModelItemComparator(new I18nizableTextWithGeneralFirstComparator(i18nUtils));
801        Comparator<ModelItem> groupsComparator = new I18nizableTextModelItemComparator(new I18nizableTextKeyComparator());
802        Comparator<ModelItem> elementsComparator = new ConfigParameterDefinitionComparator(_configParameterWrappers.values());
803        
804        return ConfigParameterDefinitionHelper.buildConfigParametersView(_categorizedDefinitions, categoriesComparator, groupsComparator, elementsComparator);
805    }
806    
807    /**
808     * Gets the parameter checker with its id
809     * @param id the id of the parameter checker to get
810     * @return the associated parameter checker descriptor
811     */
812    public ConfigParameterCheckerDescriptor getParameterChecker(String id)
813    {
814        return _parameterCheckers.get(id);
815    }
816    
817    /**
818     * Returns true if the model is initialized and all parameters are valued
819     * @return true if the model is initialized and all parameters are valued
820     */
821    public boolean isComplete()
822    {
823        return _isInitialized && _isComplete;
824    }
825    
826    /**
827     * Returns true if the config file does not exist
828     * @return true if the config file does not exist
829     */
830    public boolean isEmpty()
831    {
832        return !Config.fileExists();
833    }
834    
835    /**
836     * Dispose the manager before restarting it
837     */
838    public void dispose()
839    {
840        _isInitialized = false;
841        _isComplete = true;
842
843        _usedParamIds = null;
844        _declaredParams = null;
845        _configParameterWrappers = null;
846        _declaredParamCheckers = null;
847        _parameterCheckers = null;
848        _flatDefinitions = null;
849        
850        if (_disableConditionsManager != null)
851        {
852            _disableConditionsManager.dispose();
853            _disableConditionsManager = null;
854        }
855        if (_validatorManager != null)
856        {
857            _validatorManager.dispose();
858            _validatorManager = null;
859        }
860        if (_enumeratorManager != null)
861        {
862            _enumeratorManager.dispose();
863            _enumeratorManager = null;
864        }
865        if (_parameterCheckerManager != null)
866        {
867            _parameterCheckerManager.dispose();
868            _parameterCheckerManager = null;
869        }
870    }
871
872    public String getId()
873    {
874        return StringUtils.EMPTY;
875    }
876
877    public String getFamilyId()
878    {
879        return this.getClass().getName();
880    }
881    
882    /**
883     * I18nizableText comparator for site parameters
884     * General information category is the first one, then sort  the I18nizableText with their translation
885     */
886    class I18nizableTextWithGeneralFirstComparator extends I18nizableTextComparator
887    {
888        public I18nizableTextWithGeneralFirstComparator(I18nUtils i18nUtils)
889        {
890            super(i18nUtils);
891        }
892
893        @Override
894        public int compare(I18nizableText t1, I18nizableText t2)
895        {
896            // The general informations category always goes first
897            if (t1.isI18n() && t1.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY))
898            {
899                if (t2.isI18n() && t2.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY))
900                {
901                    return 0;
902                }
903                return -1;
904            }
905
906            if (t2.isI18n() && t2.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY))
907            {
908                return 1;
909            }
910            
911            return super.compare(t1, t2);
912        }
913    }
914}