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.web.content;
017
018import java.io.InputStream;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Optional;
023
024import org.apache.avalon.framework.configuration.Configuration;
025import org.apache.avalon.framework.context.Context;
026import org.apache.avalon.framework.context.ContextException;
027import org.apache.avalon.framework.context.Contextualizable;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.cocoon.components.ContextHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.commons.lang3.ArrayUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.excalibur.source.Source;
035import org.apache.excalibur.source.SourceResolver;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import org.ametys.cms.content.ContentViewProvider;
040import org.ametys.cms.content.DefaultContentViewProvider;
041import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
042import org.ametys.runtime.model.View;
043import org.ametys.web.WebHelper;
044import org.ametys.web.repository.site.Site;
045import org.ametys.web.repository.site.SiteManager;
046import org.ametys.web.skin.Skin;
047import org.ametys.web.skin.SkinConfigurationHelper;
048import org.ametys.web.skin.SkinsManager;
049
050/**
051 * This implementation of a {@link ContentViewProvider} looks first if the view has been defined (or overridden) in the current skin
052 */
053public class WebContentViewProvider extends DefaultContentViewProvider implements Contextualizable
054{
055    /** The logger. */
056    protected static final Logger _LOGGER = LoggerFactory.getLogger(WebContentViewProvider.class);
057    
058    /** The site manager */
059    protected SiteManager _siteManager;
060    /** The skin manager */
061    protected SkinsManager _skinsManager;
062    /** The context */
063    protected Context _context;
064    private SourceResolver _srcResolver;
065    
066    private Map<String, Long> _lastUpdate = new HashMap<>();
067    private Map<String, Map<String, Map<String, String>>> _viewMapping = new HashMap<>();
068    private SkinConfigurationHelper _skinConfigurationHelper;
069    private ContentTypeExtensionPoint _contentTypeExtensionPoint;
070
071    
072    @Override
073    public void service(ServiceManager manager) throws ServiceException
074    {
075        super.service(manager);
076        
077        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
078        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
079        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
080        _skinConfigurationHelper = (SkinConfigurationHelper) manager.lookup(SkinConfigurationHelper.ROLE);
081        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
082    }
083    
084    public void contextualize(Context context) throws ContextException
085    {
086        _context = context;
087    }
088    
089    @Override
090    public View getView(String viewName, String[] contentTypeIds, String[] mixinIds)
091    {
092        Request request = ContextHelper.getRequest(_context);
093        String siteName = WebHelper.getSiteName(request);
094        
095        Optional<View> skinView = Optional.empty();
096        if (StringUtils.isNotBlank(siteName) && _siteManager.hasSite(siteName))
097        {
098            Site site = _siteManager.getSite(siteName);
099            skinView = Arrays.stream(contentTypeIds)
100                             .findFirst()
101                             .map(contentTypeId -> _getSkinView(site.getSkinId(), contentTypeId, viewName))
102                             .orElse(Optional.empty());
103        }
104        
105        return skinView.orElseGet(() -> super.getView(viewName, contentTypeIds, mixinIds));
106    }
107    
108    /**
109     * Retrieves the view for given content in given skin
110     * @param skinName the skin name
111     * @param viewName the name of the view to retrieve
112     * @param contentTypeId the identifier of the content type
113     * @return the view brought by skin if found, an empty optional otherwise
114     */
115    private Optional<View> _getSkinView(String skinName, String contentTypeId, String viewName)
116    {
117        Map<String, Map<String, String>> viewNameMappings = _getViewMapping(skinName);
118        for (String mappingTypeId : viewNameMappings.keySet())
119        {
120            if (_containsMappedContentType(contentTypeId, mappingTypeId))
121            {
122                Map<String, String> viewNameMappingsForContentType = viewNameMappings.get(mappingTypeId);
123                if (viewNameMappingsForContentType.containsKey(viewName))
124                {
125                    String viewNameMapped = viewNameMappingsForContentType.get(viewName);
126                    return Optional.ofNullable(_contentTypesHelper.getView(viewNameMapped, new String[] {contentTypeId}, ArrayUtils.EMPTY_STRING_ARRAY));
127                }
128            }
129        }
130        
131        return Optional.empty();
132    }
133    
134    private boolean _containsMappedContentType(String contentTypesId, String mappedContentTypeId)
135    {
136        if (contentTypesId.equals(mappedContentTypeId))
137        {
138            return true;
139        }
140        
141        if (_contentTypeExtensionPoint.hasExtension(contentTypesId))
142        {
143            for (String supertypeId : _contentTypeExtensionPoint.getExtension(contentTypesId).getSupertypeIds())
144            {
145                if (_containsMappedContentType(supertypeId, mappedContentTypeId))
146                {
147                    return true;
148                }
149            }
150        }
151        
152        return false;
153    }
154    
155    private synchronized Map<String, Map<String, String>> _getViewMapping(String skinName)
156    {
157        Source src = null;
158        try
159        {
160            String confFileUri = "skin:" + skinName + "://conf/view-mapping.xml";
161            
162            src = _srcResolver.resolveURI(confFileUri);
163
164            if (src.exists())
165            {
166                if (!_lastUpdate.containsKey(skinName) || _lastUpdate.get(skinName) < src.getLastModified())
167                {
168                    Map<String, Map<String, String>> viewMappings = new HashMap<>();
169                    
170                    Skin skin = _skinsManager.getSkin(skinName);
171                    try (InputStream xslIs = this.getClass().getResourceAsStream("view-mapping-merge.xsl"))
172                    {
173                        Configuration configuration = _skinConfigurationHelper.getInheritanceMergedConfiguration(skin, "conf/view-mapping.xml", xslIs);
174                        
175                        for (Configuration contentTypeConfig : configuration.getChildren("content-type"))
176                        {
177                            String contentTypeId = contentTypeConfig.getAttribute("id");
178                            Map<String, String> mappingsForType = viewMappings.computeIfAbsent(contentTypeId, __ -> new HashMap<>());
179                            
180                            for (Configuration viewConfg : contentTypeConfig.getChildren("view"))
181                            {
182                                String viewName = viewConfg.getAttribute("name");
183                                String skinViewName = viewConfg.getValue();
184                                
185                                mappingsForType.put(viewName, skinViewName);
186                            }
187                        }
188                        
189                    }
190                    
191                    _viewMapping.put(skinName, viewMappings);
192                    _lastUpdate.put(skinName, src.getLastModified());
193                }
194                
195                return _viewMapping.get(skinName);
196            }
197        }
198        catch (Exception e)
199        {
200            _LOGGER.error("Unable to get view mapping for skin '" + skinName + "'", e);
201        }
202        finally
203        {
204            _srcResolver.release(src);
205        }
206        
207        return Map.of();
208    }
209}