    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.HashMap;
    025import java.util.Properties;
    026
    027import org.apache.commons.configuration.Configuration;
    028import org.apache.commons.io.filefilter.DirectoryFileFilter;
    029import org.apache.commons.lang.StringUtils;
    030import org.apache.commons.logging.Log;
    031import org.apache.commons.logging.LogFactory;
    032import org.apache.turbine.Turbine;
    033import org.apache.turbine.services.InitializationException;
    034import org.apache.turbine.services.TurbineBaseService;
    035import org.apache.turbine.services.pull.TurbinePull;
    036import org.apache.turbine.services.pull.tools.UITool;
    037import org.apache.turbine.services.servlet.TurbineServlet;
    038import org.apache.turbine.util.ServerData;
    039import 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 */
    057public 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        StringBuilder sb = new StringBuilder();
    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        StringBuilder sb = new StringBuilder(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        StringBuilder sb = new StringBuilder();
    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}