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