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