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 org.apache.commons.lang3.StringUtils; 022import org.apache.commons.lang3.Validate; 023import org.apache.log4j.Logger; 024 025import javax.servlet.ServletContext; 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.FileNotFoundException; 029import java.io.IOException; 030import java.io.InputStream; 031import java.util.Enumeration; 032import java.util.HashMap; 033import java.util.Iterator; 034import java.util.Map; 035import java.util.Properties; 036 037 038/** 039 * Property Reader for the WikiEngine. Reads the properties for the WikiEngine 040 * and implements the feature of cascading properties and variable substitution, 041 * which come in handy in a multi wiki installation environment: It reduces the 042 * need for (shell) scripting in order to generate different jspwiki.properties 043 * to a minimum. 044 * 045 * @since 2.5.x 046 */ 047public final class PropertyReader { 048 049 private static final Logger LOG = Logger.getLogger( PropertyReader.class ); 050 051 /** 052 * Path to the base property file, usually overridden by values provided in 053 * a jspwiki-custom.properties file 054 * {@value #DEFAULT_JSPWIKI_CONFIG} 055 */ 056 public static final String DEFAULT_JSPWIKI_CONFIG = "/ini/jspwiki.properties"; 057 058 /** The servlet context parameter (from web.xml) that defines where the 059 * config file is to be found. 060 * If it is not defined, checks the Java System Property, if that is not defined either, 061 * uses the default as defined by DEFAULT_PROPERTYFILE. 062 * {@value #DEFAULT_JSPWIKI_CONFIG} 063 */ 064 public static final String PARAM_CUSTOMCONFIG = "jspwiki.custom.config"; 065 066 /** 067 * The prefix when you are cascading properties. 068 * 069 * @see #loadWebAppProps(ServletContext) 070 */ 071 public static final String PARAM_CUSTOMCONFIG_CASCADEPREFIX = "jspwiki.custom.cascade."; 072 073 public static final String CUSTOM_JSPWIKI_CONFIG = "/jspwiki-custom.properties"; 074 075 private static final String PARAM_VAR_DECLARATION = "var."; 076 private static final String PARAM_VAR_IDENTIFIER = "$"; 077 078 /** 079 * Private constructor to prevent instantiation. 080 */ 081 private PropertyReader() 082 {} 083 084 /** 085 * Loads the webapp properties based on servlet context information, 086 * or (if absent) based on the Java System Property PARAM_CUSTOMCONFIG . 087 * Returns a Properties object containing the settings, or null if unable 088 * to load it. (The default file is ini/jspwiki.properties, and can 089 * be customized by setting PARAM_CUSTOMCONFIG in the server or webapp 090 * configuration.) 091 * 092 * <h3>Cascading Properties</h3> 093 * <p> 094 * You can define additional property files and merge them into the default 095 * properties file in a similar process to how you define cascading style 096 * sheets; hence we call this <i>cascading property files</i>. This way you 097 * can overwrite the default values and only specify the properties you 098 * need to change in a multiple wiki environment. 099 * <p> 100 * You define a cascade in the context mapping of your servlet container. 101 * <pre> 102 * jspwiki.custom.cascade.1 103 * jspwiki.custom.cascade.2 104 * jspwiki.custom.cascade.3 105 * </pre> 106 * and so on. You have to number your cascade in a descending way starting 107 * with "1". This means you cannot leave out numbers in your cascade. This 108 * method is based on an idea by Olaf Kaus, see [JSPWiki:MultipleWikis]. 109 * 110 * @param context A Servlet Context which is used to find the properties 111 * @return A filled Properties object with all the cascaded properties in place 112 */ 113 public static Properties loadWebAppProps( ServletContext context ) { 114 String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG ); 115 116 try( InputStream propertyStream = loadCustomPropertiesFile(context, propertyFile) ) { 117 Properties props = getDefaultProperties(); 118 if( propertyStream == null ) { 119 LOG.info("No custom property file found, relying on JSPWiki defaults."); 120 } else { 121 props.load( propertyStream ); 122 } 123 124 //this will add additional properties to the default ones: 125 LOG.debug( "Loading cascading properties..." ); 126 127 //now load the cascade (new in 2.5) 128 loadWebAppPropsCascade( context, props ); 129 130 //finally expand the variables (new in 2.5) 131 expandVars( props ); 132 133 return props; 134 } catch( Exception e ) { 135 LOG.error( "JSPWiki: Unable to load and setup properties from jspwiki.properties. " + e.getMessage() ); 136 } 137 138 return null; 139 } 140 141 /** 142 * Figure out where our properties lie. 143 * 144 * @param context 145 * @param propertyFile 146 * @return 147 * @throws FileNotFoundException 148 */ 149 static InputStream loadCustomPropertiesFile(ServletContext context, String propertyFile) throws FileNotFoundException { 150 InputStream propertyStream; 151 if( propertyFile == null ) { 152 LOG.info( "No " + PARAM_CUSTOMCONFIG + " defined for this context, looking for custom properties file with default name of: " + CUSTOM_JSPWIKI_CONFIG ); 153 // Use the custom property file at the default location 154 propertyStream = locateClassPathResource(context, CUSTOM_JSPWIKI_CONFIG); 155 } else { 156 LOG.info( PARAM_CUSTOMCONFIG + " defined, using " + propertyFile + " as the custom properties file." ); 157 propertyStream = new FileInputStream( new File(propertyFile) ); 158 } 159 return propertyStream; 160 } 161 162 163 /** 164 * Returns the property set as a Properties object. 165 * 166 * @return A property set. 167 */ 168 public static Properties getDefaultProperties() { 169 Properties props = new Properties(); 170 try( InputStream in = PropertyReader.class.getResourceAsStream( DEFAULT_JSPWIKI_CONFIG ) ) { 171 if( in != null ) { 172 props.load( in ); 173 } 174 } catch( IOException e ) { 175 LOG.error( "Unable to load default propertyfile '" + DEFAULT_JSPWIKI_CONFIG + "'" + e.getMessage(), e ); 176 } 177 178 return props; 179 } 180 181 /** 182 * Returns a property set consisting of the default Property Set overlaid with a custom property set 183 * 184 * @param fileName Reference to the custom override file 185 * @return A property set consisting of the default property set and custom property set, with 186 * the latter's properties replacing the former for any common values 187 */ 188 public static Properties getCombinedProperties( String fileName ) { 189 Properties newPropertySet = getDefaultProperties(); 190 try( InputStream in = PropertyReader.class.getResourceAsStream( fileName ) ) { 191 if( in != null ) { 192 newPropertySet.load( in ); 193 } else { 194 LOG.error( "*** Custom property file \"" + fileName + "\" not found, relying on default file alone." ); 195 } 196 } catch( IOException e ) { 197 LOG.error( "Unable to load propertyfile '" + fileName + "'" + e.getMessage(), e ); 198 } 199 200 return newPropertySet; 201 } 202 203 /** 204 * Returns the ServletContext Init parameter if has been set, otherwise 205 * checks for a System property of the same name. If neither are defined, 206 * returns null. This permits both Servlet- and System-defined cascading 207 * properties. 208 */ 209 private static String getInitParameter( ServletContext context, String name ) { 210 String value = context.getInitParameter( name ); 211 return ( value != null ) 212 ? value 213 : System.getProperty( name ) ; 214 } 215 216 217 /** 218 * Implement the cascade functionality. 219 * 220 * @param context where to read the cascade from 221 * @param defaultProperties properties to merge the cascading properties to 222 * @since 2.5.x 223 */ 224 private static void loadWebAppPropsCascade( ServletContext context, Properties defaultProperties ) { 225 if( getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + "1" ) == null ) { 226 LOG.debug( " No cascading properties defined for this context" ); 227 return; 228 } 229 230 // get into cascade... 231 int depth = 0; 232 boolean more = true; 233 while( more ) { 234 depth++; 235 String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + depth ); 236 237 if( propertyFile == null ) { 238 more = false; 239 break; 240 } 241 242 try( InputStream propertyStream = new FileInputStream( new File( propertyFile ) ) ) { 243 LOG.info( " Reading additional properties from " + propertyFile + " and merge to cascade." ); 244 Properties additionalProps = new Properties(); 245 additionalProps.load( propertyStream ); 246 defaultProperties.putAll( additionalProps ); 247 } catch( Exception e ) { 248 LOG.error( "JSPWiki: Unable to load and setup properties from " + propertyFile + "." + e.getMessage() ); 249 } 250 } 251 252 return; 253 } 254 255 /** 256 * You define a property variable by using the prefix "var.x" as a 257 * property. In property values you can then use the "$x" identifier 258 * to use this variable. 259 * 260 * For example you could declare a base directory for all your files 261 * like this and use it in all your other property definitions with 262 * a "$basedir". Note that it does not matter if you define the 263 * variable before its usage. 264 * <pre> 265 * var.basedir = /p/mywiki; 266 * jspwiki.fileSystemProvider.pageDir = $basedir/www/ 267 * jspwiki.basicAttachmentProvider.storageDir = $basedir/www/ 268 * jspwiki.workDir = $basedir/wrk/ 269 * </pre> 270 * 271 * @param properties - properties to expand; 272 */ 273 public static void expandVars(Properties properties) { 274 //get variable name/values from properties... 275 Map< String, String > vars = new HashMap< String, String >(); 276 Enumeration< ? > propertyList = properties.propertyNames(); 277 while( propertyList.hasMoreElements() ) { 278 String propertyName = ( String )propertyList.nextElement(); 279 String propertyValue = properties.getProperty( propertyName ); 280 281 if ( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) { 282 String varName = propertyName.substring( 4, propertyName.length() ).trim(); 283 String varValue = propertyValue.trim(); 284 vars.put( varName, varValue ); 285 } 286 } 287 288 //now, substitute $ values in property values with vars... 289 propertyList = properties.propertyNames(); 290 while( propertyList.hasMoreElements() ) { 291 String propertyName = ( String )propertyList.nextElement(); 292 String propertyValue = properties.getProperty( propertyName ); 293 294 //skip var properties itself... 295 if( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) { 296 continue; 297 } 298 299 Iterator< Map.Entry< String, String > > iter = vars.entrySet().iterator(); 300 while ( iter.hasNext() ) { 301 Map.Entry< String, String > entry = iter.next(); 302 String varName = entry.getKey(); 303 String varValue = entry.getValue(); 304 305 //replace old property value, using the same variabe. If we don't overwrite 306 //the same one the next loop works with the original one again and 307 //multiple var expansion won't work... 308 propertyValue = TextUtil.replaceString( propertyValue, PARAM_VAR_IDENTIFIER + varName, varValue ); 309 310 //add the new PropertyValue to the properties 311 properties.put(propertyName, propertyValue); 312 } 313 } 314 } 315 316 /** 317 * Locate a resource stored in the class path. Try first with "WEB-INF/classes" 318 * from the web app and fallback to "resourceName". 319 * 320 * @param context the servlet context 321 * @param resourceName the name of the resource 322 * @return the input stream of the resource or <b>null</b> if the resource was not found 323 */ 324 public static InputStream locateClassPathResource( ServletContext context, String resourceName ) { 325 InputStream result; 326 String currResourceLocation; 327 328 // garbage in - garbage out 329 if( StringUtils.isEmpty( resourceName ) ) { 330 return null; 331 } 332 333 // try with web app class loader searching in "WEB-INF/classes" 334 currResourceLocation = createResourceLocation( "/WEB-INF/classes", resourceName ); 335 result = context.getResourceAsStream( currResourceLocation ); 336 if( result != null ) { 337 LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation ); 338 return result; 339 } 340 341 // if not found - try with the current class loader and the given name 342 currResourceLocation = createResourceLocation( "", resourceName ); 343 result = PropertyReader.class.getResourceAsStream( currResourceLocation ); 344 if( result != null ) { 345 LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation ); 346 return result; 347 } 348 349 LOG.debug( " Unable to resolve the following classpath resource : " + resourceName ); 350 351 return result; 352 } 353 354 /** 355 * Create a resource location with proper usage of "/". 356 * 357 * @param path a path 358 * @param name a resource name 359 * @return a resource location 360 */ 361 static String createResourceLocation( String path, String name ) { 362 Validate.notEmpty( name, "name is empty" ); 363 StringBuilder result = new StringBuilder(); 364 365 // strip an ending "/" 366 String sanitizedPath = ( path != null && !path.isEmpty() && path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path ); 367 368 // strip leading "/" 369 String sanitizedName = ( name.startsWith( "/" ) ? name.substring( 1, name.length() ) : name ); 370 371 // append the optional path 372 if( sanitizedPath == null || sanitizedPath.isEmpty() ) { 373 result.append( "/" ); 374 } else { 375 if( !sanitizedPath.startsWith( "/" ) ) { 376 result.append( "/" ); 377 } 378 result.append( sanitizedPath ); 379 result.append( "/" ); 380 } 381 382 // append the name 383 result.append( sanitizedName ); 384 return result.toString(); 385 } 386 387}