View Javadoc

    1   package org.apache.turbine.services.ui;
    2
    3   /*
    4    * Licensed to the Apache Software Foundation (ASF) under one
    5    * or more contributor license agreements.  See the NOTICE file
    6    * distributed with this work for additional information
    7    * regarding copyright ownership.  The ASF licenses this file
    8    * to you under the Apache License, Version 2.0 (the
    9    * "License"); you may not use this file except in compliance
    10   * with the License.  You may obtain a copy of the License at
    11   *
    12   *   http://www.apache.org/licenses/LICENSE-2.0
    13   *
    14   * Unless required by applicable law or agreed to in writing,
    15   * software distributed under the License is distributed on an
    16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    17   * KIND, either express or implied.  See the License for the
    18   * specific language governing permissions and limitations
    19   * under the License.
    20   */
    21
    22  import java.io.File;
    23  import java.io.InputStream;
    24  import java.util.Properties;
    25  import java.util.concurrent.ConcurrentHashMap;
    26
    27  import org.apache.commons.configuration.Configuration;
    28  import org.apache.commons.io.IOUtils;
    29  import org.apache.commons.io.filefilter.DirectoryFileFilter;
    30  import org.apache.commons.lang.StringUtils;
    31  import org.apache.commons.logging.Log;
    32  import org.apache.commons.logging.LogFactory;
    33  import org.apache.turbine.Turbine;
    34  import org.apache.turbine.services.InitializationException;
    35  import org.apache.turbine.services.TurbineBaseService;
    36  import org.apache.turbine.services.TurbineServices;
    37  import org.apache.turbine.services.pull.PullService;
    38  import org.apache.turbine.services.pull.tools.UITool;
    39  import org.apache.turbine.services.servlet.ServletService;
    40  import org.apache.turbine.util.ServerData;
    41  import org.apache.turbine.util.uri.DataURI;
    42
    43  /**
    44   * The UI service provides for shared access to User Interface (skin) files,
    45   * as well as the ability for non-default skin files to inherit properties from
    46   * a default skin.  Use TurbineUI to access skin properties from your screen
    47   * classes and action code. UITool is provided as a pull tool for accessing
    48   * skin properties from your templates.
    49   *
    50   * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
    51   * @author <a href="mailto:james_coltman@majorband.co.uk">James Coltman</a>
    52   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    53   * @author <a href="mailto:seade@backstagetech.com.au">Scott Eade</a>
    54   * @author <a href="thomas.vandahl@tewisoft.de">Thomas Vandahl</a>
    55   * @version $Id$
    56   * @see UIService
    57   * @see UITool
    58   */
    59  public class TurbineUIService
    60          extends TurbineBaseService
    61          implements UIService
    62  {
    63      /** Logging. */
    64      private static Log log = LogFactory.getLog(TurbineUIService.class);
    65
    66      /**
    67       * The location of the skins within the application resources directory.
    68       */
    69      private static final String SKINS_DIRECTORY = "/ui/skins";
    70
    71      /**
    72       * The name of the directory where images are stored for this skin.
    73       */
    74      private static final String IMAGES_DIRECTORY = "/images";
    75
    76      /**
    77       * Property tag for the default skin that is to be used for the web
    78       * application.
    79       */
    80      private static final String SKIN_PROPERTY = "tool.ui.skin";
    81
    82      /**
    83       * Property tag for the image directory inside the skin that is to be used
    84       * for the web application.
    85       */
    86      private static final String IMAGEDIR_PROPERTY = "tool.ui.dir.image";
    87
    88      /**
    89       * Property tag for the skin directory that is to be used for the web
    90       * application.
    91       */
    92      private static final String SKINDIR_PROPERTY = "tool.ui.dir.skin";
    93
    94      /**
    95       * Property tag for the css file that is to be used for the web application.
    96       */
    97      private static final String CSS_PROPERTY = "tool.ui.css";
    98
    99      /**
    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 }