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.FileNotFoundException; 023import java.io.IOException; 024import java.io.InputStream; 025import java.nio.file.Files; 026import java.nio.file.Paths; 027import java.util.AbstractMap; 028import java.util.Enumeration; 029import java.util.HashMap; 030import java.util.Map; 031import java.util.Objects; 032import java.util.Properties; 033import java.util.stream.Collectors; 034 035import org.apache.commons.lang3.StringUtils; 036import org.apache.commons.lang3.Validate; 037import org.apache.logging.log4j.LogManager; 038import org.apache.logging.log4j.Logger; 039 040import jakarta.servlet.ServletContext; 041 042 043/** 044 * Property Reader for the WikiEngine. Reads the properties for the WikiEngine 045 * and implements the feature of cascading properties and variable substitution, 046 * which come in handy in a multi wiki installation environment: It reduces the 047 * need for (shell) scripting in order to generate different jspwiki.properties 048 * to a minimum. 049 * 050 * @since 2.5.x 051 */ 052public final class PropertyReader { 053 054 private static final Logger LOG = LogManager.getLogger( PropertyReader.class ); 055 056 /** 057 * Path to the base property file, {@value}, usually overridden by values provided in 058 * a jspwiki-custom.properties file. 059 */ 060 public static final String DEFAULT_JSPWIKI_CONFIG = "/ini/jspwiki.properties"; 061 062 /** 063 * The servlet context parameter (from web.xml) that defines where the config file is to be found. If it is not defined, checks 064 * the Java System Property, if that is not defined either, uses the default as defined by DEFAULT_PROPERTYFILE. 065 * {@value #DEFAULT_JSPWIKI_CONFIG} 066 */ 067 public static final String PARAM_CUSTOMCONFIG = "jspwiki.custom.config"; 068 069 /** 070 * The prefix when you are cascading properties. 071 * 072 * @see #loadWebAppProps(ServletContext) 073 */ 074 public static final String PARAM_CUSTOMCONFIG_CASCADEPREFIX = "jspwiki.custom.cascade."; 075 076 public static final String CUSTOM_JSPWIKI_CONFIG = "/jspwiki-custom.properties"; 077 078 private static final String PARAM_VAR_DECLARATION = "var."; 079 private static final String PARAM_VAR_IDENTIFIER = "$"; 080 081 /** 082 * Private constructor to prevent instantiation. 083 */ 084 private PropertyReader() 085 {} 086 087 /** 088 * Loads the webapp properties based on servlet context information, or 089 * (if absent) based on the Java System Property {@value #PARAM_CUSTOMCONFIG}. 090 * Returns a Properties object containing the settings, or null if unable 091 * to load it. (The default file is ini/jspwiki.properties, and can be 092 * customized by setting {@value #PARAM_CUSTOMCONFIG} in the server or webapp 093 * configuration.) 094 * 095 * <h3>Properties sources</h3> 096 * The following properties sources are taken into account: 097 * <ol> 098 * <li>JSPWiki default properties</li> 099 * <li>System environment</li> 100 * <li>JSPWiki custom property files</li> 101 * <li>JSPWiki cascading properties</li> 102 * <li>System properties</li> 103 * </ol> 104 * With later sources taking precedence over the previous ones. To avoid leaking system information, 105 * only System environment and properties beginning with {@code jspwiki} (case unsensitive) are taken into account. 106 * Also, to ease docker integration, System env properties containing "_" are turned into ".". Thus, 107 * {@code ENV jspwiki_fileSystemProvider_pageDir} is loaded as {@code jspwiki.fileSystemProvider.pageDir}. 108 * 109 * <h3>Cascading Properties</h3> 110 * <p> 111 * You can define additional property files and merge them into the default 112 * properties file in a similar process to how you define cascading style 113 * sheets; hence we call this <i>cascading property files</i>. This way you 114 * can overwrite the default values and only specify the properties you 115 * need to change in a multiple wiki environment. 116 * <p> 117 * You define a cascade in the context mapping of your servlet container. 118 * <pre> 119 * jspwiki.custom.cascade.1 120 * jspwiki.custom.cascade.2 121 * jspwiki.custom.cascade.3 122 * </pre> 123 * and so on. You have to number your cascade in a descending way starting 124 * with "1". This means you cannot leave out numbers in your cascade. This 125 * method is based on an idea by Olaf Kaus, see [JSPWiki:MultipleWikis]. 126 * 127 * @param context A Servlet Context which is used to find the properties 128 * @return A filled Properties object with all the cascaded properties in place 129 */ 130 public static Properties loadWebAppProps( final ServletContext context ) { 131 final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG ); 132 try( final InputStream propertyStream = loadCustomPropertiesFile(context, propertyFile) ) { 133 final Properties props = getDefaultProperties(); 134 135 // add system env properties beginning with jspwiki... 136 final Map< String, String > env = collectPropertiesFrom( System.getenv() ); 137 props.putAll( env ); 138 139 if( propertyStream == null ) { 140 LOG.debug( "No custom property file found, relying on JSPWiki defaults." ); 141 } else { 142 props.load( propertyStream ); 143 } 144 145 // this will add additional properties to the default ones: 146 LOG.debug( "Loading cascading properties..." ); 147 148 // now load the cascade (new in 2.5) 149 loadWebAppPropsCascade( context, props ); 150 151 // property expansion so we can resolve things like ${TOMCAT_HOME} 152 propertyExpansion( props ); 153 154 // sets the JSPWiki working directory (jspwiki.workDir) 155 setWorkDir( context, props ); 156 157 // add system properties beginning with jspwiki... 158 final Map< String, String > sysprops = collectPropertiesFrom( System.getProperties().entrySet().stream() 159 .collect( Collectors.toMap( Object::toString, Object::toString ) ) ); 160 props.putAll( sysprops ); 161 162 // finally, expand the variables (new in 2.5) 163 expandVars( props ); 164 165 return props; 166 } catch( final Exception e ) { 167 LOG.error( "JSPWiki: Unable to load and setup properties from jspwiki.properties. " + e.getMessage(), e ); 168 } 169 170 return null; 171 } 172 173 static Map< String, String > collectPropertiesFrom( final Map< String, String > map ) { 174 return map.entrySet().stream() 175 .filter( entry -> entry.getKey().toLowerCase().startsWith( "jspwiki" ) ) 176 .map( entry -> new AbstractMap.SimpleEntry<>( entry.getKey().replace( "_", "." ), entry.getValue() ) ) 177 .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ); 178 } 179 180 /** 181 * Figure out where our properties lie. 182 * 183 * @param context servlet context 184 * @param propertyFile property file 185 * @return InputStream holding the properties file 186 * @throws FileNotFoundException properties file not found 187 */ 188 static InputStream loadCustomPropertiesFile( final ServletContext context, final String propertyFile ) throws IOException { 189 final InputStream propertyStream; 190 if( propertyFile == null ) { 191 LOG.debug( "No " + PARAM_CUSTOMCONFIG + " defined for this context, looking for custom properties file with default name of: " + CUSTOM_JSPWIKI_CONFIG ); 192 // Use the custom property file at the default location 193 propertyStream = locateClassPathResource(context, CUSTOM_JSPWIKI_CONFIG); 194 } else { 195 LOG.debug( PARAM_CUSTOMCONFIG + " defined, using " + propertyFile + " as the custom properties file." ); 196 propertyStream = Files.newInputStream( new File(propertyFile).toPath() ); 197 } 198 return propertyStream; 199 } 200 201 202 /** 203 * Returns the property set as a Properties object. 204 * 205 * @return A property set. 206 */ 207 public static Properties getDefaultProperties() { 208 final Properties props = new Properties(); 209 try( final InputStream in = PropertyReader.class.getResourceAsStream( DEFAULT_JSPWIKI_CONFIG ) ) { 210 if( in != null ) { 211 props.load( in ); 212 } 213 } catch( final IOException e ) { 214 LOG.error( "Unable to load default propertyfile '{}' {}", DEFAULT_JSPWIKI_CONFIG, e.getMessage(), e ); 215 } 216 217 return props; 218 } 219 220 /** 221 * Returns a property set consisting of the default Property Set overlaid with a custom property set 222 * 223 * @param fileName Reference to the custom override file 224 * @return A property set consisting of the default property set and custom property set, with 225 * the latter's properties replacing the former for any common values 226 */ 227 public static Properties getCombinedProperties( final String fileName ) { 228 final Properties newPropertySet = getDefaultProperties(); 229 try( final InputStream in = PropertyReader.class.getResourceAsStream( fileName ) ) { 230 if( in != null ) { 231 newPropertySet.load( in ); 232 } else { 233 LOG.error( "*** Custom property file \"" + fileName + "\" not found, relying on default file alone." ); 234 } 235 } catch( final IOException e ) { 236 LOG.error( "Unable to load propertyfile '" + fileName + "'" + e.getMessage(), e ); 237 } 238 239 return newPropertySet; 240 } 241 242 /** 243 * Returns the ServletContext Init parameter if has been set, otherwise checks for a System property of the same name. If neither are 244 * defined, returns null. This permits both Servlet- and System-defined cascading properties. 245 */ 246 private static String getInitParameter( final ServletContext context, final String name ) { 247 final String value = context.getInitParameter( name ); 248 return value != null ? value : System.getProperty( name ) ; 249 } 250 251 252 /** 253 * Implement the cascade functionality. 254 * 255 * @param context where to read the cascade from 256 * @param defaultProperties properties to merge the cascading properties to 257 * @since 2.5.x 258 */ 259 private static void loadWebAppPropsCascade( final ServletContext context, final Properties defaultProperties ) { 260 if( getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + "1" ) == null ) { 261 LOG.debug( " No cascading properties defined for this context" ); 262 return; 263 } 264 265 // get into cascade... 266 int depth = 0; 267 while( true ) { 268 depth++; 269 final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + depth ); 270 if( propertyFile == null ) { 271 break; 272 } 273 274 try( final InputStream propertyStream = Files.newInputStream(Paths.get(( propertyFile ) ))) { 275 LOG.info( " Reading additional properties from {} and merge to cascade.", propertyFile ); 276 final Properties additionalProps = new Properties(); 277 additionalProps.load( propertyStream ); 278 defaultProperties.putAll( additionalProps ); 279 } catch( final Exception e ) { 280 LOG.error( "JSPWiki: Unable to load and setup properties from {}. {}", propertyFile, e.getMessage() ); 281 } 282 } 283 } 284 285 /** 286 * <p>Try to resolve properties whose value is something like {@code ${SOME_VALUE}} from a system property first and, 287 * if not found, from a system environment variable. If not found on neither, the property value will remain as 288 * {@code ${SOME_VALUE}}, and no more expansions will be processed.</p> 289 * 290 * <p>Several expansions per property is OK, but no we're not supporting fancy things like recursion. Reference to 291 * other properties is achieved through {@link #expandVars(Properties)}. More than one property expansion per entry 292 * is allowed.</p> 293 * 294 * @param properties properties to expand; 295 */ 296 public static void propertyExpansion( final Properties properties ) { 297 final Enumeration< ? > propertyList = properties.propertyNames(); 298 while( propertyList.hasMoreElements() ) { 299 final String propertyName = ( String )propertyList.nextElement(); 300 String propertyValue = properties.getProperty( propertyName ); 301 while( propertyValue.contains( "${" ) && propertyValue.contains( "}" ) ) { 302 final int start = propertyValue.indexOf( "${" ); 303 final int end = propertyValue.indexOf( "}", start ); 304 if( start >= 0 && end >= 0 && end > start ) { 305 final String substring = propertyValue.substring( start, end ).replace( "${", "" ).replace( "}", "" ); 306 final String expansion = Objects.toString( System.getProperty( substring ), System.getenv( substring ) ); 307 if( expansion != null ) { 308 propertyValue = propertyValue.replace( "${" + substring + "}", expansion ); 309 properties.setProperty( propertyName, propertyValue ); 310 } else { 311 LOG.warn( "{} referenced on {} ({}) but not found on System props or env", substring, propertyName, propertyValue ); 312 break; 313 } 314 } else { 315 // no more matches or value like foo}${bar 316 break; 317 } 318 } 319 } 320 } 321 322 /** 323 * <p>You define a property variable by using the prefix {@code var.x} as a property. In property values you can then use the "$x" identifier 324 * to use this variable.</p> 325 * 326 * <p>For example, you could declare a base directory for all your files like this and use it in all your other property definitions with 327 * a {@code $basedir}. Note that it does not matter if you define the variable before its usage. 328 * <pre> 329 * var.basedir = /p/mywiki; # var.basedir = ${TOMCAT_HOME} would also be fine 330 * jspwiki.fileSystemProvider.pageDir = $basedir/www/ 331 * jspwiki.basicAttachmentProvider.storageDir = $basedir/www/ 332 * jspwiki.workDir = $basedir/wrk/ 333 * </pre></p> 334 * 335 * @param properties - properties to expand; 336 */ 337 public static void expandVars( final Properties properties ) { 338 //get variable name/values from properties... 339 final Map< String, String > vars = new HashMap<>(); 340 Enumeration< ? > propertyList = properties.propertyNames(); 341 while( propertyList.hasMoreElements() ) { 342 final String propertyName = ( String )propertyList.nextElement(); 343 final String propertyValue = properties.getProperty( propertyName ); 344 345 if ( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) { 346 final String varName = propertyName.substring( 4 ).trim(); 347 final String varValue = propertyValue.trim(); 348 vars.put( varName, varValue ); 349 } 350 } 351 352 //now, substitute $ values in property values with vars... 353 propertyList = properties.propertyNames(); 354 while( propertyList.hasMoreElements() ) { 355 final String propertyName = ( String )propertyList.nextElement(); 356 String propertyValue = properties.getProperty( propertyName ); 357 358 //skip var properties itself... 359 if( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) { 360 continue; 361 } 362 363 for( final Map.Entry< String, String > entry : vars.entrySet() ) { 364 final String varName = entry.getKey(); 365 final String varValue = entry.getValue(); 366 367 //replace old property value, using the same variabe. If we don't overwrite 368 //the same one the next loop works with the original one again and 369 //multiple var expansion won't work... 370 propertyValue = TextUtil.replaceString( propertyValue, PARAM_VAR_IDENTIFIER + varName, varValue ); 371 372 //add the new PropertyValue to the properties 373 properties.put( propertyName, propertyValue ); 374 } 375 } 376 } 377 378 /** 379 * Locate a resource stored in the class path. Try first with "WEB-INF/classes" 380 * from the web app and fallback to "resourceName". 381 * 382 * @param context the servlet context 383 * @param resourceName the name of the resource 384 * @return the input stream of the resource or <b>null</b> if the resource was not found 385 */ 386 public static InputStream locateClassPathResource( final ServletContext context, final String resourceName ) { 387 InputStream result; 388 String currResourceLocation; 389 390 // garbage in - garbage out 391 if( StringUtils.isEmpty( resourceName ) ) { 392 return null; 393 } 394 395 // try with web app class loader searching in "WEB-INF/classes" 396 currResourceLocation = createResourceLocation( "/WEB-INF/classes", resourceName ); 397 result = context.getResourceAsStream( currResourceLocation ); 398 if( result != null ) { 399 LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation ); 400 return result; 401 } 402 403 // if not found - try with the current class loader and the given name 404 currResourceLocation = createResourceLocation( "", resourceName ); 405 result = PropertyReader.class.getResourceAsStream( currResourceLocation ); 406 if( result != null ) { 407 LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation ); 408 return result; 409 } 410 411 LOG.debug( " Unable to resolve the following classpath resource : " + resourceName ); 412 413 return result; 414 } 415 416 /** 417 * Create a resource location with proper usage of "/". 418 * 419 * @param path a path 420 * @param name a resource name 421 * @return a resource location 422 */ 423 static String createResourceLocation( final String path, final String name ) { 424 Validate.notEmpty( name, "name is empty" ); 425 final StringBuilder result = new StringBuilder(); 426 427 // strip an ending "/" 428 final String sanitizedPath = ( path != null && !path.isEmpty() && path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path ); 429 430 // strip leading "/" 431 final String sanitizedName = ( name.startsWith( "/" ) ? name.substring( 1 ) : name ); 432 433 // append the optional path 434 if( sanitizedPath != null && !sanitizedPath.isEmpty() ) { 435 if( !sanitizedPath.startsWith( "/" ) ) { 436 result.append( "/" ); 437 } 438 result.append( sanitizedPath ); 439 } 440 result.append( "/" ); 441 442 // append the name 443 result.append( sanitizedName ); 444 return result.toString(); 445 } 446 447 /** 448 * This method sets the JSPWiki working directory (jspwiki.workDir). It first checks if this property 449 * is already set. If it isn't, it attempts to use the servlet container's temporary directory 450 * (jakarta.servlet.context.tempdir). If that is also unavailable, it defaults to the system's temporary 451 * directory (java.io.tmpdir). 452 * <p> 453 * This method is package-private to allow for unit testing. 454 * 455 * @param properties the JSPWiki properties 456 * @param servletContext the Servlet context from which to fetch the tempdir if needed 457 * @since JSPWiki 2.11.1 458 */ 459 static void setWorkDir( final ServletContext servletContext, final Properties properties ) { 460 final String workDir = TextUtil.getStringProperty(properties, "jspwiki.workDir", null); 461 if (workDir == null) { 462 final File tempDir = (File) servletContext.getAttribute("jakarta.servlet.context.tempdir"); 463 if (tempDir != null) { 464 properties.setProperty("jspwiki.workDir", tempDir.getAbsolutePath()); 465 LOG.info("Setting jspwiki.workDir to ServletContext's temporary directory: {}", tempDir.getAbsolutePath()); 466 } else { 467 final String defaultTmpDir = System.getProperty("java.io.tmpdir"); 468 properties.setProperty("jspwiki.workDir", defaultTmpDir); 469 LOG.info("ServletContext's temporary directory not found. Setting jspwiki.workDir to system's temporary directory: {}", defaultTmpDir); 470 } 471 } else { 472 LOG.info("jspwiki.workDir is already set to: {}", workDir); 473 } 474 } 475 476}