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.IOException;
019import java.io.InputStream;
020import java.io.OutputStream;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.text.DateFormat;
024import java.text.SimpleDateFormat;
025import java.util.ArrayList;
026import java.util.Date;
027import java.util.HashMap;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.NoSuchElementException;
032import java.util.stream.Stream;
033
034import javax.xml.transform.TransformerConfigurationException;
035import javax.xml.xpath.XPath;
036import javax.xml.xpath.XPathExpressionException;
037import javax.xml.xpath.XPathFactory;
038
039import org.apache.avalon.framework.component.Component;
040import org.apache.avalon.framework.logger.AbstractLogEnabled;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.commons.io.IOUtils;
045import org.apache.commons.lang.StringUtils;
046import org.xml.sax.InputSource;
047import org.xml.sax.SAXException;
048
049import org.ametys.cms.languages.Language;
050import org.ametys.cms.languages.LanguagesManager;
051import org.ametys.core.ui.Callable;
052import org.ametys.core.upload.Upload;
053import org.ametys.core.upload.UploadManager;
054import org.ametys.core.user.CurrentUserProvider;
055import org.ametys.core.user.User;
056import org.ametys.core.user.UserIdentity;
057import org.ametys.core.user.UserManager;
058import org.ametys.core.util.DateUtils;
059import org.ametys.core.util.I18nUtils;
060import org.ametys.core.util.LambdaUtils;
061import org.ametys.core.util.LambdaUtils.LambdaException;
062import org.ametys.core.util.path.PathUtils;
063import org.ametys.plugins.skincommons.SkinEditionHelper;
064import org.ametys.plugins.skincommons.SkinLockManager;
065import org.ametys.runtime.i18n.I18nizableText;
066import org.ametys.skinfactory.SkinFactoryComponent;
067import org.ametys.skinfactory.filefilter.FileFilter;
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        Path tempDir = _skinHelper.getTempDirectory(skinId);
140        Path workDir = _skinHelper.getWorkDirectory(skinId);
141        Path skinDir = _skinHelper.getSkinDirectory(skinId);
142        
143        long lastModifiedLock = _lockManager.lastModified(tempDir).getTime();
144        if (lastModifiedLock <= Files.getLastModifiedTime(workDir).toMillis())
145        {
146            if (Files.getLastModifiedTime(workDir).toMillis() > Files.getLastModifiedTime(skinDir).toMillis())
147            {
148                // The modifications were not committed in production
149                return "saved-but-not-commit";
150            }
151            else
152            {
153                PathUtils.deleteDirectory(workDir);
154            }
155        }
156        else if (lastModifiedLock >= Files.getLastModifiedTime(skinDir).toMillis())
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        Path 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", DateUtils.dateToString(_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", DateUtils.dateToString(_lockManager.lastModified(tempDir)));
209            }
210        }
211        
212        Path workDir = _skinHelper.getWorkDirectory(skinId);
213        if (Files.exists(workDir))
214        {
215            result.put("work-version", DateUtils.epochMilliToString(Files.getLastModifiedTime(workDir).toMillis()));
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(Path 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        Path 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        Path 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        Path tmpDir = tempDir.getParent().resolve(skinName + "." + _DATE_FORMAT.format(new Date()));
315        
316        // Copy new model
317        Path modelDir = _modelsManager.getModel(modelName).getPath();
318        PathUtils.copyDirectory(modelDir, tmpDir, f -> !f.getFileName().equals("model"), false);
319        
320        // Apply all parameters
321        _modelsManager.generateModelFile(tmpDir, modelName, currentTheme);
322        
323        if (useDefaults)
324        {
325            String defaultColorTheme = _modelsManager.getModel(modelName).getDefaultColorTheme();
326            _skinFactoryManager.saveColorTheme(tmpDir, defaultColorTheme);
327            _skinFactoryManager.applyModelParameters(modelName, tmpDir);
328        }
329        else
330        {
331            Map<String, Object> currentValues = _skinFactoryManager.getParameterValues(tempDir, modelName);
332            _skinFactoryManager.applyModelParameters(modelName, tmpDir, currentValues);
333        }
334        
335        _skinHelper.deleteQuicklyDirectory(tempDir);
336        PathUtils.moveDirectory(tmpDir, tempDir);
337        
338        // Invalidate i18n.
339        _skinHelper.invalidateTempSkinCatalogues(skinName);
340        
341        // Update lock file
342        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
343        
344        return new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
345    }
346    
347    /**
348     * Get the languages available on a site
349     * @param siteName The site name
350     * @return The languages informations
351     */
352    @Callable
353    public Map<String, Object> getLanguages(String siteName)
354    {
355        Map<String, Object> languages = new LinkedHashMap<>();
356        
357        Site site = _siteManager.getSite(siteName);
358        Skin skin = _skinsManager.getSkin(site.getSkinId());
359        Path i18nDir = skin.getRawPath().resolve("i18n");     
360        
361        Map<String, Language> allLanguages = _languageManager.getAvailableLanguages();
362        
363        try (Stream<Path> files = Files.list(i18nDir))
364        {
365            files.forEach(file ->
366            {
367                String fileName = file.getFileName().toString();
368                if (!Files.isDirectory(file) && fileName.startsWith("messages"))
369                {
370                    String lang = null;
371                    if (fileName.equals("messages.xml"))
372                    {
373                        lang = _getDefaultLanguage(file);
374                    }
375                    else
376                    {
377                        lang = fileName.substring("messages_".length(), fileName.lastIndexOf("."));
378                    }
379                    
380                    if (allLanguages.containsKey(lang))
381                    {
382                        Language language = allLanguages.get(lang);
383                        
384                        Map<String, Object> langParams = new HashMap<>();
385                        langParams.put("label", language.getLabel());
386                        langParams.put("iconSmall", language.getSmallIcon());
387                        langParams.put("iconMedium", language.getMediumIcon());
388                        langParams.put("iconLarge", language.getLargeIcon());
389                        
390                        languages.put(lang, langParams);
391                    }
392                }
393            });
394        }
395        catch (IOException e)
396        {
397            getLogger().error("Cannot get languages for site " + siteName, e);
398        }
399        
400        return languages;
401    }
402
403    private String _getDefaultLanguage (Path i18nFile)
404    {
405        try (InputStream is = Files.newInputStream(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.getFileName().toString() + "'", 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        Path 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        Path 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        Skin skin = _skinsManager.getSkin(skinId);
571        if (!skin.isModifiable())
572        {
573            throw new IllegalStateException("The skin '" + skinId + "' is not modifiable and thus cannot be opened in skin editor.");
574        }
575        
576        Path tempDir = _skinHelper.getTempDirectory(skinId);
577        Path workDir = _skinHelper.getWorkDirectory(skinId);
578        Path skinDir = _skinHelper.getSkinDirectory(skinId);
579        
580        String modelName = null;
581        if (__PROD_MODE.equals(mode))
582        {
583            modelName = _skinHelper.getSkinModel(skinId);
584        }
585        else if (__WORK_MODE.equals(mode))
586        {
587            modelName = _skinHelper.getWorkModel(skinId);
588        }
589        else
590        {
591            modelName = _skinHelper.getTempModel(skinId);
592        }
593        
594        SkinModel model = _modelsManager.getModel(modelName);
595        if (model == null)
596        {
597            result.put("model-not-found", "true");
598            return result;
599        }
600        
601        String modelHash = _modelsManager.getModelHash(modelName);
602        
603        if (__PROD_MODE.equals(mode) || __WORK_MODE.equals(mode))
604        {
605            // Delete temp directory if exists
606            if (Files.exists(tempDir))
607            {
608                _skinHelper.deleteQuicklyDirectory(tempDir);
609            }
610            
611            if (__PROD_MODE.equals(mode))
612            {
613                // Delete work directory if exists
614                if (Files.exists(workDir))
615                {
616                    _skinHelper.deleteQuicklyDirectory(workDir);
617                }
618                
619                // Copy from skin
620                PathUtils.copyDirectory(skinDir, workDir, FileFilter.getSkinFileFilter());
621            }
622                    
623            boolean isUpTodate = modelHash.equals(_getHash (workDir));
624            if (!isUpTodate)
625            {
626                // Re-apply model to work directory
627                _reapplyModel(workDir, model.getPath(), modelHash);
628            }
629            
630            // Apply parameters
631            _skinFactoryManager.applyModelParameters(modelName, workDir);
632            
633            // Copy work in temp
634            PathUtils.copyDirectory(workDir, tempDir);
635            
636            // Create .lock file
637            _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
638        }
639        else
640        {
641            boolean isUpTodate = modelHash.equals(_getHash (tempDir));
642            if (!isUpTodate)
643            {
644                // Re-apply model to temp directory
645                _reapplyModel(tempDir, model.getPath(), modelHash);
646            }
647            
648            // Apply parameters
649            _skinFactoryManager.applyModelParameters(modelName, tempDir);
650            
651            // Update .lock file
652            _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
653        }
654        
655        result.put("skinId", skinId);
656        return result;
657    }
658    
659    private void _reapplyModel (Path skinDir, Path modelDir, String hash) throws IOException
660    {
661        // Make a copy of model.xml file
662        Path xmlFile = skinDir.resolve("model.xml");
663        _preserveFile (skinDir, xmlFile);
664        
665        // Preserve uploaded images
666        Path uploadDir = skinDir.resolve("model/_uploads");
667        if (Files.exists(uploadDir))
668        {
669            _preserveFile(skinDir, uploadDir.getParent());
670        }
671        
672        // Delete old directory
673        PathUtils.deleteQuietly(skinDir);
674        
675        // Copy the model
676        PathUtils.copyDirectory(modelDir, skinDir, FileFilter.getModelFilter(modelDir), false);
677        
678        // Copy files to preserve
679        _copyFilesToPreserve (skinDir);
680        
681        // Update hash
682        _skinFactoryManager.updateHash(xmlFile, hash);
683    }
684    
685    private void _preserveFile(Path skinDir, Path fileToPreserve) throws IOException
686    {
687        Path toPreserveDir = skinDir.getParent().resolve(skinDir.getFileName().toString() + "_tmp");
688        if (Files.isDirectory(fileToPreserve))
689        {
690            PathUtils.moveDirectoryToDirectory(fileToPreserve, toPreserveDir, true);
691        }
692        else
693        {
694            PathUtils.moveFileToDirectory(fileToPreserve, toPreserveDir, true);
695        }
696        
697    }
698    
699    private void _copyFilesToPreserve(Path skinDir) throws IOException
700    {
701        Path toPreserveDir = skinDir.getParent().resolve(skinDir.getFileName().toString() + "_tmp");
702        if (Files.exists(toPreserveDir))
703        {
704            try (Stream<Path> children = Files.list(toPreserveDir))
705            {
706                children.forEach(LambdaUtils.wrapConsumer(child -> 
707                {
708                    if (Files.isDirectory(child))
709                    {
710                        PathUtils.moveDirectoryToDirectory(child, skinDir, false);
711                    }
712                    else
713                    {
714                        PathUtils.moveFileToDirectory(child, skinDir, false);
715                    }
716                }));
717            }
718            catch (LambdaException e)
719            {
720                throw (IOException) e.getCause();
721            }
722            
723            PathUtils.deleteQuietly(toPreserveDir);
724        }
725    }
726    
727    private String _getHash(Path skinDir)
728    {
729        Path modelFile = skinDir.resolve("model.xml");
730        if (!Files.exists(modelFile))
731        {
732            // No model
733            return null;
734        }
735        
736        try (InputStream is = Files.newInputStream(modelFile))
737        {
738            XPath xpath = XPathFactory.newInstance().newXPath();
739            return xpath.evaluate("model/@hash", new InputSource(is));
740        }
741        catch (XPathExpressionException e)
742        {
743            throw new IllegalStateException("The id of model is missing", e);
744        }
745        catch (IOException e)
746        {
747            getLogger().error("Can not determine the hash the skin", e);
748            return null;
749        }
750    }
751    
752    /**
753     * Restore the default parameters for a skin
754     * @param skinName The skin name
755     * @return The skin informations, or an error code.
756     * @throws IOException if an error occurs when manipulating files
757     * @throws TransformerConfigurationException if something goes wrong when generating the model file
758     * @throws SAXException if an error occurs while saxing
759     */
760    @Callable
761    public Map<String, Object> restoreDefaults(String skinName) throws IOException, TransformerConfigurationException, SAXException
762    {
763        Map<String, Object> result = new HashMap<>();
764        
765        Path tempDir = _skinHelper.getTempDirectory(skinName);
766        String modelName = _skinHelper.getTempModel(skinName);
767        
768        Map<String, Object> lockInfos = checkLock(tempDir);
769        if (lockInfos != null)
770        {
771            return lockInfos;
772        }
773        
774        if (_modelsManager.getModel(modelName) == null)
775        {
776            result.put("unknownModel", true);
777            result.put("modelName", modelName);
778            return result;
779        }
780        
781        // Prepare skin in temporary file
782        Path tmpDir = tempDir.getParent().resolve(skinName + "." + _DATE_FORMAT.format(new Date()));
783        
784        // Copy new model
785        SkinModel model = _modelsManager.getModel(modelName);
786        Path modelDir = model.getPath();
787        PathUtils.copyDirectory(modelDir, tmpDir, f -> !f.getFileName().toString().equals("model"), false);
788        
789        
790        _modelsManager.generateModelFile(tmpDir, modelName);
791        
792        String defaultColorTheme = model.getDefaultColorTheme();
793        if (defaultColorTheme != null)
794        {
795            // Save color theme
796            _skinFactoryManager.saveColorTheme(tmpDir, defaultColorTheme);
797            
798            result.put("themeId", defaultColorTheme);
799            result.put("colors", model.getColors(defaultColorTheme));
800        }
801        
802        // Apply all parameters
803        _skinFactoryManager.applyModelParameters(modelName, tmpDir);
804        
805        _skinHelper.deleteQuicklyDirectory(tempDir);
806        PathUtils.moveDirectory(tmpDir, tempDir);
807        
808        // Invalidate i18n.
809        _skinHelper.invalidateTempSkinCatalogues(skinName);
810        
811        // Update lock file
812        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
813        
814        Map<String, Object> values = new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
815        result.put("parameters", values);
816        
817        return result;
818    }
819
820    /**
821     * Set the theme used by a skin
822     * @param skinName The skin name
823     * @param themeId The theme id
824     * @return The theme informations, or an error code.
825     * @throws IOException if an error occurs when manipulating files
826     */
827    @Callable
828    public Map<String, Object> updateColorTheme(String skinName, String themeId) throws IOException
829    {
830        Map<String, Object> result = new HashMap<>();
831
832        Path tempDir = _skinHelper.getTempDirectory(skinName);
833        String modelName = _skinHelper.getTempModel(skinName);
834        
835        Map<String, Object> lockInfos = checkLock(tempDir);
836        if (lockInfos != null)
837        {
838            return lockInfos;
839        }
840        
841        if (_modelsManager.getModel(modelName) == null)
842        {
843            result.put("unknownModel", true);
844            result.put("modelName", modelName);
845            return result;
846        }
847        
848        // Save color theme
849        _skinFactoryManager.saveColorTheme(tempDir, themeId);
850        
851        // Apply new color theme
852        _skinFactoryManager.applyColorTheme (modelName, tempDir);
853        
854        // Update lock
855        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
856        
857        result.put("themeId", themeId);
858        result.put("colors", _modelsManager.getModel(modelName).getColors(themeId));
859        
860        return result;
861    }
862
863    /**
864     * Update a parameter of the skin
865     * @param skinName The skin name
866     * @param lang The current language
867     * @param parameterId The parameter id to update
868     * @param value The new value for the parameter
869     * @param uploaded <code>true</code> if the file was uploaded
870     * @return The skin parameters updated, or an error code.
871     * @throws IOException if an error occurs when manipulating files
872     */
873    @Callable
874    public Map<String, Object> updateParameter(String skinName, String lang, String parameterId, String value, boolean uploaded) throws IOException
875    {
876        Path tempDir = _skinHelper.getTempDirectory(skinName);
877        
878        Map<String, Object> lockInfos = checkLock(tempDir);
879        if (lockInfos != null)
880        {
881            return lockInfos;
882        }
883        
884        String modelName = _skinHelper.getTempModel(skinName);
885        if (modelName == null)
886        {
887            Map<String, Object> result = new HashMap<>();
888
889            result.put("unknownModel", true);
890            result.put("modelName", modelName);
891            return result;
892        }
893        
894        Map<String, AbstractSkinParameter> modelParameters = _skinFactoryManager.getModelParameters(modelName);
895        AbstractSkinParameter skinParameter = modelParameters.get(parameterId);
896        if (skinParameter != null)
897        {
898            // Apply parameter
899            if (skinParameter instanceof ImageParameter)
900            {
901                FileValue fileValue = new FileValue(value, uploaded);
902                _skinFactoryManager.applyParameter(skinParameter, tempDir, modelName, fileValue, lang);
903            }
904            else
905            {
906                _skinFactoryManager.applyParameter(skinParameter, tempDir, modelName, value, lang);
907            }
908            
909            // Save parameter
910            if (skinParameter instanceof I18nizableTextParameter)
911            {
912                Map<String, String> values = new HashMap<>();
913                values.put(lang, value);
914                _skinFactoryManager.saveParameter(tempDir, parameterId, values);
915                
916                _skinHelper.invalidateTempSkinCatalogue (skinName, lang);
917            }
918            else if (skinParameter instanceof ImageParameter)
919            {
920                FileValue fileValue = new FileValue(value, uploaded);
921                _skinFactoryManager.saveParameter(tempDir, parameterId, fileValue);
922            }
923            else
924            {
925                _skinFactoryManager.saveParameter(tempDir, parameterId, value);
926            }
927           
928            
929            // Update lock
930            _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
931        }
932        
933        return new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
934    }
935    
936    /**
937     * Upload a local image and set it as value for a image parameter
938     * @param uploadId The upload identifier
939     * @param fileName The name of uploaded file
940     * @param skinName The skin name
941     * @param parameterId The parameter id to update
942     * @return The skin parameters updated, or an error code.
943     * @throws IOException if an error occurs when manipulating files
944     */
945    @Callable
946    public Map<String, Object> uploadLocalImage (String uploadId, String fileName, String skinName, String parameterId) throws IOException
947    {
948        Path tempDir = _skinHelper.getTempDirectory(skinName);
949        
950        Map<String, Object> lockInfos = checkLock(tempDir);
951        if (lockInfos != null)
952        {
953            return lockInfos;
954        }
955        
956        String modelName = _skinHelper.getTempModel(skinName);
957        if (modelName == null)
958        {
959            Map<String, Object> result = new HashMap<>();
960
961            result.put("unknownModel", true);
962            result.put("modelName", modelName);
963            return result;
964        }
965        
966        ImageParameter imgParam = (ImageParameter) _skinFactoryManager.getModelParamater(modelName, parameterId);
967        
968        Upload upload = null;
969        try
970        {
971            upload = _uploadManager.getUpload(_userProvider.getUser(), uploadId);
972            
973            // Copy uploaded file into model
974            Path uploadDir = _getUploadDir (tempDir, imgParam);
975            Path uploadFile = uploadDir.resolve(fileName);
976            
977            try (OutputStream os = Files.newOutputStream(uploadFile); 
978                 InputStream is = upload.getInputStream())
979            {
980                IOUtils.copy(is, os);
981            }
982            catch (IOException e)
983            {
984                // close quietly
985            }
986        }
987        catch (NoSuchElementException e)
988        {
989            // Invalid upload id
990            getLogger().error(String.format("Cannot find the temporary uploaded file for id '%s' and login '%s'.", uploadId, _userProvider.getUser()), e);
991
992            Map<String, Object> result = new HashMap<>();
993            result.put("uploadFailed", true);
994            result.put("uploadId", uploadId);
995            return result;
996        }
997        
998        FileValue fileValue = new ImageParameter.FileValue(fileName, true);
999        
1000        // Apply parameter
1001        _skinFactoryManager.applyParameter(imgParam, tempDir, modelName, fileValue, null);
1002        
1003        // Save parameter
1004        _skinFactoryManager.saveParameter(tempDir, imgParam.getId(), fileValue);
1005        
1006        // Update lock
1007        _lockManager.updateLockFile(tempDir, SKIN_FACTORY_TOOL_ID);
1008        
1009        return new HashMap<>(_skinFactoryManager.getParameterValues(tempDir, modelName));
1010    }
1011
1012    /**
1013     * Retrieve the list of images for the skin and parameter
1014     * @param skinName The skin name
1015     * @param paramId The parameter id
1016     * @return The map of images informations
1017     * @throws IOException if an error occurs when manipulating files
1018     */
1019    @Callable
1020    public Map<String, Object> getGalleryImages(String skinName, String paramId) throws IOException
1021    {
1022        Map<String, Object> gallery = new HashMap<>();
1023        
1024        String modelName = _skinHelper.getTempModel(skinName);
1025        SkinModel model = _modelsManager.getModel(modelName);
1026     
1027        if (model != null)
1028        {
1029            AbstractSkinParameter skinParameter = _skinFactoryManager.getModelParamater(modelName, paramId);
1030            if (skinParameter instanceof ImageParameter)
1031            {
1032                ImageParameter imageParam = (ImageParameter) skinParameter;
1033                
1034                Path imageDir = model.getPath().resolve("model/images");
1035                Path libraryFile = imageDir.resolve(imageParam.getLibraryPath());
1036                gallery.put("gallery", _imageFiles2JsonObject(imageDir.toAbsolutePath().toString(), libraryFile.toAbsolutePath().toString(), libraryFile, modelName, true));
1037                
1038                // Uploaded local images
1039                Path tempDir = _skinHelper.getTempDirectory(skinName);
1040                Path uploadDir = tempDir.resolve("model/_uploads/" + imageParam.getLibraryPath());
1041                if (Files.exists(uploadDir))
1042                {
1043                    gallery.put("uploadedGroup", _uploadImages2JsonObject(uploadDir, skinName, imageParam));
1044                }
1045            }
1046        }
1047        else
1048        {
1049            getLogger().warn("Unable to get gallery images : the model '" + modelName + "' does not exist anymore");
1050        }
1051
1052        return gallery;
1053    }
1054    
1055    private Map<String, Object> _uploadImages2JsonObject(Path uploadDir, String skinName, ImageParameter imageParam) throws IOException
1056    {
1057        Map<String, Object> uploadedGroup = new HashMap<>();
1058        
1059        uploadedGroup.put("label", new I18nizableText("plugin.skinfactory", "PLUGINS_SKINFACTORY_IMAGESGALLERY_GROUP_UPLOADED"));
1060        
1061        List<Object> uploadedImages = new ArrayList<>();
1062        
1063        try (Stream<Path> files = Files.list(uploadDir))
1064        {
1065            files
1066                .filter(f -> _isImage(f))
1067                .forEach(child -> 
1068                {
1069                    Map<String, Object> jsonObject = new HashMap<>();
1070                    jsonObject.put("type", "image"); 
1071                    jsonObject.put("filename", child.getFileName().toString()); 
1072                    jsonObject.put("src", child.getFileName().toString());
1073                    jsonObject.put("thumbnail", "/plugins/skinfactory/" + skinName + "/_thumbnail/64/64/model/_uploads/" + (imageParam.getLibraryPath() + '/' + child.getFileName().toString()).replaceAll("\\\\", "/"));
1074                    jsonObject.put("thumbnailLarge", "/plugins/skinfactory/" + skinName + "/_thumbnail/100/100/model/_uploads/" + (imageParam.getLibraryPath() + '/' + child.getFileName().toString()).replaceAll("\\\\", "/"));
1075                    jsonObject.put("uploaded", true);
1076                    uploadedImages.add(jsonObject);
1077                });
1078        }
1079        uploadedGroup.put("images", uploadedImages);
1080        
1081        return uploadedGroup;
1082    }
1083
1084    private List<Object> _imageFiles2JsonObject(String imageDirPath, String libraryDirPath, Path file, String modelName, boolean deep) throws IOException
1085    {
1086        List<Object> imageFilesJsonObject = new ArrayList<>();
1087        
1088        try (Stream<Path> children = Files.list(file))
1089        {
1090            children
1091                .forEach(LambdaUtils.wrapConsumer(child ->
1092                {
1093                    Map<String, Object> jsonObject = new HashMap<>();
1094
1095                    if (Files.isDirectory(child) && deep && !child.getFileName().equals(".svn"))
1096                    {
1097                        jsonObject.put("type", "group");
1098                        jsonObject.put("label", child.getFileName().toString());
1099                        jsonObject.put("childs", _imageFiles2JsonObject(imageDirPath, libraryDirPath, child, modelName, false));
1100                         
1101                        imageFilesJsonObject.add(jsonObject);
1102                    }
1103                    else if (_isImage(child))
1104                    {
1105                        jsonObject.put("type", "image"); 
1106                        jsonObject.put("filename", child.getFileName().toString());
1107                        jsonObject.put("src", child.toAbsolutePath().toString().substring(libraryDirPath.length() + 1).replaceAll("\\\\", "/"));
1108                        jsonObject.put("thumbnail", "/plugins/skinfactory/" + modelName + "/_thumbnail/64/64/model/images/" + child.toAbsolutePath().toString().substring(imageDirPath.length() + 1).replaceAll("\\\\", "/"));
1109                        jsonObject.put("thumbnailLarge", "/plugins/skinfactory/" + modelName + "/_thumbnail/100/100/model/images/" + child.toAbsolutePath().toString().substring(imageDirPath.length() + 1).replaceAll("\\\\", "/"));
1110                         
1111                        imageFilesJsonObject.add(jsonObject);
1112                    }
1113                }));
1114        }
1115        catch (LambdaException e) 
1116        {
1117            throw (IOException) e.getCause();
1118        }
1119        
1120        return imageFilesJsonObject;
1121    }
1122    
1123    private Path _getUploadDir (Path tempDir, ImageParameter imgParam) throws IOException
1124    {
1125        Path uploadDir = tempDir.resolve("model/_uploads/" + imgParam.getLibraryPath());
1126        if (!Files.exists(uploadDir))
1127        {
1128            Files.createDirectories(uploadDir);
1129        }
1130        return uploadDir;
1131    }
1132
1133    private boolean _isImage(Path file)
1134    {
1135        String name = file.getFileName().toString().toLowerCase();
1136        int index = name.lastIndexOf(".");
1137        String ext = name.substring(index + 1);
1138        
1139        if (name.equals("thumbnail_16.png") || name.equals("thumbnail_32.png") || name.equals("thumbnail_48.png"))
1140        {
1141            return false;
1142        }
1143
1144        return "png".equals(ext) || "gif".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext);
1145    }
1146
1147    /**
1148     * Retrieve the list of gallery variants available for the specified skin and parameter
1149     * @param skinName The skin name
1150     * @param paramId The parameter id
1151     * @return The list of gallery variants
1152     * @throws IOException if an error occurs when manipulating files
1153     */
1154    @Callable
1155    public List<Object> getGalleryVariants(String skinName, String paramId) throws IOException
1156    {
1157        List<Object> galleryVariants = new ArrayList<>();
1158        
1159        String modelName = _skinHelper.getTempModel(skinName);
1160     
1161        AbstractSkinParameter skinParameter = _skinFactoryManager.getModelParamater(modelName, paramId);
1162        if (skinParameter instanceof VariantParameter)
1163        {
1164            VariantParameter variantParam = (VariantParameter) skinParameter;
1165            
1166            List<Variant> variants = variantParam.getVariants();
1167            for (Variant variant : variants)
1168            {
1169                Map<String, Object> jsonObject = new HashMap<>();
1170                
1171                jsonObject.put("value", variant.getId());
1172                
1173                String thumbnail = variant.getThumbnail();
1174                if (thumbnail != null)
1175                {
1176                    jsonObject.put("thumbnail", "/plugins/skinfactory/" + modelName + "/_thumbnail/32/32/model/variants/" + thumbnail);
1177                }
1178                else
1179                {
1180                    jsonObject.put("thumbnail", "/plugins/skinfactory/resources/img/variant_default_32.png");
1181                }
1182                
1183                jsonObject.put("label", variant.getLabel());
1184                jsonObject.put("description", variant.getDescription());
1185                
1186                galleryVariants.add(jsonObject);
1187            }
1188        }
1189
1190        return galleryVariants;
1191    }
1192    
1193    /**
1194     * Get the skin model's parameters
1195     * @param modelName The model name
1196     * @return The parameters
1197     */
1198    @Callable
1199    public Map<String, Object> getSkinModelParameters(String modelName)
1200    {
1201        Map<String, Object> skinModelParameters = new HashMap<>();
1202        
1203        skinModelParameters.put("modelName", modelName);
1204
1205        Map<String, AbstractSkinParameter> skinParameters = _skinFactoryManager.getModelParameters(modelName);
1206        
1207        List<Object> parameters = new ArrayList<>();
1208        
1209        for (String id : skinParameters.keySet())
1210        {
1211            AbstractSkinParameter skinParameter = skinParameters.get(id);
1212            parameters.add(skinParameter.toJson(modelName));
1213        }
1214        
1215        skinModelParameters.put("parameters", parameters);
1216        
1217        return skinModelParameters;
1218    }
1219    
1220    /**
1221     * Retrieve the list of themes' colors for a site
1222     * @param siteName The site name
1223     * @return The model's themes colors
1224     */
1225    @Callable
1226    public List<Object> getThemeColors(String siteName) 
1227    {
1228        List<Object> themesJsonObject = new ArrayList<>();
1229        
1230        String skinId = _siteManager.getSite(siteName).getSkinId();
1231        String modelName = _skinHelper.getTempModel(skinId);
1232        
1233        SkinModel model = _modelsManager.getModel(modelName);
1234        if (model != null)
1235        {
1236            Map<String, Theme> themes = model.getThemes();
1237            for (String name : themes.keySet())
1238            {
1239                Map<String, Object> jsonObject = new HashMap<>();
1240                
1241                Theme theme = themes.get(name);
1242                
1243                jsonObject.put("id", theme.getId());
1244                jsonObject.put("label", theme.getLabel());
1245                jsonObject.put("colors", theme.getColors());
1246
1247                themesJsonObject.add(jsonObject);
1248            }
1249        }
1250        else
1251        {
1252            getLogger().warn("Unable to get theme colors : the model '" + modelName + "' does not exist anymore");
1253        }
1254        
1255        return themesJsonObject;
1256    }
1257}