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}