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 __CONF_FILE_PATH = "skin://conf/link-directory.xml";
051    private static final String __THEMES_CACHE = LinkDirectoryThemesInputDataHelper.class.getName() + "$skinInputDataThemesCache";
052    
053    private SourceResolver _sourceResolver;
054    private DirectoryHelper _directoryHelper;
055    private AbstractCacheManager _cacheManager;
056    private Map<String, String> _configurationError;
057    
058    /** The last time the file was loaded */
059    private Map<String, Long> _lastConfUpdate;
060
061    @Override
062    public void initialize() throws Exception
063    {
064        _lastConfUpdate = new HashMap<>();
065        _configurationError = new HashMap<>();
066        _cacheManager.createMemoryCache(__THEMES_CACHE,
067                new I18nizableText("plugin.link-directory", "PLUGINS_LINK_DIRECTORY_CACHE_THEMES_LABEL"),
068                new I18nizableText("plugin.link-directory", "PLUGINS_LINK_DIRECTORY_CACHE_THEMES_DESCRIPTION"),
069                true,
070                null);
071    }
072    
073    @Override
074    public void service(ServiceManager manager) throws ServiceException
075    {
076        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
077        _directoryHelper = (DirectoryHelper) manager.lookup(DirectoryHelper.ROLE);
078        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
079    }
080    
081    /**
082     * Get the list of the themes input data for a given skin
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        try
089        {
090            _updateConfigurationValues(skinId);
091            if (_configurationError.containsKey(skinId))
092            {
093                // Some configuration errors occurred, return empty list
094                return new ConfiguredThemesInputData(List.of(), _configurationError.get(skinId));
095            }
096            
097            return new ConfiguredThemesInputData(_getThemesCache().get(skinId, k -> new ArrayList<>()), null);
098        }
099        catch (Exception e)
100        {
101            getLogger().error("An error occurred while retrieving information from the skin configuration", e);
102            // Configuration file is not readable => toSAX method will not generate any xml
103            return new ConfiguredThemesInputData(List.of(), null);
104        }
105    }
106
107    /**
108     * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them
109     * @param skinId The skin
110     * @throws Exception if an exception occurs
111     */
112    private void _updateConfigurationValues(String skinId) throws Exception
113    {
114        Source source = null;
115        try
116        {
117            source = _sourceResolver.resolveURI(__CONF_FILE_PATH);
118            if (source.exists())
119            {
120                _cacheConfigurationValues(source, skinId, !_getThemesCache().hasKey(skinId));
121            }
122            else
123            {
124                if (getLogger().isInfoEnabled())
125                {
126                    getLogger().info("There is no configuration file at path '" + __CONF_FILE_PATH + "' (no input data for link directory).");
127                }
128                
129                _lastConfUpdate.put(skinId, (long) 0);
130                _getThemesCache().put(skinId, null);
131            }
132        }
133        finally
134        {
135            if (_sourceResolver != null && source != null)
136            {
137                _sourceResolver.release(source);
138            }
139        }
140    }
141    
142    /**
143     * Read the configuration values and store them
144     * @param source the file's source
145     * @param skinId The skin
146     * @param forceRead true to force reload of values even if the file was not modified
147     */
148    private synchronized void _cacheConfigurationValues (Source source, String skinId, boolean forceRead)
149    {
150        long lastModified = source.getLastModified();
151        if (!forceRead && _lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId))
152        {
153            // While waiting for synchronized, someone else may have updated the cache
154            return;
155        }
156
157        List<ThemesInputData> themesCache = new ArrayList<>();
158
159        getLogger().info("Caching configuration");
160        
161        try
162        {
163            Configuration configuration = _directoryHelper.getSkinLinksConfiguration(skinId);
164            
165            Configuration[] themesConfigurations = configuration.getChild("inputdata").getChildren("themes");
166            
167            for (Configuration themesConfiguration : themesConfigurations)
168            {
169                List<Map<String, String>> themes = new ArrayList<> ();
170                
171                Configuration[] themeConfigurations = themesConfiguration.getChildren();
172                for (Configuration themeConfiguration : themeConfigurations)
173                {
174                    Map<String, String> theme = new HashMap<> ();
175                    String id = themeConfiguration.getAttribute("id", null);
176                    theme.put("id", id);
177                    theme.put("lang", themeConfiguration.getAttribute("lang", null));
178                    themes.add(theme);
179                }
180                
181                String[] templates = StringUtils.split(themesConfiguration.getAttribute("templates", WILDCARD), ',');
182                
183                ThemesInputData themeInputData = new ThemesInputData(themesConfiguration.getAttribute("inputDataId", StringUtils.EMPTY), Arrays.asList(templates), themes, themesConfiguration.getAttributeAsBoolean("configurable", false), themesConfiguration.getAttributeAsBoolean("displayUserLinks", false));
184                themesCache.add(themeInputData);
185            }
186            
187            _configurationError.remove(skinId);
188            _getThemesCache().put(skinId, themesCache);
189            _lastConfUpdate.put(skinId, source.getLastModified());
190        }
191        catch (Exception e)
192        {
193            getLogger().warn("An error occured while getting the configuration's file values", e);
194            _configurationError.put(skinId, e.getMessage());
195        }
196    }
197       
198    private Cache<String, List<ThemesInputData>> _getThemesCache()
199    {
200        return _cacheManager.get(__THEMES_CACHE);
201    }
202    
203    /**
204     * A record representing a themes input data
205     * @param id the input data id
206     * @param templates the templates of the themes
207     * @param themes the themes in the input data
208     * @param configurable <code>true</code> if the themes are configurable
209     * @param displayUserLinks <code>true</code> to have the user links
210     */
211    public record ThemesInputData(String id, List<String> templates, List<Map<String, String>> themes, boolean configurable, boolean displayUserLinks) { /* */ }
212    
213    /**
214     * A record representing the list of themes input datas with the configuration errors
215     * @param themesInputDatas the themes input data
216     * @param error the error
217     */
218    public record ConfiguredThemesInputData(List<ThemesInputData> themesInputDatas, String error) { /* */ }
219}