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<String, AbstractSkinParameter>());
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                    }
712                    else
713                    {
714                        values.put(id, paramConf.getValue(""));
715                    }
716                }
717            }
718            
719            return values;
720        }
721        catch (Exception e)
722        {
723            getLogger().error("Unable to get values for parameters '" + StringUtils.join(ids, ", ") + "'", e);
724            return new HashMap<>();
725        }
726    }
727    
728    /**
729     * Get the current theme
730     * @param skinDir The skin directory (could be in temp, work or skin)
731     * @return The current theme id
732     */
733    public String getColorTheme (Path skinDir)
734    {
735        Path modelFile = skinDir.resolve("model.xml");
736        
737        try (InputStream is = Files.newInputStream(modelFile))
738        {
739            Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
740            return configuration.getChild("color-theme").getValue(null);
741        }
742        catch (Exception e)
743        {
744            getLogger().error("Unable to get theme value", e);
745            return null;
746        }
747    }
748    
749    /**
750     * Get all parameters' values
751     * @param skinDir The skin directory (could be in temp, work or skin)
752     * @param modelName The model name
753     * @return The all parameters' values
754     */
755    public Map<String, Object> getParameterValues (Path skinDir, String modelName)
756    {
757        Map<String, Object> values = new HashMap<>();
758        
759        Path modelFile = skinDir.resolve("model.xml");
760        
761        try (InputStream is = Files.newInputStream(modelFile))
762        {
763            Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
764            Configuration[] parametersConf = configuration.getChild("parameters").getChildren("parameter");
765            
766            Map<String, AbstractSkinParameter> modelParameters = getModelParameters(modelName);
767            
768            for (Configuration paramConf : parametersConf)
769            {
770                String id = paramConf.getAttribute("id");
771                AbstractSkinParameter modelParam = modelParameters.get(id);
772                if (modelParam != null)
773                {
774                    if (modelParam instanceof I18nizableTextParameter)
775                    {
776                        Configuration[] children = paramConf.getChildren();
777                        Map<String, String> langValues = new HashMap<>();
778                        for (Configuration langConfig : children)
779                        {
780                            langValues.put(langConfig.getName(), langConfig.getValue(""));
781                        }
782                        values.put(id, langValues);
783                    }
784                    else if (modelParam instanceof ImageParameter)
785                    {
786                        boolean uploaded = Boolean.valueOf(paramConf.getAttribute("uploaded", "false"));
787                        values.put(id, new ImageParameter.FileValue(paramConf.getValue(""), uploaded));
788                    }
789                    else
790                    {
791                        values.put(id, paramConf.getValue(""));
792                    }
793                }
794            }
795            
796            return values;
797        }
798        catch (Exception e)
799        {
800            getLogger().error("Unable to get values of all parameters", e);
801            return new HashMap<>();
802        }
803    }
804    
805    /**
806     * Modify the color theme
807     * @param skinDir The skin directory (could be in temp, work or skin)
808     * @param themeId The id of the theme. Can be null to clear theme.
809     * @return <code>true</code> is modification success
810     */
811    public boolean saveColorTheme (Path skinDir, String themeId)
812    {
813        Path currentFile = skinDir.resolve("model.xml");
814        
815        Source currentSrc = null;
816        Source src = null;
817        try
818        {
819            currentSrc = _resolver.resolveURI("file://" + currentFile.toFile().getAbsolutePath());
820            
821            Map<String, Object> parentContext = new HashMap<>();
822            parentContext.put("modelUri", currentSrc.getURI());
823            if (themeId != null)
824            {
825                parentContext.put("themeId", themeId);
826            }
827            
828            src = _resolver.resolveURI("cocoon://_plugins/skinfactory/change-color-theme", null, parentContext);
829            
830            SourceUtil.copy(src, currentSrc);
831            
832            return true;
833        }
834        catch (IOException e)
835        {
836            getLogger().error("Unable to update color theme for skin '" + skinDir.getFileName().toString() + "'", e);
837            return false;
838        }
839        finally
840        {
841            _resolver.release(src);
842            _resolver.release(currentSrc);
843        }
844    }
845    
846    /**
847     * Modify a skin parameter's value
848     * @param skinDir The skin directory (could be in temp, work or skin)
849     * @param id the id of the parameter
850     * @param value the new value
851     * @return <code>true</code> is modification success
852     */
853    public boolean saveParameter(Path skinDir, String id, Object value)
854    {
855        Map<String, Object> parameters = new HashMap<>();
856        parameters.put(id, value);
857        
858        return saveParameters (skinDir, parameters);
859    }
860    
861    /**
862     * Save skin parameters
863     * @param skinDir The skin directory (could be in temp, work or skin)
864     * @param parameters The skins parameters to save
865     * @return <code>true</code> is modification success
866     */
867    public boolean saveParameters(Path skinDir, Map<String, Object> parameters)
868    {
869        Path currentFile = skinDir.resolve("model.xml");
870        
871        Source currentSrc = null;
872        Source src = null;
873        try
874        {
875            currentSrc = _resolver.resolveURI("file://" + currentFile.toFile().getAbsolutePath());
876            
877            Map<String, Object> parentContext = new HashMap<>();
878            parentContext.put("modelUri", currentSrc.getURI());
879            parentContext.put("skinParameters", parameters);
880            
881            src = _resolver.resolveURI("cocoon://_plugins/skinfactory/change-parameters", null, parentContext);
882            
883            SourceUtil.copy(src, currentSrc);
884            
885            return true;
886        }
887        catch (IOException e)
888        {
889            getLogger().error("Unable to save parameters from skin '" + skinDir.getFileName().toString() + "'", e);
890            return false;
891        }
892        finally
893        {
894            _resolver.release(src);
895            _resolver.release(currentSrc);
896        }
897    }
898    
899    /**
900     * Apply model parameters in given skin
901     * @param modelName The model name
902     * @param skinDir The skin directory (could be temp, work or skins)
903     */
904    public void applyModelParameters (String modelName, Path skinDir)
905    {
906        Map<String, Object> currentValues = getParameterValues(skinDir, modelName);
907        applyModelParameters(modelName, skinDir, currentValues);
908    }
909    
910    /**
911     * Apply model parameters in given skin
912     * @param modelName The model name
913     * @param skinDir The skin directory
914     * @param values The values to set
915     */
916    public void applyModelParameters (String modelName, Path skinDir, Map<String, Object> values)
917    {
918        Map<String, Object> parameterValues = new HashMap<>();
919        SkinModel model = _modelsManager.getModel(modelName);
920        
921        Map<String, String> modelDefaultValues = model.getDefaultValues();
922        
923        Map<String, AbstractSkinParameter> modelParameters = getModelParameters(modelName);
924        for (AbstractSkinParameter skinParameter : modelParameters.values())
925        {
926            String paramId = skinParameter.getId();
927            
928            if (skinParameter instanceof I18nizableTextParameter)
929            {
930                Map<String, String> defaultValues = ((I18nizableTextParameter) skinParameter).getDefaultValues();
931                @SuppressWarnings("unchecked")
932                Map<String, String> currentValues = (Map<String, String>) values.get(paramId);
933                if (currentValues == null)
934                {
935                    currentValues = new HashMap<>();
936                }
937                for (String lang : defaultValues.keySet())
938                {
939                    if (currentValues.get(lang) != null)
940                    {
941                        applyParameter(skinParameter, skinDir, modelName, currentValues.get(lang), lang);
942                    }
943                    else
944                    {
945                        // Default value
946                        applyParameter(skinParameter, skinDir, modelName, defaultValues.get(lang), lang);
947                        currentValues.put(lang, defaultValues.get(lang));
948                    }
949                }
950                parameterValues.put(skinParameter.getId(), currentValues);
951            }
952            else if (skinParameter instanceof ImageParameter)
953            {
954                FileValue imgValue = (FileValue) _getValue(model, skinParameter, values, modelDefaultValues);
955                applyParameter(skinParameter, skinDir, modelName, imgValue, null);
956                parameterValues.put(skinParameter.getId(), imgValue);
957            }
958            else
959            {
960                String value = (String) _getValue (model, skinParameter, values, modelDefaultValues);
961                applyParameter(skinParameter, skinDir, modelName, value, null);
962                parameterValues.put(skinParameter.getId(), value);
963            }
964        }
965        
966        saveParameters(skinDir, parameterValues);
967    }
968    
969    private Object _getValue (SkinModel model, AbstractSkinParameter param, Map<String, Object> values, Map<String, String> defaultValues)
970    {
971        String id = param.getId();
972        
973        // First search in current values
974        if (values.containsKey(id))
975        {
976            return values.get(id);
977        }
978        
979        // Then search in default values from model
980        if (defaultValues.containsKey(id))
981        {
982            String valueAsStr =  defaultValues.get(id);
983            
984            if (param instanceof ImageParameter)
985            {
986                return new FileValue (valueAsStr, false);
987            }
988            else
989            {
990                return valueAsStr;
991            }
992        }
993        
994        // If nor found, get the parameter default value
995        return param.getDefaultValue(model);
996    }
997    /**
998     * Update hash
999     * @param xmlFile the xml {@link File}
1000     * @param hash the new hash of the file
1001     * @throws IOException if an error occurs while manipulating the file
1002     */
1003    public void updateHash (Path xmlFile, String hash) throws IOException
1004    {
1005        Source currentSrc = null;
1006        Source src = null;
1007        try
1008        {
1009            currentSrc = _resolver.resolveURI("file://" + xmlFile.toFile().getAbsolutePath());
1010            
1011            Map<String, Object> parentContext = new HashMap<>();
1012            parentContext.put("modelUri", currentSrc.getURI());
1013            parentContext.put("hash", hash);
1014            
1015            src = _resolver.resolveURI("cocoon://_plugins/skinfactory/change-hash", null, parentContext);
1016            
1017            SourceUtil.copy(src, currentSrc);
1018        }
1019        finally
1020        {
1021            _resolver.release(src);
1022            _resolver.release(currentSrc);
1023        }
1024        
1025    }
1026    
1027    /**
1028     * Apply parameter
1029     * @param parameter the skin parameter
1030     * @param skinDir the skin directory (could be temp, work or skins)
1031     * @param modelName the model name
1032     * @param value the parameter value
1033     * @param lang The language
1034     */
1035    public void applyParameter (AbstractSkinParameter parameter, Path skinDir, String modelName, Object value, String lang)
1036    {
1037        Path modelDir = _modelsManager.getModel(modelName).getPath();
1038        parameter.apply(skinDir, modelDir, value, lang);
1039    }
1040    
1041    /**
1042     * Apply new color theme 
1043     * @param skinDir the skin directory (could be temp, work or skins)
1044     * @param modelName the model name
1045     */
1046    public void applyColorTheme (String modelName, Path skinDir)
1047    {
1048        Path modelDir = _modelsManager.getModel(modelName).getPath();
1049        
1050        Map<String, Object> currentValues = getParameterValues(skinDir, modelName);
1051        
1052        Map<String, AbstractSkinParameter> modelParameters = getModelParameters(modelName);
1053        for (AbstractSkinParameter skinParameter : modelParameters.values())
1054        {
1055            if (skinParameter instanceof CSSColorParameter)
1056            {
1057                String value = (String) currentValues.get(skinParameter.getId());
1058                if (StringUtils.isEmpty(value))
1059                {
1060                    value = (String) skinParameter.getDefaultValue(_modelsManager.getModel(modelName));
1061                }
1062                
1063                skinParameter.apply(skinDir, modelDir, value, null);
1064            }
1065        }
1066    }
1067    
1068    private I18nizableText _configureI18nizableText(Configuration configuration, String modelName) throws ConfigurationException
1069    {
1070        boolean i18nSupported = configuration.getAttributeAsBoolean("i18n", false);
1071
1072        if (i18nSupported)
1073        {
1074            String catalogue = configuration.getAttribute("catalogue", null);
1075            if (catalogue == null)
1076            {
1077                catalogue = "model." + modelName;
1078            }
1079
1080            return new I18nizableText(catalogue, configuration.getValue());
1081        }
1082        else
1083        {
1084            return new I18nizableText(configuration.getValue(""));
1085        }
1086    }
1087}