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 trueif 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 trueif 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 falseif 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 nullnull 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 nullnull 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 nullnull 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 nullnull 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 nullnull 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 nullif 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