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}