001/* 002 * Copyright 2010 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.BufferedReader; 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.InputStreamReader; 022import java.net.URL; 023import java.nio.file.FileSystem; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.util.ArrayList; 027import java.util.Enumeration; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033import java.util.stream.Stream; 034 035import org.apache.avalon.framework.CascadingRuntimeException; 036import org.apache.avalon.framework.activity.Disposable; 037import org.apache.avalon.framework.activity.Initializable; 038import org.apache.avalon.framework.component.Component; 039import org.apache.avalon.framework.configuration.Configuration; 040import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 041import org.apache.avalon.framework.context.Context; 042import org.apache.avalon.framework.context.ContextException; 043import org.apache.avalon.framework.context.Contextualizable; 044import org.apache.avalon.framework.logger.AbstractLogEnabled; 045import org.apache.avalon.framework.service.ServiceException; 046import org.apache.avalon.framework.service.ServiceManager; 047import org.apache.avalon.framework.service.Serviceable; 048import org.apache.avalon.framework.thread.ThreadSafe; 049import org.apache.cocoon.Constants; 050import org.apache.cocoon.components.ContextHelper; 051import org.apache.cocoon.environment.Request; 052import org.apache.commons.lang3.StringUtils; 053import org.apache.excalibur.source.Source; 054import org.apache.excalibur.source.SourceNotFoundException; 055import org.apache.excalibur.source.SourceResolver; 056 057import org.ametys.core.util.JarFSManager; 058import org.ametys.runtime.servlet.RuntimeConfig; 059import org.ametys.runtime.servlet.RuntimeServlet; 060import org.ametys.web.WebConstants; 061import org.ametys.web.WebHelper; 062import org.ametys.web.data.type.ModelItemTypeExtensionPoint; 063import org.ametys.web.parameters.ViewAndParametersParser; 064import org.ametys.web.parameters.view.ViewParametersManager; 065import org.ametys.web.repository.site.SiteManager; 066 067/** 068 * Manages the skins 069 */ 070public class SkinsManager extends AbstractLogEnabled implements ThreadSafe, Serviceable, Component, Contextualizable, Initializable, Disposable 071{ 072 /** The avalon role name */ 073 public static final String ROLE = SkinsManager.class.getName(); 074 075 /** A cache of skins objects classified by absolute path */ 076 protected Map<Path, Skin> _skins = new HashMap<>(); 077 078 /** The skins declared as external: name of the skin and file location */ 079 protected Map<String, Path> _externalSkins = new HashMap<>(); 080 081 /** The skins declared in jar files */ 082 protected Map<String, Path> _resourcesSkins = new HashMap<>(); 083 084 /** Versions for bundled skins */ 085 protected Map<String, String> _versions = new HashMap<>(); 086 087 /** The avalon service manager */ 088 protected ServiceManager _manager; 089 /** The excalibur source resolver */ 090 protected SourceResolver _sourceResolver; 091 /** Avalon context */ 092 protected Context _context; 093 /** Cocoon context */ 094 protected org.apache.cocoon.environment.Context _cocoonContext; 095 /** The site manager */ 096 protected SiteManager _siteManager; 097 /** The view parameters manager */ 098 protected ViewParametersManager _viewParametersManager; 099 /** The view and parameters parser */ 100 protected ViewAndParametersParser _viewAndParametersParser; 101 /** The extension point for type of view parameter */ 102 protected ModelItemTypeExtensionPoint _viewParametersEP; 103 /** The skin configuration helper */ 104 protected SkinConfigurationHelper _skinConfigurationHelper; 105 106 @Override 107 public void service(ServiceManager manager) throws ServiceException 108 { 109 _manager = manager; 110 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 111 _skinConfigurationHelper = (SkinConfigurationHelper) manager.lookup(SkinConfigurationHelper.ROLE); 112 } 113 114 @Override 115 public void contextualize(Context context) throws ContextException 116 { 117 _context = context; 118 _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 119 } 120 121 ServiceManager getServiceManager() 122 { 123 return _manager; 124 } 125 126 Context getContext() 127 { 128 return _context; 129 } 130 131 SourceResolver getSourceResolver() 132 { 133 return _sourceResolver; 134 } 135 136 ViewParametersManager getViewParametersManager() 137 { 138 if (_viewParametersManager == null) 139 { 140 try 141 { 142 _viewParametersManager = (ViewParametersManager) _manager.lookup(ViewParametersManager.ROLE); 143 } 144 catch (ServiceException e) 145 { 146 throw new RuntimeException(e); 147 } 148 } 149 return _viewParametersManager; 150 } 151 152 ViewAndParametersParser getViewAndParametersParser() 153 { 154 if (_viewAndParametersParser == null) 155 { 156 try 157 { 158 _viewAndParametersParser = (ViewAndParametersParser) _manager.lookup(ViewAndParametersParser.ROLE); 159 } 160 catch (ServiceException e) 161 { 162 throw new RuntimeException(e); 163 } 164 } 165 return _viewAndParametersParser; 166 } 167 168 ModelItemTypeExtensionPoint getViewParameterTypeExtensionPoint() 169 { 170 if (_viewParametersEP == null) 171 { 172 try 173 { 174 _viewParametersEP = (ModelItemTypeExtensionPoint) _manager.lookup(ModelItemTypeExtensionPoint.ROLE_VIEW_PARAM); 175 } 176 catch (ServiceException e) 177 { 178 throw new RuntimeException(e); 179 } 180 } 181 return _viewParametersEP; 182 } 183 184 public void initialize() throws Exception 185 { 186 // Skins can be in external locations 187 _listExternalSkins("context://" + RuntimeServlet.EXTERNAL_LOCATIONS); 188 189 // Skins can be in jars 190 _listResourcesSkins(); 191 } 192 193 public void dispose() 194 { 195 for (Skin skin : _skins.values()) 196 { 197 skin.dispose(); 198 } 199 _skins.clear(); 200 } 201 202 private void _listResourcesSkins() throws IOException 203 { 204 Enumeration<URL> skinResources = getClass().getClassLoader().getResources("META-INF/ametys-skins"); 205 206 while (skinResources.hasMoreElements()) 207 { 208 URL skinResource = skinResources.nextElement(); 209 210 try (InputStream is = skinResource.openStream(); 211 BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"))) 212 { 213 String skin; 214 while ((skin = br.readLine()) != null) 215 { 216 String skinNameAndVersion = StringUtils.substringBefore(skin, ':'); 217 String skinName = StringUtils.substringBefore(skinNameAndVersion, '@'); 218 String version = StringUtils.trimToNull(StringUtils.substringAfter(skinNameAndVersion, '@')); 219 String skinResourceURI = StringUtils.substringAfter(skin, ':'); 220 221 FileSystem skinFileSystem = JarFSManager.getInstance().getFileSystemByResource(skinResourceURI); 222 Path skinPath = skinFileSystem.getPath(skinResourceURI); 223 224 if (_isASkinPath(skinPath)) 225 { 226 _resourcesSkins.put(skinName, skinPath); 227 _versions.put(skinName, version); 228 } 229 else 230 { 231 getLogger().error("The skin '" + skinName + "' declared in a JAR file will be ignored as it is not a true skin"); 232 } 233 } 234 } 235 } 236 } 237 238 private void _listExternalSkins(String uri) throws Exception 239 { 240 Configuration externalConf; 241 242 // Read file 243 Source externalLocation = null; 244 try 245 { 246 externalLocation = _sourceResolver.resolveURI(uri); 247 if (!externalLocation.exists()) 248 { 249 throw new SourceNotFoundException("No file at " + uri); 250 } 251 252 DefaultConfigurationBuilder externalConfBuilder = new DefaultConfigurationBuilder(); 253 try (InputStream external = externalLocation.getInputStream()) 254 { 255 externalConf = externalConfBuilder.build(external, uri); 256 } 257 } 258 catch (SourceNotFoundException e) 259 { 260 getLogger().debug("No external location file"); 261 return; 262 } 263 finally 264 { 265 _sourceResolver.release(externalLocation); 266 } 267 268 // Apply file 269 for (Configuration skinConf : externalConf.getChild("skins").getChildren("skin")) 270 { 271 String name = skinConf.getAttribute("name", null); 272 String location = skinConf.getValue(null); 273 274 if (name != null && location != null) 275 { 276 Path skinDir = _getPath(location); 277 // Do not check at this time, since it is not read-only, this can change 278 _externalSkins.put(name, skinDir); 279 } 280 } 281 } 282 283 /* 284 * Returns the corresponding file, either absolute or relative to the context path 285 */ 286 private Path _getPath(String path) 287 { 288 if (path == null) 289 { 290 return null; 291 } 292 293 Path directory = Path.of(path); 294 if (directory.isAbsolute()) 295 { 296 return directory; 297 } 298 else 299 { 300 return Path.of(_cocoonContext.getRealPath("/" + path)); 301 } 302 } 303 304 /** 305 * Get the list of existing skins from JARs 306 * @return A set of skin names. Can be null if there is an error. 307 */ 308 public Set<String> getResourceSkins() 309 { 310 return _resourcesSkins.keySet(); 311 } 312 /** 313 * Get the list of existing skins 314 * @return A set of skin names. Can be null if there is an error. 315 */ 316 public Set<String> getSkins() 317 { 318 try 319 { 320 Set<String> skins = new HashSet<>(); 321 322 // JAR skins 323 skins.addAll(_resourcesSkins.keySet()); 324 325 // External skins 326 _externalSkins.entrySet().stream() 327 .filter(e -> _isASkinPath(e.getValue())) 328 .map(Map.Entry::getKey) 329 .forEach(skins::add); 330 331 // Skins at location 332 Path skinsDir = getLocalSkinsLocation(); 333 if (Files.exists(skinsDir) && Files.isDirectory(skinsDir)) 334 { 335 try (Stream<Path> files = Files.list(skinsDir)) 336 { 337 files.filter(this::_isASkinPath) 338 .map(p -> p.getFileName().toString()) 339 .forEach(skins::add); 340 } 341 } 342 343 return skins; 344 } 345 catch (Exception e) 346 { 347 getLogger().error("Can not determine the list of skins available", e); 348 return null; 349 } 350 } 351 352 /** 353 * Get the skin version or null if unknown. Only available for bundled skins. 354 * @param skinName the skin name 355 * @return the skin version if known. 356 */ 357 public String getVersion(String skinName) 358 { 359 return _versions.get(skinName); 360 } 361 362 /** 363 * Get a skin 364 * @param id The id of the skin 365 * @return The skin or null if the skin does not exist 366 */ 367 public Skin getSkin(String id) 368 { 369 if (id == null) 370 { 371 return null; 372 } 373 374 Path skinPath; 375 boolean modifiable = false; 376 377 skinPath = _resourcesSkins.get(id); 378 if (skinPath == null) 379 { 380 skinPath = _externalSkins.get(id); 381 if (skinPath == null) 382 { 383 skinPath = getLocalSkinsLocation().resolve(id); 384 if (Files.exists(skinPath) && Files.isDirectory(skinPath)) 385 { 386 modifiable = true; 387 } 388 else 389 { 390 // No skin with this name 391 return null; 392 } 393 } 394 } 395 396 if (!_isASkinPath(skinPath)) 397 { 398 // A skin with this name but is not a skin 399 if (_skins.containsKey(skinPath)) 400 { 401 Skin skin = _skins.get(skinPath); 402 skin.dispose(); 403 } 404 405 _skins.put(skinPath, null); 406 return null; 407 } 408 409 Skin skin = _skins.get(skinPath); 410 if (skin == null) 411 { 412 skin = new Skin(id, skinPath, modifiable, this, _skinConfigurationHelper); 413 _skins.put(skinPath, skin); 414 } 415 416 skin.refreshValues(); 417 return skin; 418 } 419 420 /** 421 * Get the skin and recursively its parents skins in the right weight order 422 * @param skin The non null skin to check 423 * @return The list of parent skins INCLUDING the given one 424 */ 425 public List<Skin> getSkinAndParents(Skin skin) 426 { 427 List<Skin> skinAndParents = new ArrayList<>(); 428 429 skinAndParents.add(skin); 430 431 for (String parentSkinId : skin.getParents()) 432 { 433 Skin parentSkin = getSkin(parentSkinId); 434 if (parentSkin == null) 435 { 436 throw new IllegalStateException("The skin '" + skin.getId() + "' extends the unexisting skin '" + parentSkinId + "'"); 437 } 438 skinAndParents.addAll(getSkinAndParents(parentSkin)); 439 } 440 441 return skinAndParents; 442 } 443 444 /** 445 * Compute the skin that are directly derived from the given one 446 * @param skin A non null skin 447 * @return A non null list of skins 448 */ 449 public List<Skin> getDirectChildren(Skin skin) 450 { 451 List<Skin> children = new ArrayList<>(); 452 453 for (String otherSkinId : getSkins()) 454 { 455 if (!otherSkinId.equals(skin.getId())) 456 { 457 Skin otherSkin = getSkin(otherSkinId); 458 if (otherSkin.getParents().contains(skin.getId())) 459 { 460 children.add(otherSkin); 461 } 462 } 463 } 464 465 return children; 466 } 467 468 /** 469 * Get the skins location 470 * @return the skin location 471 */ 472 public Path getLocalSkinsLocation() 473 { 474 try 475 { 476 Request request = ContextHelper.getRequest(_context); 477 String skinLocation = (String) request.getAttribute("skin-location"); 478 if (skinLocation != null) 479 { 480 return Path.of(RuntimeConfig.getInstance().getAmetysHome().getAbsolutePath(), skinLocation); 481 } 482 } 483 catch (CascadingRuntimeException e) 484 { 485 // Ignore 486 } 487 488 return Path.of(_cocoonContext.getRealPath("/skins")); 489 } 490 491 /** 492 * Get the skin name from request or <code>null</code> if not found 493 * @return The skin name or <code>null</code> 494 */ 495 public String getSkinNameFromRequest () 496 { 497 Request request = null; 498 try 499 { 500 request = ContextHelper.getRequest(_context); 501 502 return getSkinNameFromRequest(request); 503 } 504 catch (RuntimeException e) 505 { 506 // No running request 507 return null; 508 } 509 } 510 511 /** 512 * Get the skin name from request or <code>null</code> if not found 513 * @param request The request 514 * @return The skin name or <code>null</code> 515 */ 516 public String getSkinNameFromRequest(Request request) 517 { 518 if (_siteManager == null) 519 { 520 try 521 { 522 _siteManager = (SiteManager) _manager.lookup(SiteManager.ROLE); 523 } 524 catch (ServiceException e) 525 { 526 throw new IllegalStateException(e); 527 } 528 } 529 530 if (request == null) 531 { 532 return null; 533 } 534 535 // First, search the skin name in the request attributes. 536 String skinName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SKIN_ID); 537 538 // Then, test if the site name is present as a request attribute to deduce the skin name. 539 if (StringUtils.isEmpty(skinName)) 540 { 541 String siteName = WebHelper.getSiteName(request); 542 543 if (StringUtils.isNotEmpty(siteName)) 544 { 545 skinName = _siteManager.getSite(siteName).getSkinId(); 546 } 547 } 548 549 return skinName; 550 } 551 552 private boolean _isASkinPath(Path skinDir) 553 { 554 if (!Files.exists(skinDir) || !Files.isDirectory(skinDir)) 555 { 556 return false; 557 } 558 559 Path templateDir = skinDir.resolve(Skin.TEMPLATES_PATH); 560 Path confDir = skinDir.resolve(Skin.CONF_PATH); 561 if ((!Files.exists(templateDir) || !Files.isDirectory(templateDir)) 562 && (!Files.exists(confDir) || Files.isDirectory(confDir))) 563 { 564 return false; 565 } 566 567 return true; 568 } 569}