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