    001    package org.apache.turbine.services.ui;
    002
    003    /*
    004     * Licensed to the Apache Software Foundation (ASF) under one
    005     * or more contributor license agreements.  See the NOTICE file
    006     * distributed with this work for additional information
    007     * regarding copyright ownership.  The ASF licenses this file
    008     * to you under the Apache License, Version 2.0 (the
    009     * "License"); you may not use this file except in compliance
    010     * with the License.  You may obtain a copy of the License at
    011     *
    012     *   http://www.apache.org/licenses/LICENSE-2.0
    013     *
    014     * Unless required by applicable law or agreed to in writing,
    015     * software distributed under the License is distributed on an
    016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    017     * KIND, either express or implied.  See the License for the
    018     * specific language governing permissions and limitations
    019     * under the License.
    020     */
    021
    022    import java.io.File;
    023    import java.io.InputStream;
    024    import java.util.HashMap;
    025    import java.util.Properties;
    026
    027    import org.apache.commons.configuration.Configuration;
    028    import org.apache.commons.io.filefilter.DirectoryFileFilter;
    029    import org.apache.commons.lang.StringUtils;
    030    import org.apache.commons.logging.Log;
    031    import org.apache.commons.logging.LogFactory;
    032    import org.apache.turbine.Turbine;
    033    import org.apache.turbine.services.InitializationException;
    034    import org.apache.turbine.services.TurbineBaseService;
    035    import org.apache.turbine.services.pull.TurbinePull;
    036    import org.apache.turbine.services.pull.tools.UITool;
    037    import org.apache.turbine.services.servlet.TurbineServlet;
    038    import org.apache.turbine.util.ServerData;
    039    import org.apache.turbine.util.uri.DataURI;
    040
    041    /**
    042     * The UI service provides for shared access to User Interface (skin) files,
    043     * as well as the ability for non-default skin files to inherit properties from
    044     * a default skin.  Use TurbineUI to access skin properties from your screen
    045     * classes and action code. UITool is provided as a pull tool for accessing
    046     * skin properties from your templates.
    047     *
    048     * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
    049     * @author <a href="mailto:james_coltman@majorband.co.uk">James Coltman</a>
    050     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    051     * @author <a href="mailto:seade@backstagetech.com.au">Scott Eade</a>
    052     * @author <a href="thomas.vandahl@tewisoft.de">Thomas Vandahl</a>
    053     * @version $Id$
    054     * @see UIService
    055     * @see UITool
    056     */
    057    public class TurbineUIService
    058            extends TurbineBaseService
    059            implements UIService
    060    {
    061        /** Logging. */
    062        private static Log log = LogFactory.getLog(TurbineUIService.class);
    063
    064        /**
    065         * The location of the skins within the application resources directory.
    066         */
    067        private static final String SKINS_DIRECTORY = "/ui/skins";
    068
    069        /**
    070         * The name of the directory where images are stored for this skin.
    071         */
    072        private static final String IMAGES_DIRECTORY = "/images";
    073
    074        /**
    075         * Property tag for the default skin that is to be used for the web
    076         * application.
    077         */
    078        private static final String SKIN_PROPERTY = "tool.ui.skin";
    079
    080        /**
    081         * Property tag for the image directory inside the skin that is to be used
    082         * for the web application.
    083         */
    084        private static final String IMAGEDIR_PROPERTY = "tool.ui.dir.image";
    085
    086        /**
    087         * Property tag for the skin directory that is to be used for the web
    088         * application.
    089         */
    090        private static final String SKINDIR_PROPERTY = "tool.ui.dir.skin";
    091
    092        /**
    093         * Property tag for the css file that is to be used for the web application.
    094         */
    095        private static final String CSS_PROPERTY = "tool.ui.css";
    096
    097        /**
    098         * Property tag for indicating if relative links are wanted for the web
    099         * application.
    100         */
    101        private static final String RELATIVE_PROPERTY = "tool.ui.want.relative";
    102
    103        /**
    104         * Default skin name. This name refers to a directory in the
    105         * WEBAPP/resources/ui/skins directory. There is a file called skin.props
    106         * which contains the name/value pairs to be made available via the skin.
    107         */
    108        public static final String SKIN_PROPERTY_DEFAULT = "default";
    109
    110        /**
    111         * The skins directory, qualified by the resources directory (which is
    112         * relative to the webapp context). This is used for constructing URIs and
    113         * for retrieving skin files.
    114         */
    115        private String skinsDirectory;
    116
    117        /**
    118         * The file within the skin directory that contains the name/value pairs for
    119         * the skin.
    120         */
    121        private static final String SKIN_PROPS_FILE = "skin.props";
    122
    123        /**
    124         * The file name for the skin style sheet.
    125         */
    126        private static final String DEFAULT_SKIN_CSS_FILE = "skin.css";
    127
    128        /**
    129         * The directory within the skin directory that contains the skin images.
    130         */
    131        private String imagesDirectory;
    132
    133        /**
    134         * The name of the css file within the skin directory.
    135         */
    136        private String cssFile;
    137
    138        /**
    139         * The flag that determines if the links that are returned are are absolute
    140         * or relative.
    141         */
    142        private boolean wantRelative = false;
    143
    144        /**
    145         * The skin Properties store.
    146         */
    147        private HashMap<String, Properties> skins = new HashMap<String, Properties>();
    148
    149        /**
    150         * Refresh the service by clearing all skins.
    151         */
    152        public void refresh()
    153        {
    154            clearSkins();
    155        }
    156
    157        /**
    158         * Refresh a particular skin by clearing it.
    159         *
    160         * @param skinName the name of the skin to clear.
    161         */
    162        public void refresh(String skinName)
    163        {
    164            clearSkin(skinName);
    165        }
    166
    167        /**
    168         * Retrieve the Properties for a specific skin.  If they are not yet loaded
    169         * they will be.  If the specified skin does not exist properties for the
    170         * default skin configured for the webapp will be returned and an error
    171         * level message will be written to the log.  If the webapp skin does not
    172         * exist the default skin will be used and id that doesn't exist an empty
    173         * Properties will be returned.
    174         *
    175         * @param skinName the name of the skin whose properties are to be
    176         * retrieved.
    177         * @return the Properties for the named skin or the properties for the
    178         * default skin configured for the webapp if the named skin does not exist.
    179         */
    180        private Properties getSkinProperties(String skinName)
    181        {
    182            Properties skinProperties = skins.get(skinName);
    183            return null != skinProperties ? skinProperties : loadSkin(skinName);
    184        }
    185
    186        /**
    187         * Retrieve a skin property from the named skin.  If the property is not
    188         * defined in the named skin the value for the default skin will be
    189         * provided.  If the named skin does not exist then the skin configured for
    190         * the webapp will be used.  If the webapp skin does not exist the default
    191         * skin will be used.  If the default skin does not exist then
    192         * <code>null</code> will be returned.
    193         *
    194         * @param skinName the name of the skin to retrieve the property from.
    195         * @param key the key to retrieve from the skin.
    196         * @return the value of the property for the named skin (defaulting to the
    197         * default skin), the webapp skin, the default skin or <code>null</code>,
    198         * depending on whether or not the property or skins exist.
    199         */
    200        public String get(String skinName, String key)
    201        {
    202            Properties skinProperties = getSkinProperties(skinName);
    203            return skinProperties.getProperty(key);
    204        }
    205
    206        /**
    207         * Retrieve a skin property from the default skin for the webapp.  If the
    208         * property is not defined in the webapp skin the value for the default skin
    209         * will be provided.  If the webapp skin does not exist the default skin
    210         * will be used.  If the default skin does not exist then <code>null</code>
    211         * will be returned.
    212         *
    213         * @param key the key to retrieve.
    214         * @return the value of the property for the webapp skin (defaulting to the
    215         * default skin), the default skin or <code>null</code>, depending on
    216         * whether or not the property or skins exist.
    217         */
    218        public String get(String key)
    219        {
    220            return get(getWebappSkinName(), key);
    221        }
    222
    223        /**
    224         * Provide access to the list of available skin names.
    225         *
    226         * @return the available skin names.
    227         */
    228        public String[] getSkinNames()
    229        {
    230            File skinsDir = new File(TurbineServlet.getRealPath(skinsDirectory));
    231            return skinsDir.list(DirectoryFileFilter.INSTANCE);
    232        }
    233
    234        /**
    235         * Clear the map of stored skins.
    236         */
    237        private void clearSkins()
    238        {
    239            synchronized (skins)
    240            {
    241                skins = new HashMap<String, Properties>();
    242            }
    243            log.debug("All skins were cleared.");
    244        }
    245
    246        /**
    247         * Clear a particular skin from the map of stored skins.
    248         *
    249         * @param skinName the name of the skin to clear.
    250         */
    251        private void clearSkin(String skinName)
    252        {
    253            synchronized (skins)
    254            {
    255                if (!skinName.equals(SKIN_PROPERTY_DEFAULT))
    256                {
    257                    skins.remove(SKIN_PROPERTY_DEFAULT);
    258                }
    259                skins.remove(skinName);
    260            }
    261            log.debug("The skin \"" + skinName
    262                    + "\" was cleared (will also clear \"default\" skin).");
    263        }
    264
    265        /**
    266         * Load the specified skin.
    267         *
    268         * @param skinName the name of the skin to load.
    269         * @return the Properties for the named skin if it exists, or the skin
    270         * configured for the web application if it does not exist, or the default
    271         * skin if that does not exist, or an empty Parameters object if even that
    272         * cannot be found.
    273         */
    274        private synchronized Properties loadSkin(String skinName)
    275        {
    276            Properties defaultSkinProperties = null;
    277
    278            if (!StringUtils.equals(skinName, SKIN_PROPERTY_DEFAULT))
    279            {
    280                defaultSkinProperties = getSkinProperties(SKIN_PROPERTY_DEFAULT);
    281            }
    282
    283            // The following line is okay even for default.
    284            Properties skinProperties = new Properties(defaultSkinProperties);
    285
    286            StringBuffer sb = new StringBuffer();
    287            sb.append('/').append(skinsDirectory);
    288            sb.append('/').append(skinName);
    289            sb.append('/').append(SKIN_PROPS_FILE);
    290            if (log.isDebugEnabled())
    291            {
    292                log.debug("Loading selected skin from: " + sb.toString());
    293            }
    294
    295            try
    296            {
    297                // This will NPE if the directory associated with the skin does not
    298                // exist, but it is habdled correctly below.
    299                InputStream is = TurbineServlet.getResourceAsStream(sb.toString());
    300
    301                skinProperties.load(is);
    302            }
    303            catch (Exception e)
    304            {
    305                log.error("Cannot load skin: " + skinName + ", from: "
    306                        + sb.toString(), e);
    307                if (!StringUtils.equals(skinName, getWebappSkinName())
    308                        && !StringUtils.equals(skinName, SKIN_PROPERTY_DEFAULT))
    309                {
    310                    log.error("Attempting to return the skin configured for "
    311                            + "webapp instead of " + skinName);
    312                    return getSkinProperties(getWebappSkinName());
    313                }
    314                else if (!StringUtils.equals(skinName, SKIN_PROPERTY_DEFAULT))
    315                {
    316                    log.error("Return the default skin instead of " + skinName);
    317                    return skinProperties; // Already contains the default skin.
    318                }
    319                else
    320                {
    321                    log.error("No skins available - returning an empty Properties");
    322                    return new Properties();
    323                }
    324            }
    325
    326            // Replace in skins HashMap
    327            synchronized (skins)
    328            {
    329                skins.put(skinName, skinProperties);
    330            }
    331
    332            return skinProperties;
    333        }
    334
    335        /**
    336         * Get the name of the default skin name for the web application from the
    337         * TurbineResources.properties file. If the property is not present the
    338         * name of the default skin will be returned.  Note that the web application
    339         * skin name may be something other than default, in which case its
    340         * properties will default to the skin with the name "default".
    341         *
    342         * @return the name of the default skin for the web application.
    343         */
    344        public String getWebappSkinName()
    345        {
    346            return Turbine.getConfiguration()
    347                    .getString(SKIN_PROPERTY, SKIN_PROPERTY_DEFAULT);
    348        }
    349
    350        /**
    351         * Retrieve the URL for an image that is part of a skin. The images are
    352         * stored in the WEBAPP/resources/ui/skins/[SKIN]/images directory.
    353         *
    354         * <p>Use this if for some reason your server name, server scheme, or server
    355         * port change on a per request basis. I'm not sure if this would happen in
    356         * a load balanced situation. I think in most cases the image(String image)
    357         * method would probably be enough, but I'm not absolutely positive.
    358         *
    359         * @param skinName the name of the skin to retrieve the image from.
    360         * @param imageId the id of the image whose URL will be generated.
    361         * @param serverData the serverData to use as the basis for the URL.
    362         */
    363        public String image(String skinName, String imageId, ServerData serverData)
    364        {
    365            return getSkinResource(serverData, skinName, imagesDirectory, imageId);
    366        }
    367
    368        /**
    369         * Retrieve the URL for an image that is part of a skin. The images are
    370         * stored in the WEBAPP/resources/ui/skins/[SKIN]/images directory.
    371         *
    372         * @param skinName the name of the skin to retrieve the image from.
    373         * @param imageId the id of the image whose URL will be generated.
    374         */
    375        public String image(String skinName, String imageId)
    376        {
    377            return image(skinName, imageId, Turbine.getDefaultServerData());
    378        }
    379
    380        /**
    381         * Retrieve the URL for the style sheet that is part of a skin. The style is
    382         * stored in the WEBAPP/resources/ui/skins/[SKIN] directory with the
    383         * filename skin.css
    384         *
    385         * <p>Use this if for some reason your server name, server scheme, or server
    386         * port change on a per request basis. I'm not sure if this would happen in
    387         * a load balanced situation. I think in most cases the style() method would
    388         * probably be enough, but I'm not absolutely positive.
    389         *
    390         * @param skinName the name of the skin to retrieve the style sheet from.
    391         * @param serverData the serverData to use as the basis for the URL.
    392         */
    393        public String getStylecss(String skinName, ServerData serverData)
    394        {
    395            return getSkinResource(serverData, skinName, null, cssFile);
    396        }
    397
    398        /**
    399         * Retrieve the URL for the style sheet that is part of a skin. The style is
    400         * stored in the WEBAPP/resources/ui/skins/[SKIN] directory with the
    401         * filename skin.css
    402         *
    403         * @param skinName the name of the skin to retrieve the style sheet from.
    404         */
    405        public String getStylecss(String skinName)
    406        {
    407            return getStylecss(skinName, Turbine.getDefaultServerData());
    408        }
    409
    410        /**
    411         * Retrieve the URL for a given script that is part of a skin. The script is
    412         * stored in the WEBAPP/resources/ui/skins/[SKIN] directory.
    413         *
    414         * <p>Use this if for some reason your server name, server scheme, or server
    415         * port change on a per request basis. I'm not sure if this would happen in
    416         * a load balanced situation. I think in most cases the style() method would
    417         * probably be enough, but I'm not absolutely positive.
    418         *
    419         * @param skinName the name of the skin to retrieve the image from.
    420         * @param filename the name of the script file.
    421         * @param serverData the serverData to use as the basis for the URL.
    422         */
    423        public String getScript(String skinName, String filename,
    424                ServerData serverData)
    425        {
    426            return getSkinResource(serverData, skinName, null, filename);
    427        }
    428
    429        /**
    430         * Retrieve the URL for a given script that is part of a skin. The script is
    431         * stored in the WEBAPP/resources/ui/skins/[SKIN] directory.
    432         *
    433         * @param skinName the name of the skin to retrieve the image from.
    434         * @param filename the name of the script file.
    435         */
    436        public String getScript(String skinName, String filename)
    437        {
    438            return getScript(skinName, filename, Turbine.getDefaultServerData());
    439        }
    440
    441        private String stripSlashes(final String path)
    442        {
    443            if (StringUtils.isEmpty(path))
    444            {
    445                return "";
    446            }
    447
    448            String ret = path;
    449            int len = ret.length() - 1;
    450
    451            if (ret.charAt(len) == '/')
    452            {
    453                ret = ret.substring(0, len);
    454            }
    455
    456            if (len > 0 && ret.charAt(0) == '/')
    457            {
    458                ret = ret.substring(1);
    459            }
    460
    461            return ret;
    462        }
    463
    464        /**
    465         * Construct the URL to the skin resource.
    466         *
    467         * @param serverData the serverData to use as the basis for the URL.
    468         * @param skinName the name of the skin.
    469         * @param subDir the sub-directory in which the resource resides or
    470         * <code>null</code> if it is in the root directory of the skin.
    471         * @param resourceName the name of the resource to be retrieved.
    472         * @return the path to the resource.
    473         */
    474        private String getSkinResource(ServerData serverData, String skinName,
    475                String subDir, String resourceName)
    476        {
    477            StringBuffer sb = new StringBuffer(skinsDirectory);
    478            sb.append("/").append(skinName);
    479            if (subDir != null)
    480            {
    481                sb.append("/").append(subDir);
    482            }
    483            sb.append("/").append(stripSlashes(resourceName));
    484
    485            DataURI du = new DataURI(serverData);
    486            du.setScriptName(sb.toString());
    487            return wantRelative ? du.getRelativeLink() : du.getAbsoluteLink();
    488        }
    489
    490        // ---- Service initilization ------------------------------------------
    491
    492        /**
    493         * Initializes the service.
    494         */
    495        @Override
    496        public void init() throws InitializationException
    497        {
    498            Configuration cfg = Turbine.getConfiguration();
    499
    500            // Get the resources directory that is specified in the TR.props or
    501            // default to "resources", relative to the webapp.
    502            StringBuffer sb = new StringBuffer();
    503            sb.append(stripSlashes(TurbinePull.getResourcesDirectory()));
    504            sb.append("/");
    505            sb.append(stripSlashes(
    506                    cfg.getString(SKINDIR_PROPERTY, SKINS_DIRECTORY)));
    507            skinsDirectory = sb.toString();
    508
    509            imagesDirectory = stripSlashes(
    510                    cfg.getString(IMAGEDIR_PROPERTY, IMAGES_DIRECTORY));
    511            cssFile = cfg.getString(CSS_PROPERTY, DEFAULT_SKIN_CSS_FILE);
    512            wantRelative = cfg.getBoolean(RELATIVE_PROPERTY, false);
    513
    514            setInit(true);
    515        }
    516
    517        /**
    518         * Returns to uninitialized state.
    519         */
    520        @Override
    521        public void shutdown()
    522        {
    523            clearSkins();
    524
    525            setInit(false);
    526        }
    527
    528    }