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