001/*
002 *  Copyright 2012 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;
017
018import java.io.File;
019import java.io.IOException;
020import java.io.InputStream;
021import java.nio.charset.StandardCharsets;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034
035import org.apache.avalon.framework.component.Component;
036import org.apache.avalon.framework.configuration.Configuration;
037import org.apache.avalon.framework.configuration.ConfigurationException;
038import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
039import org.apache.avalon.framework.logger.AbstractLogEnabled;
040import org.apache.avalon.framework.service.ServiceException;
041import org.apache.avalon.framework.service.ServiceManager;
042import org.apache.avalon.framework.service.Serviceable;
043import org.apache.avalon.framework.thread.ThreadSafe;
044import org.apache.commons.io.IOUtils;
045import org.apache.commons.lang.StringUtils;
046import org.apache.excalibur.source.Source;
047import org.apache.excalibur.source.SourceResolver;
048import org.apache.excalibur.source.SourceUtil;
049
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.skinfactory.filefilter.FileFilter;
052import org.ametys.skinfactory.parameters.AbstractSkinParameter;
053import org.ametys.skinfactory.parameters.CSSColorParameter;
054import org.ametys.skinfactory.parameters.CSSParameter;
055import org.ametys.skinfactory.parameters.I18nizableTextParameter;
056import org.ametys.skinfactory.parameters.ImageParameter;
057import org.ametys.skinfactory.parameters.ImageParameter.FileValue;
058import org.ametys.skinfactory.parameters.TextParameter;
059import org.ametys.skinfactory.parameters.Variant;
060import org.ametys.skinfactory.parameters.VariantParameter;
061import org.ametys.web.skin.SkinModel;
062import org.ametys.web.skin.SkinModelsManager;
063
064/**
065 * Manager for skin parameters
066 * 
067 */
068public class SkinFactoryComponent extends AbstractLogEnabled implements Component, ThreadSafe, Serviceable
069{
070    /** Avalon role */
071    public static final String ROLE = SkinFactoryComponent.class.getName();
072
073    /** Pattern for CSS parameters. E.g: color: #EEEEEE /* AMETYS("myuniqueid", "Label", DESCRIPTION_I18N_KEY) *\/; **/ 
074    public static final Pattern CSS_PARAM_PATTERN = Pattern.compile("\\s*([^,:\\s]*)\\s*:\\s*([^:;!]*)\\s*(?:!important)?\\s*\\/\\*\\s*AMETYS\\s*\\(\\s*\"([^\"]+)\"\\s*(?:,\\s*([^,\"\\s]+|\"[^\"]*\")\\s*)?(?:,\\s*([^,\"\\s]+|\"[^\"]*\")\\s*)?\\)\\s*\\*\\/\\s*;?\\s*", Pattern.MULTILINE); 
075    
076    /** Pattern for i18n text parameters. E.g: <message key="SKIN_TITLE">Ametys, Web Java Open Source CMS<!-- Ametys("text.i18n.site.title", "Titre du site") --></message> */
077    public static final Pattern I18N_PARAM_PATTERN = Pattern.compile("^\\s*<message key=\"([^,:\\s]*)\">([^<]*)<!--\\s*AMETYS\\s*\\(\\s*\"([^\"]+)\"\\s*(?:,\\s*([^,\"\\s]+|\"[^\"]*\")\\s*)?(?:,\\s*([^,\"\\s]+|\"[^\"]*\")\\s*)?\\)\\s*--></message>\\s*$", Pattern.MULTILINE); 
078    
079    /** Pattern for text parameters. E.g: &lt;xsl:variable name="foo"&gt;test&lt;!-- Ametys("variable.foo", "Foo", "Foo") --&gt;&lt;/xsl:variable&gt; */
080    public static final Pattern TXT_PARAM_PATTERN = Pattern.compile("^[^>]*>([^<]*)<!--\\s*AMETYS\\s*\\(\\s*\"([^\"]+)\"\\s*(?:,\\s*([^,\"\\s]+|\"[^\"]*\")\\s*)?(?:,\\s*([^,\"\\s]+|\"[^\"]*\")\\s*)?\\)\\s*-->.*$", Pattern.MULTILINE); 
081    
082    private static final Pattern __I18N_CATALOG_LANGUAGE = Pattern.compile("^\\s*<catalogue xml:lang=\"([a-z]{2})\">\\s*$", Pattern.MULTILINE); 
083    
084    private final Map<String, Map<String, AbstractSkinParameter>> _modelsParameters = new HashMap<>();
085    private final Map<String, String> _modelsHash = new HashMap<>();
086    
087    private SkinModelsManager _modelsManager;
088    private SourceResolver _resolver;
089    
090    @Override
091    public void service(ServiceManager smanager) throws ServiceException
092    {
093        _modelsManager = (SkinModelsManager) smanager.lookup(SkinModelsManager.ROLE);
094        _resolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
095    }
096
097    /**
098     * Remove all model parameters from cache
099     */
100    public void clearModelsParametersCache()
101    {
102        _modelsParameters.clear();
103    }
104    
105    /**
106     * Get the skin parameter of the given model
107     * @param modelName The model name
108     * @param paramId The id of the parameter
109     * @return The skin parameter or <code>null</code> if it doesn't exist
110     */
111    public AbstractSkinParameter getModelParamater (String modelName, String paramId)
112    {
113        Map<String, AbstractSkinParameter> modelParameters = getModelParameters(modelName);
114        return modelParameters.get(paramId);
115    }
116    
117    /**
118     * Determines if the model was changed
119     * @param modelName The model name
120     * @return <code>true</code> if the model was changed
121     */
122    public boolean isModelUpToDate (String modelName)
123    {
124        String hash = _modelsManager.getModelHash(modelName);
125        if (!_modelsHash.containsKey(modelName) || !_modelsHash.get(modelName).equals(hash))
126        {
127            // The model is not up-to-date, clear cache
128            _modelsHash.put(modelName, hash);
129            return false;
130        }
131        return true;
132    }
133    
134    /**
135     * Get the skin parameters of a model
136     * @param modelName the model name
137     * @return skin parameters in a List
138     */
139    public Map<String, AbstractSkinParameter> getModelParameters (String modelName)
140    {
141        try
142        {
143            if (!isModelUpToDate(modelName))
144            {
145                // The model is not up-to-date, clear cache
146                _modelsParameters.remove(modelName);
147            }
148            
149            if (_modelsParameters.containsKey(modelName))
150            {
151                return _modelsParameters.get(modelName);
152            }
153            
154            _modelsParameters.put(modelName, new LinkedHashMap<>());
155            
156            Map<String, AbstractSkinParameter> skinParams = _modelsParameters.get(modelName);
157            
158            Path modelDir = _modelsManager.getModel(modelName).getPath();
159    
160            // Variants parameters
161            skinParams.putAll(_getVariantParameters(modelDir, modelName));
162            
163            // Images Parameters
164            skinParams.putAll(_getImageParameters(modelDir, modelName));
165            
166            // CSS parameters
167            skinParams.putAll(_getCSSParameters(modelDir, modelName));
168            
169            // I18n parameters
170            skinParams.putAll(_getI18nTextParameters(modelDir, modelName));
171            
172            // Text parameters
173            skinParams.putAll(_getTextParameters(modelDir, modelName));
174            
175            return skinParams;
176        }
177        catch (IOException e)
178        {
179            throw new RuntimeException("Cannot find parameters for model " + modelName, e);
180        }
181    }
182    
183    private Map<String, VariantParameter> _getVariantParameters (Path modelDir, String modelName) throws IOException
184    {
185        Map<String, VariantParameter> params = new HashMap<>();
186        
187        Path variantsDir = modelDir.resolve("model/variants");
188        if (Files.exists(variantsDir))
189        {
190            _findVariantsParameters (variantsDir, params, modelName);
191        }
192        return params;
193    }
194    
195    private void _findVariantsParameters(Path variantsDir, Map<String, VariantParameter> params, String modelName) throws IOException
196    {
197        try (Stream<Path> files = Files.list(variantsDir))
198        {
199            files
200                 .filter(FileFilter.getSkinDirectoryFilter())
201                 .forEach(child -> 
202                 {
203                     String filename = child.getFileName().toString();
204                     List<Variant> values = _getVariantValues(child, modelName);
205                     
206                     // avoid false directory such as CVS
207                     if (values.size() != 0)
208                     {
209                         VariantParameter variantParameter = new VariantParameter(filename, new I18nizableText(filename), new I18nizableText(""), values);
210                         _configureVariant(child, variantParameter, modelName);
211                         params.put(variantParameter.getId(), variantParameter);
212                     }
213                     else
214                     {
215                         getLogger().debug("Discarding variant " + child.toAbsolutePath().toString() + " because it has no sub directories as values");
216                     }
217                 });
218        }
219    }
220    
221    private List<Variant> _getVariantValues(Path variant, String modelName)
222    {
223        List<Variant> values = new ArrayList<>();
224        
225        try (Stream<Path> files = Files.list(variant))
226        {
227            files
228                 .filter(FileFilter.getSkinDirectoryFilter())
229                 .forEach(child ->
230                 {
231                     String id = child.getFileName().toString();
232                     
233                     // Thumbnail
234                     String thumbnailPath = null;
235                     Path thumbnailFile = child.resolve(id + ".png");
236                     if (Files.exists(thumbnailFile))
237                     {
238                         thumbnailPath = variant.getParent().relativize(thumbnailFile).toString();
239                     }
240                     
241                     I18nizableText label = new I18nizableText(id);
242                     I18nizableText description = new I18nizableText("");
243                     Path file = child.resolve(id + ".xml");
244                     if (Files.exists(file))
245                     {
246                         try (InputStream is = Files.newInputStream(file))
247                         {
248                             Configuration configuration = new DefaultConfigurationBuilder().build(is);
249                             label = _configureI18nizableText(configuration.getChild("label"), modelName);
250                             
251                             description = _configureI18nizableText(configuration.getChild("description"), modelName);
252                         }
253                         catch (Exception e)
254                         {
255                             getLogger().error("Unable to configure variant '" + id + "'", e);
256                         }
257                     }
258                     
259                     values.add(new Variant(id, label, description, thumbnailPath));
260                 });
261        }
262        catch (IOException e)
263        {
264            throw new RuntimeException("Unable to get variants from " + variant.toAbsolutePath().toString(), e);
265        }
266        
267        return values;
268    }
269    
270    private void _configureVariant(Path variantFile, VariantParameter param, String modelName)
271    {
272        Path file = variantFile.resolve(variantFile.getFileName().toString() + ".xml");
273        
274        if (Files.exists(file))
275        {
276            try (InputStream is = Files.newInputStream(file))
277            {
278                Configuration configuration = new DefaultConfigurationBuilder().build(is);
279
280                I18nizableText label = _configureI18nizableText(configuration.getChild("label"), modelName);
281                param.setLabel(label);
282                
283                I18nizableText description = _configureI18nizableText(configuration.getChild("description"), modelName);
284                param.setDescription(description);
285                
286                param.setIconGlyph(configuration.getChild("icon-glyph").getValue(null));
287            }
288            catch (Exception e)
289            {
290                getLogger().error("Unable to configure variant '" + param.getId() + "'", e);
291            }
292        }
293        
294        File iconSmall = new File(variantFile + "/thumbnail_16.png");
295        if (iconSmall.exists())
296        {
297            param.setIconSmall(iconSmall.getName());
298        }
299        
300        File iconLarge = new File(variantFile + "/thumbnail_32.png");
301        if (iconLarge.exists())
302        {
303            param.setIconLarge(iconLarge.getName());
304        }
305    }
306    
307    private Map<String, ImageParameter> _getImageParameters (Path modelDir, String modelName)
308    {
309        Map<String, ImageParameter> params = new HashMap<>();
310        
311        Path imagesDir = modelDir.resolve("model/images");
312        if (Files.exists(imagesDir))
313        {
314            _findImagesParameters(modelDir, imagesDir, imagesDir, params, modelName);
315        }
316        return params;
317    }
318    
319    private void _findImagesParameters(Path modelDir, Path imagesDir, Path file, Map<String, ImageParameter> params, String modelName)
320    {
321        try (Stream<Path> files = Files.list(file))
322        {
323            files
324                 .filter(Files::isDirectory)
325                 .forEach(child -> 
326                 {
327                     String filename = child.getFileName().toString();
328                     String lcFilename = filename.toLowerCase();
329                     if (lcFilename.endsWith(".png") || lcFilename.endsWith(".jpg") || lcFilename.endsWith(".jpeg") || lcFilename.endsWith(".gif"))
330                     {
331                         String imagePath = imagesDir.relativize(child).toString();
332                         ImageParameter imageParameter = new ImageParameter(imagePath, new I18nizableText(filename), new I18nizableText(""));
333                         _configureImage(child, imageParameter, modelName);
334                         params.put(imageParameter.getId(), imageParameter);
335                     }
336                     else
337                     {
338                         _findImagesParameters (modelDir, imagesDir, child, params, modelName);
339                     }
340                 });
341        }
342        catch (IOException e)
343        {
344            throw new RuntimeException("Cannot find images parameters in directory " + imagesDir.toString(), e);
345        }
346    }
347    
348    private void _configureImage(Path imageFile, ImageParameter param, String modelName)
349    {
350        String filename = imageFile.getFileName().toString();
351        int i = filename.lastIndexOf(".");
352        if (i > 0)
353        {
354            filename = filename.substring(0, i);
355        }
356        Path file = imageFile.resolve(filename + ".xml");
357        
358        if (Files.exists(file))
359        {
360            try (InputStream is = Files.newInputStream(file))
361            {
362                Configuration configuration = new DefaultConfigurationBuilder().build(is);
363
364                I18nizableText label = _configureI18nizableText(configuration.getChild("label"), modelName);
365                param.setLabel(label);
366                
367                I18nizableText description = _configureI18nizableText(configuration.getChild("description"), modelName);
368                param.setDescription(description);
369                
370                param.setIconGlyph(configuration.getChild("icon-glyph").getValue(null));
371            }
372            catch (Exception e)
373            {
374                getLogger().error("Unable to configure image parameter '" + param.getId() + "'", e);
375            }
376        }
377        
378        Path iconSmall = imageFile.resolve("thumbnail_16.png");
379        if (Files.exists(iconSmall))
380        {
381            param.setIconSmall(iconSmall.getFileName().toString());
382        }
383        Path iconLarge = imageFile.resolve("thumbnail_32.png");
384        if (Files.exists(iconLarge))
385        {
386            param.setIconLarge(iconLarge.getFileName().toString());
387        }
388    }
389    
390    
391    private Map<String, CSSParameter> _getCSSParameters(Path modelDir, String modelName) throws IOException
392    {
393        Map<String, CSSParameter> cssParams = new HashMap<>();
394
395        // Parse css files in the root resources directory.
396        for (Path file : _listFiles(modelDir.resolve("resources"), "css"))
397        {
398            _parseCSSFile(cssParams, modelName, file);
399        }
400        
401        // Parse css files for each templates.
402        Path templatesDir = modelDir.resolve("templates");
403        if (Files.isDirectory(templatesDir))
404        {
405            try (Stream<Path> files = Files.list(templatesDir))
406            {
407                files
408                     .forEach(template -> 
409                     {
410                         for (Path file : _listFiles(template.resolve("resources"), "css"))
411                         {
412                             _parseCSSFile(cssParams, modelName, file);
413                         }
414                     });
415            }
416        }
417        
418        // Parse xsl files for inline css 
419        for (Path file : _listFiles(modelDir, "xsl"))
420        {
421            if (Files.isRegularFile(file))
422            {
423                _parseCSSFile(cssParams, modelName, file);
424            }
425        }
426
427        return cssParams;
428    }
429
430    private List<Path> _listFiles(Path file, String extension)
431    {
432        try
433        {
434            if (Files.exists(file))
435            {
436                return Files.walk(file)
437                        .filter(Files::isRegularFile)
438                        .filter(f -> f.getFileName().toString().endsWith("."  + extension))
439                        .collect(Collectors.toList());
440            }
441            else
442            {
443                return Collections.EMPTY_LIST;
444            }
445            
446        }
447        catch (IOException e)
448        {
449            throw new RuntimeException("Cannot list " + extension + " files in " + file.toString(), e);
450        }
451    }
452
453    private void _parseCSSFile (Map<String, CSSParameter> cssParams, String modelName, Path cssFile)
454    {
455        try (InputStream is = Files.newInputStream(cssFile))
456        {
457            String string = IOUtils.toString(is, "UTF-8");
458            
459            Matcher m = CSS_PARAM_PATTERN.matcher(string);
460            while (m.find())
461            {
462                String id = m.group(3);
463                if (cssParams.containsKey(id))
464                {
465                    CSSParameter cssParameter = cssParams.get(id);
466                    cssParameter.addCSSFile(cssFile);
467                    
468                    I18nizableText label = m.group(4) != null ? _parseI18nizableText(m.group(4), modelName) : null;
469                    if (label != null)
470                    {
471                        cssParameter.setLabel(label);
472                    }
473                    
474                    I18nizableText description = m.group(5) != null ? _parseI18nizableText(m.group(5), modelName) : null;
475                    if (description != null)
476                    {
477                        cssParameter.setDescription(description);
478                    }
479                }
480                else
481                {
482                    String cssProperty = m.group(1);
483                    String defaultValue = m.group(2).trim();
484
485                    I18nizableText label = m.group(4) != null ? _parseI18nizableText(m.group(4), modelName) : null;
486                    I18nizableText description = m.group(5) != null ? _parseI18nizableText(m.group(5), modelName) : null;
487                    
488                    if (cssProperty.equals("color") || cssProperty.equals("background-color") || cssProperty.equals("border-color"))
489                    {
490                        CSSColorParameter cssParameter = new CSSColorParameter (id, label, description, cssFile, cssProperty, defaultValue, _modelsManager.getModel(modelName), this);
491                        cssParams.put(id, cssParameter);
492                    }
493                    else
494                    {
495                        CSSParameter cssParameter = new CSSParameter(id, label, description, cssFile, cssProperty, defaultValue);
496                        cssParams.put(id, cssParameter);
497                    }
498                }
499            }
500        }
501        catch (IOException e)
502        {
503            getLogger().error("Unable to parse file '" + cssFile.getFileName().toString() + "'", e);
504        }
505    }
506    
507    private Map<String, TextParameter> _getTextParameters(Path modelDir, String modelName)
508    {
509        Map<String, TextParameter> textParams = new HashMap<>();
510
511        for (Path file : _listFiles(modelDir, "xsl"))
512        {
513            if (Files.isRegularFile(file))
514            {
515                textParams.putAll(_parseXSLFile(modelName, file));
516            }
517        }
518
519        return textParams;
520    }
521    
522    private Map<String, TextParameter> _parseXSLFile (String skinName, Path xslFile)
523    {
524        Map<String, TextParameter> txtParams = new LinkedHashMap<>();
525        
526        try (InputStream is = Files.newInputStream(xslFile))
527        {
528            String string = IOUtils.toString(is, "UTF-8");
529            
530            Matcher m = TXT_PARAM_PATTERN.matcher(string);
531            while (m.find())
532            {
533                String id = m.group(2);
534                String defaultValue = m.group(1);
535                
536                I18nizableText label = m.group(3) != null ? _parseI18nizableText(m.group(3), skinName) : new I18nizableText(id);
537                I18nizableText description = m.group(4) != null ? _parseI18nizableText(m.group(4), skinName) : new I18nizableText("");
538                
539                TextParameter txtParam = new TextParameter(id, label, description, xslFile, defaultValue);
540                txtParams.put(id, txtParam);
541            }
542        }
543        catch (IOException e)
544        {
545            getLogger().error("Unable to parse file '" + xslFile.getFileName().toString() + "'", e);
546        }
547        
548        return txtParams;
549    }
550    
551    private Map<String, I18nizableTextParameter> _getI18nTextParameters(Path modelDir, String modelName)
552    {
553        Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
554
555        Path file = modelDir.resolve("i18n/messages.xml");
556        if (Files.exists(file))
557        {
558            i18nParams.putAll(_parseI18nFile(modelName, file));
559        }
560
561        return i18nParams;
562    }
563    
564
565    private Map<String, I18nizableTextParameter> _parseI18nFile (String skinName, Path i18nFile)
566    {
567        Map<String, I18nizableTextParameter> i18nParams = new LinkedHashMap<>();
568        
569        try (InputStream is = Files.newInputStream(i18nFile))
570        {
571            String string = IOUtils.toString(is, StandardCharsets.UTF_8);
572            
573            String defaultLang = _getCatalogLanguage(string, i18nFile.getFileName().toString());
574            
575            Matcher m = I18N_PARAM_PATTERN.matcher(string);
576            while (m.find())
577            {
578                String i18nKey = m.group(1);
579                String defaultValue = m.group(2).trim();
580                String id = m.group(3);
581                I18nizableText label = m.group(4) != null ? _parseI18nizableText(m.group(4), skinName) : new I18nizableText(id);
582                I18nizableText description = m.group(5) != null ? _parseI18nizableText(m.group(5), skinName) : new I18nizableText("");
583                
584                // Default values
585                Map<String, String> defaultValues = new HashMap<>();
586                defaultValues.put(defaultLang, defaultValue);
587                defaultValues.putAll(_getI18nOtherDefaultValues(i18nFile, i18nKey));
588                
589                I18nizableTextParameter i18nParam = new I18nizableTextParameter(id, label, description, i18nKey, defaultValues);
590                i18nParams.put(id, i18nParam);
591            }
592        }
593        catch (IOException e)
594        {
595            getLogger().error("Unable to parse file '" + i18nFile.getFileName().toString() + "'", e);
596        }
597        
598        return i18nParams;
599    }
600    
601    private String _getCatalogLanguage (String string, String fileName)
602    {
603        Matcher m = __I18N_CATALOG_LANGUAGE.matcher(string);
604        if (m.find())
605        {
606            return m.group(1);
607        }
608        else if (fileName.startsWith("messages_"))
609        {
610            return fileName.substring("messages_".length(), 2);
611        }
612        return "en";
613    }
614    
615    private Map<String, String> _getI18nOtherDefaultValues(Path defaultCatalog, String i18nKey) throws IOException
616    {
617        Pattern pattern = Pattern.compile("^\\s*<message key=\"" + i18nKey + "\">([^<]*)</message>\\s*$", Pattern.MULTILINE); 
618        
619        Map<String, String> defaultValues = new HashMap<>();
620        
621        try (Stream<Path> files = Files.list(defaultCatalog.getParent()))
622        {
623            files
624                .filter(f -> !f.equals(defaultCatalog))
625                .filter(Files::isRegularFile)
626                .filter(f -> f.getFileName().toString().startsWith("messages"))
627                .forEach(file ->
628                {
629                    try (InputStream is = Files.newInputStream(file))
630                    {
631                        String string = org.apache.commons.io.IOUtils.toString(is, "UTF-8");
632                        
633                        String lang = _getCatalogLanguage(string, file.getFileName().toString());
634                        
635                        Matcher m = pattern.matcher(string);
636                        if (m.find())
637                        {
638                            String value = m.group(1);
639                            defaultValues.put(lang, value);
640                        }
641                    }
642                    catch (IOException e)
643                    {
644                        getLogger().error("Unable to parse file '" + file.getFileName().toString() + "'", e);
645                    }
646                });
647        }
648        
649        return defaultValues;
650    }
651    
652    private I18nizableText _parseI18nizableText (String label, String modelName)
653    {
654        if (label.startsWith("\"") && label.endsWith("\""))
655        {
656            return new I18nizableText(label.substring(1, label.length() - 1));
657        }
658        else
659        {
660            return new I18nizableText("model." + modelName, label);
661        }
662    }
663    
664    /**
665     * Get the parameter's value
666     * @param skinDir The skin directory (could be in temp, work or skin)
667     * @param modelName The model name
668     * @param id The parameter id
669     * @return The parameter's value
670     */
671    public Object getParameterValue (Path skinDir, String modelName, String id)
672    {
673        List<String> ids = new ArrayList<>();
674        ids.add(id);
675        
676        return getParameterValues(skinDir, modelName, ids).get(id);
677    }
678    
679    /**
680     * Get the parameters' values
681     * @param skinDir The skin directory (could be in temp, work or skin)
682     * @param modelName The model name
683     * @param ids The parameters' id
684     * @return The parameters' values
685     */
686    public Map<String, Object> getParameterValues (Path skinDir, String modelName, List<String> ids)
687    {
688        Map<String, Object> values = new HashMap<>();
689        
690        Path modelFile = skinDir.resolve("model.xml");
691        
692        try (InputStream is = Files.newInputStream(modelFile))
693        {
694            Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
695            Configuration[] parametersConf = configuration.getChild("parameters").getChildren("parameter");
696            
697            for (Configuration paramConf : parametersConf)
698            {
699                String id = paramConf.getAttribute("id");
700                if (ids.contains(id))
701                {
702                    AbstractSkinParameter modelParam = getModelParamater(modelName, id);
703                    if (modelParam instanceof I18nizableTextParameter)
704                    {
705                        Configuration[] children = paramConf.getChildren();
706                        Map<String, String> langValues = new HashMap<>();
707                        for (Configuration langConfig : children)
708                        {
709                            langValues.put(langConfig.getName(), langConfig.getValue(""));
710                        }
711                        values.put(id, langValues);
712                    }
713                    else
714                    {
715                        values.put(id, paramConf.getValue(""));
716                    }
717                }
718            }
719            
720            return values;
721        }
722        catch (Exception e)
723        {
724            getLogger().error("Unable to get values for parameters '" + StringUtils.join(ids, ", ") + "'", e);
725            return new HashMap<>();
726        }
727    }
728    
729    /**
730     * Get the current theme
731     * @param skinDir The skin directory (could be in temp, work or skin)
732     * @return The current theme id
733     */
734    public String getColorTheme (Path skinDir)
735    {
736        Path modelFile = skinDir.resolve("model.xml");
737        
738        try (InputStream is = Files.newInputStream(modelFile))
739        {
740            Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
741            return configuration.getChild("color-theme").getValue(null);
742        }
743        catch (Exception e)
744        {
745            getLogger().error("Unable to get theme value", e);
746            return null;
747        }
748    }
749    
750    /**
751     * Get all parameters' values
752     * @param skinDir The skin directory (could be in temp, work or skin)
753     * @param modelName The model name
754     * @return The all parameters' values
755     */
756    public Map<String, Object> getParameterValues (Path skinDir, String modelName)
757    {
758        Map<String, Object> values = new HashMap<>();
759        
760        Path modelFile = skinDir.resolve("model.xml");
761        
762        try (InputStream is = Files.newInputStream(modelFile))
763        {
764            Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
765            Configuration[] parametersConf = configuration.getChild("parameters").getChildren("parameter");
766            
767            Map<String, AbstractSkinParameter> modelParameters = getModelParameters(modelName);
768            
769            for (Configuration paramConf : parametersConf)
770            {
771                String id = paramConf.getAttribute("id");
772                AbstractSkinParameter modelParam = modelParameters.get(id);
773                if (modelParam != null)
774                {
775                    if (modelParam instanceof I18nizableTextParameter)
776                    {
777                        Configuration[] children = paramConf.getChildren();
778                        Map<String, String> langValues = new HashMap<>();
779                        for (Configuration langConfig : children)
780                        {
781                            langValues.put(langConfig.getName(), langConfig.getValue(""));
782                        }
783                        values.put(id, langValues);
784                    }
785                    else if (modelParam instanceof ImageParameter)
786                    {
787                        boolean uploaded = Boolean.valueOf(paramConf.getAttribute("uploaded", "false"));
788                        values.put(id, new ImageParameter.FileValue(paramConf.getValue(""), uploaded));
789                    }
790                    else
791                    {
792                        values.put(id, paramConf.getValue(""));
793                    }
794                }
795            }
796            
797            return values;
798        }
799        catch (Exception e)
800        {
801            getLogger().error("Unable to get values of all parameters", e);
802            return new HashMap<>();
803        }
804    }
805    
806    /**
807     * Modify the color theme
808     * @param skinDir The skin directory (could be in temp, work or skin)
809     * @param themeId The id of the theme. Can be null to clear theme.
810     * @return <code>true</code> is modification success
811     */
812    public boolean saveColorTheme (Path skinDir, String themeId)
813    {
814        Path currentFile = skinDir.resolve("model.xml");
815        
816        Source currentSrc = null;
817        Source src = null;
818        try
819        {
820            currentSrc = _resolver.resolveURI("file://" + currentFile.toFile().getAbsolutePath());
821            
822            Map<String, Object> parentContext = new HashMap<>();
823            parentContext.put("modelUri", currentSrc.getURI());
824            if (themeId != null)
825            {
826                parentContext.put("themeId", themeId);
827            }
828            
829            src = _resolver.resolveURI("cocoon://_plugins/skinfactory/change-color-theme", null, parentContext);
830            
831            SourceUtil.copy(src, currentSrc);
832            
833            return true;
834        }
835        catch (IOException e)
836        {
837            getLogger().error("Unable to update color theme for skin '" + skinDir.getFileName().toString() + "'", e);
838            return false;
839        }
840        finally
841        {
842            _resolver.release(src);
843            _resolver.release(currentSrc);
844        }
845    }
846    
847    /**
848     * Modify a skin parameter's value
849     * @param skinDir The skin directory (could be in temp, work or skin)
850     * @param id the id of the parameter
851     * @param value the new value
852     * @return <code>true</code> is modification success
853     */
854    public boolean saveParameter(Path skinDir, String id, Object value)
855    {
856        Map<String, Object> parameters = new HashMap<>();
857        parameters.put(id, value);
858        
859        return saveParameters (skinDir, parameters);
860    }
861    
862    /**
863     * Save skin parameters
864     * @param skinDir The skin directory (could be in temp, work or skin)
865     * @param parameters The skins parameters to save
866     * @return <code>true</code> is modification success
867     */
868    public boolean saveParameters(Path skinDir, Map<String, Object> parameters)
869    {
870        Path currentFile = skinDir.resolve("model.xml");
871        
872        Source currentSrc = null;
873        Source src = null;
874        try
875        {
876            currentSrc = _resolver.resolveURI("file://" + currentFile.toFile().getAbsolutePath());
877            
878            Map<String, Object> parentContext = new HashMap<>();
879            parentContext.put("modelUri", currentSrc.getURI());
880            parentContext.put("skinParameters", parameters);
881            
882            src = _resolver.resolveURI("cocoon://_plugins/skinfactory/change-parameters", null, parentContext);
883            
884            SourceUtil.copy(src, currentSrc);
885            
886            return true;
887        }
888        catch (IOException e)
889        {
890            getLogger().error("Unable to save parameters from skin '" + skinDir.getFileName().toString() + "'", e);
891            return false;
892        }
893        finally
894        {
895            _resolver.release(src);
896            _resolver.release(currentSrc);
897        }
898    }
899    
900    /**
901     * Apply model parameters in given skin
902     * @param modelName The model name
903     * @param skinDir The skin directory (could be temp, work or skins)
904     */
905    public void applyModelParameters (String modelName, Path skinDir)
906    {
907        Map<String, Object> currentValues = getParameterValues(skinDir, modelName);
908        applyModelParameters(modelName, skinDir, currentValues);
909    }
910    
911    /**
912     * Apply model parameters in given skin
913     * @param modelName The model name
914     * @param skinDir The skin directory
915     * @param values The values to set
916     */
917    public void applyModelParameters (String modelName, Path skinDir, Map<String, Object> values)
918    {
919        Map<String, Object> parameterValues = new HashMap<>();
920        SkinModel model = _modelsManager.getModel(modelName);
921        
922        Map<String, String> modelDefaultValues = model.getDefaultValues();
923        
924        Map<String, AbstractSkinParameter> modelParameters = getModelParameters(modelName);
925        for (AbstractSkinParameter skinParameter : modelParameters.values())
926        {
927            String paramId = skinParameter.getId();
928            
929            if (skinParameter instanceof I18nizableTextParameter)
930            {
931                Map<String, String> defaultValues = ((I18nizableTextParameter) skinParameter).getDefaultValues();
932                @SuppressWarnings("unchecked")
933                Map<String, String> currentValues = (Map<String, String>) values.get(paramId);
934                if (currentValues == null)
935                {
936                    currentValues = new HashMap<>();
937                }
938                for (String lang : defaultValues.keySet())
939                {
940                    if (currentValues.get(lang) != null)
941                    {
942                        applyParameter(skinParameter, skinDir, modelName, currentValues.get(lang), lang);
943                    }
944                    else
945                    {
946                        // Default value
947                        applyParameter(skinParameter, skinDir, modelName, defaultValues.get(lang), lang);
948                        currentValues.put(lang, defaultValues.get(lang));
949                    }
950                }
951                parameterValues.put(skinParameter.getId(), currentValues);
952            }
953            else if (skinParameter instanceof ImageParameter)
954            {
955                FileValue imgValue = (FileValue) _getValue(model, skinParameter, values, modelDefaultValues);
956                applyParameter(skinParameter, skinDir, modelName, imgValue, null);
957                parameterValues.put(skinParameter.getId(), imgValue);
958            }
959            else
960            {
961                String value = (String) _getValue (model, skinParameter, values, modelDefaultValues);
962                applyParameter(skinParameter, skinDir, modelName, value, null);
963                parameterValues.put(skinParameter.getId(), value);
964            }
965        }
966        
967        saveParameters(skinDir, parameterValues);
968    }
969    
970    private Object _getValue (SkinModel model, AbstractSkinParameter param, Map<String, Object> values, Map<String, String> defaultValues)
971    {
972        String id = param.getId();
973        
974        // First search in current values
975        if (values.containsKey(id))
976        {
977            return values.get(id);
978        }
979        
980        // Then search in default values from model
981        if (defaultValues.containsKey(id))
982        {
983            String valueAsStr =  defaultValues.get(id);
984            
985            if (param instanceof ImageParameter)
986            {
987                return new FileValue (valueAsStr, false);
988            }
989            else
990            {
991                return valueAsStr;
992            }
993        }
994        
995        // If nor found, get the parameter default value
996        return param.getDefaultValue(model);
997    }
998    /**
999     * Update hash
1000     * @param xmlFile the xml {@link File}
1001     * @param hash the new hash of the file
1002     * @throws IOException if an error occurs while manipulating the file
1003     */
1004    public void updateHash (Path xmlFile, String hash) throws IOException
1005    {
1006        Source currentSrc = null;
1007        Source src = null;
1008        try
1009        {
1010            currentSrc = _resolver.resolveURI("file://" + xmlFile.toFile().getAbsolutePath());
1011            
1012            Map<String, Object> parentContext = new HashMap<>();
1013            parentContext.put("modelUri", currentSrc.getURI());
1014            parentContext.put("hash", hash);
1015            
1016            src = _resolver.resolveURI("cocoon://_plugins/skinfactory/change-hash", null, parentContext);
1017            
1018            SourceUtil.copy(src, currentSrc);
1019        }
1020        finally
1021        {
1022            _resolver.release(src);
1023            _resolver.release(currentSrc);
1024        }
1025        
1026    }
1027    
1028    /**
1029     * Apply parameter
1030     * @param parameter the skin parameter
1031     * @param skinDir the skin directory (could be temp, work or skins)
1032     * @param modelName the model name
1033     * @param value the parameter value
1034     * @param lang The language
1035     */
1036    public void applyParameter (AbstractSkinParameter parameter, Path skinDir, String modelName, Object value, String lang)
1037    {
1038        Path modelDir = _modelsManager.getModel(modelName).getPath();
1039        parameter.apply(skinDir, modelDir, value, lang);
1040    }
1041    
1042    /**
1043     * Apply new color theme 
1044     * @param skinDir the skin directory (could be temp, work or skins)
1045     * @param modelName the model name
1046     */
1047    public void applyColorTheme (String modelName, Path skinDir)
1048    {
1049        Path modelDir = _modelsManager.getModel(modelName).getPath();
1050        
1051        Map<String, Object> currentValues = getParameterValues(skinDir, modelName);
1052        
1053        Map<String, AbstractSkinParameter> modelParameters = getModelParameters(modelName);
1054        for (AbstractSkinParameter skinParameter : modelParameters.values())
1055        {
1056            if (skinParameter instanceof CSSColorParameter)
1057            {
1058                String value = (String) currentValues.get(skinParameter.getId());
1059                if (StringUtils.isEmpty(value))
1060                {
1061                    value = (String) skinParameter.getDefaultValue(_modelsManager.getModel(modelName));
1062                }
1063                
1064                skinParameter.apply(skinDir, modelDir, value, null);
1065            }
1066        }
1067    }
1068    
1069    private I18nizableText _configureI18nizableText(Configuration configuration, String modelName) throws ConfigurationException
1070    {
1071        boolean i18nSupported = configuration.getAttributeAsBoolean("i18n", false);
1072
1073        if (i18nSupported)
1074        {
1075            String catalogue = configuration.getAttribute("catalogue", null);
1076            if (catalogue == null)
1077            {
1078                catalogue = "model." + modelName;
1079            }
1080
1081            return new I18nizableText(catalogue, configuration.getValue());
1082        }
1083        else
1084        {
1085            return new I18nizableText(configuration.getValue(""));
1086        }
1087    }
1088}