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