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.skin;
017
018import java.io.InputStream;
019import java.util.Collections;
020import java.util.HashSet;
021import java.util.Set;
022import java.util.regex.Pattern;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.activity.Initializable;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
029import org.apache.avalon.framework.logger.AbstractLogEnabled;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.excalibur.source.Source;
034import org.apache.excalibur.source.SourceResolver;
035
036import org.ametys.core.cache.AbstractCacheManager;
037import org.ametys.core.cache.AbstractCacheManager.CacheType;
038import org.ametys.core.cache.Cache;
039import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
040import org.ametys.runtime.i18n.I18nizableText;
041import org.ametys.web.repository.page.Page;
042import org.ametys.web.repository.site.Site;
043
044/**
045 * This implementation of the templates handler is based uppon a configuration. 
046 */
047public class StaticTemplatesAssignmentHandler extends AbstractLogEnabled implements TemplatesAssignmentHandler, Serviceable, Initializable
048{ 
049    private static final String __TEMPLATE_CACHE = StaticTemplatesAssignmentHandler.class.getName() + "$tplCache";
050    private static final String __LAST_CONF_UPDATE = StaticTemplatesAssignmentHandler.class.getName() + "$lastConfUpdate";
051   
052    /** The skins manager */
053    protected SkinsManager _skinsManager;
054    /** The source resolver */
055    protected SourceResolver _srcResolver;
056
057    private AbstractCacheManager _cacheManager;
058
059    @Override
060    public void service(ServiceManager smanager) throws ServiceException
061    {
062        _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE);
063        _srcResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
064        _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE);
065    }
066    
067    @Override
068    public void initialize() throws Exception
069    {
070        _createCaches();
071    }
072
073    /**
074     * Creates the caches
075     */
076    protected void _createCaches()
077    {
078        _cacheManager.createCache(__TEMPLATE_CACHE, 
079                new I18nizableText("plugin.web", "PLUGINS_WEB_TPL_CACHE_LABEL"),
080                new I18nizableText("plugin.web", "PLUGINS_WEB_TPL_CACHE_DESCRIPTION"),
081                CacheType.MEMORY,
082                true);
083        
084        _cacheManager.createCache(__LAST_CONF_UPDATE, 
085                new I18nizableText("plugin.web", "PLUGINS_WEB_LAST_CONF_UPDATE_CACHE_LABEL"),
086                new I18nizableText("plugin.web", "PLUGINS_WEB_LAST_CONF_UPDATE_CACHE_DESCRIPTION"),
087                CacheType.MEMORY,
088                true);
089    }
090    
091    @Override
092    public Set<String> getAvailablesTemplates(String skinName)
093    {
094        Skin skin = _skinsManager.getSkin(skinName);
095        if (skin == null)
096        {
097            getLogger().warn("The unexisting skin '" + skinName + "' is referenced (by a site?)");
098            return Collections.emptySet();
099        }
100
101        _refreshValues(skin);
102
103        return _getTemplatesForSkinNameFromCache(skinName);
104    }
105    
106    @Override
107    public Set<String> getAvailablesTemplates(Page page)
108    {
109        Set<String> availableTemplates = new HashSet<>();
110        
111        Site site = page.getSite();
112        
113        String skinName = site.getSkinId();
114        Skin skin = _skinsManager.getSkin(skinName);
115        if (skin == null)
116        {
117            getLogger().warn("The site '" + site.getName() + "' reference the unexisting skin '" + site.getSkinId() + "'");
118            return availableTemplates;
119        }
120        
121        _refreshValues (skin);
122        Set<String> skinTemplates = _getTemplatesForSkinNameFromCache(skinName);
123        Cache<TemplateKey, AssignmentCondition> templateCache = _getTemplateCache();
124        for (String templateName : skinTemplates)
125        {
126            AssignmentCondition assignmentCondition = templateCache.get(TemplateKey.of(skinName, templateName));
127            if (assignmentCondition.matchCondition(page))
128            {
129                availableTemplates.add(templateName);
130            }
131        }
132        
133        return availableTemplates;
134    }
135    
136    /**
137     * Get the available templates for assignment
138     * @param skin The skin
139     */
140    protected void _refreshValues(Skin skin)
141    {
142        String skinId = skin.getId();
143        Source source = null;
144        try
145        {
146            source = _srcResolver.resolveURI("skin:" + skinId + "://conf/template_assignment.xml");
147            if (source.exists())
148            {
149                Cache<String, Long> lastConfUpdateCache = _getLastConfUpdateCache();
150                if (!lastConfUpdateCache.hasKey(skinId) || lastConfUpdateCache.get(skinId) < source.getLastModified())
151                {
152                    lastConfUpdateCache.put(skinId, source.getLastModified());
153                
154                    try (InputStream is = source.getInputStream())
155                    {
156                        Configuration configuration = new DefaultConfigurationBuilder().build(is);
157                        _parseAvailableTemplates (skin, configuration);
158                    }
159                }
160            }
161            else
162            {
163                _getAllTemplatesWithoutCondition(skin);
164            }
165        }
166        catch (Exception e)
167        {
168            getLogger().error("Unable to read the available templates configuration file", e);
169        }
170    }
171    
172    private void _parseAvailableTemplates (Skin skin, Configuration configuration) throws ConfigurationException
173    {
174        String skinId = skin.getId();
175        boolean exclude = configuration.getChild("list", false) == null || "exclude".equals(configuration.getChild("list").getAttribute("mode", "include"));
176        
177        if (exclude)
178        {
179            _getAllTemplatesWithoutCondition (skin);          
180        }
181        
182        Configuration[] tplList = configuration.getChild("list").getChildren("template");
183        for (Configuration tplConf : tplList)
184        {
185            String reName = tplConf.getAttribute("name");
186            Pattern namePattern = _getPattern (reName);
187            
188            for (String templateName : skin.getTemplates())
189            {
190                if (namePattern.matcher(templateName).matches())
191                {
192                    Cache<TemplateKey, AssignmentCondition> templateCache = _getTemplateCache();
193                    TemplateKey templateKey = TemplateKey.of(skinId, templateName);
194                    if (!exclude)
195                    {
196                        templateCache.put(templateKey, new AssignmentCondition(skinId, templateName));
197                    }
198                    else
199                    {
200                        templateCache.invalidate(templateKey);
201                    }
202                }
203            }
204        }
205        
206        Set<String> findCondition = new HashSet<>();
207        
208        // Conditions
209        Configuration[] conditions = configuration.getChild("conditions").getChildren("condition");
210        for (Configuration conditionConf : conditions)
211        {
212            String reName = conditionConf.getAttribute("template");
213            Pattern namePattern = _getPattern (reName);
214            
215            Set<String> skinTemplates = _getTemplatesForSkinNameFromCache(skinId);
216            for (String templateName : skinTemplates)
217            {
218                if (!findCondition.contains(templateName) && namePattern.matcher(templateName).matches())
219                {
220                    findCondition.add(templateName);
221
222                    AssignmentCondition condition = _getTemplateCache().get(TemplateKey.of(skinId, templateName));
223                    
224                    String regexp = conditionConf.getChild("page").getAttribute("regexp_path", null);
225                    if (regexp != null)
226                    {
227                        condition.setRegExpPath(regexp, false);
228                    }
229                    else
230                    {
231                        String reverseRegexp = conditionConf.getChild("page").getAttribute("reverse_regexp_path", null);
232                        if (reverseRegexp != null)
233                        {
234                            condition.setRegExpPath(reverseRegexp, true);
235                        }
236                    }
237                }
238            }
239        }
240    }
241    
242    private void _getAllTemplatesWithoutCondition (Skin skin)
243    {
244        String skinId = skin.getId();
245        Cache<TemplateKey, AssignmentCondition> templateCache = _getTemplateCache();
246        for (String templateName : skin.getTemplates())
247        {
248            templateCache.put(TemplateKey.of(skinId, templateName), new AssignmentCondition(skinId, templateName));
249        }
250    }
251    
252    private Pattern _getPattern (String pattern)
253    {
254        String regexp = "^" + pattern.replaceAll("\\*", "([^/]*)") + "$";
255        return Pattern.compile(regexp);
256    }
257
258
259    private Cache<TemplateKey, AssignmentCondition> _getTemplateCache()
260    {
261        return _cacheManager.get(__TEMPLATE_CACHE);
262    }
263
264    private Cache<String, Long> _getLastConfUpdateCache()
265    {
266        return _cacheManager.get(__LAST_CONF_UPDATE);
267    }
268
269    private Set<String> _getTemplatesForSkinNameFromCache(String skinName)
270    {
271        return _getTemplateCache()
272                .asMap()
273                .keySet()
274                .stream()
275                .filter(key ->
276                {
277                    String currentSkinName = key.getSkinName();
278                    return currentSkinName.equals(skinName);
279                })
280                .map(TemplateKey::getTemplateName)
281                .collect(Collectors.toSet());
282    }
283    
284    /**
285     * Class representing the condition for a template assignment
286     */
287    public static class AssignmentCondition 
288    {
289        private String _skinName;
290        private String _templateName;
291        private String _rePath;
292        private boolean _reverse;
293        
294        /**
295         * Constructor
296         * @param skinName the skin name
297         * @param templateName the template name
298         */
299        public AssignmentCondition (String skinName, String templateName)
300        {
301            _skinName = skinName;
302            _templateName = templateName;
303        }
304        
305        /**
306         * Set the RegExp for path
307         * @param rePath the RegExp for path
308         * @param reverse true to reverse mode
309         */
310        public void setRegExpPath (String rePath, boolean reverse)
311        {
312            _rePath = (rePath.startsWith("^") ? "" : "^") + rePath + (rePath.startsWith("$") ? "" : "$");
313            _reverse = reverse;
314        }
315        
316        /**
317         * Test if page matches condition
318         * @param page the page to test
319         * @return true if page matches condition
320         */
321        public boolean matchCondition (Page page)
322        {
323            if (_rePath == null)
324            {
325                return true;
326            }
327            
328            if (!_reverse)
329            {
330                return Pattern.compile(_rePath).matcher(page.getPathInSitemap()).matches();
331            }
332            else
333            {
334                return !Pattern.compile(_rePath).matcher(page.getPathInSitemap()).matches();
335            }
336        }
337        
338        /**
339         * Get the skin name
340         * @return the skin name
341         */
342        public String getSkinName ()
343        {
344            return _skinName;
345        }
346        
347        /**
348         * Get the template name
349         * @return the template name
350         */
351        public String getTemplateName ()
352        {
353            return _templateName;
354        }
355    }
356    
357    private static final class TemplateKey extends AbstractCacheKey
358    {
359        private TemplateKey(String skinName, String templateName)
360        {
361            super(skinName, templateName);
362        }
363        
364        static TemplateKey of(String skinName, String templateName)
365        {
366            return new TemplateKey(skinName, templateName);
367        }
368        
369        String getSkinName()
370        {
371            return (String) getFields().get(0);
372        }
373        
374        String getTemplateName()
375        {
376            return (String) getFields().get(1);
377        }
378    }
379    
380}