001/* 002 * Copyright 2022 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.contenttype; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.util.Collection; 021import java.util.HashSet; 022import java.util.Objects; 023import java.util.Set; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.activity.Initializable; 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 033import org.apache.avalon.framework.context.Context; 034import org.apache.avalon.framework.context.ContextException; 035import org.apache.avalon.framework.context.Contextualizable; 036import org.apache.avalon.framework.service.ServiceException; 037import org.apache.avalon.framework.service.ServiceManager; 038import org.apache.avalon.framework.service.Serviceable; 039import org.apache.avalon.framework.thread.ThreadSafe; 040import org.apache.cocoon.components.ContextHelper; 041import org.apache.cocoon.environment.Request; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.excalibur.source.SourceResolver; 044import org.apache.excalibur.source.TraversableSource; 045import org.xml.sax.SAXException; 046 047import org.ametys.cms.contenttype.ContentType; 048import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 049import org.ametys.cms.repository.Content; 050import org.ametys.core.DevMode; 051import org.ametys.core.DevMode.DEVMODE; 052import org.ametys.core.cache.AbstractCacheManager; 053import org.ametys.core.cache.Cache; 054import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 055import org.ametys.runtime.i18n.I18nizableText; 056import org.ametys.runtime.plugin.component.AbstractLogEnabled; 057import org.ametys.web.WebConstants; 058import org.ametys.web.skin.SkinsManager; 059 060/** 061 * Helper to get content type's views brought by the skin, linked to a model view 062 * 063 */ 064public class SkinContentViewHelper extends AbstractLogEnabled implements Component, Serviceable, ThreadSafe, Initializable, Contextualizable 065{ 066 /** The avalon role */ 067 public static final String ROLE = SkinContentViewHelper.class.getName(); 068 069 /** The name of request parameter for skin content view */ 070 public static final String REQUEST_PARAM_RENDERING_VIEW_NAME = "renderingViewName"; 071 072 /** Constant for the {@link Cache} id for the pages in cache by sitename, lang, tag */ 073 private static final String __SKIN_CONTENT_VIEW_CACHE = SkinContentViewHelper.class.getName() + "$SkinContentView"; 074 075 private SourceResolver _srcResolver; 076 private SkinsManager _skinsManager; 077 private ContentTypeExtensionPoint _cTypeEP; 078 079 private AbstractCacheManager _cacheManager; 080 081 private Context _context; 082 083 public void service(ServiceManager smanager) throws ServiceException 084 { 085 _srcResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE); 086 _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE); 087 _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 088 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 089 } 090 091 public void initialize() throws Exception 092 { 093 _createCaches(); 094 } 095 096 public void contextualize(Context context) throws ContextException 097 { 098 _context = context; 099 100 } 101 102 private void _createCaches() 103 { 104 DEVMODE devMode = DevMode.getDeveloperMode(null); 105 106 switch (devMode) 107 { 108 case DEVELOPMENT: 109 case SUPER_DEVELOPPMENT: 110 _cacheManager.createRequestCache(__SKIN_CONTENT_VIEW_CACHE, 111 new I18nizableText("plugin.web", "PLUGINS_WEB_SKINCONTENTVIEW_CACHE_LABEL"), 112 new I18nizableText("plugin.web", "PLUGINS_WEB_SKINCONTENTVIEW_CACHE_DESCRIPTION"), 113 true); 114 break; 115 default: 116 _cacheManager.createMemoryCache(__SKIN_CONTENT_VIEW_CACHE, 117 new I18nizableText("plugin.web", "PLUGINS_WEB_SKINCONTENTVIEW_CACHE_LABEL"), 118 new I18nizableText("plugin.web", "PLUGINS_WEB_SKINCONTENTVIEW_CACHE_DESCRIPTION"), 119 true, 120 null); 121 break; 122 } 123 } 124 125 /** 126 * Rendering view of a content brought by a skin 127 * @param skinName the skin name 128 * @param name the view name 129 * @param modelViewName the model view used by this rendering view 130 * @param format the output format (html, pdf, doc, ..) 131 * @param label the view label 132 * @param description the view description 133 * @param iconGlyph the icon glyph. Can be null. 134 * @param iconDecorator the icon decoration. Can be null. 135 * @param iconSmall the small icon. Can be null. 136 * @param iconMedium the medium icon. Can be null. 137 * @param iconLarge the large icon. Can be null. 138 */ 139 public record SkinContentView(String skinName, String name, String modelViewName, String format, I18nizableText label, I18nizableText description, String iconGlyph, String iconDecorator, String iconSmall, String iconMedium, String iconLarge) { /* empty */ } 140 141 142 /** 143 * Get the HTML content view brought by the current skin for the given content 144 * @param name the name of the view 145 * @param content the content 146 * @return the skin content view or null if view does not exists in skin 147 */ 148 public SkinContentView getContentViewFromSkin(String name, Content content) 149 { 150 return getContentViewFromSkin(name, content, "html"); 151 } 152 153 /** 154 * Get the content view brought by the current skin for the given content 155 * @param name the name of the view 156 * @param content the content 157 * @param format the output format (html, pdf, doc, ...) 158 * @return the skin content view or null if view does not exists in skin 159 */ 160 public SkinContentView getContentViewFromSkin(String name, Content content, String format) 161 { 162 for (String contentTypeId : content.getTypes()) 163 { 164 SkinContentView view = getContentViewFromSkin(name, contentTypeId, format); 165 if (view != null) 166 { 167 return view; 168 } 169 } 170 171 return null; 172 } 173 174 /** 175 * Get the HTML content view brought by the current skin for a given content type 176 * @param name the name of the view 177 * @param contentTypeId the content type id 178 * @return the skin content view or null if view does not exists in skin 179 */ 180 public SkinContentView getContentViewFromSkin(String name, String contentTypeId) 181 { 182 return getContentViewFromSkin(name, contentTypeId, "html"); 183 } 184 185 /** 186 * Get the content view brought by the current skin for a given content type 187 * @param name the name of the view 188 * @param contentTypeId the content type id 189 * @param format the output format (html, pdf, doc, ...) 190 * @return the skin content view or null if view does not exists in skin 191 */ 192 public SkinContentView getContentViewFromSkin(String name, String contentTypeId, String format) 193 { 194 return getContentViewsFromSkin(contentTypeId, format) 195 .stream() 196 .filter(v -> name.equals(v.name())) 197 .findFirst() 198 .orElse(null); 199 } 200 201 /** 202 * Get the HTML content views brought by the current skin for a given content type 203 * @param contentTypeId the content type id 204 * @return the skin content view 205 */ 206 public Set<SkinContentView> getContentViewsFromSkin(String contentTypeId) 207 { 208 return getContentViewsFromSkin(contentTypeId, "html"); 209 } 210 211 /** 212 * Get all the content views brought by the current skin for a given content type 213 * @param contentTypeId the content type id 214 * @param format the output format (html, pdf, doc, ...) 215 * @return the skin content view 216 */ 217 public Set<SkinContentView> getContentViewsFromSkin(String contentTypeId, String format) 218 { 219 String skinName = _skinsManager.getSkinNameFromRequest(); 220 ContentType contentType = _cTypeEP.getExtension(contentTypeId); 221 if (contentType == null) 222 { 223 throw new IllegalStateException("Cannot get the content views for the unexisting content type '" + contentTypeId + "'"); 224 } 225 return getContentViewsFromSkin(skinName, contentType, format); 226 } 227 228 /** 229 * Get the HTML content views brought by the current skin for a given content type 230 * @param contentType the content type 231 * @return the skin content view 232 */ 233 public Set<SkinContentView> getContentViewsFromSkin(ContentType contentType) 234 { 235 String skinName = _skinsManager.getSkinNameFromRequest(); 236 return getContentViewsFromSkin(skinName, contentType); 237 } 238 239 /** 240 * Get the HTML content views brought by a given skin for a given content type 241 * @param skinName the skin name 242 * @param contentType the content type 243 * @return the skin content view 244 */ 245 public Set<SkinContentView> getContentViewsFromSkin(String skinName, ContentType contentType) 246 { 247 return getContentViewsFromSkin(skinName, contentType, "html"); 248 } 249 250 /** 251 * Get the content views brought by the a skin for a given content type and output format 252 * @param skinName the skin name 253 * @param contentType the content type 254 * @param format the output format (html, pdf, doc, ...) 255 * @return the skin content view 256 */ 257 public Set<SkinContentView> getContentViewsFromSkin(String skinName, ContentType contentType, String format) 258 { 259 return _getCache().get(SkinContentViewKey.of(skinName, contentType.getId(), format), __ -> _computeContentViewsFromSkin(skinName, contentType, format)); 260 } 261 262 private Set<SkinContentView> _computeContentViewsFromSkin(String skinName, ContentType contentType, String format) 263 { 264 Set<SkinContentView> skinViews = new HashSet<>(); 265 266 String contentTypeAlias = StringUtils.substringAfterLast(contentType.getId(), "."); 267 if (StringUtils.isNotEmpty(contentTypeAlias)) 268 { 269 Request request = ContextHelper.getRequest(_context); 270 String currentSkinName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SKIN_ID); 271 272 TraversableSource source = null; 273 try 274 { 275 request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, skinName); 276 277 source = (TraversableSource) _srcResolver.resolveURI("skin://stylesheets/content/" + contentTypeAlias); 278 if (source.exists() && source.isCollection()) 279 { 280 Collection<TraversableSource> children = source.getChildren(); 281 return children.stream() 282 .filter(f -> !f.isCollection()) 283 .filter(f -> f.getName().endsWith(".xml")) 284 .map(f -> _parseSkinContentTypeView(skinName, f, contentType, contentTypeAlias)) 285 .filter(Objects::nonNull) 286 .filter(v -> format.equals(v.format())) 287 .collect(Collectors.toSet()); 288 } 289 } 290 catch (IOException e) 291 { 292 getLogger().warn("Unable to list content type's view brought by current skin for content type '" + contentType.getId() + "'", e); 293 } 294 finally 295 { 296 request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, currentSkinName); 297 _srcResolver.release(source); 298 } 299 } 300 301 return skinViews; 302 } 303 304 private SkinContentView _parseSkinContentTypeView(String skinName, TraversableSource xmlSource, ContentType contentType, String contentTypeAlias) 305 { 306 String defaultCatalog = "skin." + skinName; 307 308 // File name is such as news-carousel.xml, article2pdf-other.xml, ... 309 Pattern viewPattern = Pattern.compile("^" + contentTypeAlias + "(?:2([^-]+))?-([^.]+)\\.xml$"); 310 311 Matcher m = viewPattern.matcher(xmlSource.getName()); 312 if (!m.matches()) 313 { 314 getLogger().info("The file at uri '{}' does not match a skin content view. It will be ignored.", xmlSource.getURI()); 315 return null; 316 } 317 318 try (InputStream inputStream = xmlSource.getInputStream()) 319 { 320 String format = m.group(1); 321 String viewName = m.group(2); 322 323 if (contentType.getView(viewName) != null) 324 { 325 getLogger().warn("The content view defined by skin '{}' for content type '{}', is named as an already existing model view '{}' at uri {}. This view is ignored.", skinName, contentType.getId(), viewName, xmlSource.getURI()); 326 return null; 327 } 328 329 Configuration conf = new DefaultConfigurationBuilder().build(inputStream); 330 331 String modelViewName = conf.getChild("view").getValue(null); 332 333 if (StringUtils.isEmpty(modelViewName)) 334 { 335 getLogger().warn("Missing <view> configuration for content view defined by skin '{}' for content type '{}' at uri {}. This view is ignored.", skinName, contentType.getId(), xmlSource.getURI()); 336 return null; 337 // throw new IllegalArgumentException("Missing <view> configuration for content view defined by skin '" + skinName + "' at uri " + xmlSource.getURI()); 338 } 339 340 if (contentType.getView(modelViewName) == null) 341 { 342 getLogger().warn("The content view defined by skin '{}' for content type '{}' refers a non-existing model view '{}' at uri {}. This view is ignored.", skinName, contentType.getId(), modelViewName, xmlSource.getURI()); 343 return null; 344 //throw new IllegalArgumentException("The content view defined by skin '" + skinName + "' for content type '" + contentType.getId() + "' refers a non-existing model view '" + modelViewName + "' at uri " + xmlSource.getURI()); 345 } 346 347 I18nizableText label = null; 348 Configuration labelConf = conf.getChild("label", false); 349 if (labelConf != null) 350 { 351 label = I18nizableText.parseI18nizableText(labelConf, defaultCatalog); 352 } 353 else 354 { 355 // No label defined 356 label = new I18nizableText(viewName); 357 } 358 359 I18nizableText description = null; 360 Configuration descConf = conf.getChild("description", false); 361 if (descConf != null) 362 { 363 description = I18nizableText.parseI18nizableText(descConf, defaultCatalog); 364 } 365 366 Configuration iconsConfig = conf.getChild("icons"); 367 String iconGlyph = iconsConfig.getChild("glyph").getValue(null); 368 String iconDecorator = iconsConfig.getChild("decorator").getValue(null); 369 String smallIcon = _parseIcon(iconsConfig, "small", skinName); 370 String mediumcon = _parseIcon(iconsConfig, "medium", skinName); 371 String largeIcon = _parseIcon(iconsConfig, "large", skinName); 372 373 return new SkinContentView(skinName, viewName, modelViewName, format != null ? format : "html", label, description, iconGlyph, iconDecorator, smallIcon, mediumcon, largeIcon); 374 } 375 catch (IOException | ConfigurationException | SAXException e) 376 { 377 getLogger().warn("Unable to parse skin content view '{}'. This view is ignored.", xmlSource.getURI(), e); 378 return null; 379 } 380 } 381 382 private String _parseIcon(Configuration iconsConfig, String name, String skinName) throws ConfigurationException 383 { 384 Configuration iconConfig = iconsConfig.getChild(name, false); 385 if (iconConfig != null) 386 { 387 return "/skins/" + skinName + "/resources/" + iconConfig.getValue(); 388 } 389 390 return null; 391 } 392 393 static class SkinContentViewKey extends AbstractCacheKey 394 { 395 SkinContentViewKey(String skinId, String contentTypeId, String format) 396 { 397 super(skinId, contentTypeId, format); 398 } 399 static SkinContentViewKey of(String skinId, String contentTypeId, String format) 400 { 401 return new SkinContentViewKey(skinId, contentTypeId, format); 402 } 403 } 404 405 private Cache<SkinContentViewKey, Set<SkinContentView>> _getCache() 406 { 407 return _cacheManager.get(__SKIN_CONTENT_VIEW_CACHE); 408 } 409 410}