001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2024, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014
015package ch.qos.logback.classic.joran;
016
017import ch.qos.logback.classic.Level;
018import ch.qos.logback.classic.Logger;
019import ch.qos.logback.classic.LoggerContext;
020import ch.qos.logback.core.Context;
021import ch.qos.logback.core.joran.JoranConstants;
022import ch.qos.logback.core.joran.spi.JoranException;
023import ch.qos.logback.core.model.util.VariableSubstitutionsHelper;
024import ch.qos.logback.core.spi.ContextAwareBase;
025import ch.qos.logback.core.spi.ErrorCodes;
026
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.IOException;
030import java.io.InputStream;
031import java.net.URL;
032import java.net.URLConnection;
033import java.util.*;
034
035import static ch.qos.logback.core.CoreConstants.DOT;
036import static ch.qos.logback.core.joran.JoranConstants.NULL;
037
038public class PropertiesConfigurator extends ContextAwareBase {
039
040    static Comparator<String> LENGTH_COMPARATOR = new Comparator<String>() {
041        @Override
042        public int compare(String o1, String o2) {
043            int len1 = o1 == null ? 0 : o1.length();
044            int len2 = o2 == null ? 0 : o2.length();
045            // longer strings first
046            int diff = len2 - len1;
047            if (diff != 0) {
048                return diff;
049            } else {
050                return o2.compareTo(o1);
051            }
052        }
053    };
054
055    static final String LOGBACK_PREFIX = "logback";
056    static final String LOGBACK_ROOT_LOGGER_PREFIX = LOGBACK_PREFIX + DOT + "root";
057    static final int LOGBACK_ROOT_LOGGER_PREFIX_LENGTH = LOGBACK_ROOT_LOGGER_PREFIX.length();
058
059    public static final String LOGBACK_LOGGER_PREFIX = LOGBACK_PREFIX + DOT + "logger" + DOT;
060    static final int LOGBACK_LOGGER_PREFIX_LENGTH = LOGBACK_LOGGER_PREFIX.length();
061
062    VariableSubstitutionsHelper variableSubstitutionsHelper;
063
064    LoggerContext getLoggerContext() {
065        return (LoggerContext) getContext();
066    }
067
068    @Override
069    public void setContext(Context context) {
070        super.setContext(context);
071    }
072
073    public void doConfigure(URL url) throws JoranException {
074        try {
075            URLConnection urlConnection = url.openConnection();
076            // per http://jira.qos.ch/browse/LOGBACK-117
077            // per http://jira.qos.ch/browse/LOGBACK-163
078            urlConnection.setUseCaches(false);
079            InputStream in = urlConnection.getInputStream();
080            doConfigure(in);
081        } catch (IOException ioe) {
082            String errMsg = "Could not open URL [" + url + "].";
083            addError(errMsg, ioe);
084            throw new JoranException(errMsg, ioe);
085        }
086    }
087
088    public void doConfigure(File file) throws JoranException {
089        try (FileInputStream fileInputStream = new FileInputStream(file)) {
090            doConfigure(fileInputStream);
091        } catch (IOException e) {
092            throw new JoranException("Failed to load file " + file, e);
093        }
094    }
095
096    public void doConfigure(String filename) throws JoranException {
097        doConfigure(new File(filename));
098    }
099
100    public void doConfigure(InputStream inputStream) throws JoranException {
101        Properties props = new Properties();
102        try {
103            props.load(inputStream);
104        } catch (IOException e) {
105            throw new JoranException("Failed to load from input stream", e);
106        } finally {
107            close(inputStream);
108        }
109
110        doConfigure(props);
111    }
112
113    private void close(InputStream inputStream) throws JoranException {
114        if (inputStream != null) {
115            try {
116                inputStream.close();
117            } catch (IOException e) {
118                throw new JoranException("failed to close stream", e);
119            }
120        }
121    }
122
123    void doConfigure(Properties properties) {
124        Map<String, String> variablesMap = extractVariablesMap(properties);
125        Map<String, String> instructionMap = extractLogbackInstructionMap(properties);
126
127        this.variableSubstitutionsHelper = new VariableSubstitutionsHelper(context, variablesMap);
128        configureLoggers(instructionMap);
129        configureRootLogger(instructionMap);
130    }
131
132    void configureRootLogger(Map<String, String> instructionMap) {
133        String val = subst(instructionMap.get(LOGBACK_ROOT_LOGGER_PREFIX));
134        if (val != null) {
135            setLevel(org.slf4j.Logger.ROOT_LOGGER_NAME, val);
136        }
137    }
138
139    void configureLoggers(Map<String, String> instructionMap) {
140        for (String key : instructionMap.keySet()) {
141            if (key.startsWith(LOGBACK_LOGGER_PREFIX)) {
142                String loggerName = key.substring(LOGBACK_LOGGER_PREFIX_LENGTH);
143                String value = subst(instructionMap.get(key));
144                setLevel(loggerName, value);
145            }
146        }
147    }
148
149    private void setLevel(String loggerName, String levelStr) {
150        Logger logger = getLoggerContext().getLogger(loggerName);
151
152        if (JoranConstants.INHERITED.equalsIgnoreCase(levelStr) || NULL.equalsIgnoreCase(levelStr)) {
153            if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(loggerName)) {
154                addError(ErrorCodes.ROOT_LEVEL_CANNOT_BE_SET_TO_NULL);
155            } else {
156                addInfo("Setting level of logger [" + loggerName + "] to null, i.e. INHERITED");
157                logger.setLevel(null);
158            }
159        } else {
160            Level level = Level.toLevel(levelStr);
161            logger.setLevel(level);
162        }
163    }
164
165    private Map<String, String> extractVariablesMap(Properties properties) {
166        Map<String, String> variablesMap = new HashMap<>();
167        for (String key : properties.stringPropertyNames()) {
168            if (key != null && !key.startsWith(LOGBACK_PREFIX)) {
169                variablesMap.put(key, properties.getProperty(key));
170            }
171        }
172
173        return variablesMap;
174    }
175
176    private Map<String, String> extractLogbackInstructionMap(Properties properties) {
177        Map<String, String> instructionMap = new TreeMap<>(LENGTH_COMPARATOR);
178        for (String key : properties.stringPropertyNames()) {
179            if (key != null && key.startsWith(LOGBACK_PREFIX)) {
180                instructionMap.put(key, properties.getProperty(key));
181            }
182        }
183        return instructionMap;
184    }
185
186    public String subst(String ref) {
187
188        String substituted = variableSubstitutionsHelper.subst(ref);
189        if (ref != null && !ref.equals(substituted)) {
190            addInfo("value \"" + substituted + "\" substituted for \"" + ref + "\"");
191        }
192        return substituted;
193    }
194
195}