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