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}