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