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, usually overridden by values provided in 049 * a jspwiki-custom.properties file {@value #DEFAULT_JSPWIKI_CONFIG} 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 // add system properties beginning with jspwiki... 143 final Map< String, String > sysprops = collectPropertiesFrom( System.getProperties().entrySet().stream() 144 .collect( Collectors.toMap( Object::toString, Object::toString ) ) ); 145 props.putAll( sysprops ); 146 147 // finally, expand the variables (new in 2.5) 148 expandVars( props ); 149 150 return props; 151 } catch( final Exception e ) { 152 LOG.error( "JSPWiki: Unable to load and setup properties from jspwiki.properties. " + e.getMessage(), e ); 153 } 154 155 return null; 156 } 157 158 static Map< String, String > collectPropertiesFrom( final Map< String, String > map ) { 159 return map.entrySet().stream() 160 .filter( entry -> entry.getKey().toLowerCase().startsWith( "jspwiki" ) ) 161 .map( entry -> new AbstractMap.SimpleEntry<>( entry.getKey().replace( "_", "." ), entry.getValue() ) ) 162 .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ); 163 } 164 165 /** 166 * Figure out where our properties lie. 167 * 168 * @param context servlet context 169 * @param propertyFile property file 170 * @return inputstream holding the properties file 171 * @throws FileNotFoundException properties file not found 172 */ 173 static InputStream loadCustomPropertiesFile( final ServletContext context, final String propertyFile ) throws IOException { 174 final InputStream propertyStream; 175 if( propertyFile == null ) { 176 LOG.debug( "No " + PARAM_CUSTOMCONFIG + " defined for this context, looking for custom properties file with default name of: " + CUSTOM_JSPWIKI_CONFIG ); 177 // Use the custom property file at the default location 178 propertyStream = locateClassPathResource(context, CUSTOM_JSPWIKI_CONFIG); 179 } else { 180 LOG.debug( PARAM_CUSTOMCONFIG + " defined, using " + propertyFile + " as the custom properties file." ); 181 propertyStream = Files.newInputStream( new File(propertyFile).toPath() ); 182 } 183 return propertyStream; 184 } 185 186 187 /** 188 * Returns the property set as a Properties object. 189 * 190 * @return A property set. 191 */ 192 public static Properties getDefaultProperties() { 193 final Properties props = new Properties(); 194 try( final InputStream in = PropertyReader.class.getResourceAsStream( DEFAULT_JSPWIKI_CONFIG ) ) { 195 if( in != null ) { 196 props.load( in ); 197 } 198 } catch( final IOException e ) { 199 LOG.error( "Unable to load default propertyfile '" + DEFAULT_JSPWIKI_CONFIG + "'" + e.getMessage(), e ); 200 } 201 202 return props; 203 } 204 205 /** 206 * Returns a property set consisting of the default Property Set overlaid with a custom property set 207 * 208 * @param fileName Reference to the custom override file 209 * @return A property set consisting of the default property set and custom property set, with 210 * the latter's properties replacing the former for any common values 211 */ 212 public static Properties getCombinedProperties( final String fileName ) { 213 final Properties newPropertySet = getDefaultProperties(); 214 try( final InputStream in = PropertyReader.class.getResourceAsStream( fileName ) ) { 215 if( in != null ) { 216 newPropertySet.load( in ); 217 } else { 218 LOG.error( "*** Custom property file \"" + fileName + "\" not found, relying on default file alone." ); 219 } 220 } catch( final IOException e ) { 221 LOG.error( "Unable to load propertyfile '" + fileName + "'" + e.getMessage(), e ); 222 } 223 224 return newPropertySet; 225 } 226 227 /** 228 * Returns the ServletContext Init parameter if has been set, otherwise checks for a System property of the same name. If neither are 229 * defined, returns null. This permits both Servlet- and System-defined cascading properties. 230 */ 231 private static String getInitParameter( final ServletContext context, final String name ) { 232 final String value = context.getInitParameter( name ); 233 return value != null ? value 234 : System.getProperty( name ) ; 235 } 236 237 238 /** 239 * Implement the cascade functionality. 240 * 241 * @param context where to read the cascade from 242 * @param defaultProperties properties to merge the cascading properties to 243 * @since 2.5.x 244 */ 245 private static void loadWebAppPropsCascade( final ServletContext context, final Properties defaultProperties ) { 246 if( getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + "1" ) == null ) { 247 LOG.debug( " No cascading properties defined for this context" ); 248 return; 249 } 250 251 // get into cascade... 252 int depth = 0; 253 while( true ) { 254 depth++; 255 final String propertyFile = getInitParameter( context, PARAM_CUSTOMCONFIG_CASCADEPREFIX + depth ); 256 if( propertyFile == null ) { 257 break; 258 } 259 260 try( final InputStream propertyStream = Files.newInputStream(Paths.get(( propertyFile ) ))) { 261 LOG.info( " Reading additional properties from " + propertyFile + " and merge to cascade." ); 262 final Properties additionalProps = new Properties(); 263 additionalProps.load( propertyStream ); 264 defaultProperties.putAll( additionalProps ); 265 } catch( final Exception e ) { 266 LOG.error( "JSPWiki: Unable to load and setup properties from " + propertyFile + "." + e.getMessage() ); 267 } 268 } 269 } 270 271 /** 272 * You define a property variable by using the prefix "var.x" as a property. In property values you can then use the "$x" identifier 273 * to use this variable. 274 * 275 * For example, you could declare a base directory for all your files like this and use it in all your other property definitions with 276 * a "$basedir". Note that it does not matter if you define the variable before its usage. 277 * <pre> 278 * var.basedir = /p/mywiki; 279 * jspwiki.fileSystemProvider.pageDir = $basedir/www/ 280 * jspwiki.basicAttachmentProvider.storageDir = $basedir/www/ 281 * jspwiki.workDir = $basedir/wrk/ 282 * </pre> 283 * 284 * @param properties - properties to expand; 285 */ 286 public static void expandVars( final Properties properties ) { 287 //get variable name/values from properties... 288 final Map< String, String > vars = new HashMap<>(); 289 Enumeration< ? > propertyList = properties.propertyNames(); 290 while( propertyList.hasMoreElements() ) { 291 final String propertyName = ( String )propertyList.nextElement(); 292 final String propertyValue = properties.getProperty( propertyName ); 293 294 if ( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) { 295 final String varName = propertyName.substring( 4 ).trim(); 296 final String varValue = propertyValue.trim(); 297 vars.put( varName, varValue ); 298 } 299 } 300 301 //now, substitute $ values in property values with vars... 302 propertyList = properties.propertyNames(); 303 while( propertyList.hasMoreElements() ) { 304 final String propertyName = ( String )propertyList.nextElement(); 305 String propertyValue = properties.getProperty( propertyName ); 306 307 //skip var properties itself... 308 if( propertyName.startsWith( PARAM_VAR_DECLARATION ) ) { 309 continue; 310 } 311 312 for( final Map.Entry< String, String > entry : vars.entrySet() ) { 313 final String varName = entry.getKey(); 314 final String varValue = entry.getValue(); 315 316 //replace old property value, using the same variabe. If we don't overwrite 317 //the same one the next loop works with the original one again and 318 //multiple var expansion won't work... 319 propertyValue = TextUtil.replaceString( propertyValue, PARAM_VAR_IDENTIFIER + varName, varValue ); 320 321 //add the new PropertyValue to the properties 322 properties.put( propertyName, propertyValue ); 323 } 324 } 325 } 326 327 /** 328 * Locate a resource stored in the class path. Try first with "WEB-INF/classes" 329 * from the web app and fallback to "resourceName". 330 * 331 * @param context the servlet context 332 * @param resourceName the name of the resource 333 * @return the input stream of the resource or <b>null</b> if the resource was not found 334 */ 335 public static InputStream locateClassPathResource( final ServletContext context, final String resourceName ) { 336 InputStream result; 337 String currResourceLocation; 338 339 // garbage in - garbage out 340 if( StringUtils.isEmpty( resourceName ) ) { 341 return null; 342 } 343 344 // try with web app class loader searching in "WEB-INF/classes" 345 currResourceLocation = createResourceLocation( "/WEB-INF/classes", resourceName ); 346 result = context.getResourceAsStream( currResourceLocation ); 347 if( result != null ) { 348 LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation ); 349 return result; 350 } 351 352 // if not found - try with the current class loader and the given name 353 currResourceLocation = createResourceLocation( "", resourceName ); 354 result = PropertyReader.class.getResourceAsStream( currResourceLocation ); 355 if( result != null ) { 356 LOG.debug( " Successfully located the following classpath resource : " + currResourceLocation ); 357 return result; 358 } 359 360 LOG.debug( " Unable to resolve the following classpath resource : " + resourceName ); 361 362 return result; 363 } 364 365 /** 366 * Create a resource location with proper usage of "/". 367 * 368 * @param path a path 369 * @param name a resource name 370 * @return a resource location 371 */ 372 static String createResourceLocation( final String path, final String name ) { 373 Validate.notEmpty( name, "name is empty" ); 374 final StringBuilder result = new StringBuilder(); 375 376 // strip an ending "/" 377 final String sanitizedPath = ( path != null && !path.isEmpty() && path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path ); 378 379 // strip leading "/" 380 final String sanitizedName = ( name.startsWith( "/" ) ? name.substring( 1 ) : name ); 381 382 // append the optional path 383 if( sanitizedPath != null && !sanitizedPath.isEmpty() ) { 384 if( !sanitizedPath.startsWith( "/" ) ) { 385 result.append( "/" ); 386 } 387 result.append( sanitizedPath ); 388 } 389 result.append( "/" ); 390 391 // append the name 392 result.append( sanitizedName ); 393 return result.toString(); 394 } 395 396}