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}