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