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