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