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}