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}