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