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 String LOG4J_SYSTEM_PROPERTIES_FILE_NAME = "log4j2.system.properties";
050    private static final String SYSTEM = "system:";
051    private static final PropertiesUtil LOG4J_PROPERTIES = new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME);
052
053    private final Environment environment;
054
055    /**
056     * Constructs a PropertiesUtil using a given Properties object as its source of defined properties.
057     *
058     * @param props the Properties to use by default
059     */
060    public PropertiesUtil(final Properties props) {
061        this.environment = new Environment(new PropertiesPropertySource(props));
062    }
063
064    /**
065     * Constructs a PropertiesUtil for a given properties file name on the classpath. The properties specified in this
066     * file are used by default. If a property is not defined in this file, then the equivalent system property is used.
067     *
068     * @param propertiesFileName the location of properties file to load
069     */
070    public PropertiesUtil(final String propertiesFileName) {
071        this.environment = new Environment(new PropertyFilePropertySource(propertiesFileName));
072    }
073
074    /**
075     * Loads and closes the given property input stream. If an error occurs, log to the status logger.
076     *
077     * @param in     a property input stream.
078     * @param source a source object describing the source, like a resource string or a URL.
079     * @return a new Properties object
080     */
081    static Properties loadClose(final InputStream in, final Object source) {
082        final Properties props = new Properties();
083        if (null != in) {
084            try {
085                props.load(in);
086            } catch (final IOException e) {
087                LowLevelLogUtil.logException("Unable to read " + source, e);
088            } finally {
089                try {
090                    in.close();
091                } catch (final IOException e) {
092                    LowLevelLogUtil.logException("Unable to close " + source, e);
093                }
094            }
095        }
096        return props;
097    }
098
099    /**
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}