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            // SAX common links
257            List<DefaultLink> links = _directoryHelper.getLinks(themeIds, user, siteName, language);
258            
259            XMLUtils.startElement(contentHandler, "links");
260            
261            
262            // SAX the user own links
263            List<DefaultLink> userLinks = null;
264            if (user != null && displayUserLinks)
265            {
266                userLinks = _directoryHelper.getUserLinks(siteName, language, user).stream().collect(Collectors.toList());
267            }
268            
269            _directoryHelper.saxLinks(siteName, contentHandler, links, userLinks, _directoryHelper.getContextVars(request), siteName + "/" + language, user, true, true);
270            
271            XMLUtils.endElement(contentHandler, "links");
272        }
273        catch (Exception e)
274        {
275            throw new ProcessingException("An error occurred while retrieving or saxing the links", e);
276        }
277    }
278
279    /**
280     * Retrieve the configured themes names defined in the skin file link-themes.xml for the current template and current language
281     * @param skinId The skin
282     * @param template the current page's template
283     * @param lang the current language
284     * @return the list of configured themes ids or null
285     */
286    private List<String> _getConfiguredThemes(String skinId, String template, String lang)
287    {
288        if (CollectionUtils.isNotEmpty(_themesCache.get(skinId)))
289        {
290            for (ThemeInputData themeInputData : _themesCache.get(skinId))
291            {
292                List<String> templates = themeInputData.getTemplates();
293                if (templates.contains(template) || templates.contains(__WILDCARD))
294                {
295                    List<String> matchingThemes = new ArrayList<>();
296                    
297                    List<Map<String, String>> themes = themeInputData.getThemes();
298                    for (Map<String, String> theme : themes)
299                    {
300                        if (theme.get("lang").equals(lang))
301                        {
302                            matchingThemes.add(theme.get("id"));
303                        }
304                    }
305                    return matchingThemes;
306                }
307            }
308        }
309        
310        // The current template is not configured for a link directory input data
311        return null;
312    }
313    
314    /**
315     * Retrieve the "configurable" and "applicable" attributes of a template of the configuration file
316     * @param skinId The skin
317     * @param template the template's name
318     * @return a Map with configurable and applicable configuration
319     */
320    private Map<String, Boolean> _getTemplateProperties(String skinId, String template) 
321    {
322        boolean isApplicable = false;
323        boolean isConfigurable = false;
324        boolean displayUserLinks = false;
325        
326        if (CollectionUtils.isNotEmpty(_themesCache.get(skinId)))
327        {
328            for (ThemeInputData themeInputData : _themesCache.get(skinId))
329            {
330                if (themeInputData.getTemplates().contains(template))
331                {
332                    isApplicable = true;
333                    isConfigurable = themeInputData.isConfigurable();
334                    displayUserLinks = themeInputData.displayUserLinks();
335                    break;
336                }
337            }
338        }
339        
340        Map<String, Boolean> result = new HashMap<> ();
341        result.put("applicable", isApplicable);
342        result.put("configurable", isConfigurable);
343        result.put("displayUserLinks", displayUserLinks);
344        
345        return result;
346    }
347    
348    /**
349     * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them
350     * @param skinId The skin
351     * @throws Exception if an exception occurs
352     */
353    private void _updateConfigurationValues(String skinId) throws Exception
354    {
355        Source source = null;
356        try
357        {
358            source = _sourceResolver.resolveURI(__CONF_FILE_PATH);
359            if (source.exists())
360            {
361                long lastModified = source.getLastModified();
362                if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId))
363                {
364                    return;
365                }
366                
367                _cacheConfigurationValues(source, skinId);
368            }
369            else
370            {
371                if (getLogger().isInfoEnabled())
372                {
373                    getLogger().info("There is no configuration file at path '" + __CONF_FILE_PATH + "' (no input data for link directory).");
374                }
375                
376                _lastConfUpdate.put(skinId, (long) 0);
377                _themesCache.put(skinId, null);
378            }
379        }
380        finally
381        {
382            if (_sourceResolver != null && source != null)
383            {
384                _sourceResolver.release(source);
385            }
386        }
387    }
388    
389    /**
390     * Read the configuration values and store them
391     * @param source the file's source
392     * @param skinId The skin
393     */
394    private synchronized void _cacheConfigurationValues (Source source, String skinId)
395    {
396        long lastModified = source.getLastModified();
397        if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId))
398        {
399            // While waiting for synchronized, someone else may have updated the cache
400            return;
401        }
402
403        List<ThemeInputData> themesCache = new ArrayList<>();
404
405        getLogger().info("Caching configuration");
406        
407        try (InputStream is = source.getInputStream())
408        {
409            
410            Configuration configuration = new DefaultConfigurationBuilder().build(is);
411            
412            Configuration[] themesConfigurations = configuration.getChildren("themes");
413            
414            for (Configuration themesConfiguration : themesConfigurations)
415            {
416                List<Map<String, String>> themes = new ArrayList<> ();
417                
418                Configuration[] themeConfigurations = themesConfiguration.getChildren();
419                for (Configuration themeConfiguration : themeConfigurations)
420                {
421                    Map<String, String> theme = new HashMap<> ();
422                    theme.put("id", themeConfiguration.getAttribute("id", null));
423                    theme.put("lang", themeConfiguration.getAttribute("lang", null));
424                    themes.add(theme);
425                }
426                
427                String[] templates = StringUtils.split(themesConfiguration.getAttribute("templates", "*"), ',');
428                
429                ThemeInputData themeInputData = new ThemeInputData(Arrays.asList(templates), themes, themesConfiguration.getAttributeAsBoolean("configurable", false), themesConfiguration.getAttributeAsBoolean("displayUserLinks", false));
430                themesCache.add(themeInputData);
431            }
432            
433            _configurationError.remove(skinId);
434            _themesCache.put(skinId, themesCache);
435            _lastConfUpdate.put(skinId, (long) 0);
436        }
437        catch (Exception e)
438        {
439            getLogger().warn("An error occured while getting the configuration's file values", e);
440            _configurationError.put(skinId, e.getMessage());
441        }
442    }
443       
444    /**
445     * Get the current template 
446     * @param request the request
447     * @return the current template
448     */
449    private String _getTemplate(Request request)
450    {
451        return (String) request.getAttribute("template");
452    }
453    
454    
455     /**
456      * Get the current skin 
457      * @param request the request
458      * @return the current skin
459      */
460    private String _getSkin(Request request)
461    {
462        return (String) request.getAttribute("skin");
463    }
464     
465    private class ThemeInputData 
466    { 
467        private List<String> _templates;
468        private List<Map<String, String>> _themes;
469        private boolean _configurable;
470        private boolean _displayUserLinks;
471        
472        ThemeInputData (List<String> templates, List<Map<String, String>> themes, boolean configurable, boolean displayUserLinks)
473        {
474            _templates = templates;
475            _themes = themes;
476            _configurable = configurable;
477            _displayUserLinks = displayUserLinks;
478        } 
479        
480        boolean isConfigurable ()
481        {
482            return _configurable;
483        }
484        
485        boolean displayUserLinks()
486        {
487            return _displayUserLinks;
488        }
489        
490        List<String> getTemplates ()
491        {
492            return _templates;
493        }
494        
495        List<Map<String, String>> getThemes ()
496        {
497            return _themes;
498        }
499        
500    }
501}