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