1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache license, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the license for the specific language governing permissions and
15 * limitations under the license.
16 */
17 package org.apache.logging.log4j.util;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.nio.charset.Charset;
22 import java.time.Duration;
23 import java.time.temporal.ChronoUnit;
24 import java.time.temporal.TemporalUnit;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Properties;
30 import java.util.ResourceBundle;
31 import java.util.ServiceLoader;
32 import java.util.Set;
33 import java.util.TreeSet;
34 import java.util.concurrent.ConcurrentHashMap;
35
36 /**
37 * <em>Consider this class private.</em>
38 * <p>
39 * Provides utility methods for managing {@link Properties} instances as well as access to the global configuration
40 * system. Properties by default are loaded from the system properties, system environment, and a classpath resource
41 * file named {@value #LOG4J_PROPERTIES_FILE_NAME}. Additional properties can be loaded by implementing a custom
42 * {@link PropertySource} service and specifying it via a {@link ServiceLoader} file called
43 * {@code META-INF/services/org.apache.logging.log4j.util.PropertySource} with a list of fully qualified class names
44 * implementing that interface.
45 * </p>
46 *
47 * @see PropertySource
48 */
49 public final class PropertiesUtil {
50
51 private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
52 private static final String LOG4J_SYSTEM_PROPERTIES_FILE_NAME = "log4j2.system.properties";
53 private static final String SYSTEM = "system:";
54 private static final PropertiesUtil LOG4J_PROPERTIES = new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME);
55
56 private final Environment environment;
57
58 /**
59 * Constructs a PropertiesUtil using a given Properties object as its source of defined properties.
60 *
61 * @param props the Properties to use by default
62 */
63 public PropertiesUtil(final Properties props) {
64 this.environment = new Environment(new PropertiesPropertySource(props));
65 }
66
67 /**
68 * Constructs a PropertiesUtil for a given properties file name on the classpath. The properties specified in this
69 * file are used by default. If a property is not defined in this file, then the equivalent system property is used.
70 *
71 * @param propertiesFileName the location of properties file to load
72 */
73 public PropertiesUtil(final String propertiesFileName) {
74 this.environment = new Environment(new PropertyFilePropertySource(propertiesFileName));
75 }
76
77 /**
78 * Loads and closes the given property input stream. If an error occurs, log to the status logger.
79 *
80 * @param in a property input stream.
81 * @param source a source object describing the source, like a resource string or a URL.
82 * @return a new Properties object
83 */
84 static Properties loadClose(final InputStream in, final Object source) {
85 final Properties props = new Properties();
86 if (null != in) {
87 try {
88 props.load(in);
89 } catch (final IOException e) {
90 LowLevelLogUtil.logException("Unable to read " + source, e);
91 } finally {
92 try {
93 in.close();
94 } catch (final IOException e) {
95 LowLevelLogUtil.logException("Unable to close " + source, e);
96 }
97 }
98 }
99 return props;
100 }
101
102 /**
103 * Returns the PropertiesUtil used by Log4j.
104 *
105 * @return the main Log4j PropertiesUtil instance.
106 */
107 public static PropertiesUtil getProperties() {
108 return LOG4J_PROPERTIES;
109 }
110
111 /**
112 * Returns {@code true} if the specified property is defined, regardless of its value (it may not have a value).
113 *
114 * @param name the name of the property to verify
115 * @return {@code true} if the specified property is defined, regardless of its value
116 */
117 public boolean hasProperty(final String name) {
118 return environment.containsKey(name);
119 }
120
121 /**
122 * Gets the named property as a boolean value. If the property matches the string {@code "true"} (case-insensitive),
123 * then it is returned as the boolean value {@code true}. Any other non-{@code null} text in the property is
124 * considered {@code false}.
125 *
126 * @param name the name of the property to look up
127 * @return the boolean value of the property or {@code false} if undefined.
128 */
129 public boolean getBooleanProperty(final String name) {
130 return getBooleanProperty(name, false);
131 }
132
133 /**
134 * Gets the named property as a boolean value.
135 *
136 * @param name the name of the property to look up
137 * @param defaultValue the default value to use if the property is undefined
138 * @return the boolean value of the property or {@code defaultValue} if undefined.
139 */
140 public boolean getBooleanProperty(final String name, final boolean defaultValue) {
141 final String prop = getStringProperty(name);
142 return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
143 }
144
145 /**
146 * Gets the named property as a boolean value.
147 *
148 * @param name the name of the property to look up
149 * @param defaultValueIfAbsent the default value to use if the property is undefined
150 * @param defaultValueIfPresent the default value to use if the property is defined but not assigned
151 * @return the boolean value of the property or {@code defaultValue} if undefined.
152 */
153 public boolean getBooleanProperty(final String name, final boolean defaultValueIfAbsent,
154 final boolean defaultValueIfPresent) {
155 final String prop = getStringProperty(name);
156 return prop == null ? defaultValueIfAbsent
157 : prop.isEmpty() ? defaultValueIfPresent : "true".equalsIgnoreCase(prop);
158 }
159
160 /**
161 * Retrieves a property that may be prefixed by more than one string.
162 * @param prefixes The array of prefixes.
163 * @param key The key to locate.
164 * @param supplier The method to call to derive the default value. If the value is null, null will be returned
165 * if no property is found.
166 * @return The value or null if it is not found.
167 * @since 2.13.0
168 */
169 public Boolean getBooleanProperty(final String[] prefixes, String key, Supplier<Boolean> supplier) {
170 for (String prefix : prefixes) {
171 if (hasProperty(prefix + key)) {
172 return getBooleanProperty(prefix + key);
173 }
174 }
175 return supplier != null ? supplier.get() : null;
176 }
177
178 /**
179 * Gets the named property as a Charset value.
180 *
181 * @param name the name of the property to look up
182 * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
183 */
184 public Charset getCharsetProperty(final String name) {
185 return getCharsetProperty(name, Charset.defaultCharset());
186 }
187
188 /**
189 * Gets the named property as a Charset value. If we cannot find the named Charset, see if it is mapped in
190 * file {@code Log4j-charsets.properties} on the class path.
191 *
192 * @param name the name of the property to look up
193 * @param defaultValue the default value to use if the property is undefined
194 * @return the Charset value of the property or {@code defaultValue} if undefined.
195 */
196 public Charset getCharsetProperty(final String name, final Charset defaultValue) {
197 final String charsetName = getStringProperty(name);
198 if (charsetName == null) {
199 return defaultValue;
200 }
201 if (Charset.isSupported(charsetName)) {
202 return Charset.forName(charsetName);
203 }
204 final ResourceBundle bundle = getCharsetsResourceBundle();
205 if (bundle.containsKey(name)) {
206 final String mapped = bundle.getString(name);
207 if (Charset.isSupported(mapped)) {
208 return Charset.forName(mapped);
209 }
210 }
211 LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
212 + defaultValue + " and continuing.");
213 return defaultValue;
214 }
215
216 /**
217 * Gets the named property as a double.
218 *
219 * @param name the name of the property to look up
220 * @param defaultValue the default value to use if the property is undefined
221 * @return the parsed double value of the property or {@code defaultValue} if it was undefined or could not be parsed.
222 */
223 public double getDoubleProperty(final String name, final double defaultValue) {
224 final String prop = getStringProperty(name);
225 if (prop != null) {
226 try {
227 return Double.parseDouble(prop);
228 } catch (final Exception ignored) {
229 }
230 }
231 return defaultValue;
232 }
233
234 /**
235 * Gets the named property as an integer.
236 *
237 * @param name the name of the property to look up
238 * @param defaultValue the default value to use if the property is undefined
239 * @return the parsed integer value of the property or {@code defaultValue} if it was undefined or could not be
240 * parsed.
241 */
242 public int getIntegerProperty(final String name, final int defaultValue) {
243 final String prop = getStringProperty(name);
244 if (prop != null) {
245 try {
246 return Integer.parseInt(prop);
247 } catch (final Exception ignored) {
248 // ignore
249 }
250 }
251 return defaultValue;
252 }
253
254 /**
255 * Retrieves a property that may be prefixed by more than one string.
256 * @param prefixes The array of prefixes.
257 * @param key The key to locate.
258 * @param supplier The method to call to derive the default value. If the value is null, null will be returned
259 * if no property is found.
260 * @return The value or null if it is not found.
261 * @since 2.13.0
262 */
263 public Integer getIntegerProperty(final String[] prefixes, String key, Supplier<Integer> supplier) {
264 for (String prefix : prefixes) {
265 if (hasProperty(prefix + key)) {
266 return getIntegerProperty(prefix + key, 0);
267 }
268 }
269 return supplier != null ? supplier.get() : null;
270 }
271
272 /**
273 * Gets the named property as a long.
274 *
275 * @param name the name of the property to look up
276 * @param defaultValue the default value to use if the property is undefined
277 * @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
278 */
279 public long getLongProperty(final String name, final long defaultValue) {
280 final String prop = getStringProperty(name);
281 if (prop != null) {
282 try {
283 return Long.parseLong(prop);
284 } catch (final Exception ignored) {
285 }
286 }
287 return defaultValue;
288 }
289
290 /**
291 * Retrieves a property that may be prefixed by more than one string.
292 * @param prefixes The array of prefixes.
293 * @param key The key to locate.
294 * @param supplier The method to call to derive the default value. If the value is null, null will be returned
295 * if no property is found.
296 * @return The value or null if it is not found.
297 * @since 2.13.0
298 */
299 public Long getLongProperty(final String[] prefixes, String key, Supplier<Long> supplier) {
300 for (String prefix : prefixes) {
301 if (hasProperty(prefix + key)) {
302 return getLongProperty(prefix + key, 0);
303 }
304 }
305 return supplier != null ? supplier.get() : null;
306 }
307
308 /**
309 * Retrieves a Duration where the String is of the format nnn[unit] where nnn represents an integer value
310 * and unit represents a time unit.
311 * @param name The property name.
312 * @param defaultValue The default value.
313 * @return The value of the String as a Duration or the default value, which may be null.
314 * @since 2.13.0
315 */
316 public Duration getDurationProperty(final String name, Duration defaultValue) {
317 final String prop = getStringProperty(name);
318 if (prop != null) {
319 return TimeUnit.getDuration(prop);
320 }
321 return defaultValue;
322 }
323
324 /**
325 * Retrieves a property that may be prefixed by more than one string.
326 * @param prefixes The array of prefixes.
327 * @param key The key to locate.
328 * @param supplier The method to call to derive the default value. If the value is null, null will be returned
329 * if no property is found.
330 * @return The value or null if it is not found.
331 * @since 2.13.0
332 */
333 public Duration getDurationProperty(final String[] prefixes, String key, Supplier<Duration> supplier) {
334 for (String prefix : prefixes) {
335 if (hasProperty(prefix + key)) {
336 return getDurationProperty(prefix + key, null);
337 }
338 }
339 return supplier != null ? supplier.get() : null;
340 }
341
342 /**
343 * Retrieves a property that may be prefixed by more than one string.
344 * @param prefixes The array of prefixes.
345 * @param key The key to locate.
346 * @param supplier The method to call to derive the default value. If the value is null, null will be returned
347 * if no property is found.
348 * @return The value or null if it is not found.
349 * @since 2.13.0
350 */
351 public String getStringProperty(final String[] prefixes, String key, Supplier<String> supplier) {
352 for (String prefix : prefixes) {
353 String result = getStringProperty(prefix + key);
354 if (result != null) {
355 return result;
356 }
357 }
358 return supplier != null ? supplier.get() : null;
359 }
360
361 /**
362 * Gets the named property as a String.
363 *
364 * @param name the name of the property to look up
365 * @return the String value of the property or {@code null} if undefined.
366 */
367 public String getStringProperty(final String name) {
368 return environment.get(name);
369 }
370
371 /**
372 * Gets the named property as a String.
373 *
374 * @param name the name of the property to look up
375 * @param defaultValue the default value to use if the property is undefined
376 * @return the String value of the property or {@code defaultValue} if undefined.
377 */
378 public String getStringProperty(final String name, final String defaultValue) {
379 final String prop = getStringProperty(name);
380 return (prop == null) ? defaultValue : prop;
381 }
382
383 /**
384 * Return the system properties or an empty Properties object if an error occurs.
385 *
386 * @return The system properties.
387 */
388 public static Properties getSystemProperties() {
389 try {
390 return new Properties(System.getProperties());
391 } catch (final SecurityException ex) {
392 LowLevelLogUtil.logException("Unable to access system properties.", ex);
393 // Sandboxed - can't read System Properties
394 return new Properties();
395 }
396 }
397
398 /**
399 * Reloads all properties. This is primarily useful for unit tests.
400 *
401 * @since 2.10.0
402 */
403 public void reload() {
404 environment.reload();
405 }
406
407 /**
408 * Provides support for looking up global configuration properties via environment variables, property files,
409 * and system properties, in three variations:
410 * <p>
411 * Normalized: all log4j-related prefixes removed, remaining property is camelCased with a log4j2 prefix for
412 * property files and system properties, or follows a LOG4J_FOO_BAR format for environment variables.
413 * <p>
414 * Legacy: the original property name as defined in the source pre-2.10.0.
415 * <p>
416 * Tokenized: loose matching based on word boundaries.
417 *
418 * @since 2.10.0
419 */
420 private static class Environment {
421
422 private final Set<PropertySource> sources = new TreeSet<>(new PropertySource.Comparator());
423 private final Map<CharSequence, String> literal = new ConcurrentHashMap<>();
424 private final Map<CharSequence, String> normalized = new ConcurrentHashMap<>();
425 private final Map<List<CharSequence>, String> tokenized = new ConcurrentHashMap<>();
426
427 private Environment(final PropertySource propertySource) {
428 PropertyFilePropertySource sysProps = new PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME);
429 try {
430 sysProps.forEach((key, value) -> {
431 if (System.getProperty(key) == null) {
432 System.setProperty(key, value);
433 }
434 });
435 } catch (SecurityException ex) {
436 // Access to System Properties is restricted so just skip it.
437 }
438 sources.add(propertySource);
439 for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
440 try {
441 for (final PropertySource source : ServiceLoader.load(PropertySource.class, classLoader)) {
442 sources.add(source);
443 }
444 } catch (final Throwable ex) {
445 /* Don't log anything to the console. It may not be a problem that a PropertySource
446 * isn't accessible.
447 */
448 }
449 }
450
451 reload();
452 }
453
454 private synchronized void reload() {
455 literal.clear();
456 normalized.clear();
457 tokenized.clear();
458 for (final PropertySource source : sources) {
459 source.forEach((key, value) -> {
460 if (key != null && value != null) {
461 literal.put(key, value);
462 final List<CharSequence> tokens = PropertySource.Util.tokenize(key);
463 if (tokens.isEmpty()) {
464 normalized.put(source.getNormalForm(Collections.singleton(key)), value);
465 } else {
466 normalized.put(source.getNormalForm(tokens), value);
467 tokenized.put(tokens, value);
468 }
469 }
470 });
471 }
472 }
473
474 private static boolean hasSystemProperty(final String key) {
475 try {
476 return System.getProperties().containsKey(key);
477 } catch (final SecurityException ignored) {
478 return false;
479 }
480 }
481
482 private String get(final String key) {
483 if (normalized.containsKey(key)) {
484 return normalized.get(key);
485 }
486 if (literal.containsKey(key)) {
487 return literal.get(key);
488 }
489 if (hasSystemProperty(key)) {
490 return System.getProperty(key);
491 }
492 for (final PropertySource source : sources) {
493 if (source.containsProperty(key)) {
494 return source.getProperty(key);
495 }
496 }
497 return tokenized.get(PropertySource.Util.tokenize(key));
498 }
499
500 private boolean containsKey(final String key) {
501 return normalized.containsKey(key) ||
502 literal.containsKey(key) ||
503 hasSystemProperty(key) ||
504 tokenized.containsKey(PropertySource.Util.tokenize(key));
505 }
506 }
507
508 /**
509 * Extracts properties that start with or are equals to the specific prefix and returns them in a new Properties
510 * object with the prefix removed.
511 *
512 * @param properties The Properties to evaluate.
513 * @param prefix The prefix to extract.
514 * @return The subset of properties.
515 */
516 public static Properties extractSubset(final Properties properties, final String prefix) {
517 final Properties subset = new Properties();
518
519 if (prefix == null || prefix.length() == 0) {
520 return subset;
521 }
522
523 final String prefixToMatch = prefix.charAt(prefix.length() - 1) != '.' ? prefix + '.' : prefix;
524
525 final List<String> keys = new ArrayList<>();
526
527 for (final String key : properties.stringPropertyNames()) {
528 if (key.startsWith(prefixToMatch)) {
529 subset.setProperty(key.substring(prefixToMatch.length()), properties.getProperty(key));
530 keys.add(key);
531 }
532 }
533 for (final String key : keys) {
534 properties.remove(key);
535 }
536
537 return subset;
538 }
539
540 static ResourceBundle getCharsetsResourceBundle() {
541 return ResourceBundle.getBundle("Log4j-charsets");
542 }
543
544 /**
545 * Partitions a properties map based on common key prefixes up to the first period.
546 *
547 * @param properties properties to partition
548 * @return the partitioned properties where each key is the common prefix (minus the period) and the values are
549 * new property maps without the prefix and period in the key
550 * @since 2.6
551 */
552 public static Map<String, Properties> partitionOnCommonPrefixes(final Properties properties) {
553 final Map<String, Properties> parts = new ConcurrentHashMap<>();
554 for (final String key : properties.stringPropertyNames()) {
555 final String prefix = key.substring(0, key.indexOf('.'));
556 if (!parts.containsKey(prefix)) {
557 parts.put(prefix, new Properties());
558 }
559 parts.get(prefix).setProperty(key.substring(key.indexOf('.') + 1), properties.getProperty(key));
560 }
561 return parts;
562 }
563
564 /**
565 * Returns true if system properties tell us we are running on Windows.
566 *
567 * @return true if system properties tell us we are running on Windows.
568 */
569 public boolean isOsWindows() {
570 return getStringProperty("os.name", "").startsWith("Windows");
571 }
572
573 private enum TimeUnit {
574 NANOS("ns,nano,nanos,nanosecond,nanoseconds", ChronoUnit.NANOS),
575 MICROS("us,micro,micros,microsecond,microseconds", ChronoUnit.MICROS),
576 MILLIS("ms,milli,millis,millsecond,milliseconds", ChronoUnit.MILLIS),
577 SECONDS("s,second,seconds", ChronoUnit.SECONDS),
578 MINUTES("m,minute,minutes", ChronoUnit.MINUTES),
579 HOURS("h,hour,hours", ChronoUnit.HOURS),
580 DAYS("d,day,days", ChronoUnit.DAYS);
581
582 private final String[] descriptions;
583 private final ChronoUnit timeUnit;
584
585 TimeUnit(String descriptions, ChronoUnit timeUnit) {
586 this.descriptions = descriptions.split(",");
587 this.timeUnit = timeUnit;
588 }
589
590 ChronoUnit getTimeUnit() {
591 return this.timeUnit;
592 }
593
594 static Duration getDuration(String time) {
595 String value = time.trim();
596 TemporalUnit temporalUnit = ChronoUnit.MILLIS;
597 long timeVal = 0;
598 for (TimeUnit timeUnit : values()) {
599 for (String suffix : timeUnit.descriptions) {
600 if (value.endsWith(suffix)) {
601 temporalUnit = timeUnit.timeUnit;
602 timeVal = Long.parseLong(value.substring(0, value.length() - suffix.length()));
603 }
604 }
605 }
606 return Duration.of(timeVal, temporalUnit);
607 }
608 }
609 }