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}