001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.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.HashMap;
025import java.util.Iterator;
026import java.util.LinkedHashMap;
027import java.util.Map;
028import java.util.Properties;
029import java.util.Set;
030import java.util.regex.Pattern;
031
032import javax.xml.transform.OutputKeys;
033import javax.xml.transform.TransformerFactory;
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.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.cocoon.xml.XMLUtils;
045import org.apache.commons.lang.BooleanUtils;
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.runtime.i18n.I18nizableText;
053import org.ametys.runtime.parameter.Enumerator;
054import org.ametys.runtime.parameter.Errors;
055import org.ametys.runtime.parameter.ParameterChecker;
056import org.ametys.runtime.parameter.ParameterHelper;
057import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
058import org.ametys.runtime.parameter.Validator;
059import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
060
061/**
062 * This manager handle the parameters of the application that have to be stored by the plugins.
063 */
064public final class ConfigManager implements Contextualizable, Serviceable, Initializable
065{
066    /** the regular expression for ids */
067    public static final Pattern CONFIG_ID_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9.\\-_]*");
068    
069    /** The field separator for the field hierarchy */
070    private static final String FIELD_SEPARATOR = "/";
071    
072    // shared instance
073    private static ConfigManager __manager;
074
075    // Logger for traces
076    Logger _logger = LoggerFactory.getLogger(ConfigManager.class);
077    
078    // Avalon stuff
079    private ServiceManager _manager;
080    private Context _context;
081    
082    // Used parameters
083    private Collection<String> _usedParamsName;
084
085    // Declared parameters (Map<id, configuration>)
086    private Map<String, ConfigParameterInfo> _declaredParams;
087
088    // Typed parameters
089    private Map<String, ConfigParameter> _params;
090
091    // Parameter checkers info
092    private Map<String, ConfigParameterInfo> _declaredParameterCheckers;
093        
094    // Parsed parameter checkers
095    private Map<String, ConfigParameterCheckerDescriptor> _parameterCheckers;
096    
097    // The parameters classified by categories and groups
098    private Map<I18nizableText, ConfigParameterCategory> _categorizedParameters;
099    
100    // Determines if the extension point is initialized
101    private boolean _isInitialized;
102
103    // Determines if all parameters are valued
104    private boolean _isComplete;
105    
106    // ComponentManager for the validators
107    private ThreadSafeComponentManager<Validator> _validatorManager;
108    
109    // ComponentManager for the enumerators
110    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
111    
112    // ComponentManager for the parameter checkers
113    private ThreadSafeComponentManager<ParameterChecker> _parameterCheckerManager;
114
115    
116    private ConfigManager()
117    {
118        // empty constructor
119    }
120
121    /**
122     * Returns the shared instance of the <code>PluginManager</code>
123     * @return the shared instance of the PluginManager
124     */
125    public static ConfigManager getInstance()
126    {
127        if (__manager == null)
128        {
129            __manager = new ConfigManager();
130        }
131
132        return __manager;
133    }
134
135    /**
136     * Returns true if the model is initialized and all parameters are valued
137     * @return true if the model is initialized and all parameters are valued
138     */
139    public boolean isComplete()
140    {
141        return _isInitialized && _isComplete;
142    }
143    
144    /**
145     * Returns true if the config file does not exist
146     * @return true if the config file does not exist
147     */
148    public boolean isEmpty()
149    {
150        return !Config.getFileExists();
151    }
152    
153    @Override
154    public void contextualize(Context context)
155    {
156        _context = context;
157    }
158    
159    @Override
160    public void service(ServiceManager manager)
161    {
162        _manager = manager;
163    }
164    
165    @Override
166    public void initialize()
167    {
168        _usedParamsName = new ArrayList<>();
169        _declaredParams = new LinkedHashMap<>();
170        _params = new LinkedHashMap<>();
171        _declaredParameterCheckers = new LinkedHashMap<>();
172        _parameterCheckers = new LinkedHashMap<>();
173        
174        _validatorManager = new ThreadSafeComponentManager<>();
175        _validatorManager.setLogger(LoggerFactory.getLogger("runtime.plugin.threadsafecomponent"));
176        _validatorManager.contextualize(_context);
177        _validatorManager.service(_manager);
178        
179        _enumeratorManager = new ThreadSafeComponentManager<>();
180        _enumeratorManager.setLogger(LoggerFactory.getLogger("runtime.plugin.threadsafecomponent"));
181        _enumeratorManager.contextualize(_context);
182        _enumeratorManager.service(_manager);
183        
184        _parameterCheckerManager = new ThreadSafeComponentManager<>();
185        _parameterCheckerManager.setLogger(LoggerFactory.getLogger("runtime.plugin.threadsafecomponent"));
186        _parameterCheckerManager.contextualize(_context);
187        _parameterCheckerManager.service(_manager);
188    }
189    
190    /**
191     * Registers new available parameters.<br>
192     * The addConfig() method allows to select which ones are actually useful.
193     * @param pluginName the name of the plugin defining the parameters
194     * @param parameters the config parameters definition
195     * @param paramCheckers the parameters checkers definition
196     */
197    public void addGlobalConfig(String pluginName, Map<String, ConfigParameterInfo> parameters, Map<String, ConfigParameterInfo> paramCheckers)
198    {
199        if (_logger.isDebugEnabled())
200        {
201            _logger.debug("Adding parameters and parameters checkers for plugin " + pluginName);
202        }
203
204        for (String id : parameters.keySet())
205        {
206            ConfigParameterInfo info = parameters.get(id);
207            
208            // Check if the parameter is not already declared
209            if (_declaredParams.containsKey(id))
210            {
211                throw new IllegalArgumentException("The config parameter '" + id + "' is already declared. Parameters ids must be unique");
212            }
213
214            // Add the new parameter to the list of declared parameters
215            _declaredParams.put(id, info);
216
217            if (_logger.isDebugEnabled())
218            {
219                _logger.debug("Parameter added: " + id);
220            }
221        }
222        
223        if (_logger.isDebugEnabled())
224        {
225            _logger.debug(parameters.size() + " parameter(s) added");
226        }
227        
228        for (String id : paramCheckers.keySet())
229        {
230            ConfigParameterInfo info = paramCheckers.get(id);
231            
232            // Check if the parameter is not already declared
233            if (_declaredParams.containsKey(id))
234            {
235                throw new IllegalArgumentException("The parameter checker '" + id + "' is already declared. Parameter checkers ids must be unique.");
236            }
237
238            // Add the new parameter to the list of declared parameters checkers
239            _declaredParameterCheckers.put(id, info);
240
241            if (_logger.isDebugEnabled())
242            {
243                _logger.debug("Parameter checker added: " + id);
244            }
245        }
246        
247        if (_logger.isDebugEnabled())
248        {
249            _logger.debug(paramCheckers.size() + " parameter checker(s) added");
250        }
251    }
252
253    /**
254     * Registers a new parameter or references a globalConfig parameter.<br>
255     * @param featureId the id of the feature defining the parameters
256     * @param parameters the config parameters definition
257     * @param parametersReferences references to already defined parameters
258     * @param paramCheckers the parameters checkers definition
259     */
260    public void addConfig(String featureId, Map<String, ConfigParameterInfo> parameters, Collection<String> parametersReferences, Map<String, ConfigParameterInfo> paramCheckers)
261    {
262        if (_logger.isDebugEnabled())
263        {
264            _logger.debug("Selecting parameters for feature " + featureId);
265        }
266
267        for (String id : parameters.keySet())
268        {
269            ConfigParameterInfo info = parameters.get(id);
270            
271            // Check if the parameter is not already declared
272            if (_declaredParams.containsKey(id))
273            {
274                throw new IllegalArgumentException("The config parameter '" + id + "' is already declared. Parameters ids must be unique");
275            }
276
277            // Add the new parameter to the list of unused parameters
278            _declaredParams.put(id, info);
279            _usedParamsName.add(id);
280
281            if (_logger.isDebugEnabled())
282            {
283                _logger.debug("Parameter added: " + id);
284            }
285        }
286        
287        if (_logger.isDebugEnabled())
288        {
289            _logger.debug(parameters.size() + " parameter(s) added");
290        }
291        
292        for (String id : parametersReferences)
293        {
294            _usedParamsName.add(id);
295        }
296        
297        for (String id : paramCheckers.keySet())
298        {
299            ConfigParameterInfo info = paramCheckers.get(id);
300            
301            // Check if the parameter is not already declared
302            if (_declaredParams.containsKey(id))
303            {
304                throw new IllegalArgumentException("The parameter checker '" + id + "' is already declared. Parameter checkers ids must be unique.");
305            }
306
307            // Add the new parameter to the list of unused parameters
308            _declaredParameterCheckers.put(id, info);
309
310            if (_logger.isDebugEnabled())
311            {
312                _logger.debug("Parameter checker added: " + id);
313            }
314        }
315        
316        if (_logger.isDebugEnabled())
317        {
318            _logger.debug(paramCheckers.size() + " parameter checker(s) added");
319        }
320    }
321
322    /**
323     * Ends the initialization of the config parameters, by checking against the
324     * already valued parameters.<br>
325     * If at least one parameter has no value, the application won't start.
326     */
327    public void validate()
328    {
329        _logger.debug("Initialization");
330
331        _isInitialized = false;
332        _isComplete = true;
333
334        // Dispose potential previous parameters 
335        Config.dispose();
336        Map<String, String> untypedValues = null;
337        try
338        {
339            untypedValues = Config.read();
340        }
341        catch (Exception e)
342        {
343            _logger.error("Cannot read the configuration file.", e);
344            _isComplete = false;
345        }
346        
347        ConfigParameterParser configParamParser = new ConfigParameterParser(_enumeratorManager, _validatorManager);
348        for (String id : _usedParamsName)
349        {
350            // Check if the parameter is not already used
351            if (_params.get(id) == null)
352            {
353                // Move the parameter from the unused list, to the used list
354                ConfigParameterInfo info = _declaredParams.get(id);
355                
356                if (info == null)
357                {
358                    throw new RuntimeException("The parameter '" + id + "' is used but not declared");
359                }
360                
361                ConfigParameter parameter = null;
362
363                try
364                {
365                    parameter = configParamParser.parseParameter(_manager, info.getPluginName(), info.getConfiguration());
366                }
367                catch (ConfigurationException ex)
368                {
369                    throw new RuntimeException("Unable to configure the config parameter : " + id, ex);
370                }
371                
372                _params.put(id, parameter);
373            }
374        }
375        
376        ConfigParameterCheckerParser parameterCheckerParser = new ConfigParameterCheckerParser(_parameterCheckerManager);
377        for (String id : _declaredParameterCheckers.keySet())
378        {
379            boolean invalidParameters = false;
380            
381            // Check if the parameter checker is not already used
382            if (_parameterCheckers.get(id) == null)
383            {
384                ConfigParameterInfo info = _declaredParameterCheckers.get(id);
385                
386                ConfigParameterCheckerDescriptor parameterChecker = null;
387                try
388                {
389                    
390                    parameterChecker = parameterCheckerParser.parseParameterChecker(info.getPluginName(), info.getConfiguration());
391                }
392                catch (ConfigurationException ex)
393                {
394                    throw new RuntimeException("Unable to configure the parameter checker: " + id, ex);
395                }
396                
397                for (String linkedParameterPath : parameterChecker.getLinkedParamsPaths())
398                {
399                    ConfigParameter linkedParameter = null;
400
401                    // Linked parameters can be declared with an absolute path, in which case they are prefixed with '/
402                    if (linkedParameterPath.startsWith(FIELD_SEPARATOR))
403                    {
404                        linkedParameter = _params.get(linkedParameterPath.substring(FIELD_SEPARATOR.length()));
405                    }
406                    else
407                    {
408                        linkedParameter = _params.get(linkedParameterPath);
409                    }
410                    
411                    // If at least one parameter used is invalid, the parameter checker is invalidated
412                    if (linkedParameter == null)
413                    {
414                        invalidParameters = true;
415                        break;
416                    }
417                }
418                
419                if (invalidParameters)
420                {
421                    if (_logger.isDebugEnabled())
422                    {
423                        _logger.debug("All the configuration parameters associated to the parameter checker '" + parameterChecker.getId() + "' are not used.\n"
424                                    + "This parameter checker will be ignored");
425                    }
426                }
427                else
428                {
429                    _parameterCheckers.put(id, parameterChecker);
430                }
431            }
432        }
433        
434        _categorizedParameters = _categorizeParameters(_params, _parameterCheckers);
435
436        try
437        {
438            configParamParser.lookupComponents();
439            parameterCheckerParser.lookupComponents();
440        }
441        catch (Exception e)
442        {
443            throw new RuntimeException("Unable to lookup parameter local components", e);
444        }
445        
446        _validateParameters(untypedValues);
447
448        _declaredParams.clear();
449        _usedParamsName.clear();
450        _declaredParameterCheckers.clear();
451
452        _isInitialized = true;
453
454        Config.setInitialized(_isComplete);
455        
456        _logger.debug("Initialization ended");
457    }
458    
459    private void _validateParameters(Map<String, String> untypedValues)
460    {
461        if (_isComplete && untypedValues != null)
462        {
463            for (ConfigParameterCategory category : _categorizedParameters.values())
464            {
465                for (ConfigParameterGroup group: category.getGroups().values())
466                {
467                    boolean isGroupSwitchedOn = true;
468                    String groupSwitch = group.getSwitch();
469                    
470                    if (groupSwitch != null)
471                    {
472                        // Check if group switch is active
473                        ConfigParameter switcher = _params.get(group.getSwitch());
474                        
475                        // we can cast directly because we already tested that it should be a boolean while categorizing
476                        isGroupSwitchedOn = BooleanUtils.toBoolean((Boolean) _validateParameter(untypedValues, switcher));
477                    }
478                    
479                    // validate parameters if there's no switch, if the switch is on or if the the parameter is not disabled
480                    if (isGroupSwitchedOn)
481                    {
482                        boolean disabled = false;
483                        for (ConfigParameter parameter: group.getParams(true))
484                        {
485                            DisableConditions disableConditions = parameter.getDisableConditions();
486                            disabled = evaluateDisableConditions(disableConditions, untypedValues);
487                            
488                            if (!StringUtils.equals(parameter.getId(), group.getSwitch()) && !disabled)
489                            {
490                                _validateParameter(untypedValues, parameter);
491                            }
492                        }
493                    }
494                }
495            }
496        }
497    }
498    
499    private Object _validateParameter(Map<String, String> untypedValues, ConfigParameter parameter)
500    {
501        String id = parameter.getId();
502        Object value = ParameterHelper.castValue(untypedValues.get(id), parameter.getType());
503
504        if (value == null && !"".equals(untypedValues.get(id)))
505        {
506            if (_logger.isWarnEnabled())
507            {
508                _logger.warn("The parameter '" + id + "' is not valued. Configuration is not initialized.");
509            }
510            
511            _isComplete = false;
512        }
513        else
514        {
515            Validator v = parameter.getValidator();
516            Errors validationErrors = new Errors();
517            if (v != null)
518            {
519                v.validate(value, validationErrors);
520            }
521            
522            if (validationErrors.getErrors().size() > 0)
523            {
524                if (_logger.isWarnEnabled())
525                {
526                    StringBuffer sb = new StringBuffer("The parameter '" + id + "' is not valid with value '" + untypedValues.get(id) + "' :");
527                    for (I18nizableText error : validationErrors.getErrors())
528                    {
529                        sb.append("\n* " + error.toString());
530                    }
531                    sb.append("\nConfiguration is not initialized");
532                    
533                    _logger.warn(sb.toString());
534                }
535                
536                _isComplete = false;
537            }
538            
539            // Make sure valued configuration parameters with an enumerator have their value in the enumeration values
540            Enumerator enumerator = parameter.getEnumerator();
541            if (enumerator != null)
542            {
543                I18nizableText entry = null;
544                try
545                {
546                    entry = enumerator.getEntry(ParameterHelper.valueToString(value));
547                }
548                catch (Exception e)
549                {
550                    if (_logger.isWarnEnabled())
551                    {
552                        _logger.warn("The value '" + value + "' for the parameter '" + id + "' led to an exception. Configuration is not initialized." , e);
553                    }
554                    
555                    _isComplete = false;
556                }
557                
558                if (entry == null)
559                {
560                    if (_logger.isWarnEnabled())
561                    {
562                        _logger.warn("The value '" + value + "' for the parameter '" + id + "' is not allowed. Configuration is not initialized.");
563                    }
564                    
565                    _isComplete = false;
566                }
567            }
568            
569        }
570        
571        return value;
572    }
573
574    /**
575     * Recursively evaluate the {@link DisableConditions} against the configuration values
576     * @param disableConditions the disable conditions to evaluate
577     * @param untypedValues the untyped configuration values
578     * @return true if the disable conditions are true, false otherwise
579     */
580    public boolean evaluateDisableConditions(DisableConditions disableConditions, Map<String, String> untypedValues)
581    {
582        if (disableConditions == null || disableConditions.getConditions().isEmpty() && disableConditions.getSubConditions().isEmpty())
583        {
584            return false;
585        }
586        
587        boolean disabled;
588        boolean andOperator = disableConditions.getAssociationType() == DisableConditions.ASSOCIATION_TYPE.AND;
589        
590        // initial value depends on OR or AND associations
591        disabled = andOperator;
592        
593        for (DisableConditions subConditions : disableConditions.getSubConditions())
594        {
595            boolean result = evaluateDisableConditions(subConditions, untypedValues);
596            disabled = andOperator ?  disabled && result : disabled || result;
597        }
598        
599        for (DisableCondition condition : disableConditions.getConditions())
600        {
601            boolean result = _evaluateCondition(condition, untypedValues);
602            disabled = andOperator ?  disabled && result : disabled || result;
603        }
604                
605        return disabled;
606    }
607    
608    private boolean _evaluateCondition(DisableCondition condition, Map<String, String> untypedValues)
609    {
610        String id = condition.getId();
611        DisableCondition.OPERATOR operator = condition.getOperator();
612        String value = condition.getValue();
613        
614        if (untypedValues.get(id) == null)
615        {
616            if (_logger.isDebugEnabled())
617            {
618                _logger.debug("Cannot evaluate the disable condition on the undefined parameter " + id + ".\nReturning false.");
619            }
620            return false;
621        }
622        
623        ParameterType type = _params.get(id).getType();
624        Object parameterValue = ParameterHelper.castValue(untypedValues.get(id), type);
625        Object compareValue = ParameterHelper.castValue(value, type);
626        if (compareValue == null)
627        {
628            throw new IllegalStateException("Cannot convert '" + value + "' to a '" + type + "' for parameter '" + id + "'");
629        }
630        
631        if (!(parameterValue instanceof Comparable) || !(compareValue instanceof Comparable))
632        {
633            throw new IllegalStateException("values '" + untypedValues.get(id) + "' and '" + compareValue + "' of type'" + type + "' for parameter '" + id + "' are not comparable");
634        }
635
636        @SuppressWarnings("unchecked")
637        Comparable<Object> comparableParameterValue = (Comparable<Object>) parameterValue;
638        @SuppressWarnings("unchecked")
639        Comparable<Object> comparableCompareValue = (Comparable<Object>) compareValue;
640
641        int comparison = comparableParameterValue.compareTo(comparableCompareValue);
642        switch (operator)
643        {
644            case NEQ:
645                return comparison != 0;
646            case GEQ:
647                return comparison >= 0;
648            case GT:
649                return comparison > 0;
650            case LT:
651                return comparison < 0;
652            case LEQ:
653                return comparison <= 0;
654            case EQ:
655            default:
656                return comparison == 0;
657        }
658    }
659                
660    /**
661     * Dispose the manager before restarting it
662     */
663    public void dispose()
664    {
665        _isInitialized = false;
666        _isComplete = true;
667        
668        _declaredParams = null;
669        _params = null;
670        _usedParamsName = null;
671        if (_validatorManager != null)
672        {
673            _validatorManager.dispose();
674            _validatorManager = null;
675        }
676        if (_enumeratorManager != null)
677        {
678            _enumeratorManager.dispose();
679            _enumeratorManager = null;
680        }
681        if (_parameterCheckerManager != null)
682        {
683            _parameterCheckerManager.dispose();
684            _parameterCheckerManager = null;
685        }
686    }
687    
688    /**
689     * Get the id of the config parameters. Use get to retrieve the parameter
690     * @return An array of String containing the id of the parameters existing in the model
691     */
692    public String[] getParametersIds()
693    {
694        Set<String> keySet = _params.keySet();
695        String[] array;
696        synchronized (_params)
697        {
698            array = new String[keySet.size()];
699            _params.keySet().toArray(array);
700        }
701        return array;
702    }
703    
704    /**
705     * Returns typed config values.
706     * @return typed config values.
707     */
708    public Map<String, Object> getValues()
709    {
710        Map<String, Object> result = new HashMap<>();
711        
712        // Get configuration parameters
713        Map<String, String> untypedValues;
714        try
715        {
716            untypedValues = Config.read();
717        }
718        catch (Exception e)
719        {
720            if (_logger.isWarnEnabled())
721            {
722                _logger.warn("Config values are unreadable. Using default values", e);
723            }
724            
725            untypedValues = new HashMap<>();
726        }
727
728        for (String parameterId : _params.keySet())
729        {
730            ConfigParameter param = _params.get(parameterId);
731            Object value = _getValue (parameterId, param.getType(), untypedValues);
732            
733            if (value != null)
734            {
735                result.put(parameterId, value);
736            }
737        }
738        
739        return result;
740    }
741    
742    /**
743     * Returns all {@link ConfigParameter} grouped by categories and groups.
744     * @return all {@link ConfigParameter}.
745     */
746    public Map<I18nizableText, ConfigParameterCategory> getCategories()
747    {
748        return _categorizedParameters;
749    }
750
751    /**
752     * Gets the config parameter by its id
753     * @param id Id of the config parameter to get
754     * @return The config parameter.
755     */
756    public ConfigParameter get(String id)
757    {
758        return _params.get(id);
759    }
760
761    /**
762     * Gets the typed configuration parameters
763     * @return the _params map 
764     */
765    public Map<String, ConfigParameter> getParameters()
766    {
767        return this._params;
768    }
769    
770    /**
771     * Returns all {@link ConfigParameterCheckerDescriptor}s.
772     * @return all {@link ConfigParameterCheckerDescriptor}s.
773     */
774    public Map<String, ConfigParameterCheckerDescriptor> getParameterCheckers()
775    {
776        return _parameterCheckers;
777    }
778    
779    /**
780     * Gets the parameter checker with its id
781     * @param id the id of the parameter checker to get
782     * @return the associated parameter checker descriptor
783     */
784    public ConfigParameterCheckerDescriptor getParameterChecker(String id)
785    {
786        return _parameterCheckers.get(id);
787    }
788    
789    private Map<I18nizableText, ConfigParameterCategory> _categorizeParameters(Map<String, ConfigParameter> params, Map<String, ConfigParameterCheckerDescriptor> paramCheckers)
790    {
791        Map<I18nizableText, ConfigParameterCategory> categories = new HashMap<> ();
792        
793        // Classify parameters by groups and categories
794        Iterator<String> it = params.keySet().iterator();
795        while (it.hasNext())
796        {
797            String key = it.next();
798            ConfigParameter param = params.get(key);
799
800            I18nizableText categoryName = param.getDisplayCategory();
801            I18nizableText groupName = param.getDisplayGroup();
802
803            // Get the map of groups of the category
804            ConfigParameterCategory category = categories.get(categoryName);
805            if (category == null)
806            {
807                category = new ConfigParameterCategory();
808                categories.put(categoryName, category);
809            }
810
811            // Get the map of parameters of the group
812            ConfigParameterGroup group = category.getGroups().get(groupName);
813            if (group == null)
814            {
815                group = new ConfigParameterGroup(groupName);
816                category.getGroups().put(groupName, group);
817            }
818
819            group.addParam(param);
820        }
821        
822        // Add parameter checkers to groups and categories
823        Iterator<String> paramCheckersIt = paramCheckers.keySet().iterator();
824        while (paramCheckersIt.hasNext())
825        {
826            String key = paramCheckersIt.next();
827            ConfigParameterCheckerDescriptor paramChecker = paramCheckers.get(key);
828            
829            I18nizableText uiCategory = paramChecker.getUiRefCategory();
830            if (uiCategory != null)
831            {
832                ConfigParameterCategory category = categories.get(uiCategory);
833                if (category == null)
834                {
835                    if (_logger.isDebugEnabled())
836                    {
837                        _logger.debug("The category " + uiCategory.toString() + " doesn't exist,"
838                                + " thus the parameter checker" + paramChecker.getId() + "will not be added");
839                    }
840                }
841                else
842                {
843                    I18nizableText uiGroup = paramChecker.getUiRefGroup();
844                    if (uiGroup == null)
845                    {
846                        category.addParamChecker(paramChecker);
847                    }
848                    else
849                    {
850                        ConfigParameterGroup group = category.getGroups().get(uiGroup);
851                        if (group == null)
852                        {
853                            if (_logger.isDebugEnabled())
854                            {
855                                _logger.debug("The group " + uiGroup.toString() + " doesn't exist."
856                                        + " thus the parameter checker" + paramChecker.getId() + "will not be added");
857                            }
858                        } 
859                        else
860                        { 
861                            group.addParamChecker(paramChecker);
862                        }
863                    }
864                } 
865            }
866        }
867        
868        return categories;
869    }
870
871
872    private Object _getValue (String paramID, ParameterType type, Map<String, String> untypedValues)
873    {
874        final String unverifiedUntypedValue = untypedValues.get(paramID);
875        
876        Object typedValue;        
877        if (unverifiedUntypedValue == null)
878        {
879            typedValue = null;
880        }
881        else if (StringUtils.isEmpty(unverifiedUntypedValue))
882        {
883            typedValue = "";
884        }
885        else 
886        {
887            typedValue = ParameterHelper.castValue(unverifiedUntypedValue, type);
888        }
889        
890        if (type.equals(ParameterType.PASSWORD) && typedValue != null && ((String) typedValue).length() > 0)
891        {
892            typedValue = "PASSWORD";
893        }
894        
895        return typedValue;
896    }
897
898    /**
899     * Update the configuration file with the given values<br>
900     * Values are untyped (all are of type String) and might be null.
901     * @param untypedValues A map (key, untyped value).
902     * @param fileName the config file absolute path
903     * @return errors The fields in error
904     * @throws Exception If an error occurred while saving values
905     */
906    public Map<String, Errors> save(Map<String, String> untypedValues, String fileName) throws Exception
907    {
908        Map<String, Errors> errorFields = new HashMap<>();
909        
910        // Retrieve the old values for password purposes
911        Map<String, String> oldUntypedValues = null;
912        if (Config.getInstance() == null)
913        {
914            try
915            {
916                oldUntypedValues = Config.read();
917            }
918            catch (Exception e)
919            {
920                oldUntypedValues = new HashMap<>();
921            }
922        }
923        
924        // Bind and validate parameters
925        Map<String, Object> typedValues = _bindAndValidateParameters (untypedValues, oldUntypedValues, errorFields);
926        
927        if (errorFields.size() > 0)
928        {
929            if (_logger.isDebugEnabled())
930            {
931                _logger.debug("Failed to save configuration because of invalid parameter values");
932            }
933            
934            return errorFields;
935        }
936
937        // SAX
938        // create the result where to write
939        File outputFile = new File(fileName);
940        outputFile.getParentFile().mkdirs();
941        
942        try (OutputStream os = new FileOutputStream(fileName))
943        {
944            // create a transformer for saving sax into a file
945            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
946            
947            StreamResult result = new StreamResult(os);
948            th.setResult(result);
949
950            // create the format of result
951            Properties format = new Properties();
952            format.put(OutputKeys.METHOD, "xml");
953            format.put(OutputKeys.INDENT, "yes");
954            format.put(OutputKeys.ENCODING, "UTF-8");
955            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
956            th.getTransformer().setOutputProperties(format);
957
958            // sax the config into the transformer
959            _toSAX(th, typedValues);
960        }
961        catch (Exception e)
962        {
963            throw new Exception("An error occured while saving the config values.", e);
964        }
965        
966        return Collections.EMPTY_MAP;
967    }
968    
969    /**
970     * Bind all parameters to typed values and for each, if enabled, validate it
971     * @param untypedValues The untyped values (from client-side)
972     * @param oldUntypedValues The old untyped values (before saving)
973     * @param errorFields The parameters in errors to be completed by validation process
974     * @return The typed values
975     */
976    private Map<String, Object> _bindAndValidateParameters (Map<String, String> untypedValues, Map<String, String> oldUntypedValues, Map<String, Errors> errorFields)
977    {
978        Map<String, Object> typedValues = new HashMap<>();
979        
980        // Iterate over categorized parameters
981        for (ConfigParameterCategory category : _categorizedParameters.values())
982        {
983            for (ConfigParameterGroup group: category.getGroups().values())
984            {
985                boolean isGroupSwitchedOn = true;
986                String groupSwitch = group.getSwitch();
987                
988                if (groupSwitch != null)
989                {
990                    // Check if group switch is active
991                    ConfigParameter switcher = _params.get(groupSwitch);
992                    isGroupSwitchedOn = (Boolean) ParameterHelper.castValue(untypedValues.get(switcher.getId()), switcher.getType());
993                }
994                
995                for (ConfigParameter parameter: group.getParams(true))
996                {
997                    String paramId = parameter.getId();
998                    Object typedValue = ParameterHelper.castValue(untypedValues.get(paramId), parameter.getType());
999                    typedValues.put(parameter.getId(), typedValue);
1000                    
1001                    if (typedValue == null && parameter.getType() == ParameterType.PASSWORD)
1002                    {
1003                        if (Config.getInstance() != null)
1004                        {
1005                            // keeps the value of an empty password field
1006                            typedValue = Config.getInstance().getValueAsString(paramId);
1007                        }
1008                        else if (oldUntypedValues != null)
1009                        {
1010                            typedValue = oldUntypedValues.get(paramId);
1011                        }
1012                    }
1013                    
1014                    typedValues.put(paramId, typedValue);
1015                    
1016                    DisableConditions disableConditions = parameter.getDisableConditions();
1017                    boolean disabled = !isGroupSwitchedOn || evaluateDisableConditions(disableConditions, untypedValues);
1018                    
1019                    if (!StringUtils.equals(parameter.getId(), group.getSwitch()) && !disabled)
1020                    {
1021                        Validator validator = parameter.getValidator();
1022                        
1023                        if (validator != null)
1024                        {
1025                            Errors errors = new Errors();
1026                            validator.validate(typedValue, errors);
1027                            
1028                            if (errors.hasErrors())
1029                            {
1030                                if (_logger.isDebugEnabled())
1031                                {
1032                                    _logger.debug("The configuration parameter '" + parameter.getId() + "' is not valid");
1033                                }
1034                                errorFields.put(parameter.getId(), errors);
1035                            }
1036                        }
1037                    }
1038                }
1039            }
1040        }
1041        
1042        return typedValues;
1043    }
1044
1045    /**
1046     * SAX the config values into a content handler
1047     * @param handler Handler where to sax
1048     * @param typedValues Map (key, typed value) to sax
1049     * @throws SAXException if an error occurred
1050     */
1051    private void _toSAX(TransformerHandler handler, Map<String, Object> typedValues) throws SAXException
1052    {
1053        handler.startDocument();
1054        XMLUtils.startElement(handler, "config");
1055        
1056        Iterator<I18nizableText> catIt = _categorizedParameters.keySet().iterator();
1057        while (catIt.hasNext())
1058        {
1059            I18nizableText categoryKey = catIt.next();
1060            ConfigParameterCategory category = _categorizedParameters.get(categoryKey);
1061            StringBuilder categoryLabel = new StringBuilder();
1062            categoryLabel.append("+\n      | ");
1063            categoryLabel.append(categoryKey.toString());
1064            categoryLabel.append("\n      +");
1065            
1066            // Commentaire de la categorie courante
1067            XMLUtils.data(handler, "\n  ");
1068            handler.comment(categoryLabel.toString().toCharArray(), 0, categoryLabel.length());
1069            XMLUtils.data(handler, "\n");
1070            XMLUtils.data(handler, "\n");
1071
1072            Iterator<I18nizableText> groupIt = category.getGroups().keySet().iterator();
1073            while (groupIt.hasNext())
1074            {
1075                I18nizableText groupKey = groupIt.next();
1076                StringBuilder groupLabel = new StringBuilder();
1077                groupLabel.append(" ");
1078                groupLabel.append(groupKey.toString());
1079                groupLabel.append(" ");
1080
1081                // Commentaire du group courant
1082                XMLUtils.data(handler, "  ");
1083                handler.comment(groupLabel.toString().toCharArray(), 0, groupLabel.length());
1084                XMLUtils.data(handler, "\n  ");
1085
1086                ConfigParameterGroup group = category.getGroups().get(groupKey);
1087                for (ConfigParameter param: group.getParams(true))
1088                {
1089                    Object typedValue = typedValues.get(param.getId());
1090                    
1091                    String untypedValue = ParameterHelper.valueToString(typedValue);
1092                    if (untypedValue == null)
1093                    {
1094                        untypedValue = "";
1095                    }
1096
1097                    XMLUtils.createElement(handler, param.getId(), untypedValue);
1098                }
1099                
1100                if (groupIt.hasNext())
1101                {
1102                    XMLUtils.data(handler, "\n");
1103                }
1104            }
1105            
1106            XMLUtils.data(handler, "\n");
1107        }
1108
1109        XMLUtils.endElement(handler, "config");
1110        handler.endDocument();
1111    }
1112}