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