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 }