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.Cache;
038import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
039import org.ametys.runtime.i18n.I18nizableText;
040import org.ametys.web.repository.page.Page;
041import org.ametys.web.repository.site.Site;
042
043/**
044 * This implementation of the templates handler is based uppon a configuration. 
045 */
046public class StaticTemplatesAssignmentHandler extends AbstractLogEnabled implements TemplatesAssignmentHandler, Serviceable, Initializable
047{ 
048    private static final String __TEMPLATE_CACHE = StaticTemplatesAssignmentHandler.class.getName() + "$tplCache";
049    private static final String __LAST_CONF_UPDATE = StaticTemplatesAssignmentHandler.class.getName() + "$lastConfUpdate";
050   
051    /** The skins manager */
052    protected SkinsManager _skinsManager;
053    /** The source resolver */
054    protected SourceResolver _srcResolver;
055
056    private AbstractCacheManager _cacheManager;
057
058    @Override
059    public void service(ServiceManager smanager) throws ServiceException
060    {
061        _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE);
062        _srcResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
063        _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE);
064    }
065    
066    @Override
067    public void initialize() throws Exception
068    {
069        _createCaches();
070    }
071
072    /**
073     * Creates the caches
074     */
075    protected void _createCaches()
076    {
077        _cacheManager.createMemoryCache(__TEMPLATE_CACHE, 
078                new I18nizableText("plugin.web", "PLUGINS_WEB_TPL_CACHE_LABEL"),
079                new I18nizableText("plugin.web", "PLUGINS_WEB_TPL_CACHE_DESCRIPTION"),
080                true,
081                null);
082        
083        _cacheManager.createMemoryCache(__LAST_CONF_UPDATE, 
084                new I18nizableText("plugin.web", "PLUGINS_WEB_LAST_CONF_UPDATE_CACHE_LABEL"),
085                new I18nizableText("plugin.web", "PLUGINS_WEB_LAST_CONF_UPDATE_CACHE_DESCRIPTION"),
086                true,
087                null);
088    }
089    
090    @Override
091    public Set<String> getAvailablesTemplates(String skinName)
092    {
093        Skin skin = _skinsManager.getSkin(skinName);
094        if (skin == null)
095        {
096            getLogger().warn("The unexisting skin '" + skinName + "' is referenced (by a site?)");
097            return Collections.emptySet();
098        }
099
100        _refreshValues(skin);
101
102        return _getTemplatesForSkinNameFromCache(skinName);
103    }
104    
105    @Override
106    public Set<String> getAvailablesTemplates(Page page)
107    {
108        Set<String> availableTemplates = new HashSet<>();
109        
110        Site site = page.getSite();
111        
112        String skinName = site.getSkinId();
113        Skin skin = _skinsManager.getSkin(skinName);
114        if (skin == null)
115        {
116            getLogger().warn("The site '" + site.getName() + "' reference the unexisting skin '" + site.getSkinId() + "'");
117            return availableTemplates;
118        }
119        
120        _refreshValues (skin);
121        Set<String> skinTemplates = _getTemplatesForSkinNameFromCache(skinName);
122        Cache<TemplateKey, AssignmentCondition> templateCache = _getTemplateCache();
123        for (String templateName : skinTemplates)
124        {
125            AssignmentCondition assignmentCondition = templateCache.get(TemplateKey.of(skinName, templateName));
126            if (assignmentCondition.matchCondition(page))
127            {
128                availableTemplates.add(templateName);
129            }
130        }
131        
132        return availableTemplates;
133    }
134    
135    /**
136     * Get the available templates for assignment
137     * @param skin The skin
138     */
139    protected void _refreshValues(Skin skin)
140    {
141        String skinId = skin.getId();
142        Source source = null;
143        try
144        {
145            source = _srcResolver.resolveURI("skin:" + skinId + "://conf/template_assignment.xml");
146            if (source.exists())
147            {
148                Cache<String, Long> lastConfUpdateCache = _getLastConfUpdateCache();
149                if (!lastConfUpdateCache.hasKey(skinId) || lastConfUpdateCache.get(skinId) < source.getLastModified())
150                {
151                    lastConfUpdateCache.put(skinId, source.getLastModified());
152                
153                    try (InputStream is = source.getInputStream())
154                    {
155                        Configuration configuration = new DefaultConfigurationBuilder().build(is);
156                        _parseAvailableTemplates (skin, configuration);
157                    }
158                }
159            }
160            else
161            {
162                _getAllTemplatesWithoutCondition(skin);
163            }
164        }
165        catch (Exception e)
166        {
167            getLogger().error("Unable to read the available templates configuration file", e);
168        }
169    }
170    
171    private void _parseAvailableTemplates (Skin skin, Configuration configuration) throws ConfigurationException
172    {
173        String skinId = skin.getId();
174        boolean exclude = configuration.getChild("list", false) == null || "exclude".equals(configuration.getChild("list").getAttribute("mode", "include"));
175        
176        if (exclude)
177        {
178            _getAllTemplatesWithoutCondition (skin);          
179        }
180        
181        Configuration[] tplList = configuration.getChild("list").getChildren("template");
182        for (Configuration tplConf : tplList)
183        {
184            String reName = tplConf.getAttribute("name");
185            Pattern namePattern = _getPattern (reName);
186            
187            for (String templateName : skin.getTemplates())
188            {
189                if (namePattern.matcher(templateName).matches())
190                {
191                    Cache<TemplateKey, AssignmentCondition> templateCache = _getTemplateCache();
192                    TemplateKey templateKey = TemplateKey.of(skinId, templateName);
193                    if (!exclude)
194                    {
195                        templateCache.put(templateKey, new AssignmentCondition(skinId, templateName));
196                    }
197                    else
198                    {
199                        templateCache.invalidate(templateKey);
200                    }
201                }
202            }
203        }
204        
205        Set<String> findCondition = new HashSet<>();
206        
207        // Conditions
208        Configuration[] conditions = configuration.getChild("conditions").getChildren("condition");
209        for (Configuration conditionConf : conditions)
210        {
211            String reName = conditionConf.getAttribute("template");
212            Pattern namePattern = _getPattern (reName);
213            
214            Set<String> skinTemplates = _getTemplatesForSkinNameFromCache(skinId);
215            for (String templateName : skinTemplates)
216            {
217                if (!findCondition.contains(templateName) && namePattern.matcher(templateName).matches())
218                {
219                    findCondition.add(templateName);
220
221                    AssignmentCondition condition = _getTemplateCache().get(TemplateKey.of(skinId, templateName));
222                    
223                    String regexp = conditionConf.getChild("page").getAttribute("regexp_path", null);
224                    if (regexp != null)
225                    {
226                        condition.setRegExpPath(regexp, false);
227                    }
228                    else
229                    {
230                        String reverseRegexp = conditionConf.getChild("page").getAttribute("reverse_regexp_path", null);
231                        if (reverseRegexp != null)
232                        {
233                            condition.setRegExpPath(reverseRegexp, true);
234                        }
235                    }
236                }
237            }
238        }
239    }
240    
241    private void _getAllTemplatesWithoutCondition (Skin skin)
242    {
243        String skinId = skin.getId();
244        Cache<TemplateKey, AssignmentCondition> templateCache = _getTemplateCache();
245        for (String templateName : skin.getTemplates())
246        {
247            templateCache.put(TemplateKey.of(skinId, templateName), new AssignmentCondition(skinId, templateName));
248        }
249    }
250    
251    private Pattern _getPattern (String pattern)
252    {
253        String regexp = "^" + pattern.replaceAll("\\*", "([^/]*)") + "$";
254        return Pattern.compile(regexp);
255    }
256
257
258    private Cache<TemplateKey, AssignmentCondition> _getTemplateCache()
259    {
260        return _cacheManager.get(__TEMPLATE_CACHE);
261    }
262
263    private Cache<String, Long> _getLastConfUpdateCache()
264    {
265        return _cacheManager.get(__LAST_CONF_UPDATE);
266    }
267
268    private Set<String> _getTemplatesForSkinNameFromCache(String skinName)
269    {
270        return _getTemplateCache()
271                .asMap()
272                .keySet()
273                .stream()
274                .filter(key ->
275                {
276                    String currentSkinName = key.getSkinName();
277                    return currentSkinName.equals(skinName);
278                })
279                .map(TemplateKey::getTemplateName)
280                .filter(s -> !s.equals("sitemap")) // The sitemap template is a special template for the SitemapPageTool
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}