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