001/* 002 * Copyright 2015 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.BufferedOutputStream; 019import java.io.IOException; 020import java.io.OutputStream; 021import java.nio.file.Files; 022import java.nio.file.Path; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.Properties; 030import java.util.function.Predicate; 031import java.util.stream.Collectors; 032 033import javax.xml.transform.OutputKeys; 034import javax.xml.transform.Result; 035import javax.xml.transform.TransformerConfigurationException; 036import javax.xml.transform.TransformerFactory; 037import javax.xml.transform.sax.SAXTransformerFactory; 038import javax.xml.transform.sax.TransformerHandler; 039import javax.xml.transform.stream.StreamResult; 040 041import org.apache.avalon.framework.component.Component; 042import org.apache.avalon.framework.configuration.Configuration; 043import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 044import org.apache.avalon.framework.context.Context; 045import org.apache.avalon.framework.context.ContextException; 046import org.apache.avalon.framework.context.Contextualizable; 047import org.apache.avalon.framework.logger.AbstractLogEnabled; 048import org.apache.avalon.framework.service.ServiceException; 049import org.apache.avalon.framework.service.ServiceManager; 050import org.apache.avalon.framework.service.Serviceable; 051import org.apache.cocoon.ProcessingException; 052import org.apache.cocoon.xml.AttributesImpl; 053import org.apache.cocoon.xml.XMLUtils; 054import org.apache.commons.io.FileUtils; 055import org.apache.xml.serializer.OutputPropertiesFactory; 056import org.slf4j.LoggerFactory; 057import org.xml.sax.SAXException; 058 059import org.ametys.core.ui.Callable; 060import org.ametys.core.util.path.PathUtils; 061import org.ametys.plugins.repository.AmetysObjectIterable; 062import org.ametys.runtime.i18n.I18nizableText; 063import org.ametys.runtime.model.CategorizedElementDefinitionHelper; 064import org.ametys.runtime.model.DefinitionAndValue; 065import org.ametys.runtime.model.ElementDefinition; 066import org.ametys.runtime.model.type.ElementType; 067import org.ametys.runtime.util.AmetysHomeHelper; 068import org.ametys.web.cocoon.I18nTransformer; 069import org.ametys.web.cocoon.I18nUtils; 070import org.ametys.web.impl.model.type.xsl.XSLElementType; 071import org.ametys.web.repository.site.Site; 072import org.ametys.web.repository.site.SiteManager; 073 074/** 075 * DAO for manipulating skins 076 */ 077public class SkinDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 078{ 079 /** The avalon role*/ 080 public static final String ROLE = SkinDAO.class.getName(); 081 082 private static final String __LICENCE = "\n" 083 + " Copyright 2012 Anyware Services\n" 084 + "\n" 085 + " Licensed under the Apache License, Version 2.0 (the \"License\");\n" 086 + " you may not use this file except in compliance with the License.\n" 087 + " You may obtain a copy of the License at\n" 088 + "\n" 089 + " http://www.apache.org/licenses/LICENSE-2.0\n" 090 + "\n" 091 + " Unless required by applicable law or agreed to in writing, software\n" 092 + " distributed under the License is distributed on an \"AS IS\" BASIS,\n" 093 + " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" 094 + " See the License for the specific language governing permissions and\n" 095 + " limitations under the License.\n" 096 + "\n"; 097 098 /** The service manager instance */ 099 protected ServiceManager _manager; 100 private Context _context; 101 private SkinsManager _skinsManager; 102 private SkinModelsManager _modelsManager; 103 private SiteManager _siteManager; 104 private I18nUtils _i18nUtils; 105 private SkinParameterTypeExtensionPoint _skinParameterTypeEP; 106 107 @Override 108 public void service(ServiceManager manager) throws ServiceException 109 { 110 _manager = manager; 111 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 112 _modelsManager = (SkinModelsManager) manager.lookup(SkinModelsManager.ROLE); 113 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 114 _i18nUtils = (I18nUtils) manager.lookup(org.ametys.core.util.I18nUtils.ROLE); 115 _skinParameterTypeEP = (SkinParameterTypeExtensionPoint) manager.lookup(SkinParameterTypeExtensionPoint.ROLE); 116 } 117 118 public void contextualize(Context context) throws ContextException 119 { 120 _context = context; 121 } 122 /** 123 * Retrieve informations on a skin 124 * @param skinId The skin id 125 * @return the informations of a skin 126 */ 127 @Callable 128 public Map<String, Object> getSkin(String skinId) 129 { 130 Map<String, Object> result = new HashMap<>(); 131 132 Skin skin = _skinsManager.getSkin(skinId); 133 134 if (skin != null) 135 { 136 result.put("id", skin.getId()); 137 result.put("label", skin.getLabel()); 138 result.put("inUse", isInUse(skinId)); 139 result.put("isModifiable", skin.isModifiable()); 140 result.put("isConfigurable", skin.isConfigurable()); 141 result.put("isExtended", _skinsManager.getDirectChildren(skin).size() > 0); 142 143 String modelName = _modelsManager.getModelOfSkin(skin); 144 result.put("model", modelName); 145 } 146 147 return result; 148 } 149 150 /** 151 * Determines if a skin exists 152 * @param skinId The skin id 153 * @return true if skin exists. 154 * @throws ProcessingException if an error occurs 155 */ 156 @Callable 157 public boolean skinExists (String skinId) throws ProcessingException 158 { 159 return _skinsManager.getSkins().contains(skinId); 160 } 161 162 /** 163 * Determines if a skin is currently used by one or more sites 164 * @param skinId The skin id 165 * @return true if skin is currently in use 166 */ 167 @Callable 168 public boolean isInUse (String skinId) 169 { 170 AmetysObjectIterable<Site> sites = _siteManager.getSites(); 171 for (Site site : sites) 172 { 173 if (skinId.equals(site.getSkinId())) 174 { 175 return true; 176 } 177 178 } 179 return false; 180 } 181 182 /** 183 * Retrieve the list of available skins 184 * @param includeAbstract Should include abstract skins 185 * @return a map of skins 186 */ 187 @Callable 188 public Map<String, Object> getSkins(boolean includeAbstract) 189 { 190 Map<String, Object> result = new HashMap<>(); 191 192 result.put("skins", _skins2JsonObject(includeAbstract)); 193 194 return result; 195 } 196 197 private List<Object> _skins2JsonObject(boolean includeAbstract) 198 { 199 List<Object> skinsList = new ArrayList<>(); 200 Map<String, List<Site>> skins = new HashMap<>(); 201 for (String id : _skinsManager.getSkins()) 202 { 203 if (includeAbstract || !_skinsManager.getSkin(id).isAbstract()) 204 { 205 skins.put(id, new ArrayList<Site>()); 206 } 207 } 208 209 AmetysObjectIterable<Site> sites = _siteManager.getSites(); 210 for (Site site : sites) 211 { 212 String skinId = site.getSkinId(); 213 if (skinId != null) 214 { 215 List<Site> skin = skins.get(skinId); 216 217 if (skin != null) 218 { 219 skin.add(site); 220 } 221 } 222 } 223 224 for (String id : skins.keySet()) 225 { 226 skinsList.add(_skin2JsonObject(id, skins.get(id))); 227 } 228 229 return skinsList; 230 } 231 232 private Map<String, Object> _skin2JsonObject(String id, List<Site> skinSites) 233 { 234 Map<String, Object> jsonSkin = new HashMap<>(); 235 236 Skin skin = _skinsManager.getSkin(id); 237 238 jsonSkin.put("id", skin.getId()); 239 jsonSkin.put("abstract", skin.isAbstract()); 240 jsonSkin.put("label", skin.getLabel()); 241 jsonSkin.put("description", skin.getDescription()); 242 jsonSkin.put("iconLarge", skin.getLargeImage()); 243 jsonSkin.put("iconSmall", skin.getSmallImage()); 244 jsonSkin.put("nbSites", skinSites.size()); 245 jsonSkin.put("extendingSkins", skin.getParents()); 246 jsonSkin.put("extendedIntoSkins", _skinsManager.getDirectChildren(skin).stream().map(Skin::getId).collect(Collectors.toList())); 247 jsonSkin.put("sites", skinSites.stream() 248 .map(site -> site.getTitle() + " (" + site.getName() + ")") 249 .collect(Collectors.joining(", "))); 250 251 String modelName = _modelsManager.getModelOfSkin(skin); 252 if (modelName != null) 253 { 254 SkinModel model = _modelsManager.getModel(modelName); 255 if (model != null) 256 { 257 Map<String, Object> skinModel = new HashMap<>(); 258 259 skinModel.put("id", modelName); 260 skinModel.put("name", model.getLabel()); 261 262 jsonSkin.put("model", skinModel); 263 } 264 } 265 266 return jsonSkin; 267 } 268 269 270 271 /** 272 * This action receive a form with the "importfile" zip file as an exported skin. 273 * Replace existing skin 274 * @param skinName The skin name 275 * @param tmpDirPath The directory where the zip was uploaded 276 * @param values the configuration's values. Can be empty. 277 * @return The skin name 278 * @throws SAXException if an error occurs during configuration file creation 279 * @throws IOException if an error occurs while manipulating files 280 * @throws TransformerConfigurationException if an error occurs during configuration file creation 281 * @throws ProcessingException error while parsing model 282 */ 283 @Callable 284 public Map<String, Object> importSkin(String skinName, String tmpDirPath, Map<String, Object> values) throws TransformerConfigurationException, IOException, SAXException, ProcessingException 285 { 286 Map<String, Object> result = new HashMap<>(); 287 288 Path tmpDir = AmetysHomeHelper.getAmetysHomeTmp().toPath().resolve(tmpDirPath); 289 if (Files.isDirectory(tmpDir)) 290 { 291 if (!values.isEmpty()) 292 { 293 SkinParametersModel model = new SkinParametersModel(skinName, tmpDir, _skinParameterTypeEP, _context, _manager); 294 Collection<ElementDefinition> skinParameterDefinitions = model.getModelItems(); 295 Map<String, DefinitionAndValue> definitionAndValues = _getDefinitionAndValues(values, skinParameterDefinitions); 296 297 Map<String, List<I18nizableText>> errorFields = CategorizedElementDefinitionHelper.validateValuesForWriting(definitionAndValues, LoggerFactory.getLogger(getClass())); 298 if (!errorFields.isEmpty()) 299 { 300 result.put("errors", errorFields); 301 return result; 302 } 303 304 _createConfigFile(model, tmpDir, values); 305 } 306 307 // If exists remove. 308 Skin existingSkin = _skinsManager.getSkin(skinName); 309 if (existingSkin != null) 310 { 311 if (existingSkin.isModifiable()) 312 { 313 deleteSkin(skinName); 314 } 315 else 316 { 317 throw new IllegalStateException("The skin '" + skinName + "' already exists and is not modifiable and thus cannot be replaced."); 318 } 319 } 320 321 // Move to skins directory 322 Path rootLocation = _skinsManager.getLocalSkinsLocation(); 323 PathUtils.moveDirectory(tmpDir, rootLocation.resolve(skinName)); 324 325 _i18nUtils.reloadCatalogues(); 326 I18nTransformer.needsReload(); 327 } 328 329 result.put("skinId", skinName); 330 return result; 331 } 332 333 /** 334 * Configure a skin 335 * @param skinName the skin name 336 * @param values the configuration's values 337 * @return A map with "errors" key that is a map <errorName> <errorMessage> 338 * @throws SAXException if an error occurs during configuration file creation 339 * @throws IOException if an error occurs during configuration file creation 340 * @throws TransformerConfigurationException if an error occurs during configuration file creation 341 * @throws ProcessingException error while parsing model 342 */ 343 @Callable 344 public Map<String, Object> configureSkin (String skinName, Map<String, Object> values) throws TransformerConfigurationException, IOException, SAXException, ProcessingException 345 { 346 Map<String, Object> result = new HashMap<>(); 347 348 Skin skin = _skinsManager.getSkin(skinName); 349 if (!skin.isConfigurable()) 350 { 351 throw new IllegalStateException("The skin '" + skinName + "' is not configurable."); 352 } 353 354 Path skinDir = skin.getRawPath(); // isConfigurable works locally only, so we can get the internal path 355 SkinParametersModel model = new SkinParametersModel(skinName, skinDir, _skinParameterTypeEP, _context, _manager); 356 357 Collection<ElementDefinition> skinParameterDefinitions = model.getModelItems(); 358 Map<String, DefinitionAndValue> definitionAndValues = _getDefinitionAndValues(values, skinParameterDefinitions); 359 360 Map<String, List<I18nizableText>> errorFields = CategorizedElementDefinitionHelper.validateValuesForWriting(definitionAndValues, LoggerFactory.getLogger(getClass())); 361 if (!errorFields.isEmpty()) 362 { 363 result.put("errors", errorFields); 364 return result; 365 } 366 367 //Delete the old file only if no error found 368 Path configFile = skinDir.resolve("stylesheets/config/config.xsl"); 369 if (Files.exists(configFile)) 370 { 371 FileUtils.deleteQuietly(configFile.toFile()); 372 } 373 _createConfigFile(model, skinDir, values); 374 375 _i18nUtils.reloadCatalogues(); 376 I18nTransformer.needsReload(); 377 378 result.put("skinId", skinName); 379 return result; 380 } 381 382 private Map<String, DefinitionAndValue> _getDefinitionAndValues(Map<String, Object> values, Collection<ElementDefinition> skinParameterDefinitions) 383 { 384 Map<String, DefinitionAndValue> definitionAndValues = new HashMap<>(); 385 for (ElementDefinition definition : skinParameterDefinitions) 386 { 387 Object value = values.get(definition.getName()); 388 DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, definition, value); 389 definitionAndValues.put(definition.getName(), definitionAndValue); 390 } 391 return definitionAndValues; 392 } 393 394 private void _createConfigFile(SkinParametersModel model, Path skinDir, Map<String, Object> values) throws IOException, SAXException, TransformerConfigurationException 395 { 396 Path configFile = skinDir.resolve("stylesheets/config/config.xsl"); 397 398 SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); 399 TransformerHandler handler = factory.newTransformerHandler(); 400 401 try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(configFile))) 402 { 403 Result result = new StreamResult(os); 404 405 Properties format = new Properties(); 406 format.put(OutputKeys.METHOD, "xml"); 407 format.put(OutputKeys.ENCODING, "UTF-8"); 408 format.put(OutputKeys.INDENT, "yes"); 409 format.put(OutputKeys.OMIT_XML_DECLARATION, "no"); 410 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 411 412 handler.setResult(result); 413 handler.getTransformer().setOutputProperties(format); 414 415 handler.startDocument(); 416 417 AttributesImpl attrs = new AttributesImpl(); 418 attrs.addCDATAAttribute("version", "1.0"); 419 attrs.addCDATAAttribute("xmlns:xsl", "http://www.w3.org/1999/XSL/Transform"); 420 421 handler.comment(__LICENCE.toCharArray(), 0, __LICENCE.length()); 422 XMLUtils.startElement(handler, "xsl:stylesheet", attrs); 423 424 for (Entry<String, Object> variable : values.entrySet()) 425 { 426 if (model.hasModelItem(variable.getKey())) 427 { 428 ElementDefinition elementDefinition = model.getModelItem(variable.getKey()); 429 ElementType type = elementDefinition.getType(); 430 if (type instanceof XSLElementType) 431 { 432 ((XSLElementType) type).write(handler, elementDefinition.getName(), variable.getValue()); 433 } 434 } 435 } 436 437 XMLUtils.endElement(handler, "xsl:stylesheet"); 438 439 handler.endDocument(); 440 } 441 } 442 443 /** 444 * Duplicate an existing skin 445 * @param skinId The new skin id 446 * @param originalSkinId The original skin id 447 * @return An error message, or null if successful 448 * @throws IOException if an I/O exception occurs during copy 449 */ 450 @Callable 451 public String copySkin(String skinId, String originalSkinId) throws IOException 452 { 453 // Check if exists 454 if (_skinsManager.getSkins().contains(skinId)) 455 { 456 return "already-exists"; 457 } 458 459 Path sourceSkinPath = _skinsManager.getSkin(originalSkinId).getRawPath(); 460 Path destinationPath = _skinsManager.getLocalSkinsLocation().resolve(skinId); 461 462 PathUtils.copyDirectory(sourceSkinPath, destinationPath, _getModelFilter(sourceSkinPath), false); 463 464 _i18nUtils.reloadCatalogues(); 465 I18nTransformer.needsReload(); 466 467 return null; 468 } 469 470 /** 471 * This filter accepts all <code>File</code>s excepted CVS and SVN directories and root directory named "model" 472 * @param skinDir The model root directory 473 * @return the filter 474 */ 475 private static final Predicate<Path> _getModelFilter(Path skinDir) 476 { 477 return f -> 478 !f.getFileName().toString().equals("CVS") 479 && !f.getFileName().toString().equals(".svn") 480 && (!f.getParent().equals(skinDir) || !f.getFileName().toString().equals("model")); 481 } 482 483 /** 484 * Delete a skin 485 * @param skinId The skin id 486 * @return the skin id 487 * @throws IOException if an I/O exception occurs during deletion 488 */ 489 @Callable 490 public String deleteSkin(String skinId) throws IOException 491 { 492 Skin skin = _skinsManager.getSkin(skinId); 493 Path file = skin.getRawPath(); 494 495 if (!skin.isModifiable()) 496 { 497 throw new IllegalStateException("The skin '" + skinId + "' is not modifiable and thus cannot be removed."); 498 } 499 500 List<Skin> directChildren = _skinsManager.getDirectChildren(skin); 501 if (directChildren.size() > 0) 502 { 503 throw new IllegalStateException("The skin '" + skinId + "' cannot be removed since the skin(s) " + directChildren.stream().map(Skin::getId).collect(Collectors.toList()) + " extend(s) it."); 504 } 505 506 if (Files.exists(file)) 507 { 508 FileUtils.deleteDirectory(file.toFile()); 509 } 510 511 return skinId; 512 } 513 514 /** 515 * Parse the values file and make a liaison with the definitions, to return a collection of {@link DefinitionAndValue} 516 * @param skinDir folder of the skin 517 * @param skinParameterDefinitions the skin parameter definitions 518 * @return a {@link Collection} of {@link DefinitionAndValue} using flatDefinitions and the values found in skinDir 519 * @throws Exception Impossible to read the file 520 */ 521 public Collection<DefinitionAndValue> readValues(Path skinDir, Collection<ElementDefinition> skinParameterDefinitions) throws Exception 522 { 523 Collection<DefinitionAndValue> definitionAndValues = new ArrayList<>(); 524 String fileName = "stylesheets/config/config.xsl"; 525 Path configFile = skinDir.resolve(fileName); 526 boolean fileExists = Files.exists(configFile); 527 528 if (fileExists) 529 { 530 Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(configFile.toFile()); 531 for (ElementDefinition definition : skinParameterDefinitions) 532 { 533 String parameterName = definition.getName(); 534 ElementType type = definition.getType(); 535 if (type instanceof XSLElementType) 536 { 537 Object value = ((XSLElementType) type).read(configuration, parameterName); 538 DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, definition, value); 539 definitionAndValues.add(definitionAndValue); 540 } 541 } 542 543 return definitionAndValues; 544 } 545 546 return null; 547 } 548}