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}