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