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