1   /*
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2024, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v1.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1
12   * as published by the Free Software Foundation.
13   */
14  
15  package ch.qos.logback.classic.joran;
16  
17  import ch.qos.logback.classic.Level;
18  import ch.qos.logback.classic.Logger;
19  import ch.qos.logback.classic.LoggerContext;
20  import ch.qos.logback.core.Context;
21  import ch.qos.logback.core.joran.JoranConstants;
22  import ch.qos.logback.core.joran.spi.JoranException;
23  import ch.qos.logback.core.model.util.VariableSubstitutionsHelper;
24  import ch.qos.logback.core.spi.ContextAwareBase;
25  import ch.qos.logback.core.spi.ErrorCodes;
26  
27  import java.io.File;
28  import java.io.FileInputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.net.URL;
32  import java.net.URLConnection;
33  import java.util.*;
34  
35  import static ch.qos.logback.core.CoreConstants.DOT;
36  import static ch.qos.logback.core.joran.JoranConstants.NULL;
37  
38  public class PropertiesConfigurator extends ContextAwareBase {
39  
40      static Comparator<String> LENGTH_COMPARATOR = new Comparator<String>() {
41          @Override
42          public int compare(String o1, String o2) {
43              int len1 = o1 == null ? 0 : o1.length();
44              int len2 = o2 == null ? 0 : o2.length();
45              // longer strings first
46              int diff = len2 - len1;
47              if (diff != 0) {
48                  return diff;
49              } else {
50                  return o2.compareTo(o1);
51              }
52          }
53      };
54  
55      static final String LOGBACK_PREFIX = "logback";
56      static final String LOGBACK_ROOT_LOGGER_PREFIX = LOGBACK_PREFIX + DOT + "root";
57      static final int LOGBACK_ROOT_LOGGER_PREFIX_LENGTH = LOGBACK_ROOT_LOGGER_PREFIX.length();
58  
59      public static final String LOGBACK_LOGGER_PREFIX = LOGBACK_PREFIX + DOT + "logger" + DOT;
60      static final int LOGBACK_LOGGER_PREFIX_LENGTH = LOGBACK_LOGGER_PREFIX.length();
61  
62      VariableSubstitutionsHelper variableSubstitutionsHelper;
63  
64      LoggerContext getLoggerContext() {
65          return (LoggerContext) getContext();
66      }
67  
68      @Override
69      public void setContext(Context context) {
70          super.setContext(context);
71      }
72  
73      public void doConfigure(URL url) throws JoranException {
74          try {
75              URLConnection urlConnection = url.openConnection();
76              // per http://jira.qos.ch/browse/LOGBACK-117
77              // per http://jira.qos.ch/browse/LOGBACK-163
78              urlConnection.setUseCaches(false);
79              InputStream in = urlConnection.getInputStream();
80              doConfigure(in);
81          } catch (IOException ioe) {
82              String errMsg = "Could not open URL [" + url + "].";
83              addError(errMsg, ioe);
84              throw new JoranException(errMsg, ioe);
85          }
86      }
87  
88      public void doConfigure(File file) throws JoranException {
89          try (FileInputStream fileInputStream = new FileInputStream(file)) {
90              doConfigure(fileInputStream);
91          } catch (IOException e) {
92              throw new JoranException("Failed to load file " + file, e);
93          }
94      }
95  
96      public void doConfigure(String filename) throws JoranException {
97          doConfigure(new File(filename));
98      }
99  
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 }