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.logging.log4j.LogManager;
024import org.apache.logging.log4j.Logger;
025
026import javax.servlet.ServletContext;
027import java.io.*;
028import java.util.*;
029import java.util.stream.Collectors;
030import java.nio.file.Files;
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 = LogManager.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, or
083     *  (if absent) based on the Java System Property {@value #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 be
086     *  customized by setting {@value #PARAM_CUSTOMCONFIG} in the server or webapp
087     *  configuration.)
088     *
089     *  <h3>Properties sources</h3>
090     *  The following properties sources are taken into account:
091     *  <ol>
092     *      <li>JSPWiki default properties</li>
093     *      <li>System environment</li>
094     *      <li>JSPWiki custom property files</li>
095     *      <li>JSPWiki cascading properties</li>
096     *      <li>System properties</li>
097     *  </ol>
098     *  With later sources taking precedence over the previous ones. To avoid leaking system information,
099     *  only System environment and properties beginning with {@code jspwiki} (case unsensitive) are taken into account.
100     *  Also, to ease docker integration, System env properties containing "_" are turned into ".". Thus,
101     *  {@code ENV jspwiki_fileSystemProvider_pageDir} is loaded as {@code jspwiki.fileSystemProvider.pageDir}.
102     *
103     *  <h3>Cascading Properties</h3>
104     *  <p>
105     *  You can define additional property files and merge them into the default
106     *  properties file in a similar process to how you define cascading style
107     *  sheets; hence we call this <i>cascading property files</i>. This way you
108     *  can overwrite the default values and only specify the properties you
109     *  need to change in a multiple wiki environment.
110     *  <p>
111     *  You define a cascade in the context mapping of your servlet container.
112     *  <pre>
113     *  jspwiki.custom.cascade.1
114     *  jspwiki.custom.cascade.2
115     *  jspwiki.custom.cascade.3
116     *  </pre>
117     *  and so on. You have to number your cascade in a descending way starting
118     *  with "1". This means you cannot leave out numbers in your cascade. This
119     *  method is based on an idea by Olaf Kaus, see [JSPWiki:MultipleWikis].
120     *  
121     *  @param context A Servlet Context which is used to find the properties
122     *  @return A filled Properties object with all the cascaded properties in place
123     */
124    public static Properties loadWebAppProps( final ServletContext context ) {
125        final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG );
126        try( final InputStream propertyStream = loadCustomPropertiesFile(context, propertyFile) ) {
127            final Properties props = getDefaultProperties();
128
129            // add system env properties beginning with jspwiki...
130            final Map< String, String > env = collectPropertiesFrom( System.getenv() );
131            props.putAll( env );
132
133            if( propertyStream == null ) {
134                LOG.debug( "No custom property file found, relying on JSPWiki defaults." );
135            } else {
136                props.load( propertyStream );
137            }
138
139            // this will add additional properties to the default ones:
140            LOG.debug( "Loading cascading properties..." );
141
142            // now load the cascade (new in 2.5)
143            loadWebAppPropsCascade( context, props );
144
145            // add system properties beginning with jspwiki...
146            final Map< String, String > sysprops = collectPropertiesFrom( System.getProperties().entrySet().stream()
147                                                                                .collect( Collectors.toMap( Object::toString, Object::toString ) ) );
148            props.putAll( sysprops );
149
150            // finally, expand the variables (new in 2.5)
151            expandVars( props );
152
153            return props;
154        } catch( final Exception e ) {
155            LOG.error( "JSPWiki: Unable to load and setup properties from jspwiki.properties. " + e.getMessage(), e );
156        }
157
158        return null;
159    }
160
161    static Map< String, String > collectPropertiesFrom( final Map< String, String > map ) {
162        return map.entrySet().stream()
163                  .filter( entry -> entry.getKey().toLowerCase().startsWith( "jspwiki" ) )
164                  .map( entry -> new AbstractMap.SimpleEntry<>( entry.getKey().replace( "_", "." ), entry.getValue() ) )
165                  .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) );
166    }
167
168    /**
169     * Figure out where our properties lie.
170     * 
171     * @param context servlet context
172     * @param propertyFile property file
173     * @return inputstream holding the properties file
174     * @throws FileNotFoundException properties file not found
175     */
176    static InputStream loadCustomPropertiesFile( final ServletContext context, final String propertyFile ) throws IOException {
177        final InputStream propertyStream;
178        if( propertyFile == null ) {
179            LOG.debug( "No " + PARAM_CUSTOMCONFIG + " defined for this context, looking for custom properties file with default name of: " + CUSTOM_JSPWIKI_CONFIG );
180            //  Use the custom property file at the default location
181            propertyStream =  locateClassPathResource(context, CUSTOM_JSPWIKI_CONFIG);
182        } else {
183            LOG.debug( PARAM_CUSTOMCONFIG + " defined, using " + propertyFile + " as the custom properties file." );
184            propertyStream = Files.newInputStream( new File(propertyFile).toPath() );
185        }
186        return propertyStream;
187    }
188
189
190    /**
191     *  Returns the property set as a Properties object.
192     *
193     *  @return A property set.
194     */
195    public static Properties getDefaultProperties() {
196        final Properties props = new Properties();
197        try( final InputStream in = PropertyReader.class.getResourceAsStream( DEFAULT_JSPWIKI_CONFIG ) ) {
198            if( in != null ) {
199                props.load( in );
200            }
201        } catch( final IOException e ) {
202            LOG.error( "Unable to load default propertyfile '" + DEFAULT_JSPWIKI_CONFIG + "'" + e.getMessage(), e );
203        }
204        
205        return props;
206    }
207
208    /**
209     *  Returns a property set consisting of the default Property Set overlaid with a custom property set
210     *
211     *  @param fileName Reference to the custom override file
212     *  @return A property set consisting of the default property set and custom property set, with
213     *          the latter's properties replacing the former for any common values
214     */
215    public static Properties getCombinedProperties( final String fileName ) {
216        final Properties newPropertySet = getDefaultProperties();
217        try( final InputStream in = PropertyReader.class.getResourceAsStream( fileName ) ) {
218            if( in != null ) {
219                newPropertySet.load( in );
220            } else {
221                LOG.error( "*** Custom property file \"" + fileName + "\" not found, relying on default file alone." );
222            }
223        } catch( final IOException e ) {
224            LOG.error( "Unable to load propertyfile '" + fileName + "'" + e.getMessage(), e );
225        }
226
227        return newPropertySet;
228    }
229
230    /**
231     * Returns the ServletContext Init parameter if has been set, otherwise checks for a System property of the same name. If neither are
232     * defined, returns null. This permits both Servlet- and System-defined cascading properties.
233     */
234    private static String getInitParameter( final ServletContext context, final String name ) {
235        final String value = context.getInitParameter( name );
236        return value != null ? value
237                             : System.getProperty( name ) ;
238    }
239
240
241    /**
242     *  Implement the cascade functionality.
243     *
244     * @param context             where to read the cascade from
245     * @param defaultProperties   properties to merge the cascading properties to
246     * @since 2.5.x
247     */
248    private static void loadWebAppPropsCascade( final ServletContext context, final Properties defaultProperties ) {
249        if( getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + "1" ) == null ) {
250            LOG.debug( " No cascading properties defined for this context" );
251            return;
252        }
253
254        // get into cascade...
255        int depth = 0;
256        while( true ) {
257            depth++;
258            final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + depth );
259            if( propertyFile == null ) {
260                break;
261            }
262
263            try( final InputStream propertyStream = new FileInputStream( propertyFile ) ) {
264                LOG.info( " Reading additional properties from " + propertyFile + " and merge to cascade." );
265                final Properties additionalProps = new Properties();
266                additionalProps.load( propertyStream );
267                defaultProperties.putAll( additionalProps );
268            } catch( final Exception e ) {
269                LOG.error( "JSPWiki: Unable to load and setup properties from " + propertyFile + "." + e.getMessage() );
270            }
271        }
272    }
273
274    /**
275     *  You define a property variable by using the prefix "var.x" as a property. In property values you can then use the "$x" identifier
276     *  to use this variable.
277     *
278     *  For example, you could declare a base directory for all your files like this and use it in all your other property definitions with
279     *  a "$basedir". Note that it does not matter if you define the variable before its usage.
280     *  <pre>
281     *  var.basedir = /p/mywiki;
282     *  jspwiki.fileSystemProvider.pageDir =         $basedir/www/
283     *  jspwiki.basicAttachmentProvider.storageDir = $basedir/www/
284     *  jspwiki.workDir =                            $basedir/wrk/
285     *  </pre>
286     *
287     * @param properties - properties to expand;
288     */
289    public static void expandVars( final Properties properties ) {
290        //get variable name/values from properties...
291        final Map< String, String > vars = new HashMap<>();
292        Enumeration< ? > propertyList = properties.propertyNames();
293        while( propertyList.hasMoreElements() ) {
294            final String propertyName = ( String )propertyList.nextElement();
295            final String propertyValue = properties.getProperty( propertyName );
296
297            if ( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) {
298                final String varName = propertyName.substring( 4 ).trim();
299                final String varValue = propertyValue.trim();
300                vars.put( varName, varValue );
301            }
302        }
303
304        //now, substitute $ values in property values with vars...
305        propertyList = properties.propertyNames();
306        while( propertyList.hasMoreElements() ) {
307            final String propertyName = ( String )propertyList.nextElement();
308            String propertyValue = properties.getProperty( propertyName );
309
310            //skip var properties itself...
311            if( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) {
312                continue;
313            }
314
315            for( final Map.Entry< String, String > entry : vars.entrySet() ) {
316                final String varName = entry.getKey();
317                final String varValue = entry.getValue();
318
319                //replace old property value, using the same variabe. If we don't overwrite
320                //the same one the next loop works with the original one again and
321                //multiple var expansion won't work...
322                propertyValue = TextUtil.replaceString( propertyValue, PARAM_VAR_IDENTIFIER + varName, varValue );
323
324                //add the new PropertyValue to the properties
325                properties.put( propertyName, propertyValue );
326            }
327        }
328    }
329
330    /**
331     * Locate a resource stored in the class path. Try first with "WEB-INF/classes"
332     * from the web app and fallback to "resourceName".
333     *
334     * @param context the servlet context
335     * @param resourceName the name of the resource
336     * @return the input stream of the resource or <b>null</b> if the resource was not found
337     */
338    public static InputStream locateClassPathResource( final ServletContext context, final String resourceName ) {
339        InputStream result;
340        String currResourceLocation;
341
342        // garbage in - garbage out
343        if( StringUtils.isEmpty( resourceName ) ) {
344            return null;
345        }
346
347        // try with web app class loader searching in "WEB-INF/classes"
348        currResourceLocation = createResourceLocation( "/WEB-INF/classes", resourceName );
349        result = context.getResourceAsStream( currResourceLocation );
350        if( result != null ) {
351            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
352            return result;
353        }
354
355        // if not found - try with the current class loader and the given name
356        currResourceLocation = createResourceLocation( "", resourceName );
357        result = PropertyReader.class.getResourceAsStream( currResourceLocation );
358        if( result != null ) {
359            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
360            return result;
361        }
362
363        LOG.debug( " Unable to resolve the following classpath resource : " + resourceName );
364
365        return result;
366    }
367
368    /**
369     * Create a resource location with proper usage of "/".
370     *
371     * @param path a path
372     * @param name a resource name
373     * @return a resource location
374     */
375    static String createResourceLocation( final String path, final String name ) {
376        Validate.notEmpty( name, "name is empty" );
377        final StringBuilder result = new StringBuilder();
378
379        // strip an ending "/"
380        final String sanitizedPath = ( path != null && !path.isEmpty() && path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path );
381
382        // strip leading "/"
383        final String sanitizedName = ( name.startsWith( "/" ) ? name.substring( 1 ) : name );
384
385        // append the optional path
386        if( sanitizedPath != null && !sanitizedPath.isEmpty() ) {
387            if( !sanitizedPath.startsWith( "/" ) ) {
388                result.append( "/" );
389            }
390            result.append( sanitizedPath );
391        }
392        result.append( "/" );
393
394        // append the name
395        result.append( sanitizedName );
396        return result.toString();
397    }
398
399}