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