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