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