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