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