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