001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.util;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.nio.charset.Charset;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Map;
026import java.util.Properties;
027import java.util.ResourceBundle;
028import java.util.ServiceLoader;
029import java.util.Set;
030import java.util.TreeSet;
031import java.util.concurrent.ConcurrentHashMap;
032
033/**
034 * <em>Consider this class private.</em>
035 * <p>
036 * Provides utility methods for managing {@link Properties} instances as well as access to the global configuration
037 * system. Properties by default are loaded from the system properties, system environment, and a classpath resource
038 * file named {@value #LOG4J_PROPERTIES_FILE_NAME}. Additional properties can be loaded by implementing a custom
039 * {@link PropertySource} service and specifying it via a {@link ServiceLoader} file called
040 * {@code META-INF/services/org.apache.logging.log4j.util.PropertySource} with a list of fully qualified class names
041 * implementing that interface.
042 * </p>
043 *
044 * @see PropertySource
045 */
046public final class PropertiesUtil {
047
048    private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
049    private static final PropertiesUtil LOG4J_PROPERTIES = new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME);
050
051    private final Environment environment;
052
053    /**
054     * Constructs a PropertiesUtil using a given Properties object as its source of defined properties.
055     *
056     * @param props the Properties to use by default
057     */
058    public PropertiesUtil(final Properties props) {
059        this.environment = new Environment(new PropertiesPropertySource(props));
060    }
061
062    /**
063     * Constructs a PropertiesUtil for a given properties file name on the classpath. The properties specified in this
064     * file are used by default. If a property is not defined in this file, then the equivalent system property is used.
065     *
066     * @param propertiesFileName the location of properties file to load
067     */
068    public PropertiesUtil(final String propertiesFileName) {
069        this.environment = new Environment(new PropertyFilePropertySource(propertiesFileName));
070    }
071
072    /**
073     * Loads and closes the given property input stream. If an error occurs, log to the status logger.
074     *
075     * @param in     a property input stream.
076     * @param source a source object describing the source, like a resource string or a URL.
077     * @return a new Properties object
078     */
079    static Properties loadClose(final InputStream in, final Object source) {
080        final Properties props = new Properties();
081        if (null != in) {
082            try {
083                props.load(in);
084            } catch (final IOException e) {
085                LowLevelLogUtil.logException("Unable to read " + source, e);
086            } finally {
087                try {
088                    in.close();
089                } catch (final IOException e) {
090                    LowLevelLogUtil.logException("Unable to close " + source, e);
091                }
092            }
093        }
094        return props;
095    }
096
097    /**
098     * Returns the PropertiesUtil used by Log4j.
099     *
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        final ResourceBundle bundle = getCharsetsResourceBundle();
182        if (bundle.containsKey(name)) {
183            final 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 ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
320                                try {
321                                        for (final PropertySource source : ServiceLoader.load(PropertySource.class, classLoader)) {
322                                                sources.add(source);
323                                        }
324                                } catch (final Throwable ex) {
325                                        /* Don't log anything to the console. It may not be a problem that a PropertySource
326                                         * isn't accessible.
327                                         */
328                                }
329                        }
330
331            reload();
332        }
333
334        private synchronized void reload() {
335            literal.clear();
336            normalized.clear();
337            tokenized.clear();
338            for (final PropertySource source : sources) {
339                source.forEach(new BiConsumer<String, String>() {
340                    @Override
341                    public void accept(final String key, final String value) {
342                        if (key != null && value != null) {
343                            literal.put(key, value);
344                            final List<CharSequence> tokens = PropertySource.Util.tokenize(key);
345                            if (tokens.isEmpty()) {
346                                normalized.put(source.getNormalForm(Collections.singleton(key)), value);
347                            } else {
348                                normalized.put(source.getNormalForm(tokens), value);
349                                tokenized.put(tokens, value);
350                            }
351                        }
352                    }
353                });
354            }
355        }
356
357        private static boolean hasSystemProperty(final String key) {
358            try {
359                return System.getProperties().containsKey(key);
360            } catch (final SecurityException ignored) {
361                return false;
362            }
363        }
364
365        private String get(final String key) {
366            if (normalized.containsKey(key)) {
367                return normalized.get(key);
368            }
369            if (literal.containsKey(key)) {
370                return literal.get(key);
371            }
372            if (hasSystemProperty(key)) {
373                return System.getProperty(key);
374            }
375            return tokenized.get(PropertySource.Util.tokenize(key));
376        }
377
378        private boolean containsKey(final String key) {
379            return normalized.containsKey(key) ||
380                literal.containsKey(key) ||
381                hasSystemProperty(key) ||
382                tokenized.containsKey(PropertySource.Util.tokenize(key));
383        }
384    }
385
386    /**
387     * Extracts properties that start with or are equals to the specific prefix and returns them in a new Properties
388     * object with the prefix removed.
389     *
390     * @param properties The Properties to evaluate.
391     * @param prefix     The prefix to extract.
392     * @return The subset of properties.
393     */
394    public static Properties extractSubset(final Properties properties, final String prefix) {
395        final Properties subset = new Properties();
396
397        if (prefix == null || prefix.length() == 0) {
398            return subset;
399        }
400
401        final String prefixToMatch = prefix.charAt(prefix.length() - 1) != '.' ? prefix + '.' : prefix;
402
403        final List<String> keys = new ArrayList<>();
404
405        for (final String key : properties.stringPropertyNames()) {
406            if (key.startsWith(prefixToMatch)) {
407                subset.setProperty(key.substring(prefixToMatch.length()), properties.getProperty(key));
408                keys.add(key);
409            }
410        }
411        for (final String key : keys) {
412            properties.remove(key);
413        }
414
415        return subset;
416    }
417
418    static ResourceBundle getCharsetsResourceBundle() {
419        return ResourceBundle.getBundle("Log4j-charsets");
420    }
421
422    /**
423     * Partitions a properties map based on common key prefixes up to the first period.
424     *
425     * @param properties properties to partition
426     * @return the partitioned properties where each key is the common prefix (minus the period) and the values are
427     * new property maps without the prefix and period in the key
428     * @since 2.6
429     */
430    public static Map<String, Properties> partitionOnCommonPrefixes(final Properties properties) {
431        final Map<String, Properties> parts = new ConcurrentHashMap<>();
432        for (final String key : properties.stringPropertyNames()) {
433            final String prefix = key.substring(0, key.indexOf('.'));
434            if (!parts.containsKey(prefix)) {
435                parts.put(prefix, new Properties());
436            }
437            parts.get(prefix).setProperty(key.substring(key.indexOf('.') + 1), properties.getProperty(key));
438        }
439        return parts;
440    }
441
442    /**
443     * Returns true if system properties tell us we are running on Windows.
444     *
445     * @return true if system properties tell us we are running on Windows.
446     */
447    public boolean isOsWindows() {
448        return getStringProperty("os.name", "").startsWith("Windows");
449    }
450
451}