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