001/* 002 * Copyright 2020 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.skin; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.nio.file.Files; 021import java.nio.file.Path; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.Date; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Set; 031import java.util.stream.Collectors; 032 033import org.apache.avalon.framework.configuration.Configuration; 034import org.apache.avalon.framework.configuration.ConfigurationException; 035import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 036import org.apache.commons.lang3.StringUtils; 037import org.apache.excalibur.source.Source; 038import org.apache.excalibur.source.SourceException; 039import org.apache.excalibur.source.TraversableSource; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043import org.ametys.core.util.LambdaUtils; 044import org.ametys.core.util.LambdaUtils.LambdaException; 045import org.ametys.runtime.i18n.I18nizableText; 046 047/** 048 * A skin 049 */ 050public class Skin 051{ 052 static final String CONF_PATH = "conf/skin.xml"; 053 static final String TEMPLATES_PATH = "templates"; 054 055 private static Logger _logger = LoggerFactory.getLogger(Skin.class); 056 057 /** The skin id (e.g. the directory name) */ 058 protected String _id; 059 /** The skin root path */ 060 protected Path _path; 061 /** The skin name */ 062 protected I18nizableText _label; 063 /** The skin description */ 064 protected I18nizableText _description; 065 /** The skin thumbnail 16x16 */ 066 protected String _smallImage; 067 /** The skin thumbnail 32x32 */ 068 protected String _mediumImage; 069 /** The skin thumbnail 48x48 */ 070 protected String _largeImage; 071 /** The map of templates id and associated templates */ 072 protected Map<String, SkinTemplate> _templates = new HashMap<>(); 073 074 /** The last time the file was loaded */ 075 protected long _lastConfUpdate; 076 /** Is the skin modifiable */ 077 protected boolean _modifiable; 078 /** Is the skin abstract */ 079 protected boolean _abstract; 080 /** Parents of the skin in the inheritance process */ 081 protected List<String> _parents = Collections.EMPTY_LIST; 082 083 /** The skins manager to access components */ 084 protected SkinsManager _skinsManager; 085 /** The skin configuration helper */ 086 protected SkinConfigurationHelper _skinConfigurationHelper; 087 088 /** 089 * Creates a skin 090 * @param id The id of the skin (e.g. the directory name) 091 * @param skinPath The skin root path 092 * @param modifiable Is this skin modifiable? 093 * @param skinsManager The skins manager 094 * @param skinConfigurationHelper The skin configuration helper 095 */ 096 public Skin(String id, Path skinPath, boolean modifiable, SkinsManager skinsManager, SkinConfigurationHelper skinConfigurationHelper) 097 { 098 _id = id; 099 _path = skinPath; 100 _modifiable = modifiable; 101 102 _skinsManager = skinsManager; 103 _skinConfigurationHelper = skinConfigurationHelper; 104 } 105 106 /** 107 * Dispose skin component 108 */ 109 public void dispose() 110 { 111 for (SkinTemplate template : _templates.values()) 112 { 113 template.dispose(); 114 } 115 } 116 117 /** 118 * Is the skin modifiable? 119 * @return true if modifiable 120 */ 121 public boolean isModifiable() 122 { 123 return _modifiable; 124 } 125 126 /** 127 * Is the skin abstract? 128 * @return true if abstract 129 */ 130 public boolean isAbstract() 131 { 132 return _abstract; 133 } 134 135 /** 136 * Is the skin modifiable? 137 * @return true if modifiable 138 */ 139 public boolean isConfigurable() 140 { 141 // Does not support inheritance... fine for now 142 return isModifiable() && Files.exists(_path.resolve("stylesheets/config/config-model.xml")); 143 } 144 145 /** 146 * Get the parent skins from the inheritance point of view. 147 * Consider using {@link SkinsManager#getSkinAndParents} 148 * @return The non null list of parents. The sooner a parent appears in the list, the stronger it is. 149 */ 150 public List<String> getParents() 151 { 152 return _parents; 153 } 154 155 /** 156 * Get the list of existing templates 157 * @return A set of skin names. Can be null if there is an error. 158 */ 159 public Set<String> getTemplates() 160 { 161 TraversableSource templatesSource = null; 162 try 163 { 164 templatesSource = (TraversableSource) _skinsManager.getSourceResolver().resolveURI("skin:" + _id + "://" + TEMPLATES_PATH); 165 166 Collection<TraversableSource> children = templatesSource.getChildren(); 167 return children.stream() 168 .filter(LambdaUtils.wrapPredicate(this::_isATemplate)) 169 .map(s -> s.getName()) 170 .collect(Collectors.toSet()); 171 } 172 catch (LambdaException | IOException e) 173 { 174 _logger.error("Can not determine the list of templates available for skin '" + _id + "'", e); 175 return null; 176 } 177 finally 178 { 179 _skinsManager.getSourceResolver().release(templatesSource); 180 } 181 } 182 183 /** 184 * Get a template 185 * @param id The id of the template 186 * @return The template or null if the template doesn't exists 187 */ 188 public SkinTemplate getTemplate(String id) 189 { 190 TraversableSource templateSource = null; 191 try 192 { 193 templateSource = (TraversableSource) _skinsManager.getSourceResolver().resolveURI("skin:" + _id + "://" + TEMPLATES_PATH + "/" + id); 194 if (_isATemplate(templateSource)) 195 { 196 SkinTemplate template = _templates.get(id); 197 if (template == null) 198 { 199 template = new SkinTemplate(this._id, id, _skinsManager, _skinConfigurationHelper); 200 _templates.put(id, template); 201 } 202 template.refreshValues(); 203 return template; 204 } 205 else 206 { 207 if (_templates.containsKey(id)) 208 { 209 SkinTemplate skinTemplate = _templates.get(id); 210 skinTemplate.dispose(); 211 _templates.remove(id); 212 } 213 return null; 214 } 215 } 216 catch (IOException e) 217 { 218 throw new IllegalStateException("Can not get the template '" + id + "' for skin '" + this._id + "'", e); 219 } 220 finally 221 { 222 _skinsManager.getSourceResolver().release(templateSource); 223 } 224 } 225 226 private boolean _isATemplate(TraversableSource path) throws SourceException 227 { 228 if (path.exists() && path.isCollection()) 229 { 230 TraversableSource stylesheets = (TraversableSource) path.getChild("stylesheets"); 231 if (stylesheets.exists() && stylesheets.isCollection()) 232 { 233 Source templateXsl = stylesheets.getChild("template.xsl"); 234 return templateXsl.exists(); 235 } 236 } 237 return false; 238 } 239 240 /** 241 * The configuration default values (if configuration file does not exist or is unreadable) 242 */ 243 protected void _defaultValues() 244 { 245 _lastConfUpdate = new Date().getTime(); 246 247 this._abstract = false; 248 this._label = new I18nizableText(this._id); 249 this._description = new I18nizableText(""); 250 this._smallImage = "/plugins/web/resources/img/skin/skin_16.png"; 251 this._mediumImage = "/plugins/web/resources/img/skin/skin_32.png"; 252 this._largeImage = "/plugins/web/resources/img/skin/skin_48.png"; 253 } 254 255 /** 256 * Refresh the configuration values 257 */ 258 public void refreshValues() 259 { 260 try 261 { 262 Path configurationPath = _path.resolve(CONF_PATH); // No inheritance here... normal since this is the file that tell us about inheritance 263 if (Files.exists(configurationPath)) 264 { 265 long fileTime = Files.getLastModifiedTime(configurationPath).toMillis(); 266 if (_lastConfUpdate < fileTime) 267 { 268 _lastConfUpdate = fileTime; 269 try (InputStream is = Files.newInputStream(configurationPath)) 270 { 271 Configuration configuration = new DefaultConfigurationBuilder().build(is); 272 273 this._abstract = configuration.getAttributeAsBoolean("abstract", false); 274 this._parents = _parseParents(configuration.getAttribute("extends", "")); 275 this._label = _configureI18n(configuration.getChild("label", false), new I18nizableText(this._id)); 276 this._description = _configureI18n(configuration.getChild("description", false), new I18nizableText("")); 277 this._smallImage = _configureThumbnail(configuration.getChild("thumbnail").getChild("small").getValue(null), "/plugins/web/resources/img/skin/skin_16.png"); 278 this._mediumImage = _configureThumbnail(configuration.getChild("thumbnail").getChild("medium").getValue(null), "/plugins/web/resources/img/skin/skin_32.png"); 279 this._largeImage = _configureThumbnail(configuration.getChild("thumbnail").getChild("large").getValue(null), "/plugins/web/resources/img/skin/skin_48.png"); 280 } 281 } 282 } 283 else 284 { 285 _defaultValues(); 286 } 287 } 288 catch (Exception e) 289 { 290 _defaultValues(); 291 if (_logger.isWarnEnabled()) 292 { 293 _logger.warn("Cannot read the configuration file " + CONF_PATH + " for the skin '" + this._id + "'. Continue as if file was not existing", e); 294 } 295 } 296 } 297 298 private List<String> _parseParents(String parents) 299 { 300 return Arrays.asList(StringUtils.split(parents, ',')).stream() 301 .map(StringUtils::trimToNull) 302 .filter(Objects::nonNull) 303 .collect(Collectors.toList()); 304 } 305 306 private String _configureThumbnail(String value, String defaultImage) 307 { 308 if (value == null) 309 { 310 return defaultImage; 311 } 312 else 313 { 314 return "/skins/" + this._id + "/resources/" + value; 315 } 316 } 317 318 private I18nizableText _configureI18n(Configuration child, I18nizableText defaultValue) throws ConfigurationException 319 { 320 if (child != null) 321 { 322 String value = child.getValue(); 323 if (child.getAttributeAsBoolean("i18n", false)) 324 { 325 return new I18nizableText("skin." + this._id, value); 326 } 327 else 328 { 329 return new I18nizableText(value); 330 } 331 } 332 else 333 { 334 return defaultValue; 335 } 336 } 337 338 /** 339 * The skin id 340 * @return the id 341 */ 342 public String getId() 343 { 344 return _id; 345 } 346 347 /** 348 * The skin label 349 * @return The label 350 */ 351 public I18nizableText getLabel() 352 { 353 return _label; 354 } 355 /** 356 * The skin description 357 * @return The description. Can not be null but can be empty 358 */ 359 public I18nizableText getDescription() 360 { 361 return _description; 362 } 363 364 /** 365 * The small image file uri 366 * @return The small image file uri 367 */ 368 public String getSmallImage() 369 { 370 return _smallImage; 371 } 372 373 /** 374 * The medium image file uri 375 * @return The medium image file uri 376 */ 377 public String getMediumImage() 378 { 379 return _mediumImage; 380 } 381 382 /** 383 * The large image file uri 384 * @return The large image file uri 385 */ 386 public String getLargeImage() 387 { 388 return _largeImage; 389 } 390 391 /** 392 * Get the skin's path. Should not be used. Use the skin:// protocol. 393 * @return the skin's path 394 */ 395 public Path getRawPath () 396 { 397 return _path; 398 } 399}