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.HashMap;
25 import java.util.Properties;
26
27 import org.apache.commons.configuration.Configuration;
28 import org.apache.commons.io.filefilter.DirectoryFileFilter;
29 import org.apache.commons.lang.StringUtils;
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32 import org.apache.turbine.Turbine;
33 import org.apache.turbine.services.InitializationException;
34 import org.apache.turbine.services.TurbineBaseService;
35 import org.apache.turbine.services.pull.TurbinePull;
36 import org.apache.turbine.services.pull.tools.UITool;
37 import org.apache.turbine.services.servlet.TurbineServlet;
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 Log log = LogFactory.getLog(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 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 }