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}