    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, {@value}, usually overridden by values provided in
    049     * a jspwiki-custom.properties file.
    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            // property expansion so we can resolve things like ${TOMCAT_HOME}
    143            propertyExpansion( props );
    144
    145            // sets the JSPWiki working directory (jspwiki.workDir)
    146            setWorkDir( context, props );
    147
    148            // add system properties beginning with jspwiki...
    149            final Map< String, String > sysprops = collectPropertiesFrom( System.getProperties().entrySet().stream()
    150                                                                                .collect( Collectors.toMap( Object::toString, Object::toString ) ) );
    151            props.putAll( sysprops );
    152
    153            // finally, expand the variables (new in 2.5)
    154            expandVars( props );
    155
    156            return props;
    157        } catch( final Exception e ) {
    158            LOG.error( "JSPWiki: Unable to load and setup properties from jspwiki.properties. " + e.getMessage(), e );
    159        }
    160
    161        return null;
    162    }
    163
    164    static Map< String, String > collectPropertiesFrom( final Map< String, String > map ) {
    165        return map.entrySet().stream()
    166                  .filter( entry -> entry.getKey().toLowerCase().startsWith( "jspwiki" ) )
    167                  .map( entry -> new AbstractMap.SimpleEntry<>( entry.getKey().replace( "_", "." ), entry.getValue() ) )
    168                  .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) );
    169    }
    170
    171    /**
    172     * Figure out where our properties lie.
    173     *
    174     * @param context servlet context
    175     * @param propertyFile property file
    176     * @return InputStream holding the properties file
    177     * @throws FileNotFoundException properties file not found
    178     */
    179    static InputStream loadCustomPropertiesFile( final ServletContext context, final String propertyFile ) throws IOException {
    180        final InputStream propertyStream;
    181        if( propertyFile == null ) {
    182            LOG.debug( "No " + PARAM_CUSTOMCONFIG + " defined for this context, looking for custom properties file with default name of: " + CUSTOM_JSPWIKI_CONFIG );
    183            //  Use the custom property file at the default location
    184            propertyStream =  locateClassPathResource(context, CUSTOM_JSPWIKI_CONFIG);
    185        } else {
    186            LOG.debug( PARAM_CUSTOMCONFIG + " defined, using " + propertyFile + " as the custom properties file." );
    187            propertyStream = Files.newInputStream( new File(propertyFile).toPath() );
    188        }
    189        return propertyStream;
    190    }
    191
    192
    193    /**
    194     *  Returns the property set as a Properties object.
    195     *
    196     *  @return A property set.
    197     */
    198    public static Properties getDefaultProperties() {
    199        final Properties props = new Properties();
    200        try( final InputStream in = PropertyReader.class.getResourceAsStream( DEFAULT_JSPWIKI_CONFIG ) ) {
    201            if( in != null ) {
    202                props.load( in );
    203            }
    204        } catch( final IOException e ) {
    205            LOG.error( "Unable to load default propertyfile '{}' {}", DEFAULT_JSPWIKI_CONFIG, e.getMessage(), e );
    206        }
    207
    208        return props;
    209    }
    210
    211    /**
    212     *  Returns a property set consisting of the default Property Set overlaid with a custom property set
    213     *
    214     *  @param fileName Reference to the custom override file
    215     *  @return A property set consisting of the default property set and custom property set, with
    216     *          the latter's properties replacing the former for any common values
    217     */
    218    public static Properties getCombinedProperties( final String fileName ) {
    219        final Properties newPropertySet = getDefaultProperties();
    220        try( final InputStream in = PropertyReader.class.getResourceAsStream( fileName ) ) {
    221            if( in != null ) {
    222                newPropertySet.load( in );
    223            } else {
    224                LOG.error( "*** Custom property file \"" + fileName + "\" not found, relying on default file alone." );
    225            }
    226        } catch( final IOException e ) {
    227            LOG.error( "Unable to load propertyfile '" + fileName + "'" + e.getMessage(), e );
    228        }
    229
    230        return newPropertySet;
    231    }
    232
    233    /**
    234     * Returns the ServletContext Init parameter if has been set, otherwise checks for a System property of the same name. If neither are
    235     * defined, returns null. This permits both Servlet- and System-defined cascading properties.
    236     */
    237    private static String getInitParameter( final ServletContext context, final String name ) {
    238        final String value = context.getInitParameter( name );
    239        return value != null ? value : System.getProperty( name ) ;
    240    }
    241
    242
    243    /**
    244     *  Implement the cascade functionality.
    245     *
    246     * @param context             where to read the cascade from
    247     * @param defaultProperties   properties to merge the cascading properties to
    248     * @since 2.5.x
    249     */
    250    private static void loadWebAppPropsCascade( final ServletContext context, final Properties defaultProperties ) {
    251        if( getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + "1" ) == null ) {
    252            LOG.debug( " No cascading properties defined for this context" );
    253            return;
    254        }
    255
    256        // get into cascade...
    257        int depth = 0;
    258        while( true ) {
    259            depth++;
    260            final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + depth );
    261            if( propertyFile == null ) {
    262                break;
    263            }
    264
    265            try( final InputStream propertyStream = Files.newInputStream(Paths.get(( propertyFile ) ))) {
    266                LOG.info( " Reading additional properties from {} and merge to cascade.", propertyFile );
    267                final Properties additionalProps = new Properties();
    268                additionalProps.load( propertyStream );
    269                defaultProperties.putAll( additionalProps );
    270            } catch( final Exception e ) {
    271                LOG.error( "JSPWiki: Unable to load and setup properties from {}. {}", propertyFile, e.getMessage() );
    272            }
    273        }
    274    }
    275
    276    /**
    277     * <p>Try to resolve properties whose value is something like {@code ${SOME_VALUE}} from a system property first and,
    278     * if not found, from a system environment variable. If not found on neither, the property value will remain as
    279     * {@code ${SOME_VALUE}}, and no more expansions will be processed.</p>
    280     *
    281     * <p>Several expansions per property is OK, but no we're not supporting fancy things like recursion. Reference to
    282     * other properties is achieved through {@link #expandVars(Properties)}. More than one property expansion per entry
    283     * is allowed.</p>
    284     *
    285     * @param properties properties to expand;
    286     */
    287    public static void propertyExpansion( final Properties properties ) {
    288        final Enumeration< ? > propertyList = properties.propertyNames();
    289        while( propertyList.hasMoreElements() ) {
    290            final String propertyName = ( String )propertyList.nextElement();
    291            String propertyValue = properties.getProperty( propertyName );
    292            while( propertyValue.contains( "${" ) && propertyValue.contains( "}" ) ) {
    293                final int start = propertyValue.indexOf( "${" );
    294                final int end = propertyValue.indexOf( "}", start );
    295                if( start >= 0 && end >= 0 && end > start ) {
    296                    final String substring = propertyValue.substring( start, end ).replace( "${", "" ).replace( "}", "" );
    297                    final String expansion = Objects.toString( System.getProperty( substring ), System.getenv( substring ) );
    298                    if( expansion != null ) {
    299                        propertyValue =  propertyValue.replace( "${" + substring + "}", expansion );
    300                        properties.setProperty( propertyName, propertyValue );
    301                    } else {
    302                        LOG.warn( "{} referenced on {} ({}) but not found on System props or env", substring, propertyName, propertyValue );
    303                        break;
    304                    }
    305                } else {
    306                    // no more matches or value like foo}${bar
    307                    break;
    308                }
    309            }
    310        }
    311    }
    312
    313    /**
    314     *  <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
    315     *  to use this variable.</p>
    316     *
    317     *  <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
    318     *  a {@code $basedir}. Note that it does not matter if you define the variable before its usage.
    319     *  <pre>
    320     *  var.basedir = /p/mywiki; # var.basedir = ${TOMCAT_HOME} would also be fine
    321     *  jspwiki.fileSystemProvider.pageDir =         $basedir/www/
    322     *  jspwiki.basicAttachmentProvider.storageDir = $basedir/www/
    323     *  jspwiki.workDir =                            $basedir/wrk/
    324     *  </pre></p>
    325     *
    326     * @param properties - properties to expand;
    327     */
    328    public static void expandVars( final Properties properties ) {
    329        //get variable name/values from properties...
    330        final Map< String, String > vars = new HashMap<>();
    331        Enumeration< ? > propertyList = properties.propertyNames();
    332        while( propertyList.hasMoreElements() ) {
    333            final String propertyName = ( String )propertyList.nextElement();
    334            final String propertyValue = properties.getProperty( propertyName );
    335
    336            if ( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) {
    337                final String varName = propertyName.substring( 4 ).trim();
    338                final String varValue = propertyValue.trim();
    339                vars.put( varName, varValue );
    340            }
    341        }
    342
    343        //now, substitute $ values in property values with vars...
    344        propertyList = properties.propertyNames();
    345        while( propertyList.hasMoreElements() ) {
    346            final String propertyName = ( String )propertyList.nextElement();
    347            String propertyValue = properties.getProperty( propertyName );
    348
    349            //skip var properties itself...
    350            if( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) {
    351                continue;
    352            }
    353
    354            for( final Map.Entry< String, String > entry : vars.entrySet() ) {
    355                final String varName = entry.getKey();
    356                final String varValue = entry.getValue();
    357
    358                //replace old property value, using the same variabe. If we don't overwrite
    359                //the same one the next loop works with the original one again and
    360                //multiple var expansion won't work...
    361                propertyValue = TextUtil.replaceString( propertyValue, PARAM_VAR_IDENTIFIER + varName, varValue );
    362
    363                //add the new PropertyValue to the properties
    364                properties.put( propertyName, propertyValue );
    365            }
    366        }
    367    }
    368
    369    /**
    370     * Locate a resource stored in the class path. Try first with "WEB-INF/classes"
    371     * from the web app and fallback to "resourceName".
    372     *
    373     * @param context the servlet context
    374     * @param resourceName the name of the resource
    375     * @return the input stream of the resource or <b>null</b> if the resource was not found
    376     */
    377    public static InputStream locateClassPathResource( final ServletContext context, final String resourceName ) {
    378        InputStream result;
    379        String currResourceLocation;
    380
    381        // garbage in - garbage out
    382        if( StringUtils.isEmpty( resourceName ) ) {
    383            return null;
    384        }
    385
    386        // try with web app class loader searching in "WEB-INF/classes"
    387        currResourceLocation = createResourceLocation( "/WEB-INF/classes", resourceName );
    388        result = context.getResourceAsStream( currResourceLocation );
    389        if( result != null ) {
    390            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
    391            return result;
    392        }
    393
    394        // if not found - try with the current class loader and the given name
    395        currResourceLocation = createResourceLocation( "", resourceName );
    396        result = PropertyReader.class.getResourceAsStream( currResourceLocation );
    397        if( result != null ) {
    398            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
    399            return result;
    400        }
    401
    402        LOG.debug( " Unable to resolve the following classpath resource : " + resourceName );
    403
    404        return result;
    405    }
    406
    407    /**
    408     * Create a resource location with proper usage of "/".
    409     *
    410     * @param path a path
    411     * @param name a resource name
    412     * @return a resource location
    413     */
    414    static String createResourceLocation( final String path, final String name ) {
    415        Validate.notEmpty( name, "name is empty" );
    416        final StringBuilder result = new StringBuilder();
    417
    418        // strip an ending "/"
    419        final String sanitizedPath = ( path != null && !path.isEmpty() && path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path );
    420
    421        // strip leading "/"
    422        final String sanitizedName = ( name.startsWith( "/" ) ? name.substring( 1 ) : name );
    423
    424        // append the optional path
    425        if( sanitizedPath != null && !sanitizedPath.isEmpty() ) {
    426            if( !sanitizedPath.startsWith( "/" ) ) {
    427                result.append( "/" );
    428            }
    429            result.append( sanitizedPath );
    430        }
    431        result.append( "/" );
    432
    433        // append the name
    434        result.append( sanitizedName );
    435        return result.toString();
    436    }
    437
    438    /**
    439     * This method sets the JSPWiki working directory (jspwiki.workDir). It first checks if this property
    440     * is already set. If it isn't, it attempts to use the servlet container's temporary directory
    441     * (javax.servlet.context.tempdir). If that is also unavailable, it defaults to the system's temporary
    442     * directory (java.io.tmpdir).
    443     * <p>
    444     * This method is package-private to allow for unit testing.
    445     *
    446     * @param properties     the JSPWiki properties
    447     * @param servletContext the Servlet context from which to fetch the tempdir if needed
    448     * @since JSPWiki 2.11.1
    449     */
    450    static void setWorkDir( final ServletContext servletContext, final Properties properties ) {
    451        final String workDir = TextUtil.getStringProperty(properties, "jspwiki.workDir", null);
    452        if (workDir == null) {
    453            final File tempDir = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
    454            if (tempDir != null) {
    455                properties.setProperty("jspwiki.workDir", tempDir.getAbsolutePath());
    456                LOG.info("Setting jspwiki.workDir to ServletContext's temporary directory: {}", tempDir.getAbsolutePath());
    457            } else {
    458                final String defaultTmpDir = System.getProperty("java.io.tmpdir");
    459                properties.setProperty("jspwiki.workDir", defaultTmpDir);
    460                LOG.info("ServletContext's temporary directory not found. Setting jspwiki.workDir to system's temporary directory: {}", defaultTmpDir);
    461            }
    462        } else {
    463            LOG.info("jspwiki.workDir is already set to: {}", workDir);
    464        }
    465    }
    466
    467}