001/*
002 *  Copyright 2025 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.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.activity.Initializable;
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.logger.AbstractLogEnabled;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.excalibur.source.Source;
033import org.apache.excalibur.source.SourceResolver;
034
035import org.ametys.core.cache.AbstractCacheManager;
036import org.ametys.core.cache.Cache;
037import org.ametys.runtime.i18n.I18nizableText;
038
039/**
040 * Helper for input data for the link directory user preferences in thumbnails mode 
041 */
042public class LinkDirectoryThemesInputDataHelper extends AbstractLogEnabled implements Component, Serviceable, Initializable
043{
044    /** The component role */
045    public static final String ROLE = LinkDirectoryThemesInputDataHelper.class.getName();
046
047    /** The wildcard */
048    public static final String WILDCARD = "*";
049    
050    private static final String __THEMES_CACHE = LinkDirectoryThemesInputDataHelper.class.getName() + "$skinInputDataThemesCache";
051    
052    private SourceResolver _sourceResolver;
053    private DirectoryHelper _directoryHelper;
054    private AbstractCacheManager _cacheManager;
055    private Map<SkinInputDataFile, String> _configurationError;
056    
057    /** The last time the file was loaded */
058    private Map<SkinInputDataFile, Long> _lastConfUpdate;
059
060    @Override
061    public void initialize() throws Exception
062    {
063        _lastConfUpdate = new HashMap<>();
064        _configurationError = new HashMap<>();
065        _cacheManager.createMemoryCache(__THEMES_CACHE,
066                new I18nizableText("plugin.link-directory", "PLUGINS_LINK_DIRECTORY_CACHE_THEMES_LABEL"),
067                new I18nizableText("plugin.link-directory", "PLUGINS_LINK_DIRECTORY_CACHE_THEMES_DESCRIPTION"),
068                true,
069                null);
070    }
071    
072    @Override
073    public void service(ServiceManager manager) throws ServiceException
074    {
075        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
076        _directoryHelper = (DirectoryHelper) manager.lookup(DirectoryHelper.ROLE);
077        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
078    }
079    
080    
081    /**
082     * Get the list of the themes input data for a given skin and default configuration file
083     * @param skinId the given skin id
084     * @return the list of the themes input data
085     */
086    public ConfiguredThemesInputData getThemesInputData(String skinId)
087    {
088        return getThemesInputData(skinId, DirectoryHelper.DEFAULT_CONF_FILE_PATH);
089    }
090    
091    /**
092     * Get the list of the themes input data for a given skin and given configuration file
093     * @param skinId the given skin id
094     * @param confFilePath The path of configuration file
095     * @return the list of the themes input data
096     */
097    public ConfiguredThemesInputData getThemesInputData(String skinId, String confFilePath)
098    {
099        try
100        {
101            SkinInputDataFile inputDataFile = new SkinInputDataFile(skinId, confFilePath);
102            _updateConfigurationValues(inputDataFile);
103            if (_configurationError.containsKey(inputDataFile))
104            {
105                // Some configuration errors occurred, return empty list
106                return new ConfiguredThemesInputData(List.of(), _configurationError.get(inputDataFile));
107            }
108
109            return new ConfiguredThemesInputData(_getThemesCache().get(inputDataFile, k -> new ArrayList<>()), null);
110        }
111        catch (Exception e)
112        {
113            getLogger().error("An error occurred while retrieving information from the skin configuration", e);
114            // Configuration file is not readable => toSAX method will not generate any xml
115            return new ConfiguredThemesInputData(List.of(), null);
116        }
117    }
118
119    /**
120     * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them
121     * @param inputDataFile The record targetting the skin and configuration file to read
122     * @throws Exception if an exception occurs
123     */
124    private void _updateConfigurationValues(SkinInputDataFile inputDataFile) throws Exception
125    {
126        Source source = null;
127        try
128        {
129            source = _sourceResolver.resolveURI(inputDataFile.filePath());
130            if (source.exists())
131            {
132                _cacheConfigurationValues(source, inputDataFile, !_getThemesCache().hasKey(inputDataFile));
133            }
134            else
135            {
136                if (getLogger().isInfoEnabled())
137                {
138                    getLogger().info("There is no configuration file at path '" + inputDataFile.filePath() + "' (no input data for link directory).");
139                }
140                
141                _lastConfUpdate.put(inputDataFile, (long) 0);
142                _getThemesCache().put(inputDataFile, null);
143            }
144        }
145        finally
146        {
147            if (_sourceResolver != null && source != null)
148            {
149                _sourceResolver.release(source);
150            }
151        }
152    }
153    
154    /**
155     * Read the configuration values and store them
156     * @param source the file's source
157     * @param inputDataFile The key targetting the skin and configuration file
158     * @param forceRead true to force reload of values even if the file was not modified
159     */
160    private synchronized void _cacheConfigurationValues (Source source, SkinInputDataFile inputDataFile, boolean forceRead)
161    {
162        long lastModified = source.getLastModified();
163
164        if (!forceRead && _lastConfUpdate.containsKey(inputDataFile) && _lastConfUpdate.get(inputDataFile) != 0 && lastModified == _lastConfUpdate.get(inputDataFile))
165        {
166            // While waiting for synchronized, someone else may have updated the cache
167            return;
168        }
169
170        List<ThemesInputData> themesCache = new ArrayList<>();
171
172        getLogger().info("Caching configuration");
173        
174        try
175        {
176            Configuration configuration = _directoryHelper.getSkinLinksConfiguration(inputDataFile.skinId(), inputDataFile.filePath());
177            
178            Configuration[] themesConfigurations = configuration.getChild("inputdata").getChildren("themes");
179            
180            for (Configuration themesConfiguration : themesConfigurations)
181            {
182                List<Map<String, String>> themes = new ArrayList<> ();
183                
184                Configuration[] themeConfigurations = themesConfiguration.getChildren();
185                for (Configuration themeConfiguration : themeConfigurations)
186                {
187                    Map<String, String> theme = new HashMap<> ();
188                    String id = themeConfiguration.getAttribute("id", null);
189                    theme.put("id", id);
190                    theme.put("lang", themeConfiguration.getAttribute("lang", null));
191                    themes.add(theme);
192                }
193                
194                String[] templates = StringUtils.split(themesConfiguration.getAttribute("templates", WILDCARD), ',');
195                
196                ThemesInputData themeInputData = new ThemesInputData(themesConfiguration.getAttribute("inputDataId", StringUtils.EMPTY), Arrays.asList(templates), themes, themesConfiguration.getAttributeAsBoolean("configurable", false), themesConfiguration.getAttributeAsBoolean("displayUserLinks", false), themesConfiguration.getAttributeAsBoolean("excludeAnonymous", false));
197                themesCache.add(themeInputData);
198            }
199            
200            _configurationError.remove(inputDataFile);
201            _getThemesCache().put(inputDataFile, themesCache);
202            _lastConfUpdate.put(inputDataFile, source.getLastModified());
203        }
204        catch (Exception e)
205        {
206            getLogger().warn("An error occured while getting the configuration's file values", e);
207            _configurationError.put(inputDataFile, e.getMessage());
208        }
209    }
210
211    private Cache<SkinInputDataFile, List<ThemesInputData>> _getThemesCache()
212    {
213        return _cacheManager.get(__THEMES_CACHE);
214    }
215    
216    /**
217     * A record representing a themes input data
218     * @param id the input data id
219     * @param templates the templates of the themes
220     * @param themes the themes in the input data
221     * @param configurable <code>true</code> if the themes are configurable
222     * @param displayUserLinks <code>true</code> to have the user links
223     * @param excludeAnonymous <code>true</code> to ignore all links of the themes for anonymous user
224     */
225    public record ThemesInputData(String id, List<String> templates, List<Map<String, String>> themes, boolean configurable, boolean displayUserLinks, boolean excludeAnonymous) { /* */ }
226    
227    /**
228     * A record representing the list of themes input datas with the configuration errors
229     * @param themesInputDatas the themes input data
230     * @param error the error
231     */
232    public record ConfiguredThemesInputData(List<ThemesInputData> themesInputDatas, String error) { /* */ }
233    
234    /**
235     * Record representing a link directory input data configuration file
236     * @param skinId the id of the skin
237     * @param filePath the path of configuration file
238     */
239    record SkinInputDataFile (String skinId, String filePath) { /* */ }
240}