zweb/src/main/java/org/zkoss/web/fn/ThemeFns.java

Summary

Maintainability
F
5 days
Test Coverage
/* ThemeFns.java

{{IS_NOTE
    Purpose:
        
    Description:
        
    History:
        Jul 10, 2012 9:35:27 AM , Created by jumperchen
}}IS_NOTE

Copyright (C) 2012 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
}}IS_RIGHT
 */
package org.zkoss.web.fn;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.lang.Library;
import org.zkoss.lang.Strings;
import org.zkoss.util.resource.Locators;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.web.theme.StandardTheme;
import org.zkoss.web.theme.Theme;
import org.zkoss.web.theme.ThemeRegistry;
import org.zkoss.web.theme.ThemeResolver;
import org.zkoss.web.util.resource.ClassWebResource;

/**
 * Providing theme relevant functions for EL.
 * 
 * @author simonpai
 * @author jumperchen
 * @since 6.5.0
 */
public class ThemeFns {

    private ThemeFns() {
    }

    private static Browser getBrowser() {
        Double number = Servlets.getBrowser(ServletFns.getCurrentRequest(), "ff");
        if (number != null && number >= 3.6)
            return Browser.Firefox;
        number = Servlets.getBrowser(ServletFns.getCurrentRequest(), "ie");
        if (number != null) {
            if (number <= 9) {
                return Browser.IE9;
            } else
                return Browser.IE;
        }
        number = Servlets.getBrowser(ServletFns.getCurrentRequest(), "webkit");
        if (number != null) {
            Double android = Servlets.getBrowser(ServletFns.getCurrentRequest(), "android");
            if (android != null && android < 3) {
                return Browser.Old_WebKit;
            }
            Double version = Servlets.getBrowser(ServletFns.getCurrentRequest(), "chrome");
            if (version != null) {
                if (version >= 10)
                    return Browser.WebKit;
                if (version >= 1)
                    return Browser.Old_WebKit;
            }

            // B65-ZK-1614: Full Screen iPad Web Apps Missing Component Buttons
            version = Servlets.getBrowser(ServletFns.getCurrentRequest(), "ios");
            if (version != null && version >= 500)
                return Browser.WebKit;

            version = Servlets.getBrowser(ServletFns.getCurrentRequest(), "safari");
            if (version != null) {
                if (version >= 5.1)
                    return Browser.WebKit;
                if (version >= 4)
                    return Browser.Old_WebKit;
            }
        }
        number = Servlets.getBrowser(ServletFns.getCurrentRequest(), "opera");
        if (number != null && number >= 11.1)
            return Browser.Opera;
        return Browser.Old;
    }

    /**
     * Generates a specific of browser CSS color gradient rules String.
     * 
     * @param direction
     *            "ver", "hor", "diag-", "diag+", "rad"
     * @param colors
     *            the colors with stops, which are separated by semicolon ";".
     *        For example,
     *        <pre><code>ThemeFns.gradient("ver", "#fefefe 0%; #eeeeee 100%");</code></pre>
     * @return A specific CSS gradient rules String
     */
    public static String gradient(String direction, String colors) {
        Browser temp = getBrowser();
        if (temp == Browser.Old || temp == Browser.W3C)
            return "\tbackground:" + grad(direction, getBrowser(), colors.split(";"));
        else {
            String[] cols = colors.split(";");
            StringBuilder sb = new StringBuilder();
            sb.append("\tbackground:");
            sb.append(grad(direction, temp, cols));
            sb.append("\tbackground:").append(grad(direction, Browser.W3C, cols));
            return sb.toString();
        }
    }

    /**
     * Generates a specific CSS color gradient value only.
     * 
     * @param direction
     *            "ver", "hor", "diag-", "diag+", "rad"
     * @param colors
     *            the colors with stops, which are separated by semicolon ";"
     *        For example,
     *        <pre><code>ThemeFns.gradValue("ver", "#fefefe 0%; #eeeeee 100%");</code></pre>
     * @return A specific CSS gradient rules String without the words
     *         "background:".
     */
    public static String gradValue(String direction, String colors) {
        return grad(direction, getBrowser(), colors.split(";")).replace(";", "");
    }

