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}