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