001package 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
022import java.io.File;
023import java.io.InputStream;
024import java.util.Properties;
025import java.util.concurrent.ConcurrentHashMap;
026
027import org.apache.commons.configuration2.Configuration;
028import org.apache.commons.lang3.StringUtils;
029import org.apache.logging.log4j.LogManager;
030import org.apache.logging.log4j.Logger;
031import org.apache.turbine.Turbine;
032import org.apache.turbine.services.InitializationException;
033import org.apache.turbine.services.TurbineBaseService;
034import org.apache.turbine.services.TurbineServices;
035import org.apache.turbine.services.pull.PullService;
036import org.apache.turbine.services.pull.tools.UITool;
037import org.apache.turbine.services.servlet.ServletService;
038import org.apache.turbine.util.ServerData;
039import 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 */
057public class TurbineUIService
058        extends TurbineBaseService
059        implements UIService
060{
061    /** Logging. */
062    private static final Logger log = LogManager.getLogger(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 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        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 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}