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