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