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