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.log4j.config;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.lang.reflect.InvocationTargetException;
022import java.util.ArrayList;
023import java.util.Enumeration;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028import java.util.Properties;
029import java.util.SortedMap;
030import java.util.StringTokenizer;
031import java.util.TreeMap;
032
033import org.apache.log4j.Appender;
034import org.apache.log4j.Layout;
035import org.apache.log4j.LogManager;
036import org.apache.log4j.PatternLayout;
037import org.apache.log4j.bridge.AppenderAdapter;
038import org.apache.log4j.bridge.AppenderWrapper;
039import org.apache.log4j.helpers.OptionConverter;
040import org.apache.log4j.spi.ErrorHandler;
041import org.apache.log4j.spi.Filter;
042import org.apache.logging.log4j.core.LoggerContext;
043import org.apache.logging.log4j.core.config.Configuration;
044import org.apache.logging.log4j.core.config.ConfigurationSource;
045import org.apache.logging.log4j.core.config.LoggerConfig;
046import org.apache.logging.log4j.core.config.status.StatusConfiguration;
047import org.apache.logging.log4j.util.LoaderUtil;
048
049/**
050 * Construct a configuration based on Log4j 1 properties.
051 */
052public class PropertiesConfiguration  extends Log4j1Configuration {
053
054    private static final String CATEGORY_PREFIX = "log4j.category.";
055    private static final String LOGGER_PREFIX = "log4j.logger.";
056    private static final String ADDITIVITY_PREFIX = "log4j.additivity.";
057    private static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory";
058    private static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";
059    private static final String APPENDER_PREFIX = "log4j.appender.";
060    private static final String LOGGER_REF      = "logger-ref";
061    private static final String ROOT_REF                = "root-ref";
062    private static final String APPENDER_REF_TAG = "appender-ref";
063    public static final long DEFAULT_DELAY = 60000;
064    public static final String DEBUG_KEY="log4j.debug";
065
066    private static final String INTERNAL_ROOT_NAME = "root";
067
068    private final Map<String, Appender> registry;
069
070    /**
071     * Constructor.
072     * @param loggerContext The LoggerContext.
073     * @param source The ConfigurationSource.
074     * @param monitorIntervalSeconds The monitoring interval in seconds.
075     */
076    public PropertiesConfiguration(final LoggerContext loggerContext, final ConfigurationSource source,
077            int monitorIntervalSeconds) {
078        super(loggerContext, source, monitorIntervalSeconds);
079        registry = new HashMap<>();
080    }
081
082    @Override
083    public void doConfigure() {
084        InputStream is = getConfigurationSource().getInputStream();
085        Properties props = new Properties();
086        try {
087            props.load(is);
088        } catch (Exception e) {
089            LOGGER.error("Could not read configuration file [{}].", getConfigurationSource().toString(), e);
090            return;
091        }
092        // If we reach here, then the config file is alright.
093        doConfigure(props);
094    }
095
096    @Override
097    public Configuration reconfigure() {
098        try {
099            final ConfigurationSource source = getConfigurationSource().resetInputStream();
100            if (source == null) {
101                return null;
102            }
103            final PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory();
104            final PropertiesConfiguration config =
105                    (PropertiesConfiguration) factory.getConfiguration(getLoggerContext(), source);
106            return config == null || config.getState() != State.INITIALIZING ? null : config;
107        } catch (final IOException ex) {
108            LOGGER.error("Cannot locate file {}: {}", getConfigurationSource(), ex);
109        }
110        return null;
111    }
112
113    /**
114     * Read configuration from a file. <b>The existing configuration is
115     * not cleared nor reset.</b> If you require a different behavior,
116     * then call {@link  LogManager#resetConfiguration
117     * resetConfiguration} method before calling
118     * <code>doConfigure</code>.
119     *
120     * <p>The configuration file consists of statements in the format
121     * <code>key=value</code>. The syntax of different configuration
122     * elements are discussed below.
123     *
124     * <p>The level value can consist of the string values OFF, FATAL,
125     * ERROR, WARN, INFO, DEBUG, ALL or a <em>custom level</em> value. A
126     * custom level value can be specified in the form
127     * level#classname. By default the repository-wide threshold is set
128     * to the lowest possible value, namely the level <code>ALL</code>.
129     * </p>
130     *
131     *
132     * <h3>Appender configuration</h3>
133     *
134     * <p>Appender configuration syntax is:
135     * <pre>
136     * # For appender named <i>appenderName</i>, set its class.
137     * # Note: The appender name can contain dots.
138     * log4j.appender.appenderName=fully.qualified.name.of.appender.class
139     *
140     * # Set appender specific options.
141     * log4j.appender.appenderName.option1=value1
142     * ...
143     * log4j.appender.appenderName.optionN=valueN
144     * </pre>
145     * <p>
146     * For each named appender you can configure its {@link Layout}. The
147     * syntax for configuring an appender's layout is:
148     * <pre>
149     * log4j.appender.appenderName.layout=fully.qualified.name.of.layout.class
150     * log4j.appender.appenderName.layout.option1=value1
151     * ....
152     * log4j.appender.appenderName.layout.optionN=valueN
153     * </pre>
154     * <p>
155     * The syntax for adding {@link Filter}s to an appender is:
156     * <pre>
157     * log4j.appender.appenderName.filter.ID=fully.qualified.name.of.filter.class
158     * log4j.appender.appenderName.filter.ID.option1=value1
159     * ...
160     * log4j.appender.appenderName.filter.ID.optionN=valueN
161     * </pre>
162     * The first line defines the class name of the filter identified by ID;
163     * subsequent lines with the same ID specify filter option - value
164     * pairs. Multiple filters are added to the appender in the lexicographic
165     * order of IDs.
166     * <p>
167     * The syntax for adding an {@link ErrorHandler} to an appender is:
168     * <pre>
169     * log4j.appender.appenderName.errorhandler=fully.qualified.name.of.errorhandler.class
170     * log4j.appender.appenderName.errorhandler.appender-ref=appenderName
171     * log4j.appender.appenderName.errorhandler.option1=value1
172     * ...
173     * log4j.appender.appenderName.errorhandler.optionN=valueN
174     * </pre>
175     *
176     * <h3>Configuring loggers</h3>
177     *
178     * <p>The syntax for configuring the root logger is:
179     * <pre>
180     * log4j.rootLogger=[level], appenderName, appenderName, ...
181     * </pre>
182     *
183     * <p>This syntax means that an optional <em>level</em> can be
184     * supplied followed by appender names separated by commas.
185     *
186     * <p>The level value can consist of the string values OFF, FATAL,
187     * ERROR, WARN, INFO, DEBUG, ALL or a <em>custom level</em> value. A
188     * custom level value can be specified in the form
189     * <code>level#classname</code>.
190     *
191     * <p>If a level value is specified, then the root level is set
192     * to the corresponding level.  If no level value is specified,
193     * then the root level remains untouched.
194     *
195     * <p>The root logger can be assigned multiple appenders.
196     *
197     * <p>Each <i>appenderName</i> (separated by commas) will be added to
198     * the root logger. The named appender is defined using the
199     * appender syntax defined above.
200     *
201     * <p>For non-root categories the syntax is almost the same:
202     * <pre>
203     * log4j.logger.logger_name=[level|INHERITED|NULL], appenderName, appenderName, ...
204     * </pre>
205     *
206     * <p>The meaning of the optional level value is discussed above
207     * in relation to the root logger. In addition however, the value
208     * INHERITED can be specified meaning that the named logger should
209     * inherit its level from the logger hierarchy.
210     *
211     * <p>If no level value is supplied, then the level of the
212     * named logger remains untouched.
213     *
214     * <p>By default categories inherit their level from the
215     * hierarchy. However, if you set the level of a logger and later
216     * decide that that logger should inherit its level, then you should
217     * specify INHERITED as the value for the level value. NULL is a
218     * synonym for INHERITED.
219     *
220     * <p>Similar to the root logger syntax, each <i>appenderName</i>
221     * (separated by commas) will be attached to the named logger.
222     *
223     * <p>See the <a href="../../../../manual.html#additivity">appender
224     * additivity rule</a> in the user manual for the meaning of the
225     * <code>additivity</code> flag.
226     *
227     *
228     * # Set options for appender named "A1".
229     * # Appender "A1" will be a SyslogAppender
230     * log4j.appender.A1=org.apache.log4j.net.SyslogAppender
231     *
232     * # The syslog daemon resides on www.abc.net
233     * log4j.appender.A1.SyslogHost=www.abc.net
234     *
235     * # A1's layout is a PatternLayout, using the conversion pattern
236     * # <b>%r %-5p %c{2} %M.%L %x - %m\n</b>. Thus, the log output will
237     * # include # the relative time since the start of the application in
238     * # milliseconds, followed by the level of the log request,
239     * # followed by the two rightmost components of the logger name,
240     * # followed by the callers method name, followed by the line number,
241     * # the nested diagnostic context and finally the message itself.
242     * # Refer to the documentation of {@link PatternLayout} for further information
243     * # on the syntax of the ConversionPattern key.
244     * log4j.appender.A1.layout=org.apache.log4j.PatternLayout
245     * log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %c{2} %M.%L %x - %m\n
246     *
247     * # Set options for appender named "A2"
248     * # A2 should be a RollingFileAppender, with maximum file size of 10 MB
249     * # using at most one backup file. A2's layout is TTCC, using the
250     * # ISO8061 date format with context printing enabled.
251     * log4j.appender.A2=org.apache.log4j.RollingFileAppender
252     * log4j.appender.A2.MaxFileSize=10MB
253     * log4j.appender.A2.MaxBackupIndex=1
254     * log4j.appender.A2.layout=org.apache.log4j.TTCCLayout
255     * log4j.appender.A2.layout.ContextPrinting=enabled
256     * log4j.appender.A2.layout.DateFormat=ISO8601
257     *
258     * # Root logger set to DEBUG using the A2 appender defined above.
259     * log4j.rootLogger=DEBUG, A2
260     *
261     * # Logger definitions:
262     * # The SECURITY logger inherits is level from root. However, it's output
263     * # will go to A1 appender defined above. It's additivity is non-cumulative.
264     * log4j.logger.SECURITY=INHERIT, A1
265     * log4j.additivity.SECURITY=false
266     *
267     * # Only warnings or above will be logged for the logger "SECURITY.access".
268     * # Output will go to A1.
269     * log4j.logger.SECURITY.access=WARN
270     *
271     *
272     * # The logger "class.of.the.day" inherits its level from the
273     * # logger hierarchy.  Output will go to the appender's of the root
274     * # logger, A2 in this case.
275     * log4j.logger.class.of.the.day=INHERIT
276     * </pre>
277     *
278     * <p>Refer to the <b>setOption</b> method in each Appender and
279     * Layout for class specific options.
280     *
281     * <p>Use the <code>#</code> or <code>!</code> characters at the
282     * beginning of a line for comments.
283     *
284     * <p>
285     */
286    private void doConfigure(Properties properties) {
287        String status = "error";
288        String value = properties.getProperty(DEBUG_KEY);
289        if (value == null) {
290            value = properties.getProperty("log4j.configDebug");
291            if (value != null) {
292                LOGGER.warn("[log4j.configDebug] is deprecated. Use [log4j.debug] instead.");
293            }
294        }
295
296        if (value != null) {
297            status = OptionConverter.toBoolean(value, false) ? "debug" : "error";
298        }
299
300        final StatusConfiguration statusConfig = new StatusConfiguration().withStatus(status);
301        statusConfig.initialize();
302
303        configureRoot(properties);
304        parseLoggers(properties);
305
306        LOGGER.debug("Finished configuring.");
307    }
308
309    // --------------------------------------------------------------------------
310    // Internal stuff
311    // --------------------------------------------------------------------------
312
313    private void configureRoot(Properties props) {
314        String effectiveFrefix = ROOT_LOGGER_PREFIX;
315        String value = OptionConverter.findAndSubst(ROOT_LOGGER_PREFIX, props);
316
317        if (value == null) {
318            value = OptionConverter.findAndSubst(ROOT_CATEGORY_PREFIX, props);
319            effectiveFrefix = ROOT_CATEGORY_PREFIX;
320        }
321
322        if (value == null) {
323            LOGGER.debug("Could not find root logger information. Is this OK?");
324        } else {
325            LoggerConfig root = getRootLogger();
326            parseLogger(props, root, effectiveFrefix, INTERNAL_ROOT_NAME, value);
327        }
328    }
329
330    /**
331     * Parse non-root elements, such non-root categories and renderers.
332     */
333    private void parseLoggers(Properties props) {
334        Enumeration<?> enumeration = props.propertyNames();
335        while (enumeration.hasMoreElements()) {
336            String key = Objects.toString(enumeration.nextElement(), null);
337            if (key.startsWith(CATEGORY_PREFIX) || key.startsWith(LOGGER_PREFIX)) {
338                String loggerName = null;
339                if (key.startsWith(CATEGORY_PREFIX)) {
340                    loggerName = key.substring(CATEGORY_PREFIX.length());
341                } else if (key.startsWith(LOGGER_PREFIX)) {
342                    loggerName = key.substring(LOGGER_PREFIX.length());
343                }
344                String value = OptionConverter.findAndSubst(key, props);
345                LoggerConfig loggerConfig = getLogger(loggerName);
346                if (loggerConfig == null) {
347                    boolean additivity = getAdditivityForLogger(props, loggerName);
348                    loggerConfig = new LoggerConfig(loggerName, org.apache.logging.log4j.Level.ERROR, additivity);
349                    addLogger(loggerName, loggerConfig);
350                }
351                parseLogger(props, loggerConfig, key, loggerName, value);
352            }
353        }
354    }
355
356    /**
357     * Parse the additivity option for a non-root category.
358     */
359    private boolean getAdditivityForLogger(Properties props, String loggerName) {
360        boolean additivity = true;
361        String key = ADDITIVITY_PREFIX + loggerName;
362        String value = OptionConverter.findAndSubst(key, props);
363        LOGGER.debug("Handling {}=[{}]", key, value);
364        // touch additivity only if necessary
365        if ((value != null) && (!value.equals(""))) {
366            additivity = OptionConverter.toBoolean(value, true);
367        }
368        return additivity;
369    }
370
371    /**
372     * This method must work for the root category as well.
373     */
374    private void parseLogger(Properties props, LoggerConfig logger, String optionKey, String loggerName, String value) {
375
376        LOGGER.debug("Parsing for [{}] with value=[{}].", loggerName, value);
377        // We must skip over ',' but not white space
378        StringTokenizer st = new StringTokenizer(value, ",");
379
380        // If value is not in the form ", appender.." or "", then we should set the level of the logger.
381
382        if (!(value.startsWith(",") || value.equals(""))) {
383
384            // just to be on the safe side...
385            if (!st.hasMoreTokens()) {
386                return;
387            }
388
389            String levelStr = st.nextToken();
390            LOGGER.debug("Level token is [{}].", levelStr);
391
392            org.apache.logging.log4j.Level level = levelStr == null ? org.apache.logging.log4j.Level.ERROR :
393                    OptionConverter.convertLevel(levelStr, org.apache.logging.log4j.Level.DEBUG);
394            logger.setLevel(level);
395            LOGGER.debug("Logger {} level set to {}", loggerName, level);
396        }
397
398        Appender appender;
399        String appenderName;
400        while (st.hasMoreTokens()) {
401            appenderName = st.nextToken().trim();
402            if (appenderName == null || appenderName.equals(",")) {
403                continue;
404            }
405            LOGGER.debug("Parsing appender named \"{}\".", appenderName);
406            appender = parseAppender(props, appenderName);
407            if (appender != null) {
408                LOGGER.debug("Adding appender named [{}] to loggerConfig [{}].", appenderName,
409                        logger.getName());
410                logger.addAppender(getAppender(appenderName), null, null);
411            } else {
412                LOGGER.debug("Appender named [{}] not found.", appenderName);
413            }
414        }
415    }
416
417    public Appender parseAppender(Properties props, String appenderName) {
418        Appender appender = registry.get(appenderName);
419        if ((appender != null)) {
420            LOGGER.debug("Appender \"" + appenderName + "\" was already parsed.");
421            return appender;
422        }
423        // Appender was not previously initialized.
424        final String prefix = APPENDER_PREFIX + appenderName;
425        final String layoutPrefix = prefix + ".layout";
426        final String filterPrefix = APPENDER_PREFIX + appenderName + ".filter.";
427        String className = OptionConverter.findAndSubst(prefix, props);
428        appender = manager.parseAppender(appenderName, className, prefix, layoutPrefix, filterPrefix, props, this);
429        if (appender == null) {
430            appender = buildAppender(appenderName, className, prefix, layoutPrefix, filterPrefix, props);
431        } else {
432            registry.put(appenderName, appender);
433            if (appender instanceof AppenderWrapper) {
434                addAppender(((AppenderWrapper) appender).getAppender());
435            } else {
436                addAppender(new AppenderAdapter(appender).getAdapter());
437            }
438        }
439        return appender;
440    }
441
442    private Appender buildAppender(final String appenderName, final String className, final String prefix,
443            final String layoutPrefix, final String filterPrefix, final Properties props) {
444        Appender appender = newInstanceOf(className, "Appender");
445        if (appender == null) {
446            return null;
447        }
448        appender.setName(appenderName);
449        appender.setLayout(parseLayout(layoutPrefix, appenderName, props));
450        final String errorHandlerPrefix = prefix + ".errorhandler";
451        String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props);
452        if (errorHandlerClass != null) {
453            ErrorHandler eh = parseErrorHandler(props, errorHandlerPrefix, errorHandlerClass, appender);
454            if (eh != null) {
455                appender.setErrorHandler(eh);
456            }
457        }
458        parseAppenderFilters(props, filterPrefix, appenderName);
459        String[] keys = new String[] {
460                layoutPrefix,
461        };
462        addProperties(appender, keys, props, prefix);
463        if (appender instanceof AppenderWrapper) {
464            addAppender(((AppenderWrapper) appender).getAppender());
465        } else {
466            addAppender(new AppenderAdapter(appender).getAdapter());
467        }
468        registry.put(appenderName, appender);
469        return appender;
470    }
471
472    public Layout parseLayout(String layoutPrefix, String appenderName, Properties props) {
473        String layoutClass = OptionConverter.findAndSubst(layoutPrefix, props);
474        if (layoutClass == null) {
475            return null;
476        }
477        Layout layout = manager.parseLayout(layoutClass, layoutPrefix, props, this);
478        if (layout == null) {
479            layout = buildLayout(layoutPrefix, layoutClass, appenderName, props);
480        }
481        return layout;
482    }
483
484    private Layout buildLayout(String layoutPrefix, String className, String appenderName, Properties props) {
485        Layout layout = newInstanceOf(className, "Layout");
486        if (layout == null) {
487            return null;
488        }
489        LOGGER.debug("Parsing layout options for \"{}\".", appenderName);
490        PropertySetter.setProperties(layout, props, layoutPrefix + ".");
491        LOGGER.debug("End of parsing for \"{}\".", appenderName);
492        return layout;
493    }
494
495    public ErrorHandler parseErrorHandler(final Properties props, final String errorHandlerPrefix,
496            final String errorHandlerClass, final Appender appender) {
497        ErrorHandler eh = newInstanceOf(errorHandlerClass, "ErrorHandler");
498        final String[] keys = new String[] {
499                errorHandlerPrefix + "." + ROOT_REF,
500                errorHandlerPrefix + "." + LOGGER_REF,
501                errorHandlerPrefix + "." + APPENDER_REF_TAG
502        };
503        addProperties(eh, keys, props, errorHandlerPrefix);
504        return eh;
505    }
506
507    public void addProperties(final Object obj, final String[] keys, final Properties props, final String prefix) {
508        final Properties edited = new Properties();
509        props.stringPropertyNames().stream().filter(name -> {
510            if (name.startsWith(prefix)) {
511                for (String key : keys) {
512                    if (name.equals(key)) {
513                        return false;
514                    }
515                }
516                return true;
517            }
518            return false;
519        }).forEach(name -> edited.put(name, props.getProperty(name)));
520        PropertySetter.setProperties(obj, edited, prefix + ".");
521    }
522
523
524    public Filter parseAppenderFilters(Properties props, String filterPrefix, String appenderName) {
525        // extract filters and filter options from props into a hashtable mapping
526        // the property name defining the filter class to a list of pre-parsed
527        // name-value pairs associated to that filter
528        int fIdx = filterPrefix.length();
529        SortedMap<String, List<NameValue>> filters = new TreeMap<>();
530        Enumeration<?> e = props.keys();
531        String name = "";
532        while (e.hasMoreElements()) {
533            String key = (String) e.nextElement();
534            if (key.startsWith(filterPrefix)) {
535                int dotIdx = key.indexOf('.', fIdx);
536                String filterKey = key;
537                if (dotIdx != -1) {
538                    filterKey = key.substring(0, dotIdx);
539                    name = key.substring(dotIdx + 1);
540                }
541                List<NameValue> filterOpts = filters.computeIfAbsent(filterKey, k -> new ArrayList<>());
542                if (dotIdx != -1) {
543                    String value = OptionConverter.findAndSubst(key, props);
544                    filterOpts.add(new NameValue(name, value));
545                }
546            }
547        }
548
549        Filter head = null;
550        Filter next = null;
551        for (Map.Entry<String, List<NameValue>> entry : filters.entrySet()) {
552            String clazz = props.getProperty(entry.getKey());
553            Filter filter = null;
554            if (clazz != null) {
555                filter = manager.parseFilter(clazz, filterPrefix, props, this);
556                if (filter == null) {
557                    LOGGER.debug("Filter key: [{}] class: [{}] props: {}", entry.getKey(), clazz, entry.getValue());
558                    filter = buildFilter(clazz, appenderName, entry.getValue());
559                }
560            }
561            if (filter != null) {
562                if (head == null) {
563                    head = filter;
564                } else {
565                    next.setNext(filter);
566                }
567                next = filter;
568            }
569        }
570        return head;
571    }
572
573    private Filter buildFilter(String className, String appenderName, List<NameValue> props) {
574        Filter filter = newInstanceOf(className, "Filter");
575        if (filter != null) {
576            PropertySetter propSetter = new PropertySetter(filter);
577            for (NameValue property : props) {
578                propSetter.setProperty(property.key, property.value);
579            }
580            propSetter.activate();
581        }
582        return filter;
583    }
584
585
586    private static <T> T newInstanceOf(String className, String type) {
587        try {
588            return LoaderUtil.newInstanceOf(className);
589        } catch (ClassNotFoundException | IllegalAccessException | NoSuchMethodException |
590                InstantiationException | InvocationTargetException ex) {
591            LOGGER.error("Unable to create {} {} due to {}:{}", type,  className,
592                    ex.getClass().getSimpleName(), ex.getMessage());
593            return null;
594        }
595    }
596
597    private static class NameValue {
598        String key, value;
599
600        NameValue(String key, String value) {
601            this.key = key;
602            this.value = value;
603        }
604
605        @Override
606        public String toString() {
607            return key + "=" + value;
608        }
609    }
610
611}