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}