View Javadoc
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 }