    private static String grad(String dir, Browser template, String[] colors) {
        if ("|ver|hor|diag-|diag+|rad|".indexOf("|" + dir + "|") == -1)
            throw new IllegalArgumentException("Type must be ver, hor, diag-, diag+, or rad.");
        if (colors.length < 2)
            throw new IllegalArgumentException("Please specify more than two colors.");
        String color1 = toHex(colors[0]);
        String color2 = toHex(colors[1]);
        StringBuilder colorAll = new StringBuilder();
        if (template == Browser.Old_WebKit) {
            for (String color : colors) {
                color = color.trim();
                boolean hex = color.startsWith("#");

                int end = hex ? color.indexOf(" ") + 1 : color.indexOf(")") + 1;
                if (end == 0 && !color.toLowerCase(java.util.Locale.ENGLISH).contains("transparent"))
                    if (hex)
                        throw new IllegalArgumentException(
                                "The format of hexadecimal is wrong! [" + color + "] or without stops (%)");
                    else
                        throw new IllegalArgumentException(
                                "The format of RGBA is wrong! [" + color + "] or without stops (%)");

                String pos = color.substring(end, color.length());
                color = color.substring(0, end);
                colorAll.append("color-stop(").append(pos).append(',').append(color).append("),");
            }
            int len = colorAll.length();
            if (len > 0)
                colorAll.delete(len - 1, len);
        } else if (template == Browser.IE9) {
            for (String color : colors) {
                color = color.trim();
                boolean hex = color.startsWith("#");

                int end = hex ? color.indexOf(" ") + 1 : color.indexOf(")") + 1;
                if (end == 0 && !color.toLowerCase(java.util.Locale.ENGLISH).contains("transparent"))
                    if (hex)
                        throw new IllegalArgumentException(
                                "The format of hexadecimal is wrong! [" + color + "] or without stops (%)");
                    else
                        throw new IllegalArgumentException(
                                "The format of RGBA is wrong! [" + color + "] or without stops (%)");

                String pos = color.substring(end, color.length());
                color = color.substring(0, end);
                colorAll.append("<stop stop-color=\"").append(color).append("\" offset=\"").append(pos).append("\"/>");
            }

        } else {
            for (String color : colors) {
                colorAll.append(color).append(',');
            }
            int len = colorAll.length();
            if (len > 0)
                colorAll.delete(len - 1, len);
        }

        String gradType = "rad".equals(dir) ? "radial" : "linear";
        int ieGradType = "hor".equals(dir) ? 1 : 0; // IE only supports
                                                    // ver/hor
        String result = String.format(template.getGradient(dir), color1, color2, "", gradType, ieGradType,
                colorAll.toString());
        if (template == Browser.IE9) {
            result = "url(data:image/svg+xml;base64," + Base64.getEncoder()
                    .encodeToString(result.getBytes(StandardCharsets.UTF_8)) + ");";
        }

        return result;
    }

    /**
     * Generates a set of cross-browser CSS color gradient rules String.
     * 
     * @param direction
     *            "ver", "hor", "diag-", "diag+", "rad"
     * @param colors
     *            the colors, which are separated by semicolon ";"
     * @return A set of cross-browser CSS gradient rules String
     */
    public static String gradients(String direction, String colors) {
        StringBuilder sb = new StringBuilder();
        String[] cols = colors.split(";");
        for (Browser grad : Browser.values()) {
            sb.append("\tbackground:");
            sb.append(grad(direction, grad, cols));
        }
        return sb.toString();
    }

    private static String CSS_TEMPLATE = "\t %1$s%2$s:\t %3$s;\n\t %2$s:\t%3$s;";

    private static String CSS_TEMPLATE_W3C = "\t %1$s:\t%2$s;";

    private static String applyCSS(String styleName, String styleValue) {
        Browser browser = getBrowser();
        if (!Strings.isEmpty(browser.getPrefix())) {
            return String.format(CSS_TEMPLATE, browser.getPrefix(), styleName, styleValue);
        }
        return String.format(CSS_TEMPLATE_W3C, styleName, styleValue);
    }

    /**
     * Generates a specific browser CSS rule String for the given style name and
     * value.
     * <p>
     * Note: the method is only applied with the browser prefix as the style
     * name, if the CSS3 style usage rule is different between browsers, please
     * use another method instead.
     * 
     * @param styleName
     *            the value of the style name, like <code>box-sizing</code>,
     *            <code>animation</code>
     * @param styleValue
     *            the value according to the style name, like
     *            <code>border-box</code>, <code>mymove 5s infinite</code>
     * @return a specific browser CSS rule string, like
     *         <code>-moz-box-sizing</code> for firefox and
     *         <code>-webkit-box-sizing</code> for safari and chrome
     */
    public static String applyCSS3(String styleName, String styleValue) {
        return applyCSS(styleName, styleValue);
    }

