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     */
019    package org.apache.wiki.util;
020    
021    import java.io.File;
022    import java.io.FileInputStream;
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.util.Enumeration;
026    import java.util.HashMap;
027    import java.util.Iterator;
028    import java.util.Map;
029    import java.util.Properties;
030    
031    import javax.servlet.ServletContext;
032    
033    import org.apache.commons.io.IOUtils;
034    import org.apache.commons.lang.StringUtils;
035    import org.apache.commons.lang.Validate;
036    import org.apache.log4j.Logger;
037    import 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     */
049    public 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    }