    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}