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.time.Duration;
023import java.time.temporal.ChronoUnit;
024import java.time.temporal.TemporalUnit;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import java.util.Map;
029import java.util.Properties;
030import java.util.ResourceBundle;
031import java.util.ServiceLoader;
032import java.util.Set;
033import java.util.TreeSet;
034import java.util.concurrent.ConcurrentHashMap;
035
036/**
037 * <em>Consider this class private.</em>
038 * <p>
039 * Provides utility methods for managing {@link Properties} instances as well as access to the global configuration
040 * system. Properties by default are loaded from the system properties, system environment, and a classpath resource
041 * file named {@value #LOG4J_PROPERTIES_FILE_NAME}. Additional properties can be loaded by implementing a custom
042 * {@link PropertySource} service and specifying it via a {@link ServiceLoader} file called
043 * {@code META-INF/services/org.apache.logging.log4j.util.PropertySource} with a list of fully qualified class names
044 * implementing that interface.
045 * </p>
046 *
047 * @see PropertySource
048 */
049public final class PropertiesUtil {
050
051    private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
052    private static final String LOG4J_SYSTEM_PROPERTIES_FILE_NAME = "log4j2.system.properties";
053    private static final String SYSTEM = "system:";
054    private static final PropertiesUtil LOG4J_PROPERTIES = new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME);
055
056    private final Environment environment;
057
058    /**
059     * Constructs a PropertiesUtil using a given Properties object as its source of defined properties.
060     *
061     * @param props the Properties to use by default
062     */
063    public PropertiesUtil(final Properties props) {
064        this.environment = new Environment(new PropertiesPropertySource(props));
065    }
066
067    /**
068     * Constructs a PropertiesUtil for a given properties file name on the classpath. The properties specified in this
069     * file are used by default. If a property is not defined in this file, then the equivalent system property is used.
070     *
071     * @param propertiesFileName the location of properties file to load
072     */
073    public PropertiesUtil(final String propertiesFileName) {
074        this.environment = new Environment(new PropertyFilePropertySource(propertiesFileName));
075    }
076
077    /**
078     * Loads and closes the given property input stream. If an error occurs, log to the status logger.
079     *
080     * @param in     a property input stream.
081     * @param source a source object describing the source, like a resource string or a URL.
082     * @return a new Properties object
083     */
084    static Properties loadClose(final InputStream in, final Object source) {
085        final Properties props = new Properties();
086        if (null != in) {
087            try {
088                props.load(in);
089            } catch (final IOException e) {
090                LowLevelLogUtil.logException("Unable to read " + source, e);
091            } finally {
092                try {
093                    in.close();
094                } catch (final IOException e) {
095                    LowLevelLogUtil.logException("Unable to close " + source, e);
096                }
097            }
098        }
099        return props;
100    }
101
102    /**
103     * Returns the PropertiesUtil used by Log4j.
104     *
105     * @return the main Log4j PropertiesUtil instance.
106     */
107    public static PropertiesUtil getProperties() {
108        return LOG4J_PROPERTIES;
109    }
110
111    /**
112     * Returns {@code true} if the specified property is defined, regardless of its value (it may not have a value).
113     *
114     * @param name the name of the property to verify
115     * @return {@code true} if the specified property is defined, regardless of its value
116     */
117    public boolean hasProperty(final String name) {
118        return environment.containsKey(name);
119    }
120
121    /**
122     * Gets the named property as a boolean value. If the property matches the string {@code "true"} (case-insensitive),
123     * then it is returned as the boolean value {@code true}. Any other non-{@code null} text in the property is
124     * considered {@code false}.
125     *
126     * @param name the name of the property to look up
127     * @return the boolean value of the property or {@code false} if undefined.
128     */
129    public boolean getBooleanProperty(final String name) {
130        return getBooleanProperty(name, false);
131    }
132
133    /**
134     * Gets the named property as a boolean value.
135     *
136     * @param name         the name of the property to look up
137     * @param defaultValue the default value to use if the property is undefined
138     * @return the boolean value of the property or {@code defaultValue} if undefined.
139     */
140    public boolean getBooleanProperty(final String name, final boolean defaultValue) {
141        final String prop = getStringProperty(name);
142        return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
143    }
144
145    /**
146     * Gets the named property as a boolean value.
147     *
148     * @param name                  the name of the property to look up
149     * @param defaultValueIfAbsent  the default value to use if the property is undefined
150     * @param defaultValueIfPresent the default value to use if the property is defined but not assigned
151     * @return the boolean value of the property or {@code defaultValue} if undefined.
152     */
153    public boolean getBooleanProperty(final String name, final boolean defaultValueIfAbsent,
154                                      final boolean defaultValueIfPresent) {
155        final String prop = getStringProperty(name);
156        return prop == null ? defaultValueIfAbsent
157            : prop.isEmpty() ? defaultValueIfPresent : "true".equalsIgnoreCase(prop);
158    }
159
160    /**
161     * Retrieves a property that may be prefixed by more than one string.
162     * @param prefixes The array of prefixes.
163     * @param key The key to locate.
164     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
165     * if no property is found.
166     * @return The value or null if it is not found.
167     * @since 2.13.0
168     */
169    public Boolean getBooleanProperty(final String[] prefixes, String key, Supplier<Boolean> supplier) {
170        for (String prefix : prefixes) {
171            if (hasProperty(prefix + key)) {
172                return getBooleanProperty(prefix + key);
173            }
174        }
175        return supplier != null ? supplier.get() : null;
176    }
177
178    /**
179     * Gets the named property as a Charset value.
180     *
181     * @param name the name of the property to look up
182     * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
183     */
184    public Charset getCharsetProperty(final String name) {
185        return getCharsetProperty(name, Charset.defaultCharset());
186    }
187
188    /**
189     * Gets the named property as a Charset value. If we cannot find the named Charset, see if it is mapped in
190     * file {@code Log4j-charsets.properties} on the class path.
191     *
192     * @param name         the name of the property to look up
193     * @param defaultValue the default value to use if the property is undefined
194     * @return the Charset value of the property or {@code defaultValue} if undefined.
195     */
196    public Charset getCharsetProperty(final String name, final Charset defaultValue) {
197        final String charsetName = getStringProperty(name);
198        if (charsetName == null) {
199            return defaultValue;
200        }
201        if (Charset.isSupported(charsetName)) {
202            return Charset.forName(charsetName);
203        }
204        final ResourceBundle bundle = getCharsetsResourceBundle();
205        if (bundle.containsKey(name)) {
206            final String mapped = bundle.getString(name);
207            if (Charset.isSupported(mapped)) {
208                return Charset.forName(mapped);
209            }
210        }
211        LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
212            + defaultValue + " and continuing.");
213        return defaultValue;
214    }
215
216    /**
217     * Gets the named property as a double.
218     *
219     * @param name         the name of the property to look up
220     * @param defaultValue the default value to use if the property is undefined
221     * @return the parsed double value of the property or {@code defaultValue} if it was undefined or could not be parsed.
222     */
223    public double getDoubleProperty(final String name, final double defaultValue) {
224        final String prop = getStringProperty(name);
225        if (prop != null) {
226            try {
227                return Double.parseDouble(prop);
228            } catch (final Exception ignored) {
229            }
230        }
231        return defaultValue;
232    }
233
234    /**
235     * Gets the named property as an integer.
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 integer value of the property or {@code defaultValue} if it was undefined or could not be
240     * parsed.
241     */
242    public int getIntegerProperty(final String name, final int defaultValue) {
243        final String prop = getStringProperty(name);
244        if (prop != null) {
245            try {
246                return Integer.parseInt(prop);
247            } catch (final Exception ignored) {
248                // ignore
249            }
250        }
251        return defaultValue;
252    }
253
254    /**
255     * Retrieves a property that may be prefixed by more than one string.
256     * @param prefixes The array of prefixes.
257     * @param key The key to locate.
258     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
259     * if no property is found.
260     * @return The value or null if it is not found.
261     * @since 2.13.0
262     */
263    public Integer getIntegerProperty(final String[] prefixes, String key, Supplier<Integer> supplier) {
264        for (String prefix : prefixes) {
265            if (hasProperty(prefix + key)) {
266                return getIntegerProperty(prefix + key, 0);
267            }
268        }
269        return supplier != null ? supplier.get() : null;
270    }
271
272    /**
273     * Gets the named property as a long.
274     *
275     * @param name         the name of the property to look up
276     * @param defaultValue the default value to use if the property is undefined
277     * @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
278     */
279    public long getLongProperty(final String name, final long defaultValue) {
280        final String prop = getStringProperty(name);
281        if (prop != null) {
282            try {
283                return Long.parseLong(prop);
284            } catch (final Exception ignored) {
285            }
286        }
287        return defaultValue;
288    }
289
290    /**
291     * Retrieves a property that may be prefixed by more than one string.
292     * @param prefixes The array of prefixes.
293     * @param key The key to locate.
294     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
295     * if no property is found.
296     * @return The value or null if it is not found.
297     * @since 2.13.0
298     */
299    public Long getLongProperty(final String[] prefixes, String key, Supplier<Long> supplier) {
300        for (String prefix : prefixes) {
301            if (hasProperty(prefix + key)) {
302                return getLongProperty(prefix + key, 0);
303            }
304        }
305        return supplier != null ? supplier.get() : null;
306    }
307
308    /**
309     * Retrieves a Duration where the String is of the format nnn[unit] where nnn represents an integer value
310     * and unit represents a time unit.
311     * @param name The property name.
312     * @param defaultValue The default value.
313     * @return The value of the String as a Duration or the default value, which may be null.
314     * @since 2.13.0
315     */
316    public Duration getDurationProperty(final String name, Duration defaultValue) {
317        final String prop = getStringProperty(name);
318        if (prop != null) {
319            return TimeUnit.getDuration(prop);
320        }
321        return defaultValue;
322    }
323
324    /**
325     * Retrieves a property that may be prefixed by more than one string.
326     * @param prefixes The array of prefixes.
327     * @param key The key to locate.
328     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
329     * if no property is found.
330     * @return The value or null if it is not found.
331     * @since 2.13.0
332     */
333    public Duration getDurationProperty(final String[] prefixes, String key, Supplier<Duration> supplier) {
334        for (String prefix : prefixes) {
335            if (hasProperty(prefix + key)) {
336                return getDurationProperty(prefix + key, null);
337            }
338        }
339        return supplier != null ? supplier.get() : null;
340    }
341
342    /**
343     * Retrieves a property that may be prefixed by more than one string.
344     * @param prefixes The array of prefixes.
345     * @param key The key to locate.
346     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
347     * if no property is found.
348     * @return The value or null if it is not found.
349     * @since 2.13.0
350     */
351    public String getStringProperty(final String[] prefixes, String key, Supplier<String> supplier) {
352        for (String prefix : prefixes) {
353            String result = getStringProperty(prefix + key);
354            if (result != null) {
355                return result;
356            }
357        }
358        return supplier != null ? supplier.get() : null;
359    }
360
361    /**
362     * Gets the named property as a String.
363     *
364     * @param name the name of the property to look up
365     * @return the String value of the property or {@code null} if undefined.
366     */
367    public String getStringProperty(final String name) {
368        return environment.get(name);
369    }
370
371    /**
372     * Gets the named property as a String.
373     *
374     * @param name         the name of the property to look up
375     * @param defaultValue the default value to use if the property is undefined
376     * @return the String value of the property or {@code defaultValue} if undefined.
377     */
378    public String getStringProperty(final String name, final String defaultValue) {
379        final String prop = getStringProperty(name);
380        return (prop == null) ? defaultValue : prop;
381    }
382
383    /**
384     * Return the system properties or an empty Properties object if an error occurs.
385     *
386     * @return The system properties.
387     */
388    public static Properties getSystemProperties() {
389        try {
390            return new Properties(System.getProperties());
391        } catch (final SecurityException ex) {
392            LowLevelLogUtil.logException("Unable to access system properties.", ex);
393            // Sandboxed - can't read System Properties
394            return new Properties();
395        }
396    }
397
398    /**
399     * Reloads all properties. This is primarily useful for unit tests.
400     *
401     * @since 2.10.0
402     */
403    public void reload() {
404        environment.reload();
405    }
406
407    /**
408     * Provides support for looking up global configuration properties via environment variables, property files,
409     * and system properties, in three variations:
410     * <p>
411     * Normalized: all log4j-related prefixes removed, remaining property is camelCased with a log4j2 prefix for
412     * property files and system properties, or follows a LOG4J_FOO_BAR format for environment variables.
413     * <p>
414     * Legacy: the original property name as defined in the source pre-2.10.0.
415     * <p>
416     * Tokenized: loose matching based on word boundaries.
417     *
418     * @since 2.10.0
419     */
420    private static class Environment {
421
422        private final Set<PropertySource> sources = new TreeSet<>(new PropertySource.Comparator());
423        private final Map<CharSequence, String> literal = new ConcurrentHashMap<>();
424        private final Map<CharSequence, String> normalized = new ConcurrentHashMap<>();
425        private final Map<List<CharSequence>, String> tokenized = new ConcurrentHashMap<>();
426
427        private Environment(final PropertySource propertySource) {
428            PropertyFilePropertySource sysProps = new PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME);
429            try {
430                sysProps.forEach((key, value) -> {
431                    if (System.getProperty(key) == null) {
432                        System.setProperty(key, value);
433                    }
434                });
435            } catch (SecurityException ex) {
436                // Access to System Properties is restricted so just skip it.
437            }
438            sources.add(propertySource);
439                        for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
440                                try {
441                                        for (final PropertySource source : ServiceLoader.load(PropertySource.class, classLoader)) {
442                                                sources.add(source);
443                                        }
444                                } catch (final Throwable ex) {
445                                        /* Don't log anything to the console. It may not be a problem that a PropertySource
446                                         * isn't accessible.
447                                         */
448                                }
449                        }
450
451            reload();
452        }
453
454        private synchronized void reload() {
455            literal.clear();
456            normalized.clear();
457            tokenized.clear();
458            for (final PropertySource source : sources) {
459                source.forEach((key, value) -> {
460                    if (key != null && value != null) {
461                        literal.put(key, value);
462                        final List<CharSequence> tokens = PropertySource.Util.tokenize(key);
463                        if (tokens.isEmpty()) {
464                            normalized.put(source.getNormalForm(Collections.singleton(key)), value);
465                        } else {
466                            normalized.put(source.getNormalForm(tokens), value);
467                            tokenized.put(tokens, value);
468                        }
469                    }
470                });
471            }
472        }
473
474        private static boolean hasSystemProperty(final String key) {
475            try {
476                return System.getProperties().containsKey(key);
477            } catch (final SecurityException ignored) {
478                return false;
479            }
480        }
481
482        private String get(final String key) {
483            if (normalized.containsKey(key)) {
484                return normalized.get(key);
485            }
486            if (literal.containsKey(key)) {
487                return literal.get(key);
488            }
489            if (hasSystemProperty(key)) {
490                return System.getProperty(key);
491            }
492            for (final PropertySource source : sources) {
493                if (source.containsProperty(key)) {
494                    return source.getProperty(key);
495                }
496            }
497            return tokenized.get(PropertySource.Util.tokenize(key));
498        }
499
500        private boolean containsKey(final String key) {
501            return normalized.containsKey(key) ||
502                literal.containsKey(key) ||
503                hasSystemProperty(key) ||
504                tokenized.containsKey(PropertySource.Util.tokenize(key));
505        }
506    }
507
508    /**
509     * Extracts properties that start with or are equals to the specific prefix and returns them in a new Properties
510     * object with the prefix removed.
511     *
512     * @param properties The Properties to evaluate.
513     * @param prefix     The prefix to extract.
514     * @return The subset of properties.
515     */
516    public static Properties extractSubset(final Properties properties, final String prefix) {
517        final Properties subset = new Properties();
518
519        if (prefix == null || prefix.length() == 0) {
520            return subset;
521        }
522
523        final String prefixToMatch = prefix.charAt(prefix.length() - 1) != '.' ? prefix + '.' : prefix;
524
525        final List<String> keys = new ArrayList<>();
526
527        for (final String key : properties.stringPropertyNames()) {
528            if (key.startsWith(prefixToMatch)) {
529                subset.setProperty(key.substring(prefixToMatch.length()), properties.getProperty(key));
530                keys.add(key);
531            }
532        }
533        for (final String key : keys) {
534            properties.remove(key);
535        }
536
537        return subset;
538    }
539
540    static ResourceBundle getCharsetsResourceBundle() {
541        return ResourceBundle.getBundle("Log4j-charsets");
542    }
543
544    /**
545     * Partitions a properties map based on common key prefixes up to the first period.
546     *
547     * @param properties properties to partition
548     * @return the partitioned properties where each key is the common prefix (minus the period) and the values are
549     * new property maps without the prefix and period in the key
550     * @since 2.6
551     */
552    public static Map<String, Properties> partitionOnCommonPrefixes(final Properties properties) {
553        final Map<String, Properties> parts = new ConcurrentHashMap<>();
554        for (final String key : properties.stringPropertyNames()) {
555            final String prefix = key.substring(0, key.indexOf('.'));
556            if (!parts.containsKey(prefix)) {
557                parts.put(prefix, new Properties());
558            }
559            parts.get(prefix).setProperty(key.substring(key.indexOf('.') + 1), properties.getProperty(key));
560        }
561        return parts;
562    }
563
564    /**
565     * Returns true if system properties tell us we are running on Windows.
566     *
567     * @return true if system properties tell us we are running on Windows.
568     */
569    public boolean isOsWindows() {
570        return getStringProperty("os.name", "").startsWith("Windows");
571    }
572
573    private enum TimeUnit {
574        NANOS("ns,nano,nanos,nanosecond,nanoseconds", ChronoUnit.NANOS),
575        MICROS("us,micro,micros,microsecond,microseconds", ChronoUnit.MICROS),
576        MILLIS("ms,milli,millis,millsecond,milliseconds", ChronoUnit.MILLIS),
577        SECONDS("s,second,seconds", ChronoUnit.SECONDS),
578        MINUTES("m,minute,minutes", ChronoUnit.MINUTES),
579        HOURS("h,hour,hours", ChronoUnit.HOURS),
580        DAYS("d,day,days", ChronoUnit.DAYS);
581
582        private final String[] descriptions;
583        private final ChronoUnit timeUnit;
584
585        TimeUnit(String descriptions, ChronoUnit timeUnit) {
586            this.descriptions = descriptions.split(",");
587            this.timeUnit = timeUnit;
588        }
589
590        ChronoUnit getTimeUnit() {
591            return this.timeUnit;
592        }
593
594        static Duration getDuration(String time) {
595            String value = time.trim();
596            TemporalUnit temporalUnit = ChronoUnit.MILLIS;
597            long timeVal = 0;
598            for (TimeUnit timeUnit : values()) {
599                for (String suffix : timeUnit.descriptions) {
600                    if (value.endsWith(suffix)) {
601                        temporalUnit = timeUnit.timeUnit;
602                        timeVal = Long.parseLong(value.substring(0, value.length() - suffix.length()));
603                    }
604                }
605            }
606            return Duration.of(timeVal, temporalUnit);
607        }
608    }
609}