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}