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}