001/* 002 * Copyright 2010 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.web.site; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Iterator; 026import java.util.LinkedHashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.TreeMap; 031import java.util.regex.Pattern; 032 033import org.apache.avalon.framework.configuration.Configuration; 034import org.apache.avalon.framework.configuration.ConfigurationException; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.commons.lang3.StringUtils; 038 039import org.ametys.core.util.I18nUtils; 040import org.ametys.core.util.I18nizableTextKeyComparator; 041import org.ametys.plugins.repository.UnknownAmetysObjectException; 042import org.ametys.plugins.repository.data.type.RepositoryElementType; 043import org.ametys.runtime.i18n.I18nizableText; 044import org.ametys.runtime.model.CategorizedElementDefinitionParser; 045import org.ametys.runtime.model.Enumerator; 046import org.ametys.runtime.model.Model; 047import org.ametys.runtime.model.ModelHelper; 048import org.ametys.runtime.model.ModelItem; 049import org.ametys.runtime.model.ModelItemGroup; 050import org.ametys.runtime.model.exception.UndefinedItemPathException; 051import org.ametys.runtime.model.type.ElementType; 052import org.ametys.runtime.parameter.Validator; 053import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint; 054import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 055import org.ametys.web.repository.site.Site; 056import org.ametys.web.repository.site.SiteManager; 057import org.ametys.web.repository.site.SiteType; 058import org.ametys.web.repository.site.SiteTypesExtensionPoint; 059 060/** 061 * Extension point holding all {@link SiteParameter} definitions. 062 */ 063public class SiteConfigurationExtensionPoint extends AbstractThreadSafeComponentExtensionPoint<RepositoryElementType> implements Model 064{ 065 /** Avalon Role */ 066 public static final String ROLE = SiteConfigurationExtensionPoint.class.getName(); 067 068 private static final Pattern __PARAM_NAME_PATTERN = Pattern.compile("[a-z][a-z0-9-_]*", Pattern.CASE_INSENSITIVE); 069 070 private static final String __GENERAL_INFORMATIONS_I18N_KEY = "PLUGINS_WEB_SITE_INFORMATION_CATEGORY"; 071 072 /** The site manager. */ 073 protected SiteManager _siteManager; 074 075 /** The site type extension point. */ 076 protected SiteTypesExtensionPoint _siteTypeEP; 077 078 /** Component gathering utility methods for internationalizable text translation {@link I18nUtils} */ 079 protected I18nUtils _i18nUtils; 080 081 /** Parameters map, indexed by parameter ID. */ 082 private Map<String, SiteParameter> _parameters; 083 084 /** Determines if all parameters are valued, by site. */ 085 private Map<String, Boolean> _isComplete; 086 087 /** ComponentManager for {@link Validator}s. */ 088 private ThreadSafeComponentManager<Validator> _validatorManager; 089 090 /** ComponentManager for {@link Enumerator}s. */ 091 private ThreadSafeComponentManager<Enumerator> _enumeratorManager; 092 093 /** Site parameter parser. */ 094 private SiteParameterParser _parameterParser; 095 096 private SiteParameterTypeExtensionPoint _siteParameterTypeEP; 097 098 099 @Override 100 public void initialize() throws Exception 101 { 102 super.initialize(); 103 104 _parameters = new LinkedHashMap<>(); 105 _isComplete = new HashMap<>(); 106 107 _validatorManager = new ThreadSafeComponentManager<>(); 108 _validatorManager.setLogger(getLogger()); 109 _validatorManager.contextualize(_context); 110 _validatorManager.service(_cocoonManager); 111 112 _enumeratorManager = new ThreadSafeComponentManager<>(); 113 _enumeratorManager.setLogger(getLogger()); 114 _enumeratorManager.contextualize(_context); 115 _enumeratorManager.service(_cocoonManager); 116 117 _parameterParser = new SiteParameterParser(_siteParameterTypeEP, _enumeratorManager, _validatorManager); 118 } 119 120 @Override 121 public void service(ServiceManager manager) throws ServiceException 122 { 123 super.service(manager); 124 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 125 _siteTypeEP = (SiteTypesExtensionPoint) manager.lookup(SiteTypesExtensionPoint.ROLE); 126 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 127 _siteParameterTypeEP = (SiteParameterTypeExtensionPoint) manager.lookup(SiteParameterTypeExtensionPoint.ROLE); 128 } 129 130 /** 131 * Dispose the manager before restarting it 132 */ 133 @Override 134 public void dispose() 135 { 136 _isComplete = new HashMap<>(); 137 138 _parameterParser = null; 139 140 _parameters = null; 141 _validatorManager.dispose(); 142 _validatorManager = null; 143 _enumeratorManager.dispose(); 144 _enumeratorManager = null; 145 146 super.dispose(); 147 } 148 149 @Override 150 public boolean hasExtension(String id) 151 { 152 return _parameters.containsKey(id); 153 } 154 155 @Override 156 public void addExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException 157 { 158 if (getLogger().isDebugEnabled()) 159 { 160 getLogger().debug("Adding site parameters from feature " + pluginName + "/" + featureName); 161 } 162 163 Configuration[] parameterConfigurations = configuration.getChildren("param"); 164 for (Configuration parameterConfiguration : parameterConfigurations) 165 { 166 _addParameter(pluginName, featureName, parameterConfiguration); 167 } 168 } 169 170 @Override 171 public RepositoryElementType getExtension(String id) 172 { 173 return (RepositoryElementType) _parameters.get(id).getType(); 174 } 175 176 @Override 177 public Set<String> getExtensionsIds() 178 { 179 return Collections.unmodifiableSet(_parameters.keySet()); 180 } 181 182 @Override 183 public void initializeExtensions() throws Exception 184 { 185 super.initializeExtensions(); 186 _parameterParser.lookupComponents(); 187 } 188 189 /** 190 * Test if a site configuration is valid. 191 * @param siteName the site name. 192 * @return true if the site configuration is valid, false otherwise. 193 * @throws UnknownAmetysObjectException if the site doesn't exist. 194 */ 195 public boolean isValid(String siteName) throws UnknownAmetysObjectException 196 { 197 if (siteName == null) 198 { 199 throw new IllegalArgumentException("Cannot determine if a null siteName is valid or not"); 200 } 201 202 // Check if the site exists. 203 if (!_siteManager.hasSite(siteName)) 204 { 205 throw new UnknownAmetysObjectException ("Unknown site '" + siteName + "'. Can not check configuration."); 206 } 207 208 // Validate the site configuration now if it's not already done. 209 if (!_isComplete.containsKey(siteName)) 210 { 211 _isComplete.put(siteName, _validateSiteConfig(siteName)); 212 } 213 214 return _isComplete.get(siteName); 215 } 216 217 /** 218 * Reload a site's configuration. 219 * @param siteName the site name. 220 * @throws UnknownAmetysObjectException if the site doesn't exist. 221 */ 222 public void reloadConfiguration(String siteName) throws UnknownAmetysObjectException 223 { 224 // Check if the site exists. 225 _siteManager.getSite(siteName); 226 227 // Reload the site configuration. 228 _isComplete.put(siteName, _validateSiteConfig(siteName)); 229 } 230 231 /** 232 * Remove a site's configuration. 233 * @param siteName the site name. 234 */ 235 public void removeConfiguration(String siteName) 236 { 237 if (_isComplete.containsKey(siteName)) 238 { 239 _isComplete.remove(siteName); 240 } 241 } 242 243 /** 244 * Get all the parameters for a given site. 245 * @param siteName the site name. 246 * @return the parameters 247 */ 248 public Map<String, SiteParameter> getParameters(String siteName) 249 { 250 Map<String, SiteParameter> siteParams = new LinkedHashMap<>(); 251 252 Site site = _siteManager.getSite(siteName); 253 SiteType siteType = _siteTypeEP.getExtension(site.getType()); 254 255 if (siteType == null) 256 { 257 throw new IllegalStateException("The site '" + siteName + "' is using un unknown type '" + site.getType() + "'"); 258 } 259 260 for (SiteParameter parameter : _parameters.values()) 261 { 262 if (parameter.isInSiteType(siteType.getName())) 263 { 264 siteParams.put(parameter.getName(), parameter); 265 } 266 } 267 268 return siteParams; 269 } 270 271 /** 272 * Get all the parameters of a given site, classified by category and group. 273 * @param siteName the site name. 274 * @return the parameters classified by category and group. 275 */ 276 public Map<I18nizableText, Map<I18nizableText, List<SiteParameter>>> getCategorizedParameters(String siteName) 277 { 278 return _categorize(getParameters(siteName).values()); 279 } 280 281 /** 282 * Declare a site parameter. 283 * @param pluginName The name of the plugin declaring the extension. 284 * @param featureName the name of the feature 285 * @param configuration The parameter configuration. 286 * @throws ConfigurationException if configuration if not complete. 287 */ 288 protected void _addParameter(String pluginName, String featureName, Configuration configuration) throws ConfigurationException 289 { 290 SiteParameter parameter = _parameterParser.parse(_cocoonManager, pluginName, configuration, this, null); 291 292 String id = parameter.getName(); 293 294 if (!__PARAM_NAME_PATTERN.matcher(id).matches()) 295 { 296 throw new ConfigurationException("The feature " + pluginName + "/" + featureName + " declared an invalid site parameter name '" + id + "'. This value is not permited: only [a-zA-Z][a-zA-Z0-9-_]* are allowed.", configuration); 297 } 298 299 if (_parameters.containsKey(id)) 300 { 301 throw new ConfigurationException("In feature " + pluginName + "/" + featureName + " the parameter '" + id + "' is already declared. Parameter ids must be unique.", configuration); 302 } 303 304 _parameters.put(id, parameter); 305 306 if (getLogger().isDebugEnabled()) 307 { 308 getLogger().debug("Site parameter added: " + id); 309 } 310 } 311 312 /** 313 * Validate the configuration of a site. 314 * @param siteName the name of the site to check. 315 * @return true if the site is correctly configured, false otherwise. 316 */ 317 protected boolean _validateSiteConfig(String siteName) 318 { 319 if (getLogger().isDebugEnabled()) 320 { 321 getLogger().debug("Validating the configuration of site '" + siteName + "'"); 322 } 323 324 boolean siteValid = true; 325 326 Iterator<SiteParameter> params = getParameters(siteName).values().iterator(); 327 while (params.hasNext() && siteValid) 328 { 329 siteValid = _validateParameter(params.next(), siteName); 330 } 331 332 return siteValid; 333 } 334 335 /** 336 * Validate a parameter value for a given site. 337 * @param parameter the site parameter to validate. 338 * @param siteName the site name on which to check. 339 * @return true if the parameter's value is valid, false otherwise. 340 */ 341 protected boolean _validateParameter(SiteParameter parameter, String siteName) 342 { 343 String parameterId = parameter.getName(); 344 345 SiteParameter siteParameter = _parameters.get(parameterId); 346 if (siteParameter == null) 347 { 348 getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter", parameterId, siteName)); 349 } 350 351 Site site = _siteManager.getSite(siteName); 352 Object value = site.getValue(parameterId, true, null); 353 354 // TODO RUNTIME-2897: call the validateValue without boolean when multiple values are managed in enumerators 355 List<I18nizableText> errors = ModelHelper.validateValue(parameter, value, false); 356 if (!errors.isEmpty()) 357 { 358 if (getLogger().isWarnEnabled()) 359 { 360 StringBuffer sb = new StringBuffer(); 361 362 sb.append("The parameter '").append(parameterId).append("' of site '").append(siteName).append("' is not valid with value '").append(parameter.getType().toString(value)).append("':"); 363 for (I18nizableText error : errors) 364 { 365 sb.append("\n* " + error.toString()); 366 } 367 sb.append("\nConfiguration is not initialized"); 368 369 getLogger().warn(sb.toString()); 370 } 371 372 return false; 373 } 374 375 return true; 376 } 377 378 /** 379 * Organize a collection of site parameters by categories and groups. 380 * @param parameters a collection of site parameters. 381 * @return a Map of parameters sorted first by category then group. 382 */ 383 protected Map<I18nizableText, Map<I18nizableText, List<SiteParameter>>> _categorize(Collection<SiteParameter> parameters) 384 { 385 Map<I18nizableText, Map<I18nizableText, List<SiteParameter>>> categories = new TreeMap<>(new I18nizableTextTranslationComparator()); 386 387 // Classify parameters by groups and categories 388 for (SiteParameter parameter : parameters) 389 { 390 I18nizableText categoryName = parameter.getDisplayCategory(); 391 I18nizableText groupName = parameter.getDisplayGroup(); 392 393 // Get the map of groups of the category 394 Map<I18nizableText, List<SiteParameter>> category = categories.get(categoryName); 395 if (category == null) 396 { 397 category = new TreeMap<>(new I18nizableTextKeyComparator()); 398 categories.put(categoryName, category); 399 } 400 401 // Get the map of parameters of the group 402 List<SiteParameter> group = category.get(groupName); 403 if (group == null) 404 { 405 group = new ArrayList<>(); 406 category.put(groupName, group); 407 } 408 409 group.add(parameter); 410 } 411 412 return categories; 413 } 414 415 /** 416 * I18nizableText comparator for site parameters 417 * General information category is the first one, then sort the I18nizableText with their translation 418 */ 419 class I18nizableTextTranslationComparator implements Comparator<I18nizableText> 420 { 421 @Override 422 public int compare(I18nizableText t1, I18nizableText t2) 423 { 424 // The general informations category always goes first 425 if (t1.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY)) 426 { 427 if (t2.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY)) 428 { 429 return 0; 430 } 431 return -1; 432 } 433 434 if (t2.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY)) 435 { 436 return 1; 437 } 438 439 String tt1 = _i18nUtils.translate(t1); 440 if (tt1 == null) 441 { 442 return -1; 443 } 444 445 String tt2 = _i18nUtils.translate(t2); 446 if (tt2 == null) 447 { 448 return 1; 449 } 450 451 return tt1.compareTo(tt2); 452 } 453 } 454 455 public ModelItem getModelItem(String itemPath) throws UndefinedItemPathException 456 { 457 ModelItem item = _parameters.get(itemPath); 458 if (item != null) 459 { 460 return item; 461 } 462 else 463 { 464 throw new UndefinedItemPathException("The parameter '" + itemPath + "' is not defined in sites."); 465 } 466 } 467 468 public Collection< ? extends ModelItem> getModelItems() 469 { 470 return _parameters.values(); 471 } 472 473 public String getId() 474 { 475 return StringUtils.EMPTY; 476 } 477 478 public String getFamilyId() 479 { 480 return this.getClass().getName(); 481 } 482 483 /** 484 * Parser for SiteParameter. 485 */ 486 class SiteParameterParser extends CategorizedElementDefinitionParser 487 { 488 /** 489 * Creates a categorized element definition parser for the skin. 490 * @param elementTypeExtensionPoint the extension point to use to get available element types 491 * @param enumeratorManager the enumerator component manager. 492 * @param validatorManager the validator component manager. 493 */ 494 public SiteParameterParser(AbstractThreadSafeComponentExtensionPoint<? extends ElementType> elementTypeExtensionPoint, 495 ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager) 496 { 497 super(elementTypeExtensionPoint, enumeratorManager, validatorManager); 498 } 499 500 @Override 501 protected String _getNameConfigurationAttribute() 502 { 503 return "id"; 504 } 505 506 @Override 507 public SiteParameter parse(ServiceManager serviceManager, String pluginName, Configuration definitionConfig, Model model, ModelItemGroup parent) throws ConfigurationException 508 { 509 SiteParameter<?> siteParameter = (SiteParameter) super.parse(serviceManager, pluginName, definitionConfig, model, parent); 510 511 Set<String> siteTypes = new HashSet<>(); 512 String siteTypesStr = definitionConfig.getChild("site-types").getValue(""); 513 if (StringUtils.isNotEmpty(siteTypesStr)) 514 { 515 siteTypes.addAll(Arrays.asList(StringUtils.split(siteTypesStr, ", "))); 516 siteParameter.setSiteTypes(siteTypes); 517 } 518 519 return siteParameter; 520 } 521 522 @Override 523 protected SiteParameter _createModelItem(Configuration definitionConfig) throws ConfigurationException 524 { 525 return new SiteParameter<>(); 526 } 527 } 528}