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.configuration2.Configuration;
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.logging.log4j.LogManager;
30  import org.apache.logging.log4j.Logger;
31  import org.apache.turbine.Turbine;
32  import org.apache.turbine.services.InitializationException;
33  import org.apache.turbine.services.TurbineBaseService;
34  import org.apache.turbine.services.TurbineServices;
35  import org.apache.turbine.services.pull.PullService;
36  import org.apache.turbine.services.pull.tools.UITool;
37  import org.apache.turbine.services.servlet.ServletService;
38  import org.apache.turbine.util.ServerData;
39  import org.apache.turbine.util.uri.DataURI;
40  
41  /**
42   * The UI service provides for shared access to User Interface (skin) files,
43   * as well as the ability for non-default skin files to inherit properties from
44   * a default skin.  Use TurbineUI to access skin properties from your screen
45   * classes and action code. UITool is provided as a pull tool for accessing
46   * skin properties from your templates.
47   *
48   * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
49   * @author <a href="mailto:james_coltman@majorband.co.uk">James Coltman</a>
50   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
51   * @author <a href="mailto:seade@backstagetech.com.au">Scott Eade</a>
52   * @author <a href="thomas.vandahl@tewisoft.de">Thomas Vandahl</a>
53   * @version $Id$
54   * @see UIService
55   * @see UITool
56   */
57  public class TurbineUIService
58          extends TurbineBaseService
59          implements UIService
60  {
61      /** Logging. */
62      private static final Logger log = LogManager.getLogger(TurbineUIService.class);
63  
64      /**
65       * The location of the skins within the application resources directory.
66       */
67      private static final String SKINS_DIRECTORY = "/ui/skins";
68  
69      /**
70       * The name of the directory where images are stored for this skin.
71       */
72      private static final String IMAGES_DIRECTORY = "/images";
73  
74      /**
75       * Property tag for the default skin that is to be used for the web
76       * application.
77       */
78      private static final String SKIN_PROPERTY = "tool.ui.skin";
79  
80      /**
81       * Property tag for the image directory inside the skin that is to be used
82       * for the web application.
83       */
84      private static final String IMAGEDIR_PROPERTY = "tool.ui.dir.image";
85  
86      /**
87       * Property tag for the skin directory that is to be used for the web
88       * application.
89       */
90      private static final String SKINDIR_PROPERTY = "tool.ui.dir.skin";
91  
92      /**
93       * Property tag for the css file that is to be used for the web application.
94       */
95      private static final String CSS_PROPERTY = "tool.ui.css";
96  
97      /**
98       * Property tag for indicating if relative links are wanted for the web
99       * 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 servlet service.
130      */
131     private ServletService servletService;
132 
133     /**
134      * The directory within the skin directory that contains the skin images.
135      */
136     private String imagesDirectory;
137 
138     /**
139      * The name of the css file within the skin directory.
140      */
141     private String cssFile;
142 
143     /**
144      * The flag that determines if the links that are returned are are absolute
145      * or relative.
146      */
147     private boolean wantRelative = false;
148 
149     /**
150      * The skin Properties store.
151      */
152     private ConcurrentHashMap<String, Properties> skins = new ConcurrentHashMap<>();
153 
154     /**
155      * Refresh the service by clearing all skins.
156      */
157     @Override
158     public void refresh()
159     {
160         clearSkins();
161     }
162 
163     /**
164      * Refresh a particular skin by clearing it.
165      *
166      * @param skinName the name of the skin to clear.
167      */
168     @Override
169     public void refresh(String skinName)
170     {
171         clearSkin(skinName);
172     }
173 
174     /**
175      * Retrieve the Properties for a specific skin.  If they are not yet loaded
176      * they will be.  If the specified skin does not exist properties for the
177      * default skin configured for the webapp will be returned and an error
178      * level message will be written to the log.  If the webapp skin does not
179      * exist the default skin will be used and id that doesn't exist an empty
180      * Properties will be returned.
181      *
182      * @param skinName the name of the skin whose properties are to be
183      * retrieved.
184      * @return the Properties for the named skin or the properties for the
185      * default skin configured for the webapp if the named skin does not exist.
186      */
187     private Properties getSkinProperties(String skinName)
188     {
189         Properties skinProperties = skins.get(skinName);
190         return null != skinProperties ? skinProperties : loadSkin(skinName);
191     }
192 
193     /**
194      * Retrieve a skin property from the named skin.  If the property is not
195      * defined in the named skin the value for the default skin will be
196      * provided.  If the named skin does not exist then the skin configured for
197      * the webapp will be used.  If the webapp skin does not exist the default
198      * skin will be used.  If the default skin does not exist then
199      * <code>null</code> will be returned.
200      *
201      * @param skinName the name of the skin to retrieve the property from.
202      * @param key the key to retrieve from the skin.
203      * @return the value of the property for the named skin (defaulting to the
204      * default skin), the webapp skin, the default skin or <code>null</code>,
205      * depending on whether or not the property or skins exist.
206      */
207     @Override
208     public String get(String skinName, String key)
209     {
210         Properties skinProperties = getSkinProperties(skinName);
211         return skinProperties.getProperty(key);
212     }
213 
214     /**
215      * Retrieve a skin property from the default skin for the webapp.  If the
216      * property is not defined in the webapp skin the value for the default skin
217      * will be provided.  If the webapp skin does not exist the default skin
218      * will be used.  If the default skin does not exist then <code>null</code>
219      * will be returned.
220      *
221      * @param key the key to retrieve.
222      * @return the value of the property for the webapp skin (defaulting to the
223      * default skin), the default skin or <code>null</code>, depending on
224      * whether or not the property or skins exist.
225      */
226     @Override
227     public String get(String key)
228     {
229         return get(getWebappSkinName(), key);
230     }
231 
232     /**
233      * Provide access to the list of available skin names.
234      *
235      * @return the available skin names.
236      */
237     @Override
238     public String[] getSkinNames()
239     {
240         File skinsDir = new File(servletService.getRealPath(skinsDirectory));
241         return skinsDir.list((dir, name) -> {
242             File directory = new File(dir, name);
243             return directory.isDirectory();
244         });
245     }
246 
247     /**
248      * Clear the map of stored skins.
249      */
250     private void clearSkins()
251     {
252         skins.clear();
253         log.debug("All skins were cleared.");
254     }
255 
256     /**
257      * Clear a particular skin from the map of stored skins.
258      *
259      * @param skinName the name of the skin to clear.
260      */
261     private void clearSkin(String skinName)
262     {
263         if (!skinName.equals(SKIN_PROPERTY_DEFAULT))
264         {
265             skins.remove(SKIN_PROPERTY_DEFAULT);
266         }
267         skins.remove(skinName);
268         log.debug("The skin \"{}\" was cleared (will also clear \"default\" skin).", skinName);
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         log.debug("Loading selected skin from: {}", sb::toString);
297 
298         try (InputStream is = servletService.getResourceAsStream(sb.toString()))
299         {
300             // This will NPE if the directory associated with the skin does not
301             // exist, but it is handled correctly below.
302             skinProperties.load(is);
303         }
304         catch (Exception e)
305         {
306             log.error("Cannot load skin: {}, from: {}", skinName, 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 webapp instead of {}", skinName);
311                 return getSkinProperties(getWebappSkinName());
312             }
313             else if (!StringUtils.equals(skinName, SKIN_PROPERTY_DEFAULT))
314             {
315                 log.error("Return the default skin instead of {}", skinName);
316                 return skinProperties; // Already contains the default skin.
317             }
318             else
319             {
320                 log.error("No skins available - returning an empty Properties");
321                 return new Properties();
322             }
323         }
324 
325         // Replace in skins HashMap
326         skins.put(skinName, skinProperties);
327 
328         return skinProperties;
329     }
330 
331     /**
332      * Get the name of the default skin name for the web application from the
333      * TurbineResources.properties file. If the property is not present the
334      * name of the default skin will be returned.  Note that the web application
335      * skin name may be something other than default, in which case its
336      * properties will default to the skin with the name "default".
337      *
338      * @return the name of the default skin for the web application.
339      */
340     @Override
341     public String getWebappSkinName()
342     {
343         return Turbine.getConfiguration()
344                 .getString(SKIN_PROPERTY, SKIN_PROPERTY_DEFAULT);
345     }
346 
347     /**
348      * Retrieve the URL for an image that is part of a skin. The images are
349      * stored in the WEBAPP/resources/ui/skins/[SKIN]/images directory.
350      *
351      * <p>Use this if for some reason your server name, server scheme, or server
352      * port change on a per request basis. I'm not sure if this would happen in
353      * a load balanced situation. I think in most cases the image(String image)
354      * method would probably be enough, but I'm not absolutely positive.
355      *
356      * @param skinName the name of the skin to retrieve the image from.
357      * @param imageId the id of the image whose URL will be generated.
358      * @param serverData the serverData to use as the basis for the URL.
359      */
360     @Override
361     public String image(String skinName, String imageId, ServerData serverData)
362     {
363         return getSkinResource(serverData, skinName, imagesDirectory, imageId);
364     }
365 
366     /**
367      * Retrieve the URL for an image that is part of a skin. The images are
368      * stored in the WEBAPP/resources/ui/skins/[SKIN]/images directory.
369      *
370      * @param skinName the name of the skin to retrieve the image from.
371      * @param imageId the id of the image whose URL will be generated.
372      */
373     @Override
374     public String image(String skinName, String imageId)
375     {
376         return image(skinName, imageId, Turbine.getDefaultServerData());
377     }
378 
379     /**
380      * Retrieve the URL for the style sheet that is part of a skin. The style is
381      * stored in the WEBAPP/resources/ui/skins/[SKIN] directory with the
382      * filename skin.css
383      *
384      * <p>Use this if for some reason your server name, server scheme, or server
385      * port change on a per request basis. I'm not sure if this would happen in
386      * a load balanced situation. I think in most cases the style() method would
387      * probably be enough, but I'm not absolutely positive.
388      *
389      * @param skinName the name of the skin to retrieve the style sheet from.
390      * @param serverData the serverData to use as the basis for the URL.
391      */
392     @Override
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     @Override
406     public String getStylecss(String skinName)
407     {
408         return getStylecss(skinName, Turbine.getDefaultServerData());
409     }
410 
411     /**
412      * Retrieve the URL for a given script that is part of a skin. The script is
413      * stored in the WEBAPP/resources/ui/skins/[SKIN] directory.
414      *
415      * <p>Use this if for some reason your server name, server scheme, or server
416      * port change on a per request basis. I'm not sure if this would happen in
417      * a load balanced situation. I think in most cases the style() method would
418      * probably be enough, but I'm not absolutely positive.
419      *
420      * @param skinName the name of the skin to retrieve the image from.
421      * @param filename the name of the script file.
422      * @param serverData the serverData to use as the basis for the URL.
423      */
424     @Override
425     public String getScript(String skinName, String filename,
426             ServerData serverData)
427     {
428         return getSkinResource(serverData, skinName, null, filename);
429     }
430 
431     /**
432      * Retrieve the URL for a given script that is part of a skin. The script is
433      * stored in the WEBAPP/resources/ui/skins/[SKIN] directory.
434      *
435      * @param skinName the name of the skin to retrieve the image from.
436      * @param filename the name of the script file.
437      */
438     @Override
439     public String getScript(String skinName, String filename)
440     {
441         return getScript(skinName, filename, Turbine.getDefaultServerData());
442     }
443 
444     private String stripSlashes(final String path)
445     {
446         if (StringUtils.isEmpty(path))
447         {
448             return "";
449         }
450 
451         String ret = path;
452         int len = ret.length() - 1;
453 
454         if (ret.charAt(len) == '/')
455         {
456             ret = ret.substring(0, len);
457         }
458 
459         if (len > 0 && ret.charAt(0) == '/')
460         {
461             ret = ret.substring(1);
462         }
463 
464         return ret;
465     }
466 
467     /**
468      * Construct the URL to the skin resource.
469      *
470      * @param serverData the serverData to use as the basis for the URL.
471      * @param skinName the name of the skin.
472      * @param subDir the sub-directory in which the resource resides or
473      * <code>null</code> if it is in the root directory of the skin.
474      * @param resourceName the name of the resource to be retrieved.
475      * @return the path to the resource.
476      */
477     private String getSkinResource(ServerData serverData, String skinName,
478             String subDir, String resourceName)
479     {
480         StringBuilder sb = new StringBuilder(skinsDirectory);
481         sb.append("/").append(skinName);
482         if (subDir != null)
483         {
484             sb.append("/").append(subDir);
485         }
486         sb.append("/").append(stripSlashes(resourceName));
487 
488         DataURIl/uri/DataURI.html#DataURI">DataURI du = new DataURI(serverData);
489         du.setScriptName(sb.toString());
490         return wantRelative ? du.getRelativeLink() : du.getAbsoluteLink();
491     }
492 
493     // ---- Service initilization ------------------------------------------
494 
495     /**
496      * Initializes the service.
497      */
498     @Override
499     public void init() throws InitializationException
500     {
501         Configuration cfg = Turbine.getConfiguration();
502 
503         servletService = (ServletService)TurbineServices.getInstance().getService(ServletService.SERVICE_NAME);
504         PullService./../org/apache/turbine/services/pull/PullService.html#PullService">PullService pullService = (PullService)TurbineServices.getInstance().getService(PullService.SERVICE_NAME);
505         // Get the resources directory that is specified in the TR.props or
506         // default to "resources", relative to the webapp.
507         StringBuilder sb = new StringBuilder();
508         sb.append(stripSlashes(pullService.getResourcesDirectory()));
509         sb.append("/");
510         sb.append(stripSlashes(
511                 cfg.getString(SKINDIR_PROPERTY, SKINS_DIRECTORY)));
512         skinsDirectory = sb.toString();
513 
514         imagesDirectory = stripSlashes(
515                 cfg.getString(IMAGEDIR_PROPERTY, IMAGES_DIRECTORY));
516         cssFile = cfg.getString(CSS_PROPERTY, DEFAULT_SKIN_CSS_FILE);
517         wantRelative = cfg.getBoolean(RELATIVE_PROPERTY, false);
518 
519         setInit(true);
520     }
521 
522     /**
523      * Returns to uninitialized state.
524      */
525     @Override
526     public void shutdown()
527     {
528         clearSkins();
529         setInit(false);
530     }
531 }