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 }