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