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.IOException;
024import java.io.InputStream;
025import java.util.Enumeration;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.Map;
029import java.util.Properties;
030
031import javax.servlet.ServletContext;
032
033import org.apache.commons.io.IOUtils;
034import org.apache.commons.lang.StringUtils;
035import org.apache.commons.lang.Validate;
036import org.apache.log4j.Logger;
037import org.apache.wiki.Release;
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        InputStream propertyStream = null;
118
119        try {
120            //
121            //  Figure out where our properties lie.
122            //
123            if( propertyFile == null ) {
124                LOG.info( "No " + PARAM_CUSTOMCONFIG + " defined for this context, " +
125                             "looking for custom properties file with default name of: " + CUSTOM_JSPWIKI_CONFIG );
126                //  Use the custom property file at the default location
127                propertyStream =  locateClassPathResource(context, CUSTOM_JSPWIKI_CONFIG);
128            } else {
129                LOG.info(PARAM_CUSTOMCONFIG + " defined, using " + propertyFile + " as the custom properties file.");
130                propertyStream = new FileInputStream( new File(propertyFile) );
131            }
132
133            Properties props = getDefaultProperties();
134            if( propertyStream == null ) {
135                LOG.info("No custom property file found, relying on JSPWiki defaults.");
136            } else {
137                props.load( propertyStream );
138            }
139
140            //this will add additional properties to the default ones:
141            LOG.debug( "Loading cascading properties..." );
142
143            //now load the cascade (new in 2.5)
144            loadWebAppPropsCascade( context, props );
145
146            //finally expand the variables (new in 2.5)
147            expandVars( props );
148
149            return props;
150        } catch( Exception e ) {
151            LOG.error( Release.APPNAME + ": Unable to load and setup properties from jspwiki.properties. " +
152                         e.getMessage() );
153        } finally {
154            IOUtils.closeQuietly( propertyStream );
155        }
156
157        return null;
158    }
159
160
161    /**
162     *  Returns the property set as a Properties object.
163     *
164     *  @return A property set.
165     */
166    public static Properties getDefaultProperties() {
167        Properties props = new Properties();
168        InputStream in = PropertyReader.class.getResourceAsStream( DEFAULT_JSPWIKI_CONFIG );
169        
170        if( in != null ) {
171            try {
172                props.load( in );
173            } catch( IOException e ) {
174                LOG.error( "Unable to load default propertyfile '" + DEFAULT_JSPWIKI_CONFIG + "'" + e.getMessage(), e );
175            } finally {
176                IOUtils.closeQuietly( in );
177            }
178        }
179        
180        return props;
181    }
182
183    /**
184     *  Returns a property set consisting of the default Property Set overlaid with a custom property set
185     *
186     *  @param fileName Reference to the custom override file
187     *  @return A property set consisting of the default property set and custom property set, with
188     *          the latter's properties replacing the former for any common values
189     */
190    public static Properties getCombinedProperties( String fileName ) {
191        Properties newPropertySet = getDefaultProperties();
192        InputStream in = PropertyReader.class.getResourceAsStream( fileName );
193
194        if( in != null ) {
195            try {
196                newPropertySet.load( in );
197            } catch( IOException e ) {
198                LOG.error( "Unable to load propertyfile '" + fileName + "'" + e.getMessage(), e );
199            } finally {
200                IOUtils.closeQuietly( in );
201            }
202        } else {
203            LOG.error( "*** Custom property file \"" + fileName + "\" not found, relying on default file alone." );
204        }
205
206        return newPropertySet;
207    }
208
209    /**
210     *  Returns the ServletContext Init parameter if has been set, otherwise
211     *  checks for a System property of the same name. If neither are defined,
212     *  returns null. This permits both Servlet- and System-defined cascading
213     *  properties.
214     */
215    private static String getInitParameter( ServletContext context, String name ) {
216        String value = context.getInitParameter( name );
217        return ( value != null )
218                ? value
219                : System.getProperty( name ) ;
220    }
221
222
223    /**
224     *  Implement the cascade functionality.
225     *
226     * @param context             where to read the cascade from
227     * @param defaultProperties   properties to merge the cascading properties to
228     * @since 2.5.x
229     */
230    private static void loadWebAppPropsCascade( ServletContext context, Properties defaultProperties ) {
231        if( getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + "1" ) == null ) {
232            LOG.debug( " No cascading properties defined for this context" );
233            return;
234        }
235
236        // get into cascade...
237        int depth = 0;
238        boolean more = true;
239        InputStream propertyStream = null;
240        while( more ) {
241            depth++;
242            String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + depth );
243
244            if( propertyFile == null ) {
245                more = false;
246                break;
247            }
248
249            try {
250                LOG.info( " Reading additional properties from " + propertyFile + " and merge to cascade." );
251                Properties additionalProps = new Properties();
252                propertyStream = new FileInputStream( new File( propertyFile ) );
253                additionalProps.load(propertyStream);
254                defaultProperties.putAll(additionalProps);
255            } catch( Exception e ) {
256                LOG.error( " " + Release.APPNAME +
257                             ": Unable to load and setup properties from " + propertyFile + "." +
258                             e.getMessage() );
259            } finally {
260                IOUtils.closeQuietly( propertyStream );
261            }
262        }
263
264        return;
265    }
266
267    /**
268     *  You define a property variable by using the prefix "var.x" as a
269     *  property. In property values you can then use the "$x" identifier
270     *  to use this variable.
271     *
272     *  For example you could declare a base directory for all your files
273     *  like this and use it in all your other property definitions with
274     *  a "$basedir". Note that it does not matter if you define the
275     *  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(Properties properties) {
286        //get variable name/values from properties...
287        Map< String, String > vars = new HashMap< String, String >();
288        Enumeration< ? > propertyList = properties.propertyNames();
289        while( propertyList.hasMoreElements() ) {
290            String propertyName = ( String )propertyList.nextElement();
291            String propertyValue = properties.getProperty( propertyName );
292
293            if ( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) {
294                String varName = propertyName.substring( 4, propertyName.length() ).trim();
295                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            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            Iterator< Map.Entry< String, String > > iter = vars.entrySet().iterator();
312            while ( iter.hasNext() ) {
313                Map.Entry< String, String > entry = iter.next();
314                String varName = entry.getKey();
315                String varValue = entry.getValue();
316
317                //replace old property value, using the same variabe. If we don't overwrite
318                //the same one the next loop works with the original one again and
319                //multiple var expansion won't work...
320                propertyValue = TextUtil.replaceString( propertyValue, PARAM_VAR_IDENTIFIER + varName, varValue );
321
322                //add the new PropertyValue to the properties
323                properties.put(propertyName, propertyValue);
324            }
325        }
326    }
327
328    /**
329     * Locate a resource stored in the class path. Try first with "WEB-INF/classes"
330     * from the web app and fallback to "resourceName".
331     *
332     * @param context the servlet context
333     * @param resourceName the name of the resource
334     * @return the input stream of the resource or <b>null</b> if the resource was not found
335     */
336    public static InputStream locateClassPathResource( ServletContext context, String resourceName ) {
337        InputStream result;
338        String currResourceLocation;
339
340        // garbage in - garbage out
341        if( StringUtils.isEmpty( resourceName ) ) {
342            return null;
343        }
344
345        // try with web app class loader searching in "WEB-INF/classes"
346        currResourceLocation = createResourceLocation( "/WEB-INF/classes", resourceName );
347        result = context.getResourceAsStream( currResourceLocation );
348        if( result != null ) {
349            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
350            return result;
351        }
352
353        // if not found - try with the current class loader and the given name
354        currResourceLocation = createResourceLocation( "", resourceName );
355        result = PropertyReader.class.getResourceAsStream( currResourceLocation );
356        if( result != null ) {
357            LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation );
358            return result;
359        }
360
361        LOG.debug( " Unable to resolve the following classpath resource : " + resourceName );
362
363        return result;
364    }
365
366    /**
367     * Create a resource location with proper usage of "/".
368     *
369     * @param path a path
370     * @param name a resource name
371     * @return a resource location
372     */
373    static String createResourceLocation( String path, String name ) {
374        Validate.notEmpty( name, "name is empty" );
375        StringBuilder result = new StringBuilder();
376
377        // strip an ending "/"
378        String sanitizedPath = ( path != null && !path.isEmpty() && path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path );
379
380        // strip leading "/"
381        String sanitizedName = ( name.startsWith( "/" ) ? name.substring( 1, name.length() ) : name );
382
383        // append the optional path
384        if( sanitizedPath == null || sanitizedPath.isEmpty() ) {
385            result.append( "/" );
386        } else {
387            if( !sanitizedPath.startsWith( "/" ) ) {
388                result.append( "/" );
389            }
390            result.append( sanitizedPath );
391            result.append( "/" );
392        }
393
394        // append the name
395        result.append( sanitizedName );
396        return result.toString();
397    }
398
399}