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