001/*
002 *  Copyright 2018 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.plugins.linkdirectory;
017
018import java.io.InputStream;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.activity.Initializable;
027import org.apache.avalon.framework.configuration.Configuration;
028import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
029import org.apache.avalon.framework.context.Context;
030import org.apache.avalon.framework.context.ContextException;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.logger.AbstractLogEnabled;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.cocoon.ProcessingException;
037import org.apache.cocoon.components.ContextHelper;
038import org.apache.cocoon.environment.Request;
039import org.apache.cocoon.xml.AttributesImpl;
040import org.apache.cocoon.xml.XMLUtils;
041import org.apache.commons.collections.CollectionUtils;
042import org.apache.commons.lang3.StringUtils;
043import org.apache.excalibur.source.Source;
044import org.apache.excalibur.source.SourceResolver;
045import org.xml.sax.ContentHandler;
046import org.xml.sax.SAXException;
047
048import org.ametys.core.user.CurrentUserProvider;
049import org.ametys.core.user.UserIdentity;
050import org.ametys.plugins.linkdirectory.repository.DefaultLink;
051import org.ametys.web.inputdata.InputData;
052import org.ametys.web.repository.page.Page;
053import org.ametys.web.repository.site.Site;
054
055/**
056 * Input data for the link directory user preferences in thumbnails mode 
057 */
058public class LinkDirectoryInputData extends AbstractLogEnabled implements Contextualizable, InputData, Initializable, Serviceable
059{
060    /** The path to the configuration file */
061    private static final String __CONF_FILE_PATH = "skin://conf/link-themes.xml";
062    
063    /** The wildcard */
064    private static final String __WILDCARD = "*";
065    
066    /** The current user provider */
067    protected CurrentUserProvider _currentUserProvider;
068    
069    /** The Avalon context */
070    private Context _context;
071    
072    /** Excalibur source resolver */
073    private SourceResolver _sourceResolver;
074    
075    private DirectoryHelper _directoryHelper;
076    
077    /** Cache for themes: <skinId, List<theme info>> */
078    private Map<String, List<ThemeInputData>> _themesCache;
079    
080    private Map<String, String> _configurationError;
081    
082    /** The last time the file was loaded */
083    private Map<String, Long> _lastConfUpdate;
084
085    @Override
086    public void contextualize(Context context) throws ContextException
087    {
088        _context = context; 
089    }
090    
091    @Override
092    public void initialize() throws Exception
093    {
094        _lastConfUpdate = new HashMap<>();
095        _themesCache = new HashMap<>();
096        _configurationError = new HashMap<>();
097    }
098    
099    @Override
100    public void service(ServiceManager manager) throws ServiceException
101    {
102        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
103        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
104        _directoryHelper = (DirectoryHelper) manager.lookup(DirectoryHelper.ROLE);
105    }
106    
107    @Override
108    public boolean isCacheable(Site site, Page currentPage)
109    {
110        Request request = ContextHelper.getRequest(_context);
111        
112        String template = _getTemplate(request);
113        if (template == null)
114        {
115            return true;
116        }
117        
118        try
119        {
120            String skinId = site.getSkinId();
121            _updateConfigurationValues(skinId);
122            if (CollectionUtils.isEmpty(_themesCache.get(skinId)) || _configurationError.containsKey(skinId))
123            {
124                // No configuration file or there are errors
125                return true;
126            }
127            
128            String language = _directoryHelper.getLanguage(request);
129            
130            Map<String, Boolean> templateProperties = _getTemplateProperties(skinId, _getTemplate(request));
131            if (!templateProperties.get("applicable"))
132            {
133                // The current template is not configured for a link directory input data
134                return true;
135            }
136            
137            if (templateProperties.get("configurable") || templateProperties.get("displayUserLinks"))
138            {
139                // The applications are configurable
140                return false;
141            }
142            
143            // Find the configured theme ids for this template 
144            List<String> configuredThemesNames = _getConfiguredThemes(skinId, template, language);
145            List<String> configuredThemesIds = _directoryHelper.getThemesIdsFromThemesNames(site, language, configuredThemesNames);
146            String siteName = _directoryHelper.getSiteName(request);
147            
148            return !_directoryHelper.hasRestrictions(siteName, language, configuredThemesIds) && !_directoryHelper.hasInternalUrl(siteName, language, configuredThemesIds);
149        }
150        catch (Exception e)
151        {
152            getLogger().error("An error occurred while retrieving information from the skin configuration", e);
153            // Configuration file is not readable => toSAX method will not generate any xml
154            return true;
155        }
156    }
157
158    @Override
159    public void toSAX(ContentHandler contentHandler) throws ProcessingException
160    {
161        Request request = ContextHelper.getRequest(_context);
162        
163        // Get the current user's login if he is in the front office
164        UserIdentity user = _currentUserProvider.getUser();
165
166        String template = _getTemplate(request);
167        if (template == null)
168        {
169            getLogger().info("There is no current template");
170            return; 
171        }
172        
173        String skinId = _getSkin(request);
174        if (skinId == null)
175        {
176            getLogger().info("There is no current skin");
177            return; 
178        }
179        
180        try
181        {
182            _updateConfigurationValues(skinId);
183            if (CollectionUtils.isEmpty(_themesCache.get(skinId)))
184            {
185                return;
186            }
187
188            contentHandler.startDocument();
189            AttributesImpl attrs = new AttributesImpl();
190            
191            // Is there an error in the configuration file ?
192            if (_configurationError.containsKey(skinId))
193            {
194                attrs.addCDATAAttribute("error", _configurationError.get(skinId));
195                XMLUtils.createElement(contentHandler, "linkDirectory", attrs);
196            }
197            else
198            {
199                Map<String, Boolean> templateProperties = _getTemplateProperties(skinId, template);
200                attrs.addCDATAAttribute("applicable", templateProperties.get("applicable") ? "true" : "false");
201                attrs.addCDATAAttribute("configurable", templateProperties.get("configurable") ? "true" : "false");
202                
203                XMLUtils.startElement(contentHandler, "linkDirectory", attrs);
204                
205                String language = _directoryHelper.getLanguage(request);
206                String siteName = _directoryHelper.getSiteName(request);
207                
208                List<String> configuredThemesNames = _getConfiguredThemes(skinId, template, _directoryHelper.getLanguage(request));
209                if (configuredThemesNames != null)
210                {
211                    Map<String, List<String>> themesMap = _directoryHelper.getThemesMap(configuredThemesNames, siteName, language);
212                    List<String> correctThemesIds = themesMap.get("themes");
213                    List<String> unknownThemesNames = themesMap.get("unknown-themes");
214                    
215                    _saxThemes(contentHandler, correctThemesIds, unknownThemesNames);
216                    _saxLinks(contentHandler, user, request, correctThemesIds, templateProperties.get("displayUserLinks"));
217                }
218                
219                XMLUtils.endElement(contentHandler, "linkDirectory");
220            }
221            
222            contentHandler.endDocument();
223        }
224        catch (Exception e)
225        {
226            getLogger().error("An exception occurred during the processing of the link directory's input data" , e);
227        }
228    }
229    
230    private void _saxThemes(ContentHandler contentHandler, List<String> themeIds, List<String> unknownThemesNames) throws SAXException
231    {
232        if (!themeIds.isEmpty())
233        {
234            XMLUtils.startElement(contentHandler, "themes");
235            for (String themeId : themeIds)
236            {
237                XMLUtils.createElement(contentHandler, "theme", themeId);
238            }
239            XMLUtils.endElement(contentHandler, "themes");
240        }
241        
242        if (!unknownThemesNames.isEmpty())
243        {
244            AttributesImpl attr = new AttributesImpl();
245            attr.addCDATAAttribute("count", Integer.toString(unknownThemesNames.size()));
246            XMLUtils.createElement(contentHandler, "unknown-themes", attr, StringUtils.join(unknownThemesNames, ", "));
247        }
248    }
249
250    private void _saxLinks(ContentHandler contentHandler, UserIdentity user, Request request, List<String> themeIds, boolean displayUserLinks) throws ProcessingException
251    {
252        String language = _directoryHelper.getLanguage(request);
253        String siteName = _directoryHelper.getSiteName(request);
254        try
255        {
256            List<DefaultLink> links = _directoryHelper.getLinks(themeIds, user, siteName, language);
257            
258            XMLUtils.startElement(contentHandler, "links");
259            
260            // SAX common links
261            _directoryHelper.saxLinks(siteName, contentHandler, links, _directoryHelper.getContextVars(request), siteName + "/" + language, user, true, true, false);
262            
263            // SAX the user own links
264            if (user != null && displayUserLinks)
265            {
266                List<DefaultLink> userLinks = _directoryHelper.getUserLinks(siteName, language, user).stream().collect(Collectors.toList());
267                _directoryHelper.saxLinks(siteName, contentHandler, userLinks, _directoryHelper.getContextVars(request), siteName + "/" + language, user, true, true, true);
268            }
269            
270            XMLUtils.endElement(contentHandler, "links");
271        }
272        catch (Exception e)
273        {
274            throw new ProcessingException("An error occurred while retrieving or saxing the links", e);
275        }
276    }
277
278    /**
279     * Retrieve the configured themes names defined in the skin file link-themes.xml for the current template and current language
280     * @param skinId The skin
281     * @param template the current page's template
282     * @param lang the current language
283     * @return the list of configured themes ids or null
284     */
285    private List<String> _getConfiguredThemes(String skinId, String template, String lang)
286    {
287        if (CollectionUtils.isNotEmpty(_themesCache.get(skinId)))
288        {
289            for (ThemeInputData themeInputData : _themesCache.get(skinId))
290            {
291                List<String> templates = themeInputData.getTemplates();
292                if (templates.contains(template) || templates.contains(__WILDCARD))
293                {
294                    List<String> matchingThemes = new ArrayList<>();
295                    
296                    List<Map<String, String>> themes = themeInputData.getThemes();
297                    for (Map<String, String> theme : themes)
298                    {
299                        if (theme.get("lang").equals(lang))
300                        {
301                            matchingThemes.add(theme.get("id"));
302                        }
303                    }
304                    return matchingThemes;
305                }
306            }
307        }
308        
309        // The current template is not configured for a link directory input data
310        return null;
311    }
312    
313    /**
314     * Retrieve the "configurable" and "applicable" attributes of a template of the configuration file
315     * @param skinId The skin
316     * @param template the template's name
317     * @return a Map with configurable and applicable configuration
318     */
319    private Map<String, Boolean> _getTemplateProperties(String skinId, String template) 
320    {
321        boolean isApplicable = false;
322        boolean isConfigurable = false;
323        boolean displayUserLinks = false;
324        
325        if (CollectionUtils.isNotEmpty(_themesCache.get(skinId)))
326        {
327            for (ThemeInputData themeInputData : _themesCache.get(skinId))
328            {
329                if (themeInputData.getTemplates().contains(template))
330                {
331                    isApplicable = true;
332                    isConfigurable = themeInputData.isConfigurable();
333                    displayUserLinks = themeInputData.displayUserLinks();
334                    break;
335                }
336            }
337        }
338        
339        Map<String, Boolean> result = new HashMap<> ();
340        result.put("applicable", isApplicable);
341        result.put("configurable", isConfigurable);
342        result.put("displayUserLinks", displayUserLinks);
343        
344        return result;
345    }
346    
347    /**
348     * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them
349     * @param skinId The skin
350     * @throws Exception if an exception occurs
351     */
352    private void _updateConfigurationValues(String skinId) throws Exception
353    {
354        Source source = null;
355        try
356        {
357            source = _sourceResolver.resolveURI(__CONF_FILE_PATH);
358            if (source.exists())
359            {
360                long lastModified = source.getLastModified();
361                if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId))
362                {
363                    return;
364                }
365                
366                _cacheConfigurationValues(source, skinId);
367            }
368            else
369            {
370                if (getLogger().isInfoEnabled())
371                {
372                    getLogger().info("There is no configuration file at path '" + __CONF_FILE_PATH + "' (no input data for link directory).");
373                }
374                
375                _lastConfUpdate.put(skinId, (long) 0);
376                _themesCache.put(skinId, null);
377            }
378        }
379        finally
380        {
381            if (_sourceResolver != null && source != null)
382            {
383                _sourceResolver.release(source);
384            }
385        }
386    }
387    
388    /**
389     * Read the configuration values and store them
390     * @param source the file's source
391     * @param skinId The skin
392     */
393    private synchronized void _cacheConfigurationValues (Source source, String skinId)
394    {
395        long lastModified = source.getLastModified();
396        if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId))
397        {
398            // While waiting for synchronized, someone else may have updated the cache
399            return;
400        }
401
402        List<ThemeInputData> themesCache = new ArrayList<>();
403
404        getLogger().info("Caching configuration");
405        
406        try (InputStream is = source.getInputStream())
407        {
408            
409            Configuration configuration = new DefaultConfigurationBuilder().build(is);
410            
411            Configuration[] themesConfigurations = configuration.getChildren("themes");
412            
413            for (Configuration themesConfiguration : themesConfigurations)
414            {
415                List<Map<String, String>> themes = new ArrayList<> ();
416                
417                Configuration[] themeConfigurations = themesConfiguration.getChildren();
418                for (Configuration themeConfiguration : themeConfigurations)
419                {
420                    Map<String, String> theme = new HashMap<> ();
421                    theme.put("id", themeConfiguration.getAttribute("id", null));
422                    theme.put("lang", themeConfiguration.getAttribute("lang", null));
423                    themes.add(theme);
424                }
425                
426                String[] templates = StringUtils.split(themesConfiguration.getAttribute("templates", "*"), ',');
427                
428                ThemeInputData themeInputData = new ThemeInputData(Arrays.asList(templates), themes, themesConfiguration.getAttributeAsBoolean("configurable", false), themesConfiguration.getAttributeAsBoolean("displayUserLinks", false));
429                themesCache.add(themeInputData);
430            }
431            
432            _configurationError.remove(skinId);
433            _themesCache.put(skinId, themesCache);
434            _lastConfUpdate.put(skinId, (long) 0);
435        }
436        catch (Exception e)
437        {
438            getLogger().warn("An error occured while getting the configuration's file values", e);
439            _configurationError.put(skinId, e.getMessage());
440        }
441    }
442       
443    /**
444     * Get the current template 
445     * @param request the request
446     * @return the current template
447     */
448    private String _getTemplate(Request request)
449    {
450        return (String) request.getAttribute("template");
451    }
452    
453    
454     /**
455      * Get the current skin 
456      * @param request the request
457      * @return the current skin
458      */
459    private String _getSkin(Request request)
460    {
461        return (String) request.getAttribute("skin");
462    }
463     
464    private class ThemeInputData 
465    { 
466        private List<String> _templates;
467        private List<Map<String, String>> _themes;
468        private boolean _configurable;
469        private boolean _displayUserLinks;
470        
471        ThemeInputData (List<String> templates, List<Map<String, String>> themes, boolean configurable, boolean displayUserLinks)
472        {
473            _templates = templates;
474            _themes = themes;
475            _configurable = configurable;
476            _displayUserLinks = displayUserLinks;
477        } 
478        
479        boolean isConfigurable ()
480        {
481            return _configurable;
482        }
483        
484        boolean displayUserLinks()
485        {
486            return _displayUserLinks;
487        }
488        
489        List<String> getTemplates ()
490        {
491            return _templates;
492        }
493        
494        List<Map<String, String>> getThemes ()
495        {
496            return _themes;
497        }
498        
499    }
500}