    /**
     * Generates a specific browser CSS rule string for box model.
     * 
     * @param styleName
     *            the value of the style name, like <code>box-orient</code>,
     *            <code>box-pack</code>
     * @param styleValue
     *            the value according to the style name, like
     *            <code>horizontal</code>, <code>center</code>
     * @return a specific browser CSS rule string, like
     *         <code>-moz-box-orient</code> for firefox and
     *         <code>-webkit-box-orient</code> for safari and chrome
     */
    public static String box(String styleName, String styleValue) {
        Browser browser = getBrowser();
        StringBuilder sb = new StringBuilder(32);
        if (browser == Browser.Firefox || browser == Browser.WebKit) {
            sb.append("\t display:\t").append(browser.getPrefix()).append("box;\n");
        }
        sb.append("\t display:\t box;\n");
        return sb.append(applyCSS(styleName, styleValue)).toString();
    }

    /**
     * Generates a specific browser CSS rule string for box model with two pair
     * styles.
     * 
     * @see #box(String, String)
     */
    public static String box2(String styleName, String styleValue, String styleName2, String styleValue2) {
        Browser browser = getBrowser();
        StringBuilder sb = new StringBuilder(32);
        if (browser == Browser.Firefox || browser == Browser.WebKit) {
            sb.append("\t display:\t").append(browser.getPrefix()).append("box;\n");
        }
        sb.append("\t display:\t box;\n");
        return sb.append(applyCSS(styleName, styleValue)).append(applyCSS(styleName2, styleValue2)).toString();
    }

    /**
     * Generates a specific browser CSS rule string for box model with three pair
     * styles.
     * 
     * @see #box(String, String)
     * @see #box2(String, String, String, String)
     */
    public static String box3(String styleName, String styleValue, String styleName2, String styleValue2,
            String styleName3, String styleValue3) {
        Browser browser = getBrowser();
        StringBuilder sb = new StringBuilder(32);
        if (browser == Browser.Firefox || browser == Browser.WebKit) {
            sb.append("\t display:\t").append(browser.getPrefix()).append("box;\n");
        }
        sb.append("\t display:\t box;\n");
        return sb.append(applyCSS(styleName, styleValue)).append(applyCSS(styleName2, styleValue2))
                .append(applyCSS(styleName3, styleValue3)).toString();
    }

    /**
     * Generates a specific browser CSS transform.
     * 
     * @param style
     *            the value of the transform
     * @return a specific browser CSS transform
     */
    public static String transform(String style) {
        return applyCSS("transform", style);
    }

    /**
     * Generates a specific browser CSS box-shadow.
     * 
     * @param style
     *            the value of the box-shadow
     * @return a specific browser CSS box-shadow
     */
    public static String boxShadow(String style) {
        return applyCSS("box-shadow", style);
    }

    /**
     * Generates a specific browser CSS border-radius.
     * 
     * @param style
     *            the value of the border-radius
     * @return a specific browser CSS border-radius
     */
    public static String borderRadius(String style) {
        return applyCSS("border-radius", style);
    }

    @SuppressWarnings("unchecked")
    private static String toHex(String color) {
        color = color.trim();
        if (color.startsWith("#")) {
            int end = color.indexOf(" ");
            if (end > 0)
                return color.substring(0, end);
            return color;
        }
        int end = color.indexOf(')') + 1;
        if (end > 0)
            color = color.substring(0, end);

        Map<String, String> colors = (Map<String, String>) ServletFns.getCurrentRequest()
                .getAttribute("themeFns.colors");
        if (colors == null)
            ServletFns.getCurrentRequest().setAttribute("themeFns.colors", colors = new HashMap<String, String>());
        if (!colors.containsKey(color))
            colors.put(color, toHex(toColor(color)));
        return colors.get(color);
    }
    
    private static String toHex(Color color) {
        return Colors.getHexString(color);
    }

