001/* 002 * Copyright 2013 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.plugins.skincommons; 017 018import java.io.File; 019import java.io.IOException; 020import java.io.InputStream; 021import java.nio.file.Files; 022import java.nio.file.Path; 023import java.text.DateFormat; 024import java.text.SimpleDateFormat; 025import java.util.Arrays; 026import java.util.Date; 027import java.util.stream.Stream; 028 029import javax.xml.xpath.XPath; 030import javax.xml.xpath.XPathExpressionException; 031import javax.xml.xpath.XPathFactory; 032 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.component.ComponentException; 035import org.apache.avalon.framework.context.Context; 036import org.apache.avalon.framework.context.ContextException; 037import org.apache.avalon.framework.context.Contextualizable; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.avalon.framework.thread.ThreadSafe; 042import org.apache.cocoon.Constants; 043import org.apache.cocoon.i18n.BundleFactory; 044import org.apache.commons.io.FileUtils; 045import org.apache.commons.io.comparator.LastModifiedFileComparator; 046import org.apache.commons.lang3.RandomStringUtils; 047import org.apache.commons.lang3.StringUtils; 048import org.xml.sax.InputSource; 049 050import org.ametys.core.cocoon.XMLResourceBundleFactory; 051import org.ametys.core.util.path.PathUtils; 052import org.ametys.runtime.plugin.component.AbstractLogEnabled; 053import org.ametys.runtime.servlet.RuntimeConfig; 054import org.ametys.web.cache.FOCommHelper; 055import org.ametys.web.cache.pageelement.PageElementCache; 056import org.ametys.web.repository.site.Site; 057import org.ametys.web.repository.site.SiteManager; 058import org.ametys.web.skin.SkinsManager; 059 060/** 061 * Helper for skin edition 062 * 063 */ 064public class SkinEditionHelper extends AbstractLogEnabled implements Component, ThreadSafe, Serviceable, Contextualizable 065{ 066 /** The Avalon role name */ 067 public static final String ROLE = SkinEditionHelper.class.getName(); 068 069 private static final DateFormat _DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); 070 071 /** The cocoon context */ 072 protected org.apache.cocoon.environment.Context _cocoonContext; 073 074 private SiteManager _siteManager; 075 private PageElementCache _zoneItemCache; 076 private PageElementCache _inputDataCache; 077 private XMLResourceBundleFactory _i18nFactory; 078 private SkinsManager _skinsManager; 079 private FOCommHelper _foCommHelper; 080 081 @Override 082 public void service(ServiceManager smanager) throws ServiceException 083 { 084 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 085 _zoneItemCache = (PageElementCache) smanager.lookup(PageElementCache.ROLE + "/zoneItem"); 086 _inputDataCache = (PageElementCache) smanager.lookup(PageElementCache.ROLE + "/inputData"); 087 _i18nFactory = (XMLResourceBundleFactory) smanager.lookup(BundleFactory.ROLE); 088 _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE); 089 _foCommHelper = (FOCommHelper) smanager.lookup(FOCommHelper.ROLE); 090 } 091 092 @Override 093 public void contextualize(Context context) throws ContextException 094 { 095 _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 096 } 097 098 /** 099 * Create a backup file of the current skin 100 * @param skinName The skin name 101 * @return The created backup directory 102 * @throws IOException If an error occurred 103 */ 104 public Path createBackupFile (String skinName) throws IOException 105 { 106 Path backupDir = getBackupDirectory(skinName, new Date()); 107 PathUtils.moveDirectoryToDirectory(getSkinDirectory(skinName), backupDir, true); 108 return backupDir; 109 } 110 111 /** 112 * Asynchronous file deletion 113 * @param file The file to delete 114 * @return <code>true</code> if the deletion succeeded 115 * @throws IOException if an error occurs while manipulating files 116 */ 117 public boolean deleteQuicklyDirectory(Path file) throws IOException 118 { 119 Path toDelete = file.getParent().resolve(file.getFileName() + "_todelete_" + RandomStringUtils.secure().next(10, false, true)); 120 121 try 122 { 123 // Move file 124 Files.move(file, toDelete); 125 126 // Then delete it in asynchronous mode 127 Thread th = new Thread(new AsynchronousPathDeletion(toDelete)); 128 th.start(); 129 130 return true; 131 } 132 catch (IOException e) 133 { 134 return false; 135 } 136 } 137 138 /** 139 * Remove the old backup 140 * @param skinName The skin name 141 * @param keepMax The max number of backup to keep 142 * @throws IOException if an error occurs while manipulating files 143 */ 144 public void deleteOldBackup (String skinName, int keepMax) throws IOException 145 { 146 // Remove old backup (keep only the 5 last backup) 147 File rootBackupDir = getRootBackupDirectory(skinName); 148 File[] allBackup = rootBackupDir.listFiles(); 149 Arrays.sort(allBackup, LastModifiedFileComparator.LASTMODIFIED_REVERSE); 150 151 int index = 0; 152 for (File f : allBackup) 153 { 154 if (index > keepMax - 1) 155 { 156 deleteQuicklyDirectory(f.toPath()); 157 } 158 index++; 159 } 160 } 161 162 /** 163 * Invalidate all caches relative to the modified skin. 164 * @param skinName the modified skin name. 165 * @throws Exception if an error occurs. 166 */ 167 public void invalidateCaches(String skinName) throws Exception 168 { 169 Exception ex = null; 170 171 // Invalidate the caches for sites with the skin. 172 for (Site site : _siteManager.getSites()) 173 { 174 if (skinName.equals(_getSkinId(site))) 175 { 176 try 177 { 178 String siteName = site.getName(); 179 180 // Invalidate static cache. 181 _foCommHelper.invalidateFOCache(site); 182 183 // Invalidate the page elements caches. 184 _zoneItemCache.clear(null, siteName); 185 _inputDataCache.clear(null, siteName); 186 } 187 catch (Exception e) 188 { 189 getLogger().error("Error clearing the cache for site " + site.toString()); 190 ex = e; 191 } 192 } 193 } 194 195 // If an exception was thrown, re-throw it. 196 if (ex != null) 197 { 198 throw ex; 199 } 200 } 201 202 private String _getSkinId(Site site) 203 { 204 return site.getSkinId(); 205 } 206 207 /** 208 * Invalidate all catalogs of the temporary skin 209 * @param skinName The site name 210 */ 211 public void invalidateTempSkinCatalogues(String skinName) 212 { 213 _invalidateSkinCatalogues(getTempDirectory(skinName), skinName, "ametys-home://skins/temp/" + skinName + "/i18n"); 214 } 215 216 /** 217 * Invalidate a catalog of the temporary skin for a given language 218 * @param skinName The site name 219 * @param lang The language 220 */ 221 public void invalidateTempSkinCatalogue(String skinName, String lang) 222 { 223 _invalidateSkinCatalogue(getTempDirectory(skinName), skinName, "ametys-home://skins/temp/" + skinName + "/i18n", lang); 224 } 225 226 /** 227 * Invalidate all catalogs of the skin 228 * @param skinName The skin name 229 */ 230 public void invalidateSkinCatalogues(String skinName) 231 { 232 _invalidateSkinCatalogues(getSkinDirectory(skinName), skinName, "skin-raw:" + skinName + "://i18n"); 233 } 234 235 /** 236 * Invalidate a catalog of the skin for a given language 237 * @param skinName The site name 238 * @param lang The language 239 */ 240 public void invalidateSkinCatalogue(String skinName, String lang) 241 { 242 _invalidateSkinCatalogue(getSkinDirectory(skinName), skinName, "skin-raw:" + skinName + "://i18n", lang); 243 } 244 245 /** 246 * Invalidate all catalogs of the skin 247 * @param skinDir The skin directory 248 * @param skinName The skin name 249 * @param catalogLocation the catalog location 250 */ 251 private void _invalidateSkinCatalogues(Path skinDir, String skinName, String catalogLocation) 252 { 253 Path i18nDir = skinDir.resolve("i18n"); 254 if (Files.exists(i18nDir)) 255 { 256 try (Stream<Path> s = Files.list(i18nDir)) 257 { 258 s.filter(Files::isRegularFile) 259 .forEach(i18nFile -> 260 { 261 String filename = i18nFile.getFileName().toString(); 262 if (filename.equals("messages.xml")) 263 { 264 _invalidateSkinCatalogue(skinDir, skinName, catalogLocation, StringUtils.EMPTY); 265 } 266 else if (filename.startsWith("messages_")) 267 { 268 String lang = filename.substring("messages_".length(), "messages_".length() + 2); 269 _invalidateSkinCatalogue(skinDir, skinName, catalogLocation, lang); 270 } 271 }); 272 } 273 catch (IOException e) 274 { 275 throw new RuntimeException("Cannot invalidate skin catalogs for skin " + skinName + " and location " + catalogLocation, e); 276 } 277 } 278 } 279 280 /** 281 * Invalidate catalog of the skin 282 * @param skinDir The skin directory 283 * @param skinName The site name 284 * @param catalogLocation the catalog location 285 * @param lang The language of catalog. Can be empty. 286 */ 287 private void _invalidateSkinCatalogue(Path skinDir, String skinName, String catalogLocation, String lang) 288 { 289 try 290 { 291 String localName = lang; 292 if (StringUtils.isNotEmpty(lang)) 293 { 294 Path f = skinDir.resolve("i18n/messages_" + lang + ".xml"); 295 if (!Files.exists(f)) 296 { 297 localName = ""; 298 } 299 } 300 301 _i18nFactory.invalidateCatalogue(catalogLocation, "messages", localName); 302 } 303 catch (ComponentException e) 304 { 305 getLogger().warn("Unable to invalidate i18n catalog for skin " + skinName + " and location " + catalogLocation , e); 306 } 307 } 308 309 /** 310 * Get the temp directory of skin 311 * @param skinName The skin name 312 * @return The temp directory 313 */ 314 public Path getTempDirectory(String skinName) 315 { 316 return RuntimeConfig.getInstance().getAmetysHome().toPath().resolve("skins/temp/" + skinName); 317 } 318 319 /** 320 * Get the work directory of skin 321 * @param skinName The skin name 322 * @return The work directory 323 */ 324 public Path getWorkDirectory(String skinName) 325 { 326 return RuntimeConfig.getInstance().getAmetysHome().toPath().resolve("skins/work/" + skinName); 327 } 328 329 /** 330 * Get the backup directory of skin 331 * @param skinName The skin name 332 * @param date The date 333 * @return The backup directory 334 */ 335 public Path getBackupDirectory (String skinName, Date date) 336 { 337 String dateStr = _DATE_FORMAT.format(date); 338 return RuntimeConfig.getInstance().getAmetysHome().toPath().resolve("skins/backup/" + skinName + "/" + dateStr); 339 } 340 341 /** 342 * Get the root backup directory of skin 343 * @param skinName The skin name 344 * @return The root backup directory 345 */ 346 public File getRootBackupDirectory (String skinName) 347 { 348 return FileUtils.getFile(RuntimeConfig.getInstance().getAmetysHome(), "skins", "backup", skinName); 349 } 350 351 /** 352 * Get the temp directory of skin 353 * @param skinName The skin name 354 * @return The temp directory URI 355 */ 356 public String getTempDirectoryURI (String skinName) 357 { 358 return "ametys-home://skins/temp/" + skinName; 359 } 360 361 /** 362 * Get the work directory of skin 363 * @param skinName The skin name 364 * @return The work directory URI 365 */ 366 public String getWorkDirectoryURI (String skinName) 367 { 368 return "ametys-home://skins/work/" + skinName; 369 } 370 371 /** 372 * Get the backup directory of skin 373 * @param skinName The skin name 374 * @param date The date 375 * @return The backup directory URI 376 */ 377 public String getBackupDirectoryURI (String skinName, Date date) 378 { 379 return "ametys-home://skins/backup/" + skinName + "/" + _DATE_FORMAT.format(date); 380 } 381 382 /** 383 * Get the root backup directory of skin 384 * @param skinName The skin name 385 * @return The root backup directory URI 386 */ 387 public String getRootBackupDirectoryURI (String skinName) 388 { 389 return "ametys-home://skins/backup/" + skinName; 390 } 391 392 /** 393 * Get the skin directory of skin 394 * @param skinName The skin name 395 * @return The skin directory 396 */ 397 public Path getSkinDirectory (String skinName) 398 { 399 return _skinsManager.getSkin(skinName).getRawPath(); 400 } 401 402 /** 403 * Get the model of temporary version of skin 404 * @param skinName The skin name 405 * @return the model name 406 */ 407 public String getTempModel (String skinName) 408 { 409 return _getModel(getTempDirectory(skinName)); 410 } 411 412 /** 413 * Get the model of working version of skin 414 * @param skinName The skin name 415 * @return the model name 416 */ 417 public String getWorkModel (String skinName) 418 { 419 return _getModel (getWorkDirectory(skinName)); 420 } 421 422 /** 423 * Get the model of the skin 424 * @param skinName skinName The skin name 425 * @return The model name or <code>null</code> 426 */ 427 public String getSkinModel (String skinName) 428 { 429 return _getModel(getSkinDirectory(skinName)); 430 } 431 432 private String _getModel(Path skinDir) 433 { 434 Path modelFile = skinDir.resolve("model.xml"); 435 if (!Files.exists(modelFile)) 436 { 437 // No model 438 return null; 439 } 440 441 try (InputStream is = Files.newInputStream(modelFile)) 442 { 443 XPath xpath = XPathFactory.newInstance().newXPath(); 444 return xpath.evaluate("model/@id", new InputSource(is)); 445 } 446 catch (IOException e) 447 { 448 getLogger().error("Can not determine the model of the skin", e); 449 return null; 450 } 451 catch (XPathExpressionException e) 452 { 453 throw new IllegalStateException("The id of model is missing", e); 454 } 455 } 456}