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