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