001/* 002 * Copyright 2023 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.web.repository.page.virtual; 017 018import java.io.InputStream; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.concurrent.ConcurrentHashMap; 024 025import org.apache.avalon.framework.activity.Initializable; 026import org.apache.avalon.framework.configuration.Configurable; 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 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.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.components.ContextHelper; 037import org.apache.cocoon.environment.Request; 038import org.apache.excalibur.source.Source; 039 040import org.ametys.core.cache.AbstractCacheManager; 041import org.ametys.core.cache.Cache; 042import org.ametys.core.util.filereloader.FileReloader; 043import org.ametys.core.util.filereloader.FileReloaderUtils; 044import org.ametys.runtime.i18n.I18nizableText; 045import org.ametys.runtime.plugin.component.AbstractLogEnabled; 046import org.ametys.runtime.plugin.component.PluginAware; 047import org.ametys.web.WebConstants; 048import org.ametys.web.repository.page.Page; 049import org.ametys.web.repository.site.Site; 050import org.ametys.web.repository.site.SiteManager; 051 052/** 053 * This class represents the configuration of a virtual page based on an XML configuration 054 */ 055public class VirtualPageConfiguration extends AbstractLogEnabled implements Configurable, Serviceable, Contextualizable, PluginAware, Initializable 056{ 057 private static final String __CACHE_PREFIX = VirtualPageConfiguration.class.getName() + "$Cache$"; 058 059 /** The file reloader utils */ 060 protected FileReloaderUtils _fileReloaderUtils; 061 /** The site manager */ 062 protected SiteManager _siteManager; 063 /** The cache manager */ 064 protected AbstractCacheManager _cacheManager; 065 /** The context */ 066 protected Context _context; 067 068 private String _id; 069 private String _path; 070 071 private Map<String, VirtualPageConfigurationFileReloader> _reloaders; 072 073 public void contextualize(Context context) throws ContextException 074 { 075 _context = context; 076 } 077 078 public void setPluginInfo(String pluginName, String featureName, String id) 079 { 080 _id = id; 081 } 082 083 public void service(ServiceManager manager) throws ServiceException 084 { 085 _fileReloaderUtils = (FileReloaderUtils) manager.lookup(FileReloaderUtils.ROLE); 086 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 087 _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 088 } 089 090 public void configure(Configuration configuration) throws ConfigurationException 091 { 092 _path = configuration.getChild("path").getValue(); 093 } 094 095 public void initialize() throws Exception 096 { 097 _reloaders = new ConcurrentHashMap<>(); 098 099 _cacheManager.createMemoryCache(_getCacheKey(), 100 new I18nizableText("plugin.web", "PLUGINS_WEB_VIRTUAL_PAGE_CONF_CACHE_LABEL", List.of(_id)), 101 new I18nizableText("plugin.web", "PLUGINS_WEB_VIRTUAL_PAGE_CONF_CACHE_DESCRIPTION", List.of(_id)), 102 true, 103 null); 104 } 105 106 private String _getCacheKey() 107 { 108 return __CACHE_PREFIX + _id; 109 } 110 111 private Cache<String, SkinConfiguration> _getCache() 112 { 113 return _cacheManager.get(_getCacheKey()); 114 } 115 116 /** 117 * Get the template's id 118 * @param rootPage The root page 119 * @return The template's id 120 */ 121 public String getTemplate(Page rootPage) 122 { 123 return _getSkinConfiguration(rootPage).template(); 124 } 125 126 /** 127 * Get the zones configurations 128 * @param rootPage The root page 129 * @return The collection of VirtualPageZoneConfiguration 130 */ 131 public Collection<VirtualZoneConfiguration> getZonesConfigurations(Page rootPage) 132 { 133 return _getSkinConfiguration(rootPage).zonesConfiguration().values(); 134 } 135 136 /** 137 * Check if the virtual page has a zone, by its id 138 * @param id The zone id 139 * @param rootPage The root page 140 * @return true if the page has a zone of the id given, false otherwise 141 */ 142 public boolean hasZoneConfiguration(String id, Page rootPage) 143 { 144 return _getSkinConfiguration(rootPage).zonesConfiguration().containsKey(id); 145 } 146 147 /** 148 * Get the zone configuration for a zone id 149 * @param id The zone id 150 * @param rootPage The root page 151 * @return The VirtualPageZoneConfiguration, null if none is found 152 */ 153 public VirtualZoneConfiguration getZoneConfiguration(String id, Page rootPage) 154 { 155 return _getSkinConfiguration(rootPage).zonesConfiguration().get(id); 156 } 157 158 /** 159 * Load the configuration from skin if one is found 160 * @param rootPage The root page 161 */ 162 private SkinConfiguration _getSkinConfiguration(Page rootPage) 163 { 164 Site site = rootPage.getSite(); 165 String siteName = site.getName(); 166 String skinId = site.getSkinId(); 167 168 Request request = ContextHelper.getRequest(_context); 169 Site oldSite = (Site) request.getAttribute(WebConstants.REQUEST_ATTR_SITE); 170 String oldSiteName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME); 171 String oldSkinId = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SKIN_ID); 172 173 try 174 { 175 request.setAttribute(WebConstants.REQUEST_ATTR_SITE, site); 176 request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, siteName); 177 request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, skinId); 178 179 try 180 { 181 VirtualPageConfigurationFileReloader fileReloader = _reloaders.computeIfAbsent(skinId, id -> new VirtualPageConfigurationFileReloader(id, this)); 182 // Force file reading if the cache is empty for this key (on initialization or while empty caches) 183 _fileReloaderUtils.updateFile(_path, !_getCache().hasKey(skinId), fileReloader); 184 185 return _getCache().get(skinId); 186 } 187 catch (Exception e) 188 { 189 throw new IllegalStateException("No configurations found at path '" + _path + "' in the skin for virtual page of id " + _id, e); 190 } 191 } 192 finally 193 { 194 request.setAttribute(WebConstants.REQUEST_ATTR_SITE, oldSite); 195 request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, oldSiteName); 196 request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, oldSkinId); 197 } 198 } 199 200 private void _updateFile(String skinId, Source source, InputStream is) throws Exception 201 { 202 if (is != null) 203 { 204 Configuration cfg = new DefaultConfigurationBuilder().build(is, source.getURI()); 205 SkinConfiguration skinConfiguration = SkinConfiguration.fromConfiguration(cfg); 206 _getCache().put(skinId, skinConfiguration); 207 } 208 else 209 { 210 throw new UnsupportedOperationException("Components extending AbstractVirtualPageConfiguration should always have a default configuration file"); 211 } 212 } 213 214 /** 215 * Class representing a virtual page conf file reloader 216 */ 217 private static class VirtualPageConfigurationFileReloader implements FileReloader 218 { 219 private String _skinId; 220 private VirtualPageConfiguration _parent; 221 222 /** 223 * Constructor 224 * @param skinId the skin id 225 * @param parent the parent component 226 */ 227 public VirtualPageConfigurationFileReloader (String skinId, VirtualPageConfiguration parent) 228 { 229 _skinId = skinId; 230 _parent = parent; 231 } 232 233 @Override 234 public void updateFile(String sourceUrl, Source source, InputStream is) throws Exception 235 { 236 _parent._updateFile(_skinId, source, is); 237 } 238 239 @Override 240 public String getId(String sourceUrl) 241 { 242 return VirtualPageConfigurationFileReloader.class.getName() + "#" + sourceUrl + "#" + _skinId; 243 } 244 } 245 246 private record SkinConfiguration(String template, Map<String, VirtualZoneConfiguration> zonesConfiguration) 247 { 248 /** 249 * Create a {@link SkinConfiguration} from a given configuration 250 * @param configuration The configuration 251 * @return {@link SkinConfiguration} that describe the virtual page configuration of the current skin 252 * @throws ConfigurationException if an error occurs 253 */ 254 public static SkinConfiguration fromConfiguration(Configuration configuration) throws ConfigurationException 255 { 256 Map<String, VirtualZoneConfiguration> zonesConfiguration = new HashMap<>(); 257 for (Configuration zoneConfiguration : configuration.getChildren("zone")) 258 { 259 VirtualZoneConfiguration zone = new VirtualZoneConfiguration(zoneConfiguration); 260 zonesConfiguration.put(zone.getId(), zone); 261 } 262 263 return new SkinConfiguration( 264 configuration.getAttribute("template", "page"), 265 zonesConfiguration 266 ); 267 } 268 } 269}