001/*
002 *  Copyright 2010 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.web.skin;
017
018import java.io.InputStream;
019import java.nio.file.Files;
020import java.nio.file.Path;
021import java.util.ArrayList;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
031import org.apache.commons.lang.StringUtils;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import org.ametys.runtime.i18n.I18nizableText;
036
037
038/**
039 * A skin
040 * Do not mistake with {@link SkinParametersModel}
041 */
042public class SkinModel
043{
044    private static Logger _logger = LoggerFactory.getLogger(SkinModel.class);
045    
046    /** The skin id (e.g. the directory name) */
047    protected String _id;
048    /** The skin directory */
049    protected Path _file;
050    /** The skin name */
051    protected I18nizableText _label;
052    /** The skin description */
053    protected I18nizableText _description;
054    /** The skin thumbnail 16x16 */
055    protected String _smallImage;
056    /** The skin thumbnail 32x32 */
057    protected String _mediumImage;
058    /** The skin thumbnail 48x48 */
059    protected String _largeImage;
060    
061    /** The last time the file was loaded */
062    protected long _lastConfUpdate;
063    /** Is the skin modifiable */
064    protected boolean _modifiable;
065    
066    private long _lastDefaultValuesUpdate;
067    private Map<String, String> _defaultValues;
068    private String _defaultColorTheme;
069    
070    private long _lastColorsUpdate;
071    private Map<String, Theme> _themes = new HashMap<>();
072    private List<String> _colors = new ArrayList<>();
073    
074    private long _lastCssStylesUpdate;
075    private Map<String, List<CssMenuItem>> _cssStyles = new HashMap<>();
076    
077    /**
078     * Creates a skin
079     * @param id The id of the skin (e.g. the directory name)
080     * @param file The skin file
081     * @param modifiable Is this model modifiable?
082     */
083    public SkinModel(String id, Path file, boolean modifiable)
084    {
085        _id = id;
086        _file = file;
087        _modifiable = modifiable;
088    }
089    
090    /**
091     * The configuration default values (if configuration file does not exist or is unreadable)
092     */
093    protected void _defaultValues()
094    {
095        _lastConfUpdate = new Date().getTime();
096        
097        _label = new I18nizableText(_id);
098        _description = new I18nizableText("");
099        _smallImage = "/plugins/web/resources/img/skin/skin_16.png";
100        _mediumImage = "/plugins/web/resources/img/skin/skin_32.png";
101        _largeImage = "/plugins/web/resources/img/skin/skin_48.png";
102    }
103    
104    /**
105     * Is the model modifiable?
106     * @return true if modifiable
107     */
108    public boolean isModifiable()
109    {
110        return _modifiable;
111    }
112    
113    /**
114     * Refresh the conf values
115     */
116    public void refreshValues()
117    {
118        Path configurationFile = _file.resolve(Skin.CONF_PATH);
119        if (Files.exists(configurationFile))
120        {
121            try
122            {
123                long millis = Files.getLastModifiedTime(configurationFile).toMillis();
124                if (_lastConfUpdate < millis)
125                {
126                    _lastConfUpdate = millis;
127                    try (InputStream is = Files.newInputStream(configurationFile))
128                    {
129                        Configuration configuration = new DefaultConfigurationBuilder().build(is);
130                        
131                        this._label = _configureI18nizableText(configuration.getChild("label", false), new I18nizableText(this._id));
132                        this._description = _configureI18nizableText(configuration.getChild("description", false), new I18nizableText(""));
133                        this._smallImage = _configureThumbnail(configuration.getChild("thumbnail").getChild("small").getValue(null), "/plugins/web/resources/img/skin/skin_16.png");
134                        this._mediumImage = _configureThumbnail(configuration.getChild("thumbnail").getChild("medium").getValue(null), "/plugins/web/resources/img/skin/skin_32.png");
135                        this._largeImage = _configureThumbnail(configuration.getChild("thumbnail").getChild("large").getValue(null), "/plugins/web/resources/img/skin/skin_48.png");
136                    }
137                }
138            }
139            catch (Exception e)
140            {
141                _defaultValues();
142                if (_logger.isWarnEnabled())
143                {
144                    _logger.warn("Cannot read the configuration file " + Skin.CONF_PATH + " for the model '" + this._id + "'. Continue as if file was not existing", e);
145                }
146            }
147        }
148        else
149        {
150            _defaultValues();
151        }
152    }
153    
154    /**
155     * Get the default color theme
156     * @return The default color theme or <code>null</code>
157     */
158    public String getDefaultColorTheme ()
159    {
160        _refreshDefaultValues ();
161        return _defaultColorTheme;
162    }
163    
164    /**
165     * Get color themes of this model
166     * @return color themes of this model
167     */
168    public Map<String, Theme> getThemes ()
169    {
170        _refreshColorsValues ();
171        return _themes;
172    }
173    
174    /**
175     * Get a theme of this model
176     * @param themeId the theme id
177     * @return the theme
178     */
179    public Theme getTheme (String themeId)
180    {
181        _refreshColorsValues();
182        return _themes.get(themeId);
183    }
184    
185    /**
186     * Get the default colors of this model
187     * @return The default colors of this model
188     */
189    public List<String> getDefaultColors ()
190    {
191        _refreshColorsValues();
192        return _colors;
193    }
194    
195    /**
196     * Get the default colors of this theme
197     * @param themeId The theme id
198     * @return the default colors of this theme
199     */
200    public List<String> getColors (String themeId)
201    {
202        _refreshColorsValues();
203        if (_themes.containsKey(themeId))
204        {
205            return _themes.get(themeId).getColors();
206        }
207        return new ArrayList<>();
208    }
209    
210    /**
211     * Get all style items of this model
212     * @return the list of items of css style
213     */
214    public Map<String, List<CssMenuItem>> getStyleItems ()
215    {
216        _refreshCssStyles();
217        return _cssStyles;
218    }
219    
220    /**
221     * Get the items of this css style
222     * @param styleId The css style id
223     * @return the list of items of css style
224     */
225    public List<CssMenuItem> getStyleItems (String styleId)
226    {
227        _refreshCssStyles();
228        return _cssStyles.get(styleId);
229    }
230    
231    /**
232     * Get model default values
233     * @return The default values
234     */
235    public Map<String, String> getDefaultValues ()
236    {
237        _refreshDefaultValues ();
238        return _defaultValues;
239    }
240    
241    private void _refreshColorsValues ()
242    {
243        Path configurationFile = _file.resolve("model/colors.xml");
244        if (Files.exists(configurationFile))
245        {
246            try
247            {
248                long millis = Files.getLastModifiedTime(configurationFile).toMillis();
249                if (_lastColorsUpdate < millis)
250                {
251                    _lastColorsUpdate = millis;
252                    _colors = new ArrayList<>();
253                    _themes = new LinkedHashMap<>();
254                    
255                    try (InputStream is = Files.newInputStream(configurationFile))
256                    {
257                        Configuration configuration = new DefaultConfigurationBuilder().build(is);
258                        Configuration[] themesConf = configuration.getChildren("theme");
259                        
260                        for (Configuration themeConf : themesConf)
261                        {
262                            String name = themeConf.getAttribute("id");
263                            I18nizableText label = _configureI18nizableText (themeConf.getChild("label"), new I18nizableText(""));
264                            I18nizableText description = _configureI18nizableText (themeConf.getChild("description"), new I18nizableText(""));
265                            
266                            Configuration[] colorsConf = themeConf.getChild("colors").getChildren("color");
267                            
268                            List<String> colors = new ArrayList<>();
269                            for (Configuration colorConf : colorsConf)
270                            {
271                                colors.add(colorConf.getValue().toLowerCase());
272                            }
273                            
274                            _themes.put(name, new Theme(name, label, description, colors));
275                        }
276                        
277                        Configuration[] children = configuration.getChild("default", true).getChildren("color");
278                        for (Configuration colorConf : children)
279                        {
280                            _colors.add(colorConf.getValue().toLowerCase());
281                        }
282                    }
283                }
284            }
285            catch (Exception e)
286            {
287                if (_logger.isWarnEnabled())
288                {
289                    _logger.warn("Cannot read the configuration file model/colors.xml for the model '" + this._id + "'. Continue as if file was not existing", e);
290                }
291                
292                _colors = new ArrayList<>();
293                _themes = new HashMap<>();
294            }
295        }
296        else
297        {
298            _colors = new ArrayList<>();
299            _themes = new HashMap<>();
300        }
301    }
302    
303    private void _refreshCssStyles ()
304    {
305        Path configurationFile = _file.resolve("model/css-styles.xml");
306        if (Files.exists(configurationFile))
307        {
308            try
309            {
310                long millis = Files.getLastModifiedTime(configurationFile).toMillis();
311                if (_lastCssStylesUpdate < millis)
312                {
313                    _lastCssStylesUpdate = millis;
314                    _cssStyles = new HashMap<>();
315                    
316                    try (InputStream is = Files.newInputStream(configurationFile))
317                    {
318                        Configuration configuration = new DefaultConfigurationBuilder().build(is);
319                        Configuration[] children = configuration.getChildren();
320                        for (Configuration styleConf : children)
321                        {
322                            String name = styleConf.getName();
323                            List<CssMenuItem> items = _configureStyleItems (styleConf);
324                            _cssStyles.put(name, items);
325                        }
326                    }
327                }
328            }
329            catch (Exception e)
330            {
331                if (_logger.isWarnEnabled())
332                {
333                    _logger.warn("Cannot read the configuration file model/css-styles.xml for the model '" + this._id + "'. Continue as if file was not existing", e);
334                }
335                
336                _cssStyles = new HashMap<>();
337            }
338        }
339        else
340        {
341            _cssStyles = new HashMap<>();
342        }
343    }
344    
345    private List<CssMenuItem> _configureStyleItems (Configuration styleConfig) throws ConfigurationException
346    {
347        List<CssMenuItem> items = new ArrayList<>();
348        
349        Configuration[] children = styleConfig.getChildren();
350        for (Configuration child : children)
351        {
352            if (child.getName().equals("separator"))
353            {
354                items.add(new Separator());
355            }
356            else
357            {
358                String value = child.getChild("value").getValue();
359                I18nizableText label = _configureI18nizableText (child.getChild("label"), new I18nizableText(value));
360                String iconCls = child.getChild("iconCls").getValue(null);
361                String icon = _configureIcon (child.getChild("icon", false));
362                String cssClass = child.getChild("cssclass", true).getValue(null);
363                
364                items.add(new CssStyleItem(value, label, iconCls, icon, cssClass));
365            }
366        }
367        
368        return items;
369    }
370    
371    
372    private void _refreshDefaultValues ()
373    {
374        Path configurationFile = _file.resolve("model/default-values.xml");
375        if (Files.exists(configurationFile))
376        {
377            try
378            {
379                long millis = Files.getLastModifiedTime(configurationFile).toMillis();
380
381                if (_lastDefaultValuesUpdate < millis)
382                {
383                    _defaultValues = new HashMap<>();
384                    _lastDefaultValuesUpdate = millis;
385                    
386                    try (InputStream is = Files.newInputStream(configurationFile))
387                    {
388                        Configuration configuration = new DefaultConfigurationBuilder().build(is);
389                        Configuration[] parametersConf = configuration.getChildren("parameter");
390                        
391                        for (Configuration paramConf : parametersConf)
392                        {
393                            String value = paramConf.getValue(null);
394                            if (StringUtils.isNotEmpty(value))
395                            {
396                                _defaultValues.put(paramConf.getAttribute("id"), paramConf.getValue());
397                            }
398                        }
399                        
400                        _defaultColorTheme = configuration.getChild("color-theme", true).getValue(null);
401                    }
402                }
403            }
404            catch (Exception e)
405            {
406                _logger.error("Cannot read the configuration file model/default-values.xml for the model '" + this._id + "'. Continue as if file was not existing", e);
407                
408                _defaultValues = new HashMap<>();
409                _defaultColorTheme = null;
410            }
411        }
412        else
413        {
414            _defaultValues = new HashMap<>();
415            _defaultColorTheme = null;
416        }
417    }
418    
419    private String _configureThumbnail(String value, String defaultImage)
420    {
421        if (value == null)
422        {
423            return defaultImage;
424        }
425        else
426        {
427            return "/models/" + this._id + "/resources/" + value;
428        }
429    }
430    
431    private I18nizableText _configureI18nizableText(Configuration configuration, I18nizableText defaultValue) throws ConfigurationException
432    {
433        if (configuration != null)
434        {
435            boolean i18nSupported = configuration.getAttributeAsBoolean("i18n", false);
436            if (i18nSupported)
437            {
438                String catalogue = configuration.getAttribute("catalogue", null);
439                if (catalogue == null)
440                {
441                    catalogue = "model." + this._id;
442                }
443
444                return new I18nizableText(catalogue, configuration.getValue());
445            }
446            else
447            {
448                return new I18nizableText(configuration.getValue(""));
449            }
450        }
451        else
452        {
453            return defaultValue;
454        }
455        
456    }
457    
458    private String _configureIcon (Configuration iconConf) throws ConfigurationException
459    {
460        if (iconConf != null)
461        {
462            String pluginName = iconConf.getAttribute("plugin");
463            String path = iconConf.getValue();
464            
465            return "/plugins/" + pluginName + "/resources/" + path;
466        }
467        return null;
468    }
469
470    /**
471     * The skin id
472     * @return the id
473     */
474    public String getId()
475    {
476        return _id;
477    }
478    
479    /**
480     * The skin label
481     * @return The label
482     */
483    public I18nizableText getLabel()
484    {
485        return _label;
486    }
487    /**
488     * The skin description
489     * @return The description. Can not be null but can be empty
490     */
491    public I18nizableText getDescription()
492    {
493        return _description;
494    }
495    
496    /**
497     * The small image file uri
498     * @return The small image file uri
499     */
500    public String getSmallImage()
501    {
502        return _smallImage;
503    }
504
505    /**
506     * The medium image file uri
507     * @return The medium image file uri
508     */
509    public String getMediumImage()
510    {
511        return _mediumImage;
512    }
513    
514    /**
515     * The large image file uri
516     * @return The large image file uri
517     */
518    public String getLargeImage()
519    {
520        return _largeImage;
521    }
522
523    /**
524     * Get the skin's file directory
525     * @return the skin's file directory
526     */
527    public Path getPath ()
528    {
529        return _file;
530    }
531    
532    /*----------------------------------------------------*/
533    /*                 Internal classes                   */
534    /*----------------------------------------------------*/
535    /**
536     * Bean representing a theme
537     *
538     */
539    public class Theme
540    {
541        private String _themeId;
542        private I18nizableText _themeLabel;
543        private I18nizableText _themeDescription;
544        private List<String> _themeColors;
545
546        /**
547         * Constructor
548         * @param id the theme id
549         * @param label the theme's label
550         * @param description the theme's description
551         * @param colors the theme's colors
552         */
553        public Theme (String id, I18nizableText label, I18nizableText description, List<String> colors)
554        {
555            _themeId = id;
556            _themeLabel = label;
557            _themeDescription = description;
558            _themeColors = colors;
559        }
560        
561        /**
562         * Get the id
563         * @return the id
564         */
565        public String getId ()
566        {
567            return _themeId;
568        }
569        
570        /**
571         * Get the label
572         * @return the label
573         */
574        public I18nizableText getLabel ()
575        {
576            return _themeLabel;
577        }
578        
579        /**
580         * Get the description
581         * @return the description
582         */
583        public I18nizableText getDescription ()
584        {
585            return _themeDescription;
586        }
587        
588        /**
589         * Get the colors
590         * @return the colors
591         */
592        public List<String> getColors ()
593        {
594            return _themeColors;
595        }
596        
597        /**
598         * Get the color to the given index
599         * @param index The index of color
600         * @return The color
601         */
602        public String getColor (int index)
603        {
604            try
605            {
606                return _themeColors.get(index);
607            }
608            catch (IndexOutOfBoundsException e)
609            {
610                // Returns the last color
611                return _themeColors.get(_themeColors.size() - 1);
612            }
613        }
614    }
615    
616    /**
617     * Bean representing a item of css style
618     *
619     */
620    public class CssStyleItem implements CssMenuItem
621    {
622        private String _cssValue;
623        private I18nizableText _styleLabel;
624        private String _styleIcon;
625        private String _styleIconCls;
626        private String _cssClass;
627
628        /**
629         * Constructor
630         * @param value the value
631         * @param label the item's label
632         * @param iconCls the CSS class for icon. Can be null.
633         * @param icon the icon. Can be null.
634         * @param cssClass the css class. Can be null.
635         */
636        public CssStyleItem (String value, I18nizableText label, String iconCls, String icon, String cssClass)
637        {
638            _cssValue = value;
639            _styleLabel = label;
640            _styleIcon = icon;
641            _styleIconCls = iconCls;
642            _cssClass = cssClass;
643        }
644        
645        /**
646         * Get the value
647         * @return the value
648         */
649        public String getValue ()
650        {
651            return _cssValue;
652        }
653        
654        /**
655         * Get the label
656         * @return the label
657         */
658        public I18nizableText getLabel ()
659        {
660            return _styleLabel;
661        }
662        
663        /**
664         * Get the icon css class
665         * @return the icon css class or <code>null</code>
666         */
667        public String getIconCls ()
668        {
669            return _styleIconCls;
670        }
671        
672        /**
673         * Get the icon
674         * @return the icon or <code>null</code>
675         */
676        public String getIcon ()
677        {
678            return _styleIcon;
679        }
680        
681        /**
682         * Get the css class
683         * @return The css class or <code>null</code>
684         */
685        public String getCssClass ()
686        {
687            return _cssClass;
688        }
689    }
690    
691    /**
692     * Item representing a separator
693     */
694    public class Separator implements CssMenuItem
695    {
696        /**
697         * Constructor
698         */
699        public Separator()
700        {
701            // Nothing to do
702        }
703        
704    }
705    
706    /**
707     * Abstract representation of a menu item
708     */
709    public interface CssMenuItem
710    {
711        // Empty
712    }
713}