    @SuppressWarnings("unchecked")
    private static String toIEHex(String color) {
        color = color.trim();
        if (color.startsWith("#")) {
            int end = color.indexOf(" ");
            if (end > 0)
                return color.substring(0, end);
            return color;
        }
        int end = color.indexOf(')') + 1;
        if (end > 0)
            color = color.substring(0, end);

        Map<String, String> colors = (Map<String, String>) ServletFns.getCurrentRequest()
                .getAttribute("themeFns.IEcolors");
        if (colors == null)
            ServletFns.getCurrentRequest().setAttribute("themeFns.IEcolors", colors = new HashMap<String, String>());
        if (!colors.containsKey(color))
            colors.put(color, toIEHex(toColor(color)));
        return colors.get(color);
    }
    
    private static String toIEHex(Color color) {
        return Colors.getIEHexString(color);
    }

    private static String locate(String path) {
        try {
            if (path.startsWith("~./")) {
                path = Servlets.locate(ServletFns.getCurrentServletContext(), ServletFns.getCurrentRequest(),
                        ClassWebResource.PATH_PREFIX + path.substring(2), Locators.getDefault());
                return path;
            }
            return Servlets.locate(ServletFns.getCurrentServletContext(), ServletFns.getCurrentRequest(), path, null);
        } catch (ServletException ex) {
            log(ex.getLocalizedMessage());
        }
        return path;
    }

    private static void log(String msg) {
        Logger log = LoggerFactory.getLogger("global");
        if (log.isErrorEnabled())
            log.error(msg);
        else
            System.err.println(msg);
    }

    /**
     * Loads a theme properties and apply them into the request scope.
     * @param path a file path
     */
    public static void loadProperties(String path) {
        // add ability to load theme properties from a folder
        // @since 6.5.2
        if (!ThemeProperties.loadProperties(ServletFns.getCurrentRequest(), locate(ServletFns.resolveThemeURL(path)))) {
            log("The properties file is not loaded correctly! [" + path + "]");
        }
    }

    private static Color toColor(String color) {
        return Colors.parseCSS(color);
    }

    private enum Browser {
        WebKit("-webkit-", "Chrome10+,Safari5.1+"), W3C("", "W3C"), Firefox("-moz-", "FF3.6+"), Opera("-o-",
                "Opera 11.10+"), IE("-ms-", "IE10+"), IE9("-ms-", "IE9"), Old(null, null), Old_WebKit("-webkit-", "Chrome,Safari4+");

        private final String _template;

        private final String _prefix;

        private HashMap<String, String> _GRAD_TYPE = null;

        Browser(String prefix, String browser) {
            _prefix = prefix;
            if ("IE9".equals(browser)) {
                _template = new StringBuilder(
                        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100%%\" height=\"100%%\" viewBox=\"0 0 1 1\" preserveAspectRatio=\"none\">\n")
                                .append("%3$s\n").append("%6$s\n").append("</linearGradient>\n")
                                .append("<rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill=\"url(#zkie9)\" /></svg>")
                                .toString();
                _GRAD_TYPE = new HashMap<String, String>();
                _GRAD_TYPE.put("ver",
                        "<linearGradient id=\"zkie9\" gradientUnits=\"userSpaceOnUse\" x1=\"0%%\" y1=\"0%%\" x2=\"0%%\" y2=\"100%%\">");
                _GRAD_TYPE.put("hor",
                        "<linearGradient id=\"zkie9\" gradientUnits=\"userSpaceOnUse\" x1=\"0%%\" y1=\"0%%\" x2=\"100%%\" y2=\"0%%\">");
                _GRAD_TYPE.put("diag-",
                        "<linearGradient id=\"zkie9\" gradientUnits=\"userSpaceOnUse\" x1=\"0%%\" y1=\"0%%\" x2=\"100%%\" y2=\"100%%\">");
                _GRAD_TYPE.put("diag+",
                        "<linearGradient id=\"zkie9\" gradientUnits=\"userSpaceOnUse\" x1=\"0%%\" y1=\"100%%\" x2=\"100%%\" y2=\"0%%\">");
                _GRAD_TYPE.put("rad",
                        "<radialGradient id=\"zkie9\" gradientUnits=\"userSpaceOnUse\" cx=\"50%%\" cy=\"50%%\" r=\"50%%\">");
            } else if ("Chrome,Safari4+".equals(browser)) {
                _template = new StringBuilder().append("\t").append(prefix).append("gradient(%4$s, %3$s, %6$s); /* ")
                        .append(browser).append(" */\n").toString();
                _GRAD_TYPE = new HashMap<String, String>();
                _GRAD_TYPE.put("ver", "left top, left bottom");
                _GRAD_TYPE.put("hor", "left top, right top");
                _GRAD_TYPE.put("diag-", "left top, right bottom");
                _GRAD_TYPE.put("diag+", "left bottom, right top");
                _GRAD_TYPE.put("rad", "center center, 0px, center center, 100%");
            } else if (browser != null) {
                _template = new StringBuilder().append("\t").append(prefix).append("%4$s-gradient(%3$s, %6$s); /* ")
                        .append(browser).append(" */\n").toString();
                if ("W3C".equals(browser)) {
                    _GRAD_TYPE = new HashMap<String, String>();
                    _GRAD_TYPE.put("ver", "to bottom");
                    _GRAD_TYPE.put("hor", "to right");
                    _GRAD_TYPE.put("diag-", "135deg");
                    _GRAD_TYPE.put("diag+", "45deg");
                    _GRAD_TYPE.put("rad", "ellipse at center");
                } else {
                    _GRAD_TYPE = new HashMap<String, String>();
                    _GRAD_TYPE.put("ver", "top");
                    _GRAD_TYPE.put("hor", "left");
                    _GRAD_TYPE.put("diag-", "-45deg");
                    _GRAD_TYPE.put("diag+", "45deg");
                    _GRAD_TYPE.put("rad", "center, ellipse cover");
                }
            } else
                _template = "\t%1$s; /* Old browsers */\n";
        }

