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 253 try (Stream<Path> s = Files.list(i18nDir)) 254 { 255 s.filter(Files::isRegularFile) 256 .forEach(i18nFile -> 257 { 258 String filename = i18nFile.getFileName().toString(); 259 if (filename.equals("messages.xml")) 260 { 261 _invalidateSkinCatalogue(skinDir, skinName, catalogLocation, StringUtils.EMPTY); 262 } 263 else if (filename.startsWith("messages_")) 264 { 265 String lang = filename.substring("messages_".length(), "messages_".length() + 2); 266 _invalidateSkinCatalogue(skinDir, skinName, catalogLocation, lang); 267 } 268 }); 269 } 270 catch (IOException e) 271 { 272 throw new RuntimeException("Cannot invalidate skin catalogs for skin " + skinName + " and location " + catalogLocation, e); 273 } 274 } 275 276 /** 277 * Invalidate catalog of the skin 278 * @param skinDir The skin directory 279 * @param skinName The site name 280 * @param catalogLocation the catalog location 281 * @param lang The language of catalog. Can be empty. 282 */ 283 private void _invalidateSkinCatalogue(Path skinDir, String skinName, String catalogLocation, String lang) 284 { 285 try 286 { 287 String localName = lang; 288 if (StringUtils.isNotEmpty(lang)) 289 { 290 Path f = skinDir.resolve("i18n/messages_" + lang + ".xml"); 291 if (!Files.exists(f)) 292 { 293 localName = ""; 294 } 295 } 296 297 _i18nFactory.invalidateCatalogue(catalogLocation, "messages", localName); 298 } 299 catch (ComponentException e) 300 { 301 getLogger().warn("Unable to invalidate i18n catalog for skin " + skinName + " and location " + catalogLocation , e); 302 } 303 } 304 305 /** 306 * Get the temp directory of skin 307 * @param skinName The skin name 308 * @return The temp directory 309 */ 310 public Path getTempDirectory(String skinName) 311 { 312 return RuntimeConfig.getInstance().getAmetysHome().toPath().resolve("skins/temp/" + skinName); 313 } 314 315 /** 316 * Get the work directory of skin 317 * @param skinName The skin name 318 * @return The work directory 319 */ 320 public Path getWorkDirectory(String skinName) 321 { 322 return RuntimeConfig.getInstance().getAmetysHome().toPath().resolve("skins/work/" + skinName); 323 } 324 325 /** 326 * Get the backup directory of skin 327 * @param skinName The skin name 328 * @param date The date 329 * @return The backup directory 330 */ 331 public Path getBackupDirectory (String skinName, Date date) 332 { 333 String dateStr = _DATE_FORMAT.format(date); 334 return RuntimeConfig.getInstance().getAmetysHome().toPath().resolve("skins/backup/" + skinName + "/" + dateStr); 335 } 336 337 /** 338 * Get the root backup directory of skin 339 * @param skinName The skin name 340 * @return The root backup directory 341 */ 342 public File getRootBackupDirectory (String skinName) 343 { 344 return FileUtils.getFile(RuntimeConfig.getInstance().getAmetysHome(), "skins", "backup", skinName); 345 } 346 347 /** 348 * Get the temp directory of skin 349 * @param skinName The skin name 350 * @return The temp directory URI 351 */ 352 public String getTempDirectoryURI (String skinName) 353 { 354 return "ametys-home://skins/temp/" + skinName; 355 } 356 357 /** 358 * Get the work directory of skin 359 * @param skinName The skin name 360 * @return The work directory URI 361 */ 362 public String getWorkDirectoryURI (String skinName) 363 { 364 return "ametys-home://skins/work/" + skinName; 365 } 366 367 /** 368 * Get the backup directory of skin 369 * @param skinName The skin name 370 * @param date The date 371 * @return The backup directory URI 372 */ 373 public String getBackupDirectoryURI (String skinName, Date date) 374 { 375 return "ametys-home://skins/backup/" + skinName + "/" + _DATE_FORMAT.format(date); 376 } 377 378 /** 379 * Get the root backup directory of skin 380 * @param skinName The skin name 381 * @return The root backup directory URI 382 */ 383 public String getRootBackupDirectoryURI (String skinName) 384 { 385 return "ametys-home://skins/backup/" + skinName; 386 } 387 388 /** 389 * Get the skin directory of skin 390 * @param skinName The skin name 391 * @return The skin directory 392 */ 393 public Path getSkinDirectory (String skinName) 394 { 395 return _skinsManager.getSkin(skinName).getRawPath(); 396 } 397 398 /** 399 * Get the model of temporary version of skin 400 * @param skinName The skin name 401 * @return the model name 402 */ 403 public String getTempModel (String skinName) 404 { 405 return _getModel(getTempDirectory(skinName)); 406 } 407 408 /** 409 * Get the model of working version of skin 410 * @param skinName The skin name 411 * @return the model name 412 */ 413 public String getWorkModel (String skinName) 414 { 415 return _getModel (getWorkDirectory(skinName)); 416 } 417 418 /** 419 * Get the model of the skin 420 * @param skinName skinName The skin name 421 * @return The model name or <code>null</code> 422 */ 423 public String getSkinModel (String skinName) 424 { 425 return _getModel(getSkinDirectory(skinName)); 426 } 427 428 private String _getModel(Path skinDir) 429 { 430 Path modelFile = skinDir.resolve("model.xml"); 431 if (!Files.exists(modelFile)) 432 { 433 // No model 434 return null; 435 } 436 437 try (InputStream is = Files.newInputStream(modelFile)) 438 { 439 XPath xpath = XPathFactory.newInstance().newXPath(); 440 return xpath.evaluate("model/@id", new InputSource(is)); 441 } 442 catch (IOException e) 443 { 444 getLogger().error("Can not determine the model of the skin", e); 445 return null; 446 } 447 catch (XPathExpressionException e) 448 { 449 throw new IllegalStateException("The id of model is missing", e); 450 } 451 } 452}