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 Path skinPath; 347 boolean modifiable = false; 348 349 skinPath = _resourcesSkins.get(id); 350 if (skinPath == null) 351 { 352 skinPath = _externalSkins.get(id); 353 if (skinPath == null) 354 { 355 skinPath = getLocalSkinsLocation().resolve(id); 356 if (Files.exists(skinPath) && Files.isDirectory(skinPath)) 357 { 358 modifiable = true; 359 } 360 else 361 { 362 // No skin with this name 363 return null; 364 } 365 } 366 } 367 368 if (!_isASkinPath(skinPath)) 369 { 370 // A skin with this name but is not a skin 371 if (_skins.containsKey(skinPath)) 372 { 373 Skin skin = _skins.get(skinPath); 374 skin.dispose(); 375 } 376 377 _skins.put(skinPath, null); 378 return null; 379 } 380 381 Skin skin = _skins.get(skinPath); 382 if (skin == null) 383 { 384 skin = new Skin(id, skinPath, modifiable, this); 385 _skins.put(skinPath, skin); 386 } 387 388 skin.refreshValues(); 389 return skin; 390 } 391 392 /** 393 * Get the skin and recursively its parents skins in the right weight order 394 * @param skin The non null skin to check 395 * @return The list of parent skins INCLUDING the given one 396 */ 397 public List<Skin> getSkinAndParents(Skin skin) 398 { 399 List<Skin> skinAndParents = new ArrayList<>(); 400 401 skinAndParents.add(skin); 402 403 for (String parentSkinId : skin.getParents()) 404 { 405 Skin parentSkin = getSkin(parentSkinId); 406 if (parentSkin == null) 407 { 408 throw new IllegalStateException("The skin '" + skin.getId() + "' extends the unexisting skin '" + parentSkinId + "'"); 409 } 410 skinAndParents.addAll(getSkinAndParents(parentSkin)); 411 } 412 413 return skinAndParents; 414 } 415 416 /** 417 * Compute the skin that are directly derived from the given one 418 * @param skin A non null skin 419 * @return A non null list of skins 420 */ 421 public List<Skin> getDirectChildren(Skin skin) 422 { 423 List<Skin> children = new ArrayList<>(); 424 425 for (String otherSkinId : getSkins()) 426 { 427 if (!otherSkinId.equals(skin.getId())) 428 { 429 Skin otherSkin = getSkin(otherSkinId); 430 if (otherSkin.getParents().contains(skin.getId())) 431 { 432 children.add(otherSkin); 433 } 434 } 435 } 436 437 return children; 438 } 439 440 /** 441 * Get the skins location 442 * @return the skin location 443 */ 444 public Path getLocalSkinsLocation() 445 { 446 try 447 { 448 Request request = ContextHelper.getRequest(_context); 449 String skinLocation = (String) request.getAttribute("skin-location"); 450 if (skinLocation != null) 451 { 452 return Path.of(RuntimeConfig.getInstance().getAmetysHome().getAbsolutePath(), skinLocation); 453 } 454 } 455 catch (CascadingRuntimeException e) 456 { 457 // Ignore 458 } 459 460 return Path.of(_cocoonContext.getRealPath("/skins")); 461 } 462 463 /** 464 * Get the skin name from request or <code>null</code> if not found 465 * @return The skin name or <code>null</code> 466 */ 467 public String getSkinNameFromRequest () 468 { 469 Request request = null; 470 try 471 { 472 request = ContextHelper.getRequest(_context); 473 474 return getSkinNameFromRequest(request); 475 } 476 catch (RuntimeException e) 477 { 478 // No running request 479 return null; 480 } 481 } 482 483 /** 484 * Get the skin name from request or <code>null</code> if not found 485 * @param request The request 486 * @return The skin name or <code>null</code> 487 */ 488 public String getSkinNameFromRequest(Request request) 489 { 490 if (_siteManager == null) 491 { 492 try 493 { 494 _siteManager = (SiteManager) _manager.lookup(SiteManager.ROLE); 495 } 496 catch (ServiceException e) 497 { 498 throw new IllegalStateException(e); 499 } 500 } 501 502 if (request == null) 503 { 504 return null; 505 } 506 507 // First, search the skin name in the request attributes. 508 String skinName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SKIN_ID); 509 510 // Then, test if the site name is present as a request attribute to deduce the skin name. 511 if (StringUtils.isEmpty(skinName)) 512 { 513 String siteName = WebHelper.getSiteName(request); 514 515 if (StringUtils.isNotEmpty(siteName)) 516 { 517 skinName = _siteManager.getSite(siteName).getSkinId(); 518 } 519 } 520 521 return skinName; 522 } 523 524 private boolean _isASkinPath(Path skinDir) 525 { 526 if (!Files.exists(skinDir) || !Files.isDirectory(skinDir)) 527 { 528 return false; 529 } 530 531 Path templateDir = skinDir.resolve(Skin.TEMPLATES_PATH); 532 Path confDir = skinDir.resolve(Skin.CONF_PATH); 533 if ((!Files.exists(templateDir) || !Files.isDirectory(templateDir)) 534 && (!Files.exists(confDir) || Files.isDirectory(confDir))) 535 { 536 return false; 537 } 538 539 return true; 540 } 541}