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