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}