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