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