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 }