001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.    
018 */
019package org.apache.wiki.util;
020
021import org.apache.commons.lang3.StringUtils;
022import org.apache.commons.lang3.Validate;
023import org.apache.log4j.Logger;
024
025import javax.servlet.ServletContext;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileNotFoundException;
029import java.io.IOException;
030import java.io.InputStream;
031import java.util.Enumeration;
032import java.util.HashMap;
033import java.util.Map;
034import java.util.Properties;
035
036
037/**
038 * Property Reader for the WikiEngine. Reads the properties for the WikiEngine
039 * and implements the feature of cascading properties and variable substitution,
040 * which come in handy in a multi wiki installation environment: It reduces the
041 * need for (shell) scripting in order to generate different jspwiki.properties
042 * to a minimum.
043 *
044 * @since 2.5.x
045 */
046public final class PropertyReader {
047    
048    private static final Logger LOG = Logger.getLogger( PropertyReader.class );
049    
050    /**
051     * Path to the base property file, usually overridden by values provided in
052     * a jspwiki-custom.properties file {@value #DEFAULT_JSPWIKI_CONFIG}
053     */
054    public static final String DEFAULT_JSPWIKI_CONFIG = "/ini/jspwiki.properties";
055
056    /**
057     * The servlet context parameter (from web.xml)  that defines where the config file is to be found. If it is not defined, checks
058     * the Java System Property, if that is not defined either, uses the default as defined by DEFAULT_PROPERTYFILE.
059     * {@value #DEFAULT_JSPWIKI_CONFIG}
060     */
061    public static final String PARAM_CUSTOMCONFIG = "jspwiki.custom.config";
062
063    /**
064     *  The prefix when you are cascading properties.  
065     *  
066     *  @see #loadWebAppProps(ServletContext)
067     */
068    public static final String PARAM_CUSTOMCONFIG_CASCADEPREFIX = "jspwiki.custom.cascade.";
069
070    public static final String  CUSTOM_JSPWIKI_CONFIG = "/jspwiki-custom.properties";
071
072    private static final String PARAM_VAR_DECLARATION = "var.";
073    private static final String PARAM_VAR_IDENTIFIER  = "$";
074
075    /**
076     *  Private constructor to prevent instantiation.
077     */
078    private PropertyReader()
079    {}
080
081    /**
082     *  Loads the webapp properties based on servlet context information, 
083     *  or (if absent) based on the Java System Property PARAM_CUSTOMCONFIG .
084     *  Returns a Properties object containing the settings, or null if unable
085     *  to load it. (The default file is ini/jspwiki.properties, and can
086     *  be customized by setting PARAM_CUSTOMCONFIG in the server or webapp
087     *  configuration.)
088     *
089     *  <h3>Cascading Properties</h3>
090     *  <p>
091     *  You can define additional property files and merge them into the default
092     *  properties file in a similar process to how you define cascading style
093     *  sheets; hence we call this <i>cascading property files</i>. This way you
094     *  can overwrite the default values and only specify the properties you
095     *  need to change in a multiple wiki environment.
096     *  <p>
097     *  You define a cascade in the context mapping of your servlet container.
098     *  <pre>
099     *  jspwiki.custom.cascade.1
100     *  jspwiki.custom.cascade.2
101     *  jspwiki.custom.cascade.3
102     *  </pre>
103     *  and so on. You have to number your cascade in a descending way starting
104     *  with "1". This means you cannot leave out numbers in your cascade. This
105     *  method is based on an idea by Olaf Kaus, see [JSPWiki:MultipleWikis].
106     *  
107     *  @param context A Servlet Context which is used to find the properties
108     *  @return A filled Properties object with all the cascaded properties in place
109     */
110    public static Properties loadWebAppProps( final ServletContext context ) {
111        final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG );
112        try( final InputStream propertyStream = loadCustomPropertiesFile(context, propertyFile) ) {
113            final Properties props = getDefaultProperties();
114            if( propertyStream == null ) {
115                LOG.info("No custom property file found, relying on JSPWiki defaults.");
116            } else {
117                props.load( propertyStream );
118            }
119
120            //this will add additional properties to the default ones:
121            LOG.debug( "Loading cascading properties..." );
122
123            //now load the cascade (new in 2.5)
124            loadWebAppPropsCascade( context, props );
125
126            //finally expand the variables (new in 2.5)
127            expandVars( props );
128
129            return props;
130        } catch( final Exception e ) {
131            LOG.error( "JSPWiki: Unable to load and setup properties from jspwiki.properties. " + e.getMessage() );
132        }
133
134        return null;
135    }
136
137    /**
138     * Figure out where our properties lie.
139     * 
140     * @param context servlet context
141     * @param propertyFile property file
142     * @return inputstream holding the properties file
143     * @throws FileNotFoundException properties file not found
144     */
145    static InputStream loadCustomPropertiesFile( final ServletContext context, final String propertyFile ) throws FileNotFoundException {
146        final InputStream propertyStream;
147        if( propertyFile == null ) {
148            LOG.info( "No " + PARAM_CUSTOMCONFIG + " defined for this context, looking for custom properties file with default name of: " + CUSTOM_JSPWIKI_CONFIG );
149            //  Use the custom property file at the default location
150            propertyStream =  locateClassPathResource(context, CUSTOM_JSPWIKI_CONFIG);
151        } else {
152            LOG.info( PARAM_CUSTOMCONFIG + " defined, using " + propertyFile + " as the custom properties file." );
153            propertyStream = new FileInputStream( new File(propertyFile) );
154        }
155        return propertyStream;
156    }
157
158
159    /**
160     *  Returns the property set as a Properties object.
161     *
162     *  @return A property set.
163     */
164    public static Properties getDefaultProperties() {
165        final Properties props = new Properties();
166        try( final InputStream in = PropertyReader.class.getResourceAsStream( DEFAULT_JSPWIKI_CONFIG ) ) {
167            if( in != null ) {
168                props.load( in );
169            }
170        } catch( final IOException e ) {
171            LOG.error( "Unable to load default propertyfile '" + DEFAULT_JSPWIKI_CONFIG + "'" + e.getMessage(), e );
172        }
173        
174        return props;
175    }
176
177    /**
178     *  Returns a property set consisting of the default Property Set overlaid with a custom property set
179     *
180     *  @param fileName Reference to the custom override file
181     *  @return A property set consisting of the default property set and custom property set, with
182     *          the latter's properties replacing the former for any common values
183     */
184    public static Properties getCombinedProperties( final String fileName ) {
185        final Properties newPropertySet = getDefaultProperties();
186        try( final InputStream in = PropertyReader.class.getResourceAsStream( fileName ) ) {
187            if( in != null ) {
188                newPropertySet.load( in );
189            } else {
190                LOG.error( "*** Custom property file \"" + fileName + "\" not found, relying on default file alone." );
191            }
192        } catch( final IOException e ) {
193            LOG.error( "Unable to load propertyfile '" + fileName + "'" + e.getMessage(), e );
194        }
195
196        return newPropertySet;
197    }
198
199    /**
200     * Returns the ServletContext Init parameter if has been set, otherwise checks for a System property of the same name. If neither are
201     * defined, returns null. This permits both Servlet- and System-defined cascading properties.
202     */
203    private static String getInitParameter( final ServletContext context, final String name ) {
204        final String value = context.getInitParameter( name );
205        return value != null ? value
206                             : System.getProperty( name ) ;
207    }
208
209
210    /**
211     *  Implement the cascade functionality.
212     *
213     * @param context             where to read the cascade from
214     * @param defaultProperties   properties to merge the cascading properties to
215     * @since 2.5.x
216     */
217    private static void loadWebAppPropsCascade( final ServletContext context, final Properties defaultProperties ) {
218        if( getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + "1" ) == null ) {
219            LOG.debug( " No cascading properties defined for this context" );
220            return;
221        }
222
223        // get into cascade...
224        int depth = 0;
225        while( true ) {
226            depth++;
227            final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + depth );
228            if( propertyFile == null ) {
229                break;
230            }
231
232            try( final InputStream propertyStream = new FileInputStream( new File( propertyFile ) ) ) {
233                LOG.info( " Reading additional properties from " + propertyFile + " and merge to cascade." );
234                final Properties additionalProps = new Properties();
235                additionalProps.load( propertyStream );
236                defaultProperties.putAll( additionalProps );
237            } catch( final Exception e ) {
238                LOG.error( "JSPWiki: Unable to load and setup properties from " + propertyFile + "." + e.getMessage() );
239            }
240        }
241    }
242
243    /**
244     *  You define a property variable by using the prefix "var.x" as a property. In property values you can then use the "$x" identifier
245     *  to use this variable.
246     *
247     *  For example you could declare a base directory for all your files like this and use it in all your other property definitions with
248     *  a "$basedir". Note that it does not matter if you define the variable before its usage.
249     *  <pre>
250     *  var.basedir = /p/mywiki;
251     *  jspwiki.fileSystemProvider.pageDir =         $basedir/www/
252     *  jspwiki.basicAttachmentProvider.storageDir = $basedir/www/
253     *  jspwiki.workDir =                            $basedir/wrk/
254     *  </pre>
255     *
256     * @param properties - properties to expand;
257     */
258    public static void expandVars( final Properties properties ) {
259        //get variable name/values from properties...
260        final Map< String, String > vars = new HashMap<>();
261        Enumeration< ? > propertyList = properties.propertyNames();
262        while( propertyList.hasMoreElements() ) {
263            final String propertyName = ( String )propertyList.nextElement();
264            final String propertyValue = properties.getProperty( propertyName );
265
266            if ( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) {
267                final String varName = propertyName.substring( 4 ).trim();
268                final String varValue = propertyValue.trim();
269                vars.put( varName, varValue );
270            }
271        }
272
273        //now, substitute $ values in property values with vars...
274        propertyList = properties.propertyNames();
275        while( propertyList.hasMoreElements() ) {
276            final String propertyName = ( String )propertyList.nextElement();
277            String propertyValue = properties.getProperty( propertyName );
278
279            //skip var properties itself...
280            if( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) {
281                continue;
282            }
283
284            for( final Map.Entry< String, String > entry : vars.entrySet() ) {
285                final String varName = entry.getKey();
286                final String varValue = entry.getValue();
287
288                //replace old property value, using the same variabe. If we don't overwrite
289                //the same one the next loop works with the original one again and
290                //multiple var expansion won't work...
291                propertyValue = TextUtil.replaceString( propertyValue, PARAM_VAR_IDENTIFIER + varName, varValue );
292
293                //add the new PropertyValue to the properties
294                properties.put( propertyName, propertyValue );
295            }
296        }
297    }
298
299    /**
300     * Locate a resource stored in the class path. Try first with "WEB-INF/classes"
301     * from the web app and fallback to "resourceName".
302     *
303     * @param context the servlet context
304     * @param resourceName the name of the resource
305     * @return the input stream of the resource or <b>null</b> if the resource was not found
306     */
307    public static InputStream locateClassPathResource( final ServletContext context, final String resourceName ) {
308        InputStream result;
309        String currResourceLocation;
310
311        // garbage in - garbage out
312        if( StringUtils.isEmpty( resourceName ) ) {
313            return null;
314        }
315
316        // try with web app class loader searching in "WEB-INF/classes"
317        currResourceLocation = createResourceLocation( "/WEB-INF/classes", resourceName );
318        result = context.getResourceAsStream( currResourceLocation );
319        if( result != null ) {
320            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
321            return result;
322        }
323
324        // if not found - try with the current class loader and the given name
325        currResourceLocation = createResourceLocation( "", resourceName );
326        result = PropertyReader.class.getResourceAsStream( currResourceLocation );
327        if( result != null ) {
328            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
329            return result;
330        }
331
332        LOG.debug( " Unable to resolve the following classpath resource : " + resourceName );
333
334        return result;
335    }
336
337    /**
338     * Create a resource location with proper usage of "/".
339     *
340     * @param path a path
341     * @param name a resource name
342     * @return a resource location
343     */
344    static String createResourceLocation( final String path, final String name ) {
345        Validate.notEmpty( name, "name is empty" );
346        final StringBuilder result = new StringBuilder();
347
348        // strip an ending "/"
349        final String sanitizedPath = ( path != null && !path.isEmpty() && path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path );
350
351        // strip leading "/"
352        final String sanitizedName = ( name.startsWith( "/" ) ? name.substring( 1 ) : name );
353
354        // append the optional path
355        if( sanitizedPath != null && !sanitizedPath.isEmpty() ) {
356            if( !sanitizedPath.startsWith( "/" ) ) {
357                result.append( "/" );
358            }
359            result.append( sanitizedPath );
360        }
361        result.append( "/" );
362
363        // append the name
364        result.append( sanitizedName );
365        return result.toString();
366    }
367
368}