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.Date;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.Iterator;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.TreeMap;
032import java.util.regex.Pattern;
033
034import org.apache.avalon.framework.configuration.Configuration;
035import org.apache.avalon.framework.configuration.ConfigurationException;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.commons.lang.StringUtils;
039
040import org.ametys.core.util.I18nUtils;
041import org.ametys.plugins.repository.AmetysRepositoryException;
042import org.ametys.plugins.repository.UnknownAmetysObjectException;
043import org.ametys.plugins.repository.metadata.UnknownMetadataException;
044import org.ametys.runtime.i18n.I18nizableText;
045import org.ametys.runtime.parameter.AbstractParameterParser;
046import org.ametys.runtime.parameter.Enumerator;
047import org.ametys.runtime.parameter.Errors;
048import org.ametys.runtime.parameter.ParameterHelper;
049import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
050import org.ametys.runtime.parameter.Validator;
051import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
052import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
053import org.ametys.web.repository.site.Site;
054import org.ametys.web.repository.site.SiteManager;
055import org.ametys.web.repository.site.SiteType;
056import org.ametys.web.repository.site.SiteTypesExtensionPoint;
057
058/**
059 * Extension point holding all {@link SiteParameter} definitions.
060 */
061public class SiteConfigurationExtensionPoint extends AbstractThreadSafeComponentExtensionPoint<SiteParameter>
062{
063    /** Avalon Role */
064    public static final String ROLE = SiteConfigurationExtensionPoint.class.getName();
065    
066    private static final Pattern __PARAM_NAME_PATTERN = Pattern.compile("[a-z][a-z0-9-_]*", Pattern.CASE_INSENSITIVE);
067    
068    private static final String __GENERAL_INFORMATIONS_I18N_KEY = "PLUGINS_WEB_SITE_INFORMATION_CATEGORY";
069    
070    /** The site manager. */
071    protected SiteManager _siteManager;
072    
073    /** The site type extension point. */
074    protected SiteTypesExtensionPoint _siteTypeEP;
075    
076    /** Component gathering utility methods for internationalizable text translation {@link I18nUtils} */
077    protected I18nUtils _i18nUtils;
078    
079    /** Parameters map, indexed by parameter ID. */
080    private Map<String, SiteParameter> _parameters;
081    
082    /** Determines if all parameters are valued, by site. */
083    private Map<String, Boolean> _isComplete;
084    
085    /** ComponentManager for {@link Validator}s. */
086    private ThreadSafeComponentManager<Validator> _validatorManager;
087    
088    /** ComponentManager for {@link Enumerator}s. */
089    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
090    
091    /** Site parameter parser. */
092    private SiteParameterParser _parameterParser;
093    
094    
095    @Override
096    public void initialize() throws Exception
097    {
098        super.initialize();
099        
100        _parameters = new LinkedHashMap<>();
101        _isComplete = new HashMap<>();
102        
103        _validatorManager = new ThreadSafeComponentManager<>();
104        _validatorManager.setLogger(getLogger());
105        _validatorManager.contextualize(_context);
106        _validatorManager.service(_cocoonManager);
107        
108        _enumeratorManager = new ThreadSafeComponentManager<>();
109        _enumeratorManager.setLogger(getLogger());
110        _enumeratorManager.contextualize(_context);
111        _enumeratorManager.service(_cocoonManager);
112        
113        _parameterParser = new SiteParameterParser(_enumeratorManager, _validatorManager);
114    }
115    
116    @Override
117    public void service(ServiceManager manager) throws ServiceException
118    {
119        super.service(manager);
120        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
121        _siteTypeEP = (SiteTypesExtensionPoint) manager.lookup(SiteTypesExtensionPoint.ROLE);
122        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
123    }
124    
125    /**
126     * Dispose the manager before restarting it
127     */
128    @Override
129    public void dispose()
130    {
131        _isComplete = new HashMap<>();
132        
133        _parameterParser = null;
134        
135        _parameters = null;
136        _validatorManager.dispose();
137        _validatorManager = null;
138        _enumeratorManager.dispose();
139        _enumeratorManager = null;
140        
141        super.dispose();
142    }
143    
144    @Override
145    public boolean hasExtension(String id)
146    {
147        return _parameters.containsKey(id);
148    }
149    
150    @Override
151    public void addExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException
152    {
153        if (getLogger().isDebugEnabled())
154        {
155            getLogger().debug("Adding site parameters from feature " + pluginName + "/" + featureName);
156        }
157        
158        Configuration[] parameterConfigurations = configuration.getChildren("param");
159        for (Configuration parameterConfiguration : parameterConfigurations)
160        {
161            _addParameter(pluginName, featureName, parameterConfiguration);
162        }
163    }
164    
165    @Override
166    public SiteParameter getExtension(String id)
167    {
168        return _parameters.get(id);
169    }
170    
171    @Override
172    public Set<String> getExtensionsIds()
173    {
174        return Collections.unmodifiableSet(_parameters.keySet());
175    }
176    
177    @Override
178    public void initializeExtensions() throws Exception
179    {
180        super.initializeExtensions();
181        _parameterParser.lookupComponents();
182    }
183    
184    /**
185     * Test if a site configuration is valid.
186     * @param siteName the site name.
187     * @return true if the site configuration is valid, false otherwise.
188     * @throws UnknownAmetysObjectException if the site doesn't exist.
189     */
190    public boolean isValid(String siteName) throws UnknownAmetysObjectException
191    {
192        if (siteName == null)
193        {
194            throw new IllegalArgumentException("Cannot determine if a null siteName is valid or not");
195        }
196        
197        // Check if the site exists.
198        if (!_siteManager.hasSite(siteName))
199        {
200            throw new UnknownAmetysObjectException ("Unknown site '" + siteName + "'. Can not check configuration.");
201        }
202        
203        // Validate the site configuration now if it's not already done.
204        if (!_isComplete.containsKey(siteName))
205        {
206            _isComplete.put(siteName, _validateSiteConfig(siteName));
207        }
208        
209        return _isComplete.get(siteName);
210    }
211    
212    /**
213     * Reload a site's configuration.
214     * @param siteName the site name.
215     * @throws UnknownAmetysObjectException if the site doesn't exist.
216     */
217    public void reloadConfiguration(String siteName) throws UnknownAmetysObjectException
218    {
219        // Check if the site exists.
220        _siteManager.getSite(siteName);
221        
222        // Reload the site configuration.
223        _isComplete.put(siteName, _validateSiteConfig(siteName));
224    }
225    
226    /**
227     * Remove a site's configuration.
228     * @param siteName the site name.
229     */
230    public void removeConfiguration(String siteName)
231    {
232        if (_isComplete.containsKey(siteName))
233        {
234            _isComplete.remove(siteName);
235        }
236    }
237    
238    /**
239     * Get all the parameters for a given site.
240     * @param siteName the site name.
241     * @return the parameters
242     */
243    public Map<String, SiteParameter> getParameters(String siteName)
244    {
245        Map<String, SiteParameter> siteParams = new LinkedHashMap<>();
246        
247        Site site = _siteManager.getSite(siteName);
248        SiteType siteType = _siteTypeEP.getExtension(site.getType());
249        
250        if (siteType == null)
251        {
252            throw new IllegalStateException("The site '" + siteName + "' is using un unknown type '" + site.getType() + "'");
253        }
254        
255        for (SiteParameter parameter : _parameters.values())
256        {
257            if (parameter.isInSiteType(siteType.getName()))
258            {
259                siteParams.put(parameter.getId(), parameter);
260            }
261        }
262        
263        return siteParams;
264    }
265    
266    /**
267     * Get all the parameters of a given site, classified by category and group.
268     * @param siteName the site name.
269     * @return the parameters classified by category and group.
270     */
271    public Map<I18nizableText, Map<I18nizableText, List<SiteParameter>>> getCategorizedParameters(String siteName)
272    {
273        return _categorize(getParameters(siteName).values());
274    }
275    
276    /**
277     * Return the untyped value of a site parameter as String.
278     * @param siteName the site name.
279     * @param id Id of the parameter to get.
280     * @return the typed value as String, the default value or null if the parameter does not exist.
281     * @throws UnknownAmetysObjectException if the site does not exist.
282     * @throws AmetysRepositoryException if another repository error occurs.
283     */
284    public String getUntypedValue (String siteName, String id) throws UnknownAmetysObjectException, AmetysRepositoryException
285    {
286        SiteParameter siteParameter = _parameters.get(id);
287        if (siteParameter == null)
288        {
289            getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter", id, siteName));
290            return null;
291        }
292        
293        Site site = _siteManager.getSite(siteName);
294        try
295        {
296            return site.getMetadataHolder().getString(id);
297        }
298        catch (UnknownMetadataException e)
299        {
300            return ParameterHelper.valueToString(siteParameter.getDefaultValue());
301        }
302    }
303    
304    /**
305     * Return the value of a site parameter.
306     * @param siteName the site name.
307     * @param id Id of the parameter to get.
308     * @return the typed value as String, the default value or null if the parameter does not exist.
309     * @throws UnknownAmetysObjectException if the site does not exist.
310     * @throws AmetysRepositoryException if another repository error occurs.
311     */
312    public Object getValue (String siteName, String id) throws UnknownAmetysObjectException, AmetysRepositoryException
313    {
314        SiteParameter siteParameter = _parameters.get(id);
315        if (siteParameter == null)
316        {
317            getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter", id, siteName));
318            return null;
319        }
320        
321        Site site = _siteManager.getSite(siteName);
322        try
323        {
324            String untypedValue = site.getMetadataHolder().getString(id);
325            return ParameterHelper.castValue(untypedValue, siteParameter.getType());
326        }
327        catch (UnknownMetadataException e)
328        {
329            return siteParameter.getDefaultValue();
330        }
331    }
332    
333    /**
334     * Return the typed value as String.
335     * @param siteName the site name.
336     * @param id Id of the parameter to get.
337     * @return the typed value as String, the default value or null if the parameter does not exist.
338     * @throws UnknownAmetysObjectException if the site does not exist.
339     * @throws AmetysRepositoryException if another repository error occurs.
340     */
341    public String getValueAsString(String siteName, String id) throws UnknownAmetysObjectException, AmetysRepositoryException
342    {
343        SiteParameter siteParameter = _parameters.get(id);
344        if (siteParameter == null || siteParameter.getType() != ParameterType.STRING)
345        {
346            getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter or it is not of type STRING.", id, siteName));
347            return null;
348        }
349        
350        Site site = _siteManager.getSite(siteName);
351        try
352        {
353            return site.getMetadataHolder().getString(id);
354        }
355        catch (UnknownMetadataException e)
356        {
357            return (String) siteParameter.getDefaultValue();
358        }
359    }
360    
361    /**
362     * Return the typed value as Date.
363     * @param siteName the site name.
364     * @param id Id of the parameter to get.
365     * @return the typed value as Date, the default value or null if the parameter does not exist.
366     * @throws UnknownAmetysObjectException if the site does not exist.
367     * @throws AmetysRepositoryException if another repository error occurs.
368     */
369    public Date getValueAsDate(String siteName, String id) throws UnknownAmetysObjectException, AmetysRepositoryException
370    {
371        SiteParameter siteParameter = _parameters.get(id);
372        if (siteParameter == null || siteParameter.getType() != ParameterType.DATE)
373        {
374            getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter or it is not of type DATE.", id, siteName));
375            return null;
376        }
377        
378        Site site = _siteManager.getSite(siteName);
379        try
380        {
381            return site.getMetadataHolder().getDate(id);
382        }
383        catch (UnknownMetadataException e)
384        {
385            return (Date) siteParameter.getDefaultValue();
386        }
387    }
388    
389    /**
390     * Return the typed value as long.
391     * @param siteName the site name.
392     * @param id Id of the parameter to get.
393     * @return the typed value as a Long, the default value or null if the parameter does not exist.
394     * @throws UnknownAmetysObjectException if the site does not exist.
395     * @throws AmetysRepositoryException if another repository error occurs.
396     */
397    public Long getValueAsLong(String siteName, String id) throws UnknownAmetysObjectException, AmetysRepositoryException
398    {
399        SiteParameter siteParameter = _parameters.get(id);
400        if (siteParameter == null || siteParameter.getType() != ParameterType.LONG)
401        {
402            getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter or it is not of type LONG.", id, siteName));
403            return null;
404        }
405        
406        Site site = _siteManager.getSite(siteName);
407        try
408        {
409            return site.getMetadataHolder().getLong(id);
410        }
411        catch (UnknownMetadataException e)
412        {
413            return (Long) siteParameter.getDefaultValue();
414        }
415    }
416    
417    /**
418     * Return the typed value as boolean.
419     * @param siteName the site name.
420     * @param id Id of the parameter to get
421     * @return the typed value as a Boolean, the default value or null if the parameter does not exist.
422     * @throws UnknownAmetysObjectException if the site does not exist.
423     * @throws AmetysRepositoryException if another repository error occurs.
424     */
425    public Boolean getValueAsBoolean(String siteName, String id) throws UnknownAmetysObjectException, AmetysRepositoryException
426    {
427        SiteParameter siteParameter = _parameters.get(id);
428        if (siteParameter == null || siteParameter.getType() != ParameterType.BOOLEAN)
429        {
430            getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter or it is not of type BOOLEAN.", id, siteName));
431            return null;
432        }
433        
434        Site site = _siteManager.getSite(siteName);
435        try
436        {
437            return site.getMetadataHolder().getBoolean(id);
438        }
439        catch (UnknownMetadataException e)
440        {
441            return (Boolean) siteParameter.getDefaultValue();
442        }
443    }
444    
445    /**
446     * Return the typed value casted as double.
447     * @param siteName the site name.
448     * @param id Id of the parameter to get.
449     * @return the typed value as a Double, the default value or null if the parameter does not exist.
450     * @throws UnknownAmetysObjectException if the site does not exist.
451     * @throws AmetysRepositoryException if another repository error occurs.
452     */
453    public Double getValueAsDouble(String siteName, String id) throws UnknownAmetysObjectException, AmetysRepositoryException
454    {
455        SiteParameter siteParameter = _parameters.get(id);
456        if (siteParameter == null || siteParameter.getType() != ParameterType.DOUBLE)
457        {
458            getLogger().warn(String.format("Unable to get site parameter of id '%s' for site '%s': it is not a site's parameter or it is not of type DOUBLE.", id, siteName));
459            return null;
460        }
461        
462        Site site = _siteManager.getSite(siteName);
463        try
464        {
465            return site.getMetadataHolder().getDouble(id);
466        }
467        catch (UnknownMetadataException e)
468        {
469            return (Double) siteParameter.getDefaultValue();
470        }
471    }
472    
473    /**
474     * Declare a site parameter.
475     * @param pluginName The name of the plugin declaring the extension.
476     * @param featureName the name of the feature
477     * @param configuration The parameter configuration.
478     * @throws ConfigurationException if configuration if not complete.
479     */
480    protected void _addParameter(String pluginName, String featureName, Configuration configuration) throws ConfigurationException
481    {
482        SiteParameter parameter = _parameterParser.parseParameter(_cocoonManager, pluginName, configuration);
483        String id = parameter.getId();
484        
485        if (!__PARAM_NAME_PATTERN.matcher(id).matches())
486        {
487            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);
488        }
489        
490        if (_parameters.containsKey(id))
491        {
492            throw new ConfigurationException("In feature " + pluginName + "/" + featureName + " the parameter '" + id + "' is already declared. Parameter ids must be unique.", configuration);
493        }
494        
495        _parameters.put(id, parameter);
496        
497        if (getLogger().isDebugEnabled())
498        {
499            getLogger().debug("Site parameter added: " + id);
500        }
501    }
502    
503    /**
504     * Validate the configuration of a site.
505     * @param siteName the name of the site to check.
506     * @return true if the site is correctly configured, false otherwise.
507     */
508    protected boolean _validateSiteConfig(String siteName)
509    {
510        if (getLogger().isDebugEnabled())
511        {
512            getLogger().debug("Validating the configuration of site '" + siteName + "'");
513        }
514        
515        boolean siteValid = true;
516        
517        Iterator<SiteParameter> params = getParameters(siteName).values().iterator();
518        while (params.hasNext() && siteValid)
519        {
520            siteValid = _validateParameter(params.next(), siteName);
521        }
522        
523        return siteValid;
524    }
525    
526    /**
527     * Validate a parameter value for a given site.
528     * @param parameter the site parameter to validate.
529     * @param siteName the site name on which to check.
530     * @return true if the parameter's value is valid, false otherwise.
531     */
532    protected boolean _validateParameter(SiteParameter parameter, String siteName)
533    {
534        String parameterId = parameter.getId();
535        
536        Object typedValue = getValue(siteName, parameterId);
537        
538        Validator v = parameter.getValidator();
539        Errors validationErrors = new Errors();
540        if (v != null)
541        {
542            v.validate(typedValue != null ? typedValue : "", validationErrors);
543        }
544        
545        if (validationErrors.hasErrors())
546        {
547            if (getLogger().isWarnEnabled())
548            {
549                StringBuffer sb = new StringBuffer();
550                sb.append("The parameter '").append(parameterId).append("' of site '").append(siteName).append("' is not valid with value '").append(ParameterHelper.valueToString(typedValue)).append("' :");
551                for (I18nizableText error : validationErrors.getErrors())
552                {
553                    sb.append("\n* " + error.toString());
554                }
555                sb.append("\nConfiguration is not initialized");
556                
557                getLogger().warn(sb.toString());
558            }
559            
560            return false;
561        }
562        
563        return true;
564    }
565    
566    /**
567     * Organize a collection of site parameters by categories and groups.
568     * @param parameters a collection of site parameters.
569     * @return a Map of parameters sorted first by category then group.
570     */
571    protected Map<I18nizableText, Map<I18nizableText, List<SiteParameter>>> _categorize(Collection<SiteParameter> parameters)
572    {
573        Map<I18nizableText, Map<I18nizableText, List<SiteParameter>>> categories = new TreeMap<>(new I18nizableTextTranslationComparator());
574
575        // Classify parameters by groups and categories
576        for (SiteParameter parameter : parameters)
577        {
578            I18nizableText categoryName = parameter.getDisplayCategory();
579            I18nizableText groupName = parameter.getDisplayGroup();
580
581            // Get the map of groups of the category
582            Map<I18nizableText, List<SiteParameter>> category = categories.get(categoryName);
583            if (category == null)
584            {
585                category = new TreeMap<>(new I18nizableTextComparator());
586                categories.put(categoryName, category);
587            }
588
589            // Get the map of parameters of the group
590            List<SiteParameter> group = category.get(groupName);
591            if (group == null)
592            {
593                group = new ArrayList<>();
594                category.put(groupName, group);
595            }
596
597            group.add(parameter);
598        }
599        
600        return categories;
601    }
602    
603    class I18nizableTextComparator implements Comparator<I18nizableText>
604    {
605        @Override
606        public int compare(I18nizableText t1, I18nizableText t2)
607        {
608            String str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
609            String str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
610
611            return str1.toString().compareTo(str2.toString());
612        }
613    }
614    
615    class I18nizableTextTranslationComparator implements Comparator<I18nizableText>
616    {
617        @Override
618        public int compare(I18nizableText t1, I18nizableText t2)
619        {
620            // The general informations category always goes first
621            if (t1.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY))
622            {
623                if (t2.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY))
624                {
625                    return 0;
626                }
627                return -1;
628            }
629
630            if (t2.getKey().equals(__GENERAL_INFORMATIONS_I18N_KEY))
631            {
632                return 1;
633            }
634            
635            String tt1 = _i18nUtils.translate(t1);
636            if (tt1 == null)
637            {
638                return -1;
639            }
640            
641            String tt2 = _i18nUtils.translate(t2);
642            if (tt2 == null)
643            {
644                return 1;
645            }
646            
647            return tt1.compareTo(tt2);
648        }
649    }
650    
651    /**
652     * Parser for SiteParameter.
653     */
654    class SiteParameterParser extends AbstractParameterParser<SiteParameter, ParameterType>
655    {
656        public SiteParameterParser(ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager)
657        {
658            super(enumeratorManager, validatorManager);
659        }
660        
661        @Override
662        protected SiteParameter _createParameter(Configuration parameterConfig) throws ConfigurationException
663        {
664            return new SiteParameter();
665        }
666        
667        @Override
668        protected String _parseId(Configuration parameterConfig) throws ConfigurationException
669        {
670            return parameterConfig.getAttribute("id");
671        }
672        
673        @Override
674        protected ParameterType _parseType(Configuration parameterConfig) throws ConfigurationException
675        {
676            try
677            {
678                return ParameterType.valueOf(parameterConfig.getAttribute("type").toUpperCase());
679            }
680            catch (IllegalArgumentException e)
681            {
682                throw new ConfigurationException("Invalid parameter type", parameterConfig, e);
683            }
684        }
685        
686        @Override
687        protected Object _parseDefaultValue(Configuration parameterConfig, SiteParameter parameter)
688        {
689            String value;
690            
691            Configuration childNode = parameterConfig.getChild("default-value", false);
692            if (childNode == null)
693            {
694                value = null;
695            }
696            else
697            {
698                value = childNode.getValue("");
699            }
700            
701            return ParameterHelper.castValue(value, parameter.getType());
702        }
703        
704        @Override
705        protected void _additionalParsing(ServiceManager manager, String pluginName, Configuration parameterConfig, String parameterId, SiteParameter parameter) throws ConfigurationException
706        {
707            super._additionalParsing(manager, pluginName, parameterConfig, parameterId, parameter);
708            
709            parameter.setId(parameterId);
710            parameter.setDisplayCategory(_parseI18nizableText(parameterConfig, pluginName, "category"));
711            parameter.setDisplayGroup(_parseI18nizableText(parameterConfig, pluginName, "group"));
712            
713            Set<String> siteTypes = new HashSet<>();
714            String siteTypesStr = parameterConfig.getChild("site-types").getValue("");
715            if (StringUtils.isNotEmpty(siteTypesStr))
716            {
717                siteTypes.addAll(Arrays.asList(StringUtils.split(siteTypesStr, ", ")));
718                parameter.setSiteTypes(siteTypes);
719            }
720        }
721    }
722
723}