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.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Properties;
27  import java.util.ResourceBundle;
28  import java.util.ServiceLoader;
29  import java.util.Set;
30  import java.util.TreeSet;
31  import java.util.concurrent.ConcurrentHashMap;
32  
33  /**
34   * <em>Consider this class private.</em>
35   * <p>
36   * Provides utility methods for managing {@link Properties} instances as well as access to the global configuration
37   * system. Properties by default are loaded from the system properties, system environment, and a classpath resource
38   * file named {@value #LOG4J_PROPERTIES_FILE_NAME}. Additional properties can be loaded by implementing a custom
39   * {@link PropertySource} service and specifying it via a {@link ServiceLoader} file called
40   * {@code META-INF/services/org.apache.logging.log4j.util.PropertySource} with a list of fully qualified class names
41   * implementing that interface.
42   * </p>
43   *
44   * @see PropertySource
45   */
46  public final class PropertiesUtil {
47  
48      private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
49      private static final PropertiesUtil LOG4J_PROPERTIES = new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME);
50  
51      private final Environment environment;
52  
53      /**
54       * Constructs a PropertiesUtil using a given Properties object as its source of defined properties.
55       *
56       * @param props the Properties to use by default
57       */
58      public PropertiesUtil(final Properties props) {
59          this.environment = new Environment(new PropertiesPropertySource(props));
60      }
61  
62      /**
63       * Constructs a PropertiesUtil for a given properties file name on the classpath. The properties specified in this
64       * file are used by default. If a property is not defined in this file, then the equivalent system property is used.
65       *
66       * @param propertiesFileName the location of properties file to load
67       */
68      public PropertiesUtil(final String propertiesFileName) {
69          this.environment = new Environment(new PropertyFilePropertySource(propertiesFileName));
70      }
71  
72      /**
73       * Loads and closes the given property input stream. If an error occurs, log to the status logger.
74       *
75       * @param in     a property input stream.
76       * @param source a source object describing the source, like a resource string or a URL.
77       * @return a new Properties object
78       */
79      static Properties loadClose(final InputStream in, final Object source) {
80          final Properties props = new Properties();
81          if (null != in) {
82              try {
83                  props.load(in);
84              } catch (final IOException e) {
85                  LowLevelLogUtil.logException("Unable to read " + source, e);
86              } finally {
87                  try {
88                      in.close();
89                  } catch (final IOException e) {
90                      LowLevelLogUtil.logException("Unable to close " + source, e);
91                  }
92              }
93          }
94          return props;
95      }
96  
97      /**
98       * Returns the PropertiesUtil used by Log4j.
99       *
100      * @return the main Log4j PropertiesUtil instance.
101      */
102     public static PropertiesUtil getProperties() {
103         return LOG4J_PROPERTIES;
104     }
105 
106     /**
107      * Returns {@code true} if the specified property is defined, regardless of its value (it may not have a value).
108      *
109      * @param name the name of the property to verify
110      * @return {@code true} if the specified property is defined, regardless of its value
111      */
112     public boolean hasProperty(final String name) {
113         return environment.containsKey(name);
114     }
115 
116     /**
117      * Gets the named property as a boolean value. If the property matches the string {@code "true"} (case-insensitive),
118      * then it is returned as the boolean value {@code true}. Any other non-{@code null} text in the property is
119      * considered {@code false}.
120      *
121      * @param name the name of the property to look up
122      * @return the boolean value of the property or {@code false} if undefined.
123      */
124     public boolean getBooleanProperty(final String name) {
125         return getBooleanProperty(name, false);
126     }
127 
128     /**
129      * Gets the named property as a boolean value.
130      *
131      * @param name         the name of the property to look up
132      * @param defaultValue the default value to use if the property is undefined
133      * @return the boolean value of the property or {@code defaultValue} if undefined.
134      */
135     public boolean getBooleanProperty(final String name, final boolean defaultValue) {
136         final String prop = getStringProperty(name);
137         return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
138     }
139 
140     /**
141      * Gets the named property as a boolean value.
142      *
143      * @param name                  the name of the property to look up
144      * @param defaultValueIfAbsent  the default value to use if the property is undefined
145      * @param defaultValueIfPresent the default value to use if the property is defined but not assigned
146      * @return the boolean value of the property or {@code defaultValue} if undefined.
147      */
148     public boolean getBooleanProperty(final String name, final boolean defaultValueIfAbsent,
149                                       final boolean defaultValueIfPresent) {
150         final String prop = getStringProperty(name);
151         return prop == null ? defaultValueIfAbsent
152             : prop.isEmpty() ? defaultValueIfPresent : "true".equalsIgnoreCase(prop);
153     }
154 
155     /**
156      * Gets the named property as a Charset value.
157      *
158      * @param name the name of the property to look up
159      * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
160      */
161     public Charset getCharsetProperty(final String name) {
162         return getCharsetProperty(name, Charset.defaultCharset());
163     }
164 
165     /**
166      * Gets the named property as a Charset value. If we cannot find the named Charset, see if it is mapped in
167      * file {@code Log4j-charsets.properties} on the class path.
168      *
169      * @param name         the name of the property to look up
170      * @param defaultValue the default value to use if the property is undefined
171      * @return the Charset value of the property or {@code defaultValue} if undefined.
172      */
173     public Charset getCharsetProperty(final String name, final Charset defaultValue) {
174         final String charsetName = getStringProperty(name);
175         if (charsetName == null) {
176             return defaultValue;
177         }
178         if (Charset.isSupported(charsetName)) {
179             return Charset.forName(charsetName);
180         }
181         ResourceBundle bundle = getCharsetsResourceBundle();
182         if (bundle.containsKey(name)) {
183             String mapped = bundle.getString(name);
184             if (Charset.isSupported(mapped)) {
185                 return Charset.forName(mapped);
186             }
187         }
188         LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
189             + defaultValue + " and continuing.");
190         return defaultValue;
191     }
192 
193     /**
194      * Gets the named property as a double.
195      *
196      * @param name         the name of the property to look up
197      * @param defaultValue the default value to use if the property is undefined
198      * @return the parsed double value of the property or {@code defaultValue} if it was undefined or could not be parsed.
199      */
200     public double getDoubleProperty(final String name, final double defaultValue) {
201         final String prop = getStringProperty(name);
202         if (prop != null) {
203             try {
204                 return Double.parseDouble(prop);
205             } catch (final Exception ignored) {
206                 return defaultValue;
207             }
208         }
209         return defaultValue;
210     }
211 
212     /**
213      * Gets the named property as an integer.
214      *
215      * @param name         the name of the property to look up
216      * @param defaultValue the default value to use if the property is undefined
217      * @return the parsed integer value of the property or {@code defaultValue} if it was undefined or could not be
218      * parsed.
219      */
220     public int getIntegerProperty(final String name, final int defaultValue) {
221         final String prop = getStringProperty(name);
222         if (prop != null) {
223             try {
224                 return Integer.parseInt(prop);
225             } catch (final Exception ignored) {
226                 return defaultValue;
227             }
228         }
229         return defaultValue;
230     }
231 
232     /**
233      * Gets the named property as a long.
234      *
235      * @param name         the name of the property to look up
236      * @param defaultValue the default value to use if the property is undefined
237      * @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
238      */
239     public long getLongProperty(final String name, final long defaultValue) {
240         final String prop = getStringProperty(name);
241         if (prop != null) {
242             try {
243                 return Long.parseLong(prop);
244             } catch (final Exception ignored) {
245                 return defaultValue;
246             }
247         }
248         return defaultValue;
249     }
250 
251     /**
252      * Gets the named property as a String.
253      *
254      * @param name the name of the property to look up
255      * @return the String value of the property or {@code null} if undefined.
256      */
257     public String getStringProperty(final String name) {
258         return environment.get(name);
259     }
260 
261     /**
262      * Gets the named property as a String.
263      *
264      * @param name         the name of the property to look up
265      * @param defaultValue the default value to use if the property is undefined
266      * @return the String value of the property or {@code defaultValue} if undefined.
267      */
268     public String getStringProperty(final String name, final String defaultValue) {
269         final String prop = getStringProperty(name);
270         return (prop == null) ? defaultValue : prop;
271     }
272 
273     /**
274      * Return the system properties or an empty Properties object if an error occurs.
275      *
276      * @return The system properties.
277      */
278     public static Properties getSystemProperties() {
279         try {
280             return new Properties(System.getProperties());
281         } catch (final SecurityException ex) {
282             LowLevelLogUtil.logException("Unable to access system properties.", ex);
283             // Sandboxed - can't read System Properties
284             return new Properties();
285         }
286     }
287 
288     /**
289      * Reloads all properties. This is primarily useful for unit tests.
290      *
291      * @since 2.10.0
292      */
293     public void reload() {
294         environment.reload();
295     }
296 
297     /**
298      * Provides support for looking up global configuration properties via environment variables, property files,
299      * and system properties, in three variations:
300      * <p>
301      * Normalized: all log4j-related prefixes removed, remaining property is camelCased with a log4j2 prefix for
302      * property files and system properties, or follows a LOG4J_FOO_BAR format for environment variables.
303      * <p>
304      * Legacy: the original property name as defined in the source pre-2.10.0.
305      * <p>
306      * Tokenized: loose matching based on word boundaries.
307      *
308      * @since 2.10.0
309      */
310     private static class Environment {
311 
312         private final Set<PropertySource> sources = new TreeSet<>(new PropertySource.Comparator());
313         private final Map<CharSequence, String> literal = new ConcurrentHashMap<>();
314         private final Map<CharSequence, String> normalized = new ConcurrentHashMap<>();
315         private final Map<List<CharSequence>, String> tokenized = new ConcurrentHashMap<>();
316 
317         private Environment(final PropertySource propertySource) {
318             sources.add(propertySource);
319             for (final PropertySource source : ServiceLoader.load(PropertySource.class)) {
320                 sources.add(source);
321             }
322             reload();
323         }
324 
325         private synchronized void reload() {
326             literal.clear();
327             normalized.clear();
328             tokenized.clear();
329             for (final PropertySource source : sources) {
330                 source.forEach(new BiConsumer<String, String>() {
331                     @Override
332                     public void accept(final String key, final String value) {
333                         if (key != null && value != null) {
334                             literal.put(key, value);
335                             final List<CharSequence> tokens = PropertySource.Util.tokenize(key);
336                             if (tokens.isEmpty()) {
337                                 normalized.put(source.getNormalForm(Collections.singleton(key)), value);
338                             } else {
339                                 normalized.put(source.getNormalForm(tokens), value);
340                                 tokenized.put(tokens, value);
341                             }
342                         }
343                     }
344                 });
345             }
346         }
347 
348         private static boolean hasSystemProperty(final String key) {
349             try {
350                 return System.getProperties().containsKey(key);
351             } catch (final SecurityException ignored) {
352                 return false;
353             }
354         }
355 
356         private String get(final String key) {
357             if (normalized.containsKey(key)) {
358                 return normalized.get(key);
359             }
360             if (literal.containsKey(key)) {
361                 return literal.get(key);
362             }
363             if (hasSystemProperty(key)) {
364                 return System.getProperty(key);
365             }
366             return tokenized.get(PropertySource.Util.tokenize(key));
367         }
368 
369         private boolean containsKey(final String key) {
370             return normalized.containsKey(key) ||
371                 literal.containsKey(key) ||
372                 hasSystemProperty(key) ||
373                 tokenized.containsKey(PropertySource.Util.tokenize(key));
374         }
375     }
376 
377     /**
378      * Extracts properties that start with or are equals to the specific prefix and returns them in a new Properties
379      * object with the prefix removed.
380      *
381      * @param properties The Properties to evaluate.
382      * @param prefix     The prefix to extract.
383      * @return The subset of properties.
384      */
385     public static Properties extractSubset(final Properties properties, final String prefix) {
386         final Properties subset = new Properties();
387 
388         if (prefix == null || prefix.length() == 0) {
389             return subset;
390         }
391 
392         final String prefixToMatch = prefix.charAt(prefix.length() - 1) != '.' ? prefix + '.' : prefix;
393 
394         final List<String> keys = new ArrayList<>();
395 
396         for (final String key : properties.stringPropertyNames()) {
397             if (key.startsWith(prefixToMatch)) {
398                 subset.setProperty(key.substring(prefixToMatch.length()), properties.getProperty(key));
399                 keys.add(key);
400             }
401         }
402         for (final String key : keys) {
403             properties.remove(key);
404         }
405 
406         return subset;
407     }
408 
409     static ResourceBundle getCharsetsResourceBundle() {
410         return ResourceBundle.getBundle("Log4j-charsets");
411     }
412 
413     /**
414      * Partitions a properties map based on common key prefixes up to the first period.
415      *
416      * @param properties properties to partition
417      * @return the partitioned properties where each key is the common prefix (minus the period) and the values are
418      * new property maps without the prefix and period in the key
419      * @since 2.6
420      */
421     public static Map<String, Properties> partitionOnCommonPrefixes(final Properties properties) {
422         final Map<String, Properties> parts = new ConcurrentHashMap<>();
423         for (final String key : properties.stringPropertyNames()) {
424             final String prefix = key.substring(0, key.indexOf('.'));
425             if (!parts.containsKey(prefix)) {
426                 parts.put(prefix, new Properties());
427             }
428             parts.get(prefix).setProperty(key.substring(key.indexOf('.') + 1), properties.getProperty(key));
429         }
430         return parts;
431     }
432 
433     /**
434      * Returns true if system properties tell us we are running on Windows.
435      *
436      * @return true if system properties tell us we are running on Windows.
437      */
438     public boolean isOsWindows() {
439         return getStringProperty("os.name", "").startsWith("Windows");
440     }
441 
442 }