        public String getPrefix() {
            return _prefix;
        }

        public String getGradient(String dir) {
            return _template.replace("%3$s", getType(dir));
        }

        private String getType(String dir) {
            return _GRAD_TYPE != null ? _GRAD_TYPE.get(dir) : "";
        }
    }

    // the current theme registry
    private static ThemeRegistry _themeRegistry = null;
    // the current theme resolver
    private static ThemeResolver _themeResolver = null;

    /**
     * Returns the current theme registry
     * 
     * @return the current theme registry
     * @since 6.5.2
     */
    public static ThemeRegistry getThemeRegistry() {
        return _themeRegistry;
    }

    /**
     * Change the theme registry
     * 
     * @param themeRegistry the new theme registry
     * @since 6.5.2
     */
    public static void setThemeRegistry(ThemeRegistry themeRegistry) {
        _themeRegistry = themeRegistry;
    }

    /**
     * Returns the current theme resolver
     * 
     * @return the current theme resolver
     * @since 6.5.2
     */
    public static ThemeResolver getThemeResolver() {
        return _themeResolver;
    }

    /**
     * Change the current theme resolver
     * 
     * @param themeResolver the new theme resolver
     */
    public static void setThemeResolver(ThemeResolver themeResolver) {
        _themeResolver = themeResolver;
    }

    private static final String THEME_PREFERRED_KEY = "org.zkoss.theme.preferred";

    /**
     * Returns the current theme name
     * 
     * @return the current theme name
     * @since 6.5.2
     */
    public static String getCurrentTheme() {
        // 1. cookie's key
        String t = getTheme();
        if (_themeRegistry.hasTheme(t))
            return t;

        // 2. library property
        t = Library.getProperty(THEME_PREFERRED_KEY);
        if (_themeRegistry.hasTheme(t))
            return t;

        // 3. theme of highest priority
        Theme[] themes = _themeRegistry.getThemes();
        StandardTheme highest = null;
        Comparator<StandardTheme> comparator = StandardTheme.getComparator();
        for (Theme theme : themes) {
            if (theme instanceof StandardTheme) {
                if (comparator.compare((StandardTheme) theme, highest) < 0) {
                    highest = (StandardTheme) theme;
                }
            }
        }
        return (highest != null) ? highest.getName() : StandardTheme.DEFAULT_NAME;
    }

    /**
     * Returns the theme specified in cookies
     * @return the name of the theme or default theme.
     */
    private static String getTheme() {
        ServletRequest request = ServletFns.getCurrentRequest();

        if (!(request instanceof HttpServletRequest))
            return StandardTheme.DEFAULT_NAME;

        return _themeResolver.getTheme((HttpServletRequest) request);
    }

}