001/*
002 *  Copyright 2015 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.skinfactory.skins;
017
018import java.io.File;
019import java.io.FileFilter;
020import java.io.FileInputStream;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.text.DateFormat;
026import java.text.SimpleDateFormat;
027import java.util.ArrayList;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.LinkedHashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.NoSuchElementException;
034
035import javax.xml.transform.TransformerConfigurationException;
036import javax.xml.xpath.XPath;
037import javax.xml.xpath.XPathExpressionException;
038import javax.xml.xpath.XPathFactory;
039
040import org.apache.avalon.framework.component.Component;
041import org.apache.avalon.framework.logger.AbstractLogEnabled;
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.avalon.framework.service.Serviceable;
045import org.apache.commons.io.FileUtils;
046import org.apache.commons.io.IOUtils;
047import org.apache.commons.lang.StringUtils;
048import org.xml.sax.InputSource;
049import org.xml.sax.SAXException;
050
051import org.ametys.cms.languages.Language;
052import org.ametys.cms.languages.LanguagesManager;
053import org.ametys.core.ui.Callable;
054import org.ametys.core.upload.Upload;
055import org.ametys.core.upload.UploadManager;
056import org.ametys.core.user.CurrentUserProvider;
057import org.ametys.core.user.User;
058import org.ametys.core.user.UserIdentity;
059import org.ametys.core.user.UserManager;
060import org.ametys.core.util.I18nUtils;
061import org.ametys.plugins.skincommons.SkinEditionHelper;
062import org.ametys.plugins.skincommons.SkinLockManager;
063import org.ametys.runtime.i18n.I18nizableText;
064import org.ametys.runtime.parameter.ParameterHelper;
065import org.ametys.skinfactory.SkinFactoryComponent;
066import org.ametys.skinfactory.filefilter.ModelFileFilter;
067import org.ametys.skinfactory.filefilter.SkinFileFilter;
068import org.ametys.skinfactory.model.ModelDesignsManager;
069import org.ametys.skinfactory.parameters.AbstractSkinParameter;
070import org.ametys.skinfactory.parameters.I18nizableTextParameter;
071import org.ametys.skinfactory.parameters.ImageParameter;
072import org.ametys.skinfactory.parameters.ImageParameter.FileValue;
073import org.ametys.skinfactory.parameters.SkinParameterException;
074import org.ametys.skinfactory.parameters.Variant;
075import org.ametys.skinfactory.parameters.VariantParameter;
076import org.ametys.web.repository.site.Site;
077import org.ametys.web.repository.site.SiteManager;
078import org.ametys.web.skin.Skin;
079import org.ametys.web.skin.SkinModel;
080import org.ametys.web.skin.SkinModel.CssMenuItem;
081import org.ametys.web.skin.SkinModel.CssStyleItem;
082import org.ametys.web.skin.SkinModel.Separator;
083import org.ametys.web.skin.SkinModel.Theme;
084import org.ametys.web.skin.SkinModelsManager;
085import org.ametys.web.skin.SkinsManager;
086
087/**
088 * Component to interact with a skin
089 */
090public class SkinDAO extends AbstractLogEnabled implements Serviceable, Component
091{
092    /** Constant for skin editor tool id */
093    public static final String SKIN_FACTORY_TOOL_ID = "uitool-skinfactory";
094    
095    private static final DateFormat _DATE_FORMAT = new SimpleDateFormat("yyyyMMdd-HHmm");
096    
097    private static final String __WORK_MODE = "work";
098    private static final String __PROD_MODE = "prod";
099    
100    private CurrentUserProvider _userProvider;
101    private I18nUtils _i18nUtils;
102    private LanguagesManager _languageManager;
103    private ModelDesignsManager _designsManager;
104    private SiteManager _siteManager;
105    private SkinEditionHelper _skinHelper;
106    private SkinFactoryComponent _skinFactoryManager;
107    private SkinLockManager _lockManager;
108    private SkinModelsManager _modelsManager;
109    private SkinsManager _skinsManager;
110    private UserManager _userManager;
111    private UploadManager _uploadManager;
112
113    @Override
114    public void service(ServiceManager manager) throws ServiceException
115    {
116        _designsManager = (ModelDesignsManager) manager.lookup(ModelDesignsManager.ROLE);
117        _i18nUtils = (I18nUtils) manager.lookup(org.ametys.core.util.I18nUtils.ROLE);
118        _languageManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
119        _lockManager = (SkinLockManager) manager.lookup(SkinLockManager.ROLE);
120        _modelsManager = (SkinModelsManager) manager.lookup(SkinModelsManager.ROLE);
121        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
122        _skinFactoryManager = (SkinFactoryComponent) manager.lookup(SkinFactoryComponent.ROLE);
123        _skinHelper = (SkinEditionHelper) manager.lookup(SkinEditionHelper.ROLE);
124        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
125        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
126        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
127        _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE);
128    }
129
130    /**
131     * Check the current save state of a skin
132     * @param skinId The skin id
133     * @return The state message.
134     * @throws IOException if an error occurs when manipulating files
135     */
136    @Callable
137    public String checkManualCloseCondition(String skinId) throws IOException
138    {
139        File tempDir = _skinHelper.getTempDirectory(skinId);
140        File workDir = _skinHelper.getWorkDirectory(skinId);
141        File skinDir = _skinHelper.getSkinDirectory(skinId);
142        
143        long lastModifiedLock = _lockManager.lastModified(tempDir).getTime();
144        if (lastModifiedLock <= workDir.lastModified())
145        {
146            if (workDir.lastModified() > skinDir.lastModified())
147            {
148                // The modifications were not committed in production
149                return "saved-but-not-commit";
150            }
151            else
152            {
153                FileUtils.deleteDirectory(workDir);
154            }
155        }
156        else if (lastModifiedLock >= skinDir.lastModified())
157        {
158            // The modifications were not saved
159            return "not-saved";
160        }
161        
162        return "saved";
163    }
164    
165    /**
166     * Check the conditions for opening the current skin of a site
167     * @param siteName The name of the site
168     * @return The informations on the state of the site's skin
169     * @throws IOException if an error occurs when manipulating files
170     */
171    @Callable
172    public Map<String, String> checkOpenCondition(String siteName) throws IOException
173    {
174        Map<String, String> result = new HashMap<>();
175        
176        Site site = _siteManager.getSite(siteName);
177        String skinId = site.getSkinId();
178        Skin skin = _skinsManager.getSkin(skinId);
179        
180        String modelName = _modelsManager.getModelOfSkin(skin);
181        if (modelName == null)
182        {
183            // The skin has no model
184            result.put("error", "model-not-found");
185        }
186        
187        SkinModel skinModel = _modelsManager.getModel(modelName);
188        if (skinModel == null)
189        {
190            // The model does not exist anymore
191            result.put("error", "model-not-exist");
192        }
193        
194        File tempDir = _skinHelper.getTempDirectory(skinId);
195        
196        if (_lockManager.isLocked(tempDir))
197        {
198            UserIdentity lockOwner = _lockManager.getLockOwner(tempDir);
199            if (_userProvider.getUser().equals(lockOwner))
200            {
201                result.put("unsave-modifications", lockOwner.toString());
202                result.put("unsave-modifications-date", ParameterHelper.valueToString(_lockManager.lastModified(tempDir)));
203            }
204            else
205            {
206                User user = _userManager.getUser(lockOwner.getPopulationId(), lockOwner.getLogin());
207                result.put("locked-by-user", user != null ? user.getFullName() + " (" + lockOwner + ")" : lockOwner.toString());
208                result.put("locked-date", ParameterHelper.valueToString(_lockManager.lastModified(tempDir)));
209            }
210        }
211        
212        File workDir = _skinHelper.getWorkDirectory(skinId);
213        if (workDir.exists())
214        {
215            result.put("work-version", ParameterHelper.valueToString(new Date(workDir.lastModified())));
216        }
217        
218        return result;
219    }
220    
221    /**
222     * Determines the skin directory is locked. If no, the lock owner is set in JSON map request attribute
223     * @param skinDir The skin directory
224     * @return information about the lock, or null if not locked
225     * @throws IOException if an error occurs when manipulating files
226     */
227    protected Map<String, Object> checkLock(File skinDir) throws IOException
228    {
229        if (!_lockManager.canWrite(skinDir))
230        {
231            Map<String, Object> result = new HashMap<>();
232
233            UserIdentity lockOwner = _lockManager.getLockOwner(skinDir);
234            User user = _userManager.getUser(lockOwner.getPopulationId(), lockOwner.getLogin());
235
236            result.put("isLocked", true);
237            result.put("lockOwner", user != null ? user.getFullName() + " (" + lockOwner + ")" : lockOwner);
238            
239            return result;
240        }
241        
242        return null;
243    }
244    
245    /**
246     * Affect a new design configuration
247     * @param skinName The skin name
248     * @param designId The design id
249     * @return the information on the design, or the error.
250     * @throws IOException if an error occurs when manipulating files
251     */
252    @Callable
253    public Map<String, Object> affectDesign(String skinName, String designId) throws IOException
254    {
255        Map<String, Object> result = new HashMap<>();
256        
257        File tempDir = _skinHelper.getTempDirectory(skinName);
258        String modelName = _skinHelper.getTempModel(skinName);
259        
260        Map<String, Object> lockInfos = checkLock(tempDir);
261        if (lockInfos != null)
262        {
263            return lockInfos;
264        }
265        
266        if (_modelsManager.getModel(modelName) == null)
267        {
268            result.put("unknownModel", true);
269            result.put("modelName", modelName);
270            return result;
271        }
272        
273        _designsManager.applyDesign(modelName, designId, tempDir);
274        
275        Map<String, Object> values = new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
276        result.put("parameters", values);
277        
278        String colorTheme = _skinFactoryManager.getColorTheme(tempDir);
279        if (colorTheme != null)
280        {
281            result.put("themeId", colorTheme);
282            result.put("colors", _modelsManager.getModel(modelName).getColors(colorTheme));
283        }
284        
285        result.put("designId", designId);
286        
287        return result;
288    }
289
290    /**
291     * Change the model of a skin
292     * @param modelName The model name
293     * @param skinName The skin name
294     * @param useDefaults <code>true</code> to use default model parameters 
295     * @return The skin parameters, or the error informations.
296     * @throws IOException if an error occurs when manipulating files
297     * @throws TransformerConfigurationException if something goes wrong when generating the model file
298     * @throws SAXException if an error occurs while saxing
299     */
300    @Callable
301    public Map<String, Object> changeModel(String modelName, String skinName, boolean useDefaults) throws IOException, TransformerConfigurationException, SAXException
302    {
303        File tempDir = _skinHelper.getTempDirectory(skinName);
304        
305        Map<String, Object> lockInfos = checkLock(tempDir);
306        if (lockInfos != null)
307        {
308            return lockInfos;
309        }
310        
311        String currentTheme = _skinFactoryManager.getColorTheme(tempDir);
312        
313        // Prepare skin in temporary file
314        File tmpDir = new File (tempDir.getParentFile(), skinName + "." + _DATE_FORMAT.format(new Date()));
315        
316        // Copy new model
317        File modelDir = _modelsManager.getModel(modelName).getFile();
318        FileUtils.copyDirectory(modelDir, tmpDir, new FileFilter()
319        {
320            @Override
321            public boolean accept(File pathname)
322            {
323                return !pathname.getName().equals("model");
324            }
325        }, false);
326        
327        // Apply all parameters
328        _modelsManager.generateModelFile(tmpDir, modelName, currentTheme);
329        
330        if (useDefaults)
331        {
332            String defaultColorTheme = _modelsManager.getModel(modelName).getDefaultColorTheme();
333            _skinFactoryManager.saveColorTheme(tmpDir, defaultColorTheme);
334            _skinFactoryManager.applyModelParameters(modelName, tmpDir);
335        }
336        else
337        {
338            Map<String, Object> currentValues = _skinFactoryManager.getParameterValues(tempDir, modelName);
339            _skinFactoryManager.applyModelParameters(modelName, tmpDir, currentValues);
340        }
341        
342        _skinHelper.deleteQuicklyDirectory(tempDir);
343        FileUtils.moveDirectory(tmpDir, tempDir);
344        
345        // Invalidate i18n.
346        _skinHelper.invalidateTempSkinCatalogues(skinName);
347        
348        // Update lock file
349        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
350        
351        return new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
352    }
353    
354    /**
355     * Get the languages available on a site
356     * @param siteName The site name
357     * @return The languages informations
358     */
359    @Callable
360    public Map<String, Object> getLanguages(String siteName)
361    {
362        Map<String, Object> languages = new LinkedHashMap<>();
363        
364        Site site = _siteManager.getSite(siteName);
365        Skin skin = _skinsManager.getSkin(site.getSkinId());
366        File i18nDir = new File(skin.getFile(), "i18n");     
367        
368        Map<String, Language> allLanguages = _languageManager.getAvailableLanguages();
369        
370        for (File file : i18nDir.listFiles())
371        {
372            String fileName = file.getName();
373            if (file.isFile() && fileName.startsWith("messages"))
374            {
375                String lang = null;
376                if (fileName.equals("messages.xml"))
377                {
378                    lang = _getDefaultLanguage (file);
379                }
380                else
381                {
382                    lang = fileName.substring("messages_".length(), fileName.lastIndexOf("."));
383                }
384                
385                if (allLanguages.containsKey(lang))
386                {
387                    Language language = allLanguages.get(lang);
388                    
389                    Map<String, Object> langParams = new HashMap<>();
390                    langParams.put("label", language.getLabel());
391                    langParams.put("iconSmall", language.getSmallIcon());
392                    langParams.put("iconMedium", language.getMediumIcon());
393                    langParams.put("iconLarge", language.getLargeIcon());
394                    
395                    languages.put(lang, langParams);
396                }
397            }
398        }
399        
400        return languages;
401    }
402
403    private String _getDefaultLanguage (File i18nFile)
404    {
405        try (InputStream is = new FileInputStream(i18nFile))
406        {
407            String string = org.apache.commons.io.IOUtils.toString(is, "UTF-8");
408
409            // Not very pretty but more efficient than SAXparsing the all file to get the language
410            int i = string.indexOf("xml:lang=\"");
411            if (i != -1)
412            {
413                return string.substring(i + "xml:lang=\"".length(), i + "xml:lang=\"".length() + 2);
414            }
415            return null;
416        }
417        catch (IOException e)
418        {
419            throw new SkinParameterException ("Unable to parse file '" + i18nFile.getName() + "'", e);
420        }
421    }
422    
423    /**
424     * Get the colors of a model and its theme for a site.
425     * @param siteName The site name
426     * @return The colors and theme informations.
427     */
428    @Callable
429    public Map<String, Object> getColors(String siteName)
430    {
431        Map<String, Object> params = new LinkedHashMap<>();
432        
433        Site site = _siteManager.getSite(siteName);
434        String skinId = site.getSkinId();
435        String modelName = _skinHelper.getTempModel(skinId);
436        
437        File tempDir = _skinHelper.getTempDirectory(skinId);
438        String colorTheme = _skinFactoryManager.getColorTheme(tempDir);
439        
440        SkinModel model = _modelsManager.getModel(modelName);
441        List<String> defaultColors = model.getDefaultColors();
442        params.put("colors", defaultColors);
443        
444        if (StringUtils.isNotEmpty(colorTheme))
445        {
446            Theme theme = model.getTheme(colorTheme);
447            if (theme != null)
448            {
449                params.put("themeId", theme.getId());
450                params.put("themeColors", theme.getColors());
451            }
452        }
453        
454        return params;    
455    }
456    
457    /**
458     * Get the css style items used by a site
459     * @param siteName The site name
460     * @return The css style items.
461     */
462    @Callable
463    public Map<String, Object> getCssStyleItems(String siteName)
464    {
465        Map<String, Object> styles = new LinkedHashMap<>();
466        
467        Site site = _siteManager.getSite(siteName);
468        String skinId = site.getSkinId();
469        String modelName = _skinHelper.getTempModel(skinId);
470        
471        Map<String, List<CssMenuItem>> styleItems = _modelsManager.getModel(modelName).getStyleItems();
472        
473        for (String styleId : styleItems.keySet())
474        {
475            List<Object> menuItems = new ArrayList<>();
476            
477            List<CssMenuItem> items = styleItems.get(styleId);
478            for (CssMenuItem item : items)
479            {
480                if (item instanceof CssStyleItem)
481                {
482                    CssStyleItem cssItem = (CssStyleItem) item;
483                    Map<String, String> itemParams = new HashMap<>();
484                    
485                    itemParams.put("value", cssItem.getValue());
486                    itemParams.put("label", _i18nUtils.translate(cssItem.getLabel()));
487                    
488                    String iconCls = cssItem.getIconCls();
489                    if (iconCls != null)
490                    {
491                        itemParams.put("iconCls", iconCls);
492                    }
493                    
494                    String icon = cssItem.getIcon();
495                    if (icon != null)
496                    {
497                        itemParams.put("icon", icon);
498                    }
499                    
500                    String cssClass = cssItem.getCssClass();
501                    if (cssClass != null)
502                    {
503                        itemParams.put("cssclass", cssClass);
504                    }
505                    
506                    menuItems.add(itemParams);
507                }
508                else if (item instanceof Separator)
509                {
510                    menuItems.add("separator");
511                }
512            }
513            
514            styles.put(styleId, menuItems);
515        }
516        
517        return styles;    
518    }
519    
520    /**
521     * Get the parameters of the skin of a site
522     * @param siteName The site name
523     * @param paramIds If not null, specify the ids of the parameters to retrieve
524     * @return The parameters
525     */
526    @Callable
527    public Map<String, Object> getParametersValues(String siteName, List<String> paramIds)
528    {
529        Map<String, Object> values = new LinkedHashMap<>();
530        
531        Site site = _siteManager.getSite(siteName);
532        String skinId = site.getSkinId();
533        String modelName = _skinHelper.getTempModel(skinId);
534        
535        File tempDir = _skinHelper.getTempDirectory(skinId);
536        
537        if (paramIds != null)
538        {
539            values.putAll(_skinFactoryManager.getParameterValues(tempDir, modelName, paramIds));
540        }
541        else
542        {
543            values.putAll(_skinFactoryManager.getParameterValues(tempDir, modelName));
544        }
545        
546        Map<String, Object> result = new LinkedHashMap<>();
547        result.put("skinName", skinId);
548        result.put("modelName", modelName);
549        result.put("siteName", siteName);
550        result.put("values", values);
551        
552        return result;    
553    }
554
555    /**
556     * Open the skin of a site for edition
557     * @param siteName The site name
558     * @param mode The open mode
559     * @return The skin id, or an error message.
560     * @throws IOException if an error occurs when manipulating files
561     */
562    @Callable
563    public Map<String, String> openSkin(String siteName, String mode) throws IOException
564    {
565        Map<String, String> result = new HashMap<>();
566        
567        Site site = _siteManager.getSite(siteName);
568        String skinId = site.getSkinId();
569        
570        File tempDir = _skinHelper.getTempDirectory(skinId);
571        File workDir = _skinHelper.getWorkDirectory(skinId);
572        File skinDir = _skinHelper.getSkinDirectory(skinId);
573        
574        String modelName = null;
575        if (__PROD_MODE.equals(mode))
576        {
577            modelName = _skinHelper.getSkinModel(skinId);
578        }
579        else if (__WORK_MODE.equals(mode))
580        {
581            modelName = _skinHelper.getWorkModel(skinId);
582        }
583        else
584        {
585            modelName = _skinHelper.getTempModel(skinId);
586        }
587        
588        SkinModel model = _modelsManager.getModel(modelName);
589        if (model == null)
590        {
591            result.put("model-not-found", "true");
592            return result;
593        }
594        
595        String modelHash = _modelsManager.getModelHash(modelName);
596        
597        if (__PROD_MODE.equals(mode) || __WORK_MODE.equals(mode))
598        {
599            // Delete temp directory if exists
600            if (tempDir.exists())
601            {
602                _skinHelper.deleteQuicklyDirectory(tempDir);
603            }
604            
605            if (__PROD_MODE.equals(mode))
606            {
607                // Delete work directory if exists
608                if (workDir.exists())
609                {
610                    _skinHelper.deleteQuicklyDirectory(workDir);
611                }
612                
613                // Copy from skin
614                FileUtils.copyDirectory(skinDir, workDir, new SkinFileFilter());
615            }
616                    
617            boolean isUpTodate = modelHash.equals(_getHash (workDir));
618            if (!isUpTodate)
619            {
620                // Re-apply model to work directory
621                _reapplyModel(workDir, model.getFile(), modelHash);
622            }
623            
624            // Apply parameters
625            _skinFactoryManager.applyModelParameters(modelName, workDir);
626            
627            // Copy work in temp
628            FileUtils.copyDirectory(workDir, tempDir);
629            
630            // Create .lock file
631            _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
632        }
633        else
634        {
635            boolean isUpTodate = modelHash.equals(_getHash (tempDir));
636            if (!isUpTodate)
637            {
638                // Re-apply model to temp directory
639                _reapplyModel(tempDir, model.getFile(), modelHash);
640            }
641            
642            // Apply parameters
643            _skinFactoryManager.applyModelParameters(modelName, tempDir);
644            
645            // Update .lock file
646            _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
647        }
648        
649        result.put("skinId", skinId);
650        return result;
651    }
652    
653    private void _reapplyModel (File skinDir, File modelDir, String hash) throws IOException
654    {
655        // Make a copy of model.xml file
656        File xmlFile = new File(skinDir, "model.xml");
657        _preserveFile (skinDir, xmlFile);
658        
659        // Preserve uploaded images
660        File uploadDir = new File(skinDir, "model/_uploads");
661        if (uploadDir.exists())
662        {
663            _preserveFile(skinDir, uploadDir.getParentFile());
664        }
665        
666        // Delete old directory
667        FileUtils.deleteQuietly(skinDir);
668        
669        // Copy the model
670        FileUtils.copyDirectory(modelDir, skinDir, new ModelFileFilter(modelDir), false);
671        
672        // Copy files to preserve
673        _copyFilesToPreserve (skinDir);
674        
675        // Update hash
676        _skinFactoryManager.updateHash(xmlFile, hash);
677    }
678    
679    private void _preserveFile (File skinDir, File fileToPreserve) throws IOException
680    {
681        File toPreserveDir = new File(skinDir.getParentFile(), skinDir.getName() + "_tmp");
682        if (fileToPreserve.isDirectory())
683        {
684            FileUtils.moveDirectoryToDirectory(fileToPreserve, toPreserveDir, true);
685        }
686        else
687        {
688            FileUtils.moveFileToDirectory(fileToPreserve, toPreserveDir, true);
689        }
690        
691    }
692    
693    private void _copyFilesToPreserve (File skinDir) throws IOException
694    {
695        File toPreserveDir = new File(skinDir.getParentFile(), skinDir.getName() + "_tmp");
696        if (toPreserveDir.exists())
697        {
698            for (File child : toPreserveDir.listFiles())
699            {
700                if (child.isDirectory())
701                {
702                    FileUtils.moveDirectoryToDirectory (child, skinDir, false);
703                }
704                else
705                {
706                    FileUtils.moveFileToDirectory(child, skinDir, false);
707                }
708            }
709            
710            FileUtils.deleteQuietly(toPreserveDir);
711        }
712    }
713    
714    private String _getHash(File skinDir)
715    {
716        File modelFile = new File (skinDir, "model.xml");
717        if (!modelFile.exists())
718        {
719            // No model
720            return null;
721        }
722        
723        try (InputStream is = new FileInputStream(modelFile))
724        {
725            XPath xpath = XPathFactory.newInstance().newXPath();
726            return xpath.evaluate("model/@hash", new InputSource(is));
727        }
728        catch (XPathExpressionException e)
729        {
730            throw new IllegalStateException("The id of model is missing", e);
731        }
732        catch (IOException e)
733        {
734            getLogger().error("Can not determine the hash the skin", e);
735            return null;
736        }
737    }
738    
739    /**
740     * Restore the default parameters for a skin
741     * @param skinName The skin name
742     * @return The skin informations, or an error code.
743     * @throws IOException if an error occurs when manipulating files
744     * @throws TransformerConfigurationException if something goes wrong when generating the model file
745     * @throws SAXException if an error occurs while saxing
746     */
747    @Callable
748    public Map<String, Object> restoreDefaults(String skinName) throws IOException, TransformerConfigurationException, SAXException
749    {
750        Map<String, Object> result = new HashMap<>();
751        
752        File tempDir = _skinHelper.getTempDirectory(skinName);
753        String modelName = _skinHelper.getTempModel(skinName);
754        
755        Map<String, Object> lockInfos = checkLock(tempDir);
756        if (lockInfos != null)
757        {
758            return lockInfos;
759        }
760        
761        if (_modelsManager.getModel(modelName) == null)
762        {
763            result.put("unknownModel", true);
764            result.put("modelName", modelName);
765            return result;
766        }
767        
768        // Prepare skin in temporary file
769        File tmpDir = new File (tempDir.getParentFile(), skinName + "." + _DATE_FORMAT.format(new Date()));
770        
771        // Copy new model
772        SkinModel model = _modelsManager.getModel(modelName);
773        File modelDir = model.getFile();
774        FileUtils.copyDirectory(modelDir, tmpDir, new FileFilter()
775        {
776            @Override
777            public boolean accept(File pathname)
778            {
779                return !pathname.getName().equals("model");
780            }
781        }, false);
782        
783        
784        _modelsManager.generateModelFile(tmpDir, modelName);
785        
786        String defaultColorTheme = model.getDefaultColorTheme();
787        if (defaultColorTheme != null)
788        {
789            // Save color theme
790            _skinFactoryManager.saveColorTheme(tmpDir, defaultColorTheme);
791            
792            result.put("themeId", defaultColorTheme);
793            result.put("colors", model.getColors(defaultColorTheme));
794        }
795        
796        // Apply all parameters
797        _skinFactoryManager.applyModelParameters(modelName, tmpDir);
798        
799        _skinHelper.deleteQuicklyDirectory(tempDir);
800        FileUtils.moveDirectory(tmpDir, tempDir);
801        
802        // Invalidate i18n.
803        _skinHelper.invalidateTempSkinCatalogues(skinName);
804        
805        // Update lock file
806        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
807        
808        Map<String, Object> values = new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
809        result.put("parameters", values);
810        
811        return result;
812    }
813
814    /**
815     * Set the theme used by a skin
816     * @param skinName The skin name
817     * @param themeId The theme id
818     * @return The theme informations, or an error code.
819     * @throws IOException if an error occurs when manipulating files
820     */
821    @Callable
822    public Map<String, Object> updateColorTheme(String skinName, String themeId) throws IOException
823    {
824        Map<String, Object> result = new HashMap<>();
825
826        File tempDir = _skinHelper.getTempDirectory(skinName);
827        String modelName = _skinHelper.getTempModel(skinName);
828        
829        Map<String, Object> lockInfos = checkLock(tempDir);
830        if (lockInfos != null)
831        {
832            return lockInfos;
833        }
834        
835        if (_modelsManager.getModel(modelName) == null)
836        {
837            result.put("unknownModel", true);
838            result.put("modelName", modelName);
839            return result;
840        }
841        
842        // Save color theme
843        _skinFactoryManager.saveColorTheme(tempDir, themeId);
844        
845        // Apply new color theme
846        _skinFactoryManager.applyColorTheme (modelName, tempDir);
847        
848        // Update lock
849        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
850        
851        result.put("themeId", themeId);
852        result.put("colors", _modelsManager.getModel(modelName).getColors(themeId));
853        
854        return result;
855    }
856
857    /**
858     * Update a parameter of the skin
859     * @param skinName The skin name
860     * @param lang The current language
861     * @param parameterId The parameter id to update
862     * @param value The new value for the parameter
863     * @param uploaded <code>true</code> if the file was uploaded
864     * @return The skin parameters updated, or an error code.
865     * @throws IOException if an error occurs when manipulating files
866     */
867    @Callable
868    public Map<String, Object> updateParameter(String skinName, String lang, String parameterId, String value, boolean uploaded) throws IOException
869    {
870        File tempDir = _skinHelper.getTempDirectory(skinName);
871        
872        Map<String, Object> lockInfos = checkLock(tempDir);
873        if (lockInfos != null)
874        {
875            return lockInfos;
876        }
877        
878        String modelName = _skinHelper.getTempModel(skinName);
879        if (modelName == null)
880        {
881            Map<String, Object> result = new HashMap<>();
882
883            result.put("unknownModel", true);
884            result.put("modelName", modelName);
885            return result;
886        }
887        
888        Map<String, AbstractSkinParameter> modelParameters = _skinFactoryManager.getModelParameters(modelName);
889        AbstractSkinParameter skinParameter = modelParameters.get(parameterId);
890        if (skinParameter != null)
891        {
892            // Apply parameter
893            if (skinParameter instanceof ImageParameter)
894            {
895                FileValue fileValue = new FileValue(value, uploaded);
896                _skinFactoryManager.applyParameter(skinParameter, tempDir, modelName, fileValue, lang);
897            }
898            else
899            {
900                _skinFactoryManager.applyParameter(skinParameter, tempDir, modelName, value, lang);
901            }
902            
903            // Save parameter
904            if (skinParameter instanceof I18nizableTextParameter)
905            {
906                Map<String, String> values = new HashMap<>();
907                values.put(lang, value);
908                _skinFactoryManager.saveParameter(tempDir, parameterId, values);
909                
910                _skinHelper.invalidateTempSkinCatalogue (skinName, lang);
911            }
912            else if (skinParameter instanceof ImageParameter)
913            {
914                FileValue fileValue = new FileValue(value, uploaded);
915                _skinFactoryManager.saveParameter(tempDir, parameterId, fileValue);
916            }
917            else
918            {
919                _skinFactoryManager.saveParameter(tempDir, parameterId, value);
920            }
921           
922            
923            // Update lock
924            _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
925        }
926        
927        return new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
928    }
929    
930    /**
931     * Upload a local image and set it as value for a image parameter
932     * @param uploadId The upload identifier
933     * @param fileName The name of uploaded file
934     * @param skinName The skin name
935     * @param parameterId The parameter id to update
936     * @return The skin parameters updated, or an error code.
937     * @throws IOException if an error occurs when manipulating files
938     */
939    @Callable
940    public Map<String, Object> uploadLocalImage (String uploadId, String fileName, String skinName, String parameterId) throws IOException
941    {
942        File tempDir = _skinHelper.getTempDirectory(skinName);
943        
944        Map<String, Object> lockInfos = checkLock(tempDir);
945        if (lockInfos != null)
946        {
947            return lockInfos;
948        }
949        
950        String modelName = _skinHelper.getTempModel(skinName);
951        if (modelName == null)
952        {
953            Map<String, Object> result = new HashMap<>();
954
955            result.put("unknownModel", true);
956            result.put("modelName", modelName);
957            return result;
958        }
959        
960        ImageParameter imgParam = (ImageParameter) _skinFactoryManager.getModelParamater(modelName, parameterId);
961        
962        Upload upload = null;
963        try
964        {
965            upload = _uploadManager.getUpload(_userProvider.getUser(), uploadId);
966            
967            // Copy uploaded file into model
968            File uploadDir = _getUploadDir (tempDir, imgParam);
969            File uploadFile = new File(uploadDir, fileName);
970            
971            try (OutputStream os = new FileOutputStream(uploadFile); 
972                 InputStream is = upload.getInputStream())
973            {
974                IOUtils.copy(is, os);
975            }
976            catch (IOException e)
977            {
978                // close quietly
979            }
980        }
981        catch (NoSuchElementException e)
982        {
983            // Invalid upload id
984            getLogger().error(String.format("Cannot find the temporary uploaded file for id '%s' and login '%s'.", uploadId, _userProvider.getUser()), e);
985
986            Map<String, Object> result = new HashMap<>();
987            result.put("uploadFailed", true);
988            result.put("uploadId", uploadId);
989            return result;
990        }
991        
992        FileValue fileValue = new ImageParameter.FileValue(fileName, true);
993        
994        // Apply parameter
995        _skinFactoryManager.applyParameter(imgParam, tempDir, modelName, fileValue, null);
996        
997        // Save parameter
998        _skinFactoryManager.saveParameter(tempDir, imgParam.getId(), fileValue);
999        
1000        // Update lock
1001        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
1002        
1003        return new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
1004    }
1005
1006    /**
1007     * Retrieve the list of images for the skin and parameter
1008     * @param skinName The skin name
1009     * @param paramId The parameter id
1010     * @return The map of images informations
1011     * @throws IOException if an error occurs when manipulating files
1012     */
1013    @Callable
1014    public Map<String, Object> getGalleryImages(String skinName, String paramId) throws IOException
1015    {
1016        Map<String, Object> gallery = new HashMap<>();
1017        
1018        String modelName = _skinHelper.getTempModel(skinName);
1019        SkinModel model = _modelsManager.getModel(modelName);
1020     
1021        if (model != null)
1022        {
1023            AbstractSkinParameter skinParameter = _skinFactoryManager.getModelParamater(modelName, paramId);
1024            if (skinParameter instanceof ImageParameter)
1025            {
1026                ImageParameter imageParam = (ImageParameter) skinParameter;
1027                
1028                File imageDir = new File(model.getFile(), "model/images");
1029                File libraryFile = new File(imageDir, imageParam.getLibraryPath());
1030                gallery.put("gallery", _imageFiles2JsonObject(imageDir.getAbsolutePath(), libraryFile.getAbsolutePath(), libraryFile, modelName, true));
1031                
1032                // Uploaded local images
1033                File tempDir = _skinHelper.getTempDirectory(skinName);
1034                File uploadDir = new File (tempDir, "model/_uploads/" + imageParam.getLibraryPath());
1035                if (uploadDir.exists())
1036                {
1037                    gallery.put("uploadedGroup", _uploadImages2JsonObject (uploadDir, skinName, imageParam));
1038                }
1039            }
1040        }
1041        else
1042        {
1043            getLogger().warn("Unable to get gallery images : the model '" + modelName + "' does not exist anymore");
1044        }
1045
1046        return gallery;
1047    }
1048    
1049    private Map<String, Object> _uploadImages2JsonObject(File uploadDir, String skinName, ImageParameter imageParam)
1050    {
1051        Map<String, Object> uploadedGroup = new HashMap<>();
1052        
1053        uploadedGroup.put("label", new I18nizableText("plugin.skinfactory", "PLUGINS_SKINFACTORY_IMAGESGALLERY_GROUP_UPLOADED"));
1054        
1055        List<Object> uploadedImages = new ArrayList<>();
1056        for (File child : uploadDir.listFiles())
1057        {
1058            if (_isImage(child))
1059            {
1060                Map<String, Object> jsonObject = new HashMap<>();
1061                jsonObject.put("type", "image"); 
1062                jsonObject.put("filename", child.getName()); 
1063                jsonObject.put("src", child.getName());
1064                jsonObject.put("thumbnail", "/plugins/skinfactory/" + skinName + "/_thumbnail/64/64/model/_uploads/" + (imageParam.getLibraryPath() + '/' + child.getName()).replaceAll("\\\\", "/"));
1065                jsonObject.put("thumbnailLarge", "/plugins/skinfactory/" + skinName + "/_thumbnail/100/100/model/_uploads/" + (imageParam.getLibraryPath() + '/' + child.getName()).replaceAll("\\\\", "/"));
1066                jsonObject.put("uploaded", true);
1067                uploadedImages.add(jsonObject);
1068            }
1069        }
1070        
1071        uploadedGroup.put("images", uploadedImages);
1072        
1073        return uploadedGroup;
1074    }
1075
1076    private List<Object> _imageFiles2JsonObject(String imageDirPath, String libraryDirPath, File file, String modelName, boolean deep)
1077    {
1078        List<Object> imageFilesJsonObject = new ArrayList<>();
1079        
1080        for (File child : file.listFiles())
1081        {
1082            Map<String, Object> jsonObject = new HashMap<>();
1083            
1084            if (child.isDirectory() && deep && !child.getName().equals(".svn"))
1085            {
1086                jsonObject.put("type", "group");
1087                jsonObject.put("label", child.getName());
1088                jsonObject.put("childs", _imageFiles2JsonObject(imageDirPath, libraryDirPath, child, modelName, false));
1089                
1090                imageFilesJsonObject.add(jsonObject);
1091            }
1092            else if (_isImage(child))
1093            {
1094                jsonObject.put("type", "image"); 
1095                jsonObject.put("filename", child.getName());
1096                jsonObject.put("src", child.getAbsolutePath().substring(libraryDirPath.length() + 1).replaceAll("\\\\", "/"));
1097                jsonObject.put("thumbnail", "/plugins/skinfactory/" + modelName + "/_thumbnail/64/64/model/images/" + child.getAbsolutePath().substring(imageDirPath.length() + 1).replaceAll("\\\\", "/"));
1098                jsonObject.put("thumbnailLarge", "/plugins/skinfactory/" + modelName + "/_thumbnail/100/100/model/images/" + child.getAbsolutePath().substring(imageDirPath.length() + 1).replaceAll("\\\\", "/"));
1099                
1100                imageFilesJsonObject.add(jsonObject);
1101            }
1102        }
1103        
1104        return imageFilesJsonObject;
1105    }
1106    
1107    private File _getUploadDir (File tempDir, ImageParameter imgParam)
1108    {
1109        File uploadDir = new File (tempDir, "model/_uploads/" + imgParam.getLibraryPath());
1110        if (!uploadDir.exists())
1111        {
1112            uploadDir.mkdirs();
1113        }
1114        return uploadDir;
1115    }
1116
1117    private boolean _isImage(File file)
1118    {
1119        String name = file.getName().toLowerCase();
1120        int index = name.lastIndexOf(".");
1121        String ext = name.substring(index + 1);
1122        
1123        if (name.equals("thumbnail_16.png") || name.equals("thumbnail_32.png") || name.equals("thumbnail_48.png"))
1124        {
1125            return false;
1126        }
1127
1128        return "png".equals(ext) || "gif".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext);
1129    }
1130
1131    /**
1132     * Retrieve the list of gallery variants available for the specified skin and parameter
1133     * @param skinName The skin name
1134     * @param paramId The parameter id
1135     * @return The list of gallery variants
1136     * @throws IOException if an error occurs when manipulating files
1137     */
1138    @Callable
1139    public List<Object> getGalleryVariants(String skinName, String paramId) throws IOException
1140    {
1141        List<Object> galleryVariants = new ArrayList<>();
1142        
1143        String modelName = _skinHelper.getTempModel(skinName);
1144     
1145        AbstractSkinParameter skinParameter = _skinFactoryManager.getModelParamater(modelName, paramId);
1146        if (skinParameter instanceof VariantParameter)
1147        {
1148            VariantParameter variantParam = (VariantParameter) skinParameter;
1149            
1150            List<Variant> variants = variantParam.getVariants();
1151            for (Variant variant : variants)
1152            {
1153                Map<String, Object> jsonObject = new HashMap<>();
1154                
1155                jsonObject.put("value", variant.getId());
1156                
1157                String thumbnail = variant.getThumbnail();
1158                if (thumbnail != null)
1159                {
1160                    jsonObject.put("thumbnail", "/plugins/skinfactory/" + modelName + "/_thumbnail/32/32/model/variants/" + thumbnail);
1161                }
1162                else
1163                {
1164                    jsonObject.put("thumbnail", "/plugins/skinfactory/resources/img/variant_default_32.png");
1165                }
1166                
1167                jsonObject.put("label", variant.getLabel());
1168                jsonObject.put("description", variant.getDescription());
1169                
1170                galleryVariants.add(jsonObject);
1171            }
1172        }
1173
1174        return galleryVariants;
1175    }
1176    
1177    /**
1178     * Get the skin model's parameters
1179     * @param modelName The model name
1180     * @return The parameters
1181     */
1182    @Callable
1183    public Map<String, Object> getSkinModelParameters(String modelName)
1184    {
1185        Map<String, Object> skinModelParameters = new HashMap<>();
1186        
1187        skinModelParameters.put("modelName", modelName);
1188
1189        Map<String, AbstractSkinParameter> skinParameters = _skinFactoryManager.getModelParameters(modelName);
1190        
1191        List<Object> parameters = new ArrayList<>();
1192        
1193        for (String id : skinParameters.keySet())
1194        {
1195            AbstractSkinParameter skinParameter = skinParameters.get(id);
1196            parameters.add(skinParameter.toJson(modelName));
1197        }
1198        
1199        skinModelParameters.put("parameters", parameters);
1200        
1201        return skinModelParameters;
1202    }
1203    
1204    /**
1205     * Retrieve the list of themes' colors for a site
1206     * @param siteName The site name
1207     * @return The model's themes colors
1208     */
1209    @Callable
1210    public List<Object> getThemeColors(String siteName) 
1211    {
1212        List<Object> themesJsonObject = new ArrayList<>();
1213        
1214        String skinId = _siteManager.getSite(siteName).getSkinId();
1215        String modelName = _skinHelper.getTempModel(skinId);
1216        
1217        SkinModel model = _modelsManager.getModel(modelName);
1218        if (model != null)
1219        {
1220            Map<String, Theme> themes = model.getThemes();
1221            for (String name : themes.keySet())
1222            {
1223                Map<String, Object> jsonObject = new HashMap<>();
1224                
1225                Theme theme = themes.get(name);
1226                
1227                jsonObject.put("id", theme.getId());
1228                jsonObject.put("label", theme.getLabel());
1229                jsonObject.put("colors", theme.getColors());
1230
1231                themesJsonObject.add(jsonObject);
1232            }
1233        }
1234        else
1235        {
1236            getLogger().warn("Unable to get theme colors : the model '" + modelName + "' does not exist anymore");
1237        }
1238        
1239        return themesJsonObject;
1240    }
1241}