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.ArrayList; 020import java.util.HashMap; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026 027import org.apache.avalon.framework.activity.Initializable; 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.cocoon.components.ContextHelper; 037import org.apache.cocoon.environment.Request; 038import org.apache.commons.lang3.ArrayUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.lang3.tuple.ImmutablePair; 041import org.apache.commons.lang3.tuple.Pair; 042import org.apache.excalibur.source.Source; 043import org.apache.excalibur.source.SourceResolver; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047import org.ametys.cms.content.ContentViewProvider; 048import org.ametys.cms.content.DefaultContentViewProvider; 049import org.ametys.cms.contenttype.ContentType; 050import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 051import org.ametys.cms.contenttype.ContentTypesParserHelper; 052import org.ametys.cms.contenttype.ContentTypesParserHelper.ViewConfigurations; 053import org.ametys.cms.contenttype.ContentTypesParserHelper.ViewConfigurationsByType; 054import org.ametys.core.cache.AbstractCacheManager; 055import org.ametys.core.cache.Cache; 056import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 057import org.ametys.runtime.i18n.I18nizableText; 058import org.ametys.runtime.model.ItemParserHelper.ConfigurationAndPluginName; 059import org.ametys.runtime.model.View; 060import org.ametys.runtime.model.ViewParserContext; 061import org.ametys.web.WebHelper; 062import org.ametys.web.contenttype.SkinContentTypeOverridesExtensionPoint; 063import org.ametys.web.contenttype.WebViewParserContext; 064import org.ametys.web.repository.site.Site; 065import org.ametys.web.repository.site.SiteManager; 066import org.ametys.web.skin.Skin; 067import org.ametys.web.skin.SkinConfigurationHelper; 068import org.ametys.web.skin.SkinsManager; 069 070/** 071 * This implementation of a {@link ContentViewProvider} looks first if the view has been defined (or overridden) in the current skin 072 */ 073public class WebContentViewProvider extends DefaultContentViewProvider implements Contextualizable, Initializable 074{ 075 /** The logger. */ 076 protected static final Logger _LOGGER = LoggerFactory.getLogger(WebContentViewProvider.class); 077 078 private static final String __VIEW_CACHE = WebContentViewProvider.class.getName() + "$view.cache"; 079 080 /** The skin manager */ 081 protected SkinsManager _skinsManager; 082 /** The content type extension point */ 083 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 084 /** The content types parser helper */ 085 protected ContentTypesParserHelper _contentTypesParserHelper; 086 /** The source resolver */ 087 protected SourceResolver _srcResolver; 088 /** The skin configuration helper */ 089 protected SkinConfigurationHelper _skinConfigurationHelper; 090 /** The extension point with overrides specific to skins */ 091 protected SkinContentTypeOverridesExtensionPoint _skinContentTypeOverridesExtensionPoint; 092 /** The site manager */ 093 protected SiteManager _siteManager; 094 /** The cache manager */ 095 protected AbstractCacheManager _cacheManager; 096 /** The context */ 097 protected Context _context; 098 099 private enum Priority 100 { 101 HIGH, 102 LOW 103 } 104 105 private Map<String, Map<ContentType, Map<String, ViewConfigurations>>> _skinsViewConfigurations = new HashMap<>(); 106 private Map<String, Map<ContentType, Map<String, View>>> _skinsViews = new HashMap<>(); 107 108 @Override 109 public void service(ServiceManager manager) throws ServiceException 110 { 111 super.service(manager); 112 113 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 114 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 115 _contentTypesParserHelper = (ContentTypesParserHelper) manager.lookup(ContentTypesParserHelper.ROLE); 116 _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 117 _skinConfigurationHelper = (SkinConfigurationHelper) manager.lookup(SkinConfigurationHelper.ROLE); 118 _skinContentTypeOverridesExtensionPoint = (SkinContentTypeOverridesExtensionPoint) manager.lookup(SkinContentTypeOverridesExtensionPoint.ROLE); 119 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 120 _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 121 } 122 123 public void contextualize(Context context) throws ContextException 124 { 125 _context = context; 126 } 127 128 /** 129 * Initialize 130 */ 131 @Override 132 public void initialize() 133 { 134 _createCaches(); 135 } 136 137 /** 138 * Creates the caches 139 */ 140 protected void _createCaches() 141 { 142 _cacheManager.createMemoryCache(__VIEW_CACHE, 143 new I18nizableText("plugin.web", "PLUGINS_WEB_SKIN_CONTENTTYPE_VIEW_CACHE_LABEL"), 144 new I18nizableText("plugin.web", "PLUGINS_WEB_SKIN_CONTENTTYPE_VIEW_CACHE_DESCRIPTION"), 145 true, 146 null); 147 } 148 149 @Override 150 public void initializeViewConfigurations() throws ConfigurationException 151 { 152 super.initializeViewConfigurations(); 153 154 for (String skinId : _skinsManager.getSkins()) 155 { 156 Map<ContentType, Map<String, ViewConfigurations>> skinViewConfigurations = new HashMap<>(); 157 158 for (String contentTypeId : _contentTypeExtensionPoint.getExtensionsIds()) 159 { 160 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 161 162 // Look for overrides in the skin 163 List<Pair<Priority, ConfigurationAndPluginName>> overrideConfigurationsFromSkinFolder = _getOverrideConfigurationsFromSkinFolder(skinId, contentType); 164 List<ConfigurationAndPluginName> overrideConfigurationsFromSkinExtensions = _getOverrideConfigurationsFromSkinExtensions(skinId, contentType); 165 166 if (!overrideConfigurationsFromSkinFolder.isEmpty() || !overrideConfigurationsFromSkinExtensions.isEmpty()) 167 { 168 // If there are overrides in the skin 169 170 // 1. Get configurations from content type's extensions and _override folder) 171 Map<String, ViewConfigurations> viewConfigurationsFromContentTypeExtensions = contentType.getViewConfigurationsFromExtensions(); 172 173 // 2. Get views overrides from skin with low priority 174 List<ConfigurationAndPluginName> lowPriorityOverrideConfiguration = _filterOverrideConfigurationsFromSkinFolder(overrideConfigurationsFromSkinFolder, Priority.LOW); 175 Map<String, ViewConfigurations> lowPriorityOverrideViewConfigurations = _contentTypesParserHelper.getApplicableViewConfigurations(contentType, lowPriorityOverrideConfiguration); 176 177 // Merge configurations from content type extensions with low priority configurations from skin _override folder 178 Map<String, ViewConfigurations> merge1 = _contentTypesParserHelper.mergeViewConfigurationsWithOverrides(viewConfigurationsFromContentTypeExtensions, lowPriorityOverrideViewConfigurations); 179 180 // 3. Get overrides from extensions specific to the skin 181 Map<String, ViewConfigurations> viewConfigurationsFromOverrideSkinExtension = _contentTypesParserHelper.getApplicableViewConfigurations(contentType, overrideConfigurationsFromSkinExtensions); 182 183 // Merge configurations with specific extensions of the skin 184 Map<String, ViewConfigurations> merge2 = _contentTypesParserHelper.mergeViewConfigurationsWithOverrides(merge1, viewConfigurationsFromOverrideSkinExtension); 185 186 // 4. Get configurations from content type's extensions and _override folder) 187 Map<String, ViewConfigurations> projectViewConfigurations = contentType.getViewConfigurationsFromOverrideFolder(); 188 189 // Merge configurations with _override folder 190 Map<String, ViewConfigurations> merge3 = _contentTypesParserHelper.mergeViewConfigurationsWithOverrides(merge2, projectViewConfigurations); 191 192 // 5. Get views overrides from skin with high priority 193 List<ConfigurationAndPluginName> highPriorityOverrideConfigurations = _filterOverrideConfigurationsFromSkinFolder(overrideConfigurationsFromSkinFolder, Priority.HIGH); 194 Map<String, ViewConfigurations> highPriorityOverrideViewConfigurations = _contentTypesParserHelper.getApplicableViewConfigurations(contentType, highPriorityOverrideConfigurations); 195 196 // Merge configurations with high priority configurations from skin _override folder 197 Map<String, ViewConfigurations> merge4 = _contentTypesParserHelper.mergeViewConfigurationsWithOverrides(merge3, highPriorityOverrideViewConfigurations); 198 199 skinViewConfigurations.put(contentType, merge4); 200 } 201 } 202 203 if (!skinViewConfigurations.isEmpty()) 204 { 205 _skinsViewConfigurations.put(skinId, skinViewConfigurations); 206 } 207 } 208 } 209 210 private List<Pair<Priority, ConfigurationAndPluginName>> _getOverrideConfigurationsFromSkinFolder(String skinId, ContentType contentType) throws ConfigurationException 211 { 212 List<Pair<Priority, ConfigurationAndPluginName>> overriddenConfigurations = new ArrayList<>(); 213 String filePath = "content-types-views/_override/" + contentType.getId() + ".xml"; 214 215 Skin skin = _skinsManager.getSkin(skinId); 216 for (Skin skinOrParentSkin : _skinsManager.getSkinAndParents(skin)) 217 { 218 Source src = null; 219 try 220 { 221 String confFileUri = "skin-raw:" + skinOrParentSkin.getId() + "://" + filePath; 222 src = _srcResolver.resolveURI(confFileUri); 223 224 if (src.exists()) 225 { 226 try (InputStream is = src.getInputStream()) 227 { 228 Configuration configuration = new DefaultConfigurationBuilder(true).build(is); 229 230 // Check that only view overrides are present in the configuration 231 for (Configuration child : configuration.getChildren()) 232 { 233 if (!"view".equals(child.getName())) 234 { 235 throw new ConfigurationException("The overridden configuration at content-types-views/_override/" + contentType.getId() + ".xml for skin '" + skinOrParentSkin.getId() + "' should contain onlmy view overrides"); 236 } 237 } 238 239 // Get priority level of the override, default to high if not specified 240 String priorityAsString = configuration.getAttribute("priority", "high"); 241 Priority priority = Priority.valueOf(priorityAsString.toUpperCase()); 242 243 overriddenConfigurations.add(new ImmutablePair<>(priority, new ConfigurationAndPluginName(configuration, StringUtils.EMPTY, "skin." + skinOrParentSkin.getId()))); 244 } 245 } 246 } 247 catch (Exception e) 248 { 249 throw new ConfigurationException("Unable to parse overridden configuration for content type '" + contentType.getId() + "' and skin '" + skinId + "' at /content-types-views/_override/" + contentType.getId() + ".xml", e); 250 } 251 finally 252 { 253 _srcResolver.release(src); 254 } 255 } 256 257 return overriddenConfigurations.reversed(); 258 } 259 260 private List<ConfigurationAndPluginName> _filterOverrideConfigurationsFromSkinFolder(List<Pair<Priority, ConfigurationAndPluginName>> configurations, Priority priority) 261 { 262 return configurations.stream() 263 .filter(pair -> priority.equals(pair.getLeft())) 264 .map(Pair::getRight) 265 .toList(); 266 } 267 268 private List<ConfigurationAndPluginName> _getOverrideConfigurationsFromSkinExtensions(String skinId, ContentType contentType) 269 { 270 return _skinContentTypeOverridesExtensionPoint.getOverrideConfigurations(skinId, contentType.getId()); 271 } 272 273 @Override 274 public Map<String, ViewConfigurations> getViewConfigurations(ContentType contentType, ViewParserContext context) 275 { 276 if (context instanceof WebViewParserContext webContext && webContext.getSkinId().isPresent()) 277 { 278 Map<ContentType, Map<String, ViewConfigurations>> skinViewConfigurations = _skinsViewConfigurations.get(webContext.getSkinId().get()); 279 if (skinViewConfigurations.containsKey(contentType)) 280 { 281 return skinViewConfigurations.get(contentType); 282 } 283 } 284 285 // No skin, or content type not overridden in the skin 286 return super.getViewConfigurations(contentType, context); 287 } 288 289 public void parseViews() throws ConfigurationException 290 { 291 super.parseViews(); 292 293 for (String skinId : _skinsViewConfigurations.keySet()) 294 { 295 Map<ContentType, Map<String, ViewConfigurations>> contentTypesViewConfigurations = _skinsViewConfigurations.get(skinId); 296 ViewParserContext viewParserContext = WebViewParserContext.newInstance() 297 .withSkinId(skinId); 298 299 Map<ContentType, Map<String, View>> contentTypesViews = new HashMap<>(); 300 for (ContentType contentType : contentTypesViewConfigurations.keySet()) 301 { 302 Map<ContentType, Map<String, View>> contentTypeViews = _parseContentTypeViews(skinId, contentType, viewParserContext); 303 contentTypesViews.putAll(contentTypeViews); 304 } 305 306 _skinsViews.put(skinId, contentTypesViews); 307 } 308 } 309 310 private Map<ContentType, Map<String, View>> _parseContentTypeViews(String skinId, ContentType contentType, ViewParserContext viewParserContext) throws ConfigurationException 311 { 312 Map<ContentType, Map<String, View>> contentTypeViews = new HashMap<>(); 313 314 Map<String, View> views = new LinkedHashMap<>(); 315 316 Map<String, ViewConfigurationsByType> viewConfigurations = _contentTypesParserHelper.mergeViewConfigurationsFromCurrentTypeAndSuperTypes(contentType, viewParserContext); 317 for (String viewName : viewConfigurations.keySet()) 318 { 319 ViewConfigurationsByType viewConfigurationsByType = viewConfigurations.get(viewName); 320 View view = _contentTypesParserHelper.parseView(contentType, viewConfigurationsByType, viewParserContext); 321 views.put(viewName, view); 322 } 323 324 contentTypeViews.put(contentType, views); 325 326 for (ContentType child : _getContentTypeChildren(contentType)) 327 { 328 if (_isThereOverridesForContentTypeInSkin(skinId, contentType)) 329 { 330 Map<ContentType, Map<String, View>> childViews = _parseContentTypeViews(skinId, child, viewParserContext); 331 contentTypeViews.putAll(childViews); 332 } 333 } 334 335 return contentTypeViews; 336 } 337 338 private List<ContentType> _getContentTypeChildren(ContentType contentType) 339 { 340 List<ContentType> children = new ArrayList<>(); 341 342 for (String childId : _contentTypeExtensionPoint.getExtensionsIds()) 343 { 344 ContentType child = _contentTypeExtensionPoint.getExtension(childId); 345 if (ArrayUtils.contains(child.getSupertypeIds(), contentType.getId())) 346 { 347 children.add(child); 348 } 349 } 350 351 return children; 352 } 353 354 private boolean _isThereOverridesForContentTypeInSkin(String skinId, ContentType contentType) 355 { 356 return _skinsViewConfigurations.containsKey(skinId) && _skinsViewConfigurations.get(skinId).containsKey(contentType); 357 } 358 359 public void resolveViewReferences() throws ConfigurationException 360 { 361 for (String skinId : _skinsViews.keySet()) 362 { 363 Map<ContentType, Map<String, View>> contentTypesViews = _skinsViews.get(skinId); 364 ViewParserContext context = WebViewParserContext.newInstance() 365 .withSkinId(skinId); 366 367 for (ContentType contentType : contentTypesViews.keySet()) 368 { 369 Map<String, View> views = contentTypesViews.get(contentType); 370 for (String viewName : views.keySet()) 371 { 372 View view = views.get(viewName); 373 _contentTypesParserHelper.resolveViewReferences(contentType, view, viewName, context); 374 } 375 } 376 } 377 } 378 379 public View getContentTypeView(ContentType contentType, String viewName, ViewParserContext context) 380 { 381 return Optional.ofNullable(context) 382 .filter(WebViewParserContext.class::isInstance) 383 .map(WebViewParserContext.class::cast) 384 .map(webContext -> webContext.getSkinId()) 385 .filter(Optional::isPresent) 386 .map(Optional::get) 387 .map(skinId -> _getSkinView(skinId, contentType, viewName)) 388 .filter(Optional::isPresent) 389 .map(Optional::get) 390 .orElseGet(() -> super.getContentTypeView(contentType, viewName, context)); 391 } 392 393 @Override 394 public Set<String> getOverriddenContentTypes() 395 { 396 Set<String> overriddenContentTypes = super.getOverriddenContentTypes(); 397 overriddenContentTypes.addAll(_skinContentTypeOverridesExtensionPoint.getOverriddenContentTypes()); 398 return overriddenContentTypes; 399 } 400 401 @Override 402 public View getView(String viewName, String[] contentTypeIds, String[] mixinIds) 403 { 404 Request request = ContextHelper.getRequest(_context); 405 String siteName = WebHelper.getSiteName(request); 406 407 Optional<View> skinView = Optional.empty(); 408 if (StringUtils.isNotBlank(siteName) && _siteManager.hasSite(siteName)) 409 { 410 Site site = _siteManager.getSite(siteName); 411 String skinId = site.getSkinId(); 412 413 CacheKey cacheKey = CacheKey.of(skinId, Set.of(contentTypeIds), Set.of(mixinIds), viewName); 414 skinView = _getViewCache().get(cacheKey, __ -> _getSkinView(skinId, viewName, contentTypeIds, mixinIds)); 415 } 416 417 return skinView.orElseGet(() -> super.getView(viewName, contentTypeIds, mixinIds)); 418 } 419 420 private Optional<View> _getSkinView(String skinId, String viewName, String[] contentTypeIds, String[] mixinIds) 421 { 422 if (_isViewOverridden(skinId, contentTypeIds, mixinIds, viewName)) 423 { 424 ViewParserContext context = WebViewParserContext.newInstance() 425 .withSkinId(skinId); 426 List<View> views = new ArrayList<>(); 427 for (String contentTypeId : contentTypeIds) 428 { 429 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 430 views.add(getContentTypeView(contentType, viewName, context)); 431 } 432 433 for (String mixinId : mixinIds) 434 { 435 ContentType mixin = _contentTypeExtensionPoint.getExtension(mixinId); 436 views.add(getContentTypeView(mixin, viewName, context)); 437 } 438 439 return Optional.of(_contentTypesHelper.joinViews(views)); 440 } 441 else 442 { 443 return Optional.empty(); 444 } 445 } 446 447 private boolean _isViewOverridden(String skinId, String[] contentTypeIds, String[] mixinIds, String viewName) 448 { 449 if (_skinsViews.containsKey(skinId)) 450 { 451 Map<ContentType, Map<String, View>> contentTypesViews = _skinsViews.get(skinId); 452 for (String contentTypeId : contentTypeIds) 453 { 454 if (_isViewOverridden(contentTypeId, viewName, contentTypesViews)) 455 { 456 return true; 457 } 458 } 459 460 for (String mixinId : mixinIds) 461 { 462 if (_isViewOverridden(mixinId, viewName, contentTypesViews)) 463 { 464 return true; 465 } 466 } 467 } 468 469 return false; 470 } 471 472 private boolean _isViewOverridden(String contentTypeId, String viewName, Map<ContentType, Map<String, View>> views) 473 { 474 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 475 return views.containsKey(contentType) && views.get(contentType).containsKey(viewName); 476 } 477 478 private Optional<View> _getSkinView(String skinId, ContentType contentType, String viewName) 479 { 480 Optional<View> skinView = Optional.empty(); 481 482 if (_skinsViews.containsKey(skinId)) 483 { 484 Map<ContentType, Map<String, View>> skinViews = _skinsViews.get(skinId); 485 if (skinViews.containsKey(contentType)) 486 { 487 Map<String, View> contentTypeViews = skinViews.get(contentType); 488 if (contentTypeViews.containsKey(viewName)) 489 { 490 skinView = Optional.of(contentTypeViews.get(viewName)); 491 } 492 } 493 } 494 495 return skinView; 496 } 497 498 private Cache<CacheKey, Optional<View>> _getViewCache() 499 { 500 return _cacheManager.get(__VIEW_CACHE); 501 } 502 503 static final class CacheKey extends AbstractCacheKey 504 { 505 private CacheKey(String skinId, Set<String> contentTypeIds, Set<String> mixinIds, String viewName) 506 { 507 super(skinId, contentTypeIds, mixinIds, viewName); 508 } 509 510 static CacheKey of(String skinId, Set<String> contentTypeIds, Set<String> mixinIds, String viewName) 511 { 512 return new CacheKey(skinId, contentTypeIds, mixinIds, viewName); 513 } 514 } 515}