1   /*
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2026, 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 v2.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  package ch.qos.logback.classic.joran;
15  
16  import java.io.File;
17  import java.net.URL;
18  import java.util.List;
19  import java.util.concurrent.ScheduledFuture;
20  
21  import ch.qos.logback.classic.LoggerContext;
22  import ch.qos.logback.core.CoreConstants;
23  import ch.qos.logback.core.joran.spi.ConfigurationWatchList;
24  import ch.qos.logback.core.joran.spi.JoranException;
25  import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
26  import ch.qos.logback.core.model.Model;
27  import ch.qos.logback.core.model.ModelUtil;
28  import ch.qos.logback.core.spi.ConfigurationEvent;
29  import ch.qos.logback.core.spi.ContextAwareBase;
30  import ch.qos.logback.core.status.StatusUtil;
31  
32  import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;
33  import static ch.qos.logback.core.spi.ConfigurationEvent.*;
34  
35  public class ReconfigureOnChangeTask extends ContextAwareBase implements Runnable {
36  
37      public static final String DETECTED_CHANGE_IN_CONFIGURATION_FILES = "Detected change in configuration files.";
38      public static final String RE_REGISTERING_PREVIOUS_SAFE_CONFIGURATION = "Re-registering previous fallback configuration once more as a fallback configuration point";
39      public static final String FALLING_BACK_TO_SAFE_CONFIGURATION = "Given previous errors, falling back to previously registered safe configuration.";
40  
41      long birthdate = System.currentTimeMillis();
42      List<ReconfigureOnChangeTaskListener> listeners = null;
43  
44      ScheduledFuture<?> scheduledFuture;
45  
46      @Override
47      public void run() {
48          context.fireConfigurationEvent(newConfigurationChangeDetectorRunningEvent(this));
49  
50          ConfigurationWatchList configurationWatchList = ConfigurationWatchListUtil.getConfigurationWatchList(context);
51          if (configurationWatchList == null) {
52              addWarn("Empty ConfigurationWatchList in context");
53              return;
54          }
55  
56          if (configurationWatchList.emptyWatchLists()) {
57              addInfo("Both watch lists are empty. Disabling ");
58              return;
59          }
60  
61          File changedFile = configurationWatchList.changeDetectedInFile();
62          URL changedURL = configurationWatchList.changeDetectedInURL();
63  
64          if (changedFile == null && changedURL == null) {
65              return;
66          }
67  
68          context.fireConfigurationEvent(ConfigurationEvent.newConfigurationChangeDetectedEvent(this));
69          addInfo(DETECTED_CHANGE_IN_CONFIGURATION_FILES);
70          if(changedFile != null) {
71              changeInFile(changedFile, configurationWatchList);
72          }
73  
74          if(changedURL != null) {
75              changeInURL(changedURL);
76          }
77      }
78  
79      private void changeInURL(URL url) {
80          String path = url.getPath();
81          if(path.endsWith(PROPERTIES_FILE_EXTENSION)) {
82              runPropertiesConfigurator(url);
83          }
84      }
85      private void changeInFile(File changedFile, ConfigurationWatchList configurationWatchList) {
86  
87          if(changedFile.getName().endsWith(PROPERTIES_FILE_EXTENSION)) {
88              runPropertiesConfigurator(changedFile);
89              return;
90          }
91  
92          // ======== fuller processing below
93          addInfo(CoreConstants.RESET_MSG_PREFIX + "named [" + context.getName() + "]");
94          cancelFutureInvocationsOfThisTaskInstance();
95          URL mainConfigurationURL = configurationWatchList.getTopURL();
96  
97          LoggerContext lc = (LoggerContext) context;
98          if (mainConfigurationURL.toString().endsWith("xml")) {
99              performXMLConfiguration(lc, mainConfigurationURL);
100         }
101     }
102 
103     private void runPropertiesConfigurator(Object changedObject) {
104         addInfo("Will run PropertyConfigurator on "+changedObject);
105         PropertiesConfigurator propertiesConfigurator = new PropertiesConfigurator();
106         propertiesConfigurator.setContext(context);
107         try {
108             if(changedObject instanceof File) {
109                 File changedFile = (File) changedObject;
110                 propertiesConfigurator.doConfigure(changedFile);
111             } else if(changedObject instanceof URL) {
112                 URL changedURL = (URL) changedObject;
113                 propertiesConfigurator.doConfigure(changedURL);
114             }
115             context.fireConfigurationEvent(newPartialConfigurationEndedSuccessfullyEvent(this));
116         } catch (JoranException e) {
117             addError("Failed to reload "+ changedObject);
118         }
119     }
120 
121     private void cancelFutureInvocationsOfThisTaskInstance() {
122         boolean result = scheduledFuture.cancel(false);
123         if(!result) {
124             addWarn("could not cancel "+ this.toString());
125         }
126     }
127 
128     private void performXMLConfiguration(LoggerContext loggerContext, URL mainConfigurationURL) {
129 
130         JoranConfigurator jc = new JoranConfigurator();
131         jc.setContext(loggerContext);
132         StatusUtil statusUtil = new StatusUtil(loggerContext);
133         Model failsafeTop = jc.recallSafeConfiguration();
134         URL topURL = ConfigurationWatchListUtil.getMainWatchURL(context);
135         addInfo("Resetting loggerContext ["+loggerContext.getName()+"]");
136         loggerContext.reset();
137         long threshold = System.currentTimeMillis();
138         try {
139             jc.doConfigure(mainConfigurationURL);
140             // e.g. IncludeAction will add a status regarding XML parsing errors but no exception will reach here
141             if (statusUtil.hasXMLParsingErrors(threshold)) {
142                 fallbackConfiguration(loggerContext, failsafeTop, topURL);
143             }
144         } catch (JoranException e) {
145             addWarn("Exception occurred during reconfiguration", e);
146             fallbackConfiguration(loggerContext, failsafeTop, topURL);
147         }
148     }
149 
150     private void fallbackConfiguration(LoggerContext loggerContext, Model failsafeTopModel, URL topURL) {
151         // failsafe events are used only in case of errors. Therefore, we must *not*
152         // invoke file inclusion since the included files may be the cause of the error.
153 
154         // List<SaxEvent> failsafeEvents = removeIncludeEvents(eventList);
155         JoranConfigurator joranConfigurator = new JoranConfigurator();
156         joranConfigurator.setContext(loggerContext);
157         joranConfigurator.setTopURL(topURL);
158 
159 //        ConfigurationWatchList oldCWL = ConfigurationWatchListUtil.getConfigurationWatchList(loggerContext);
160 //        System.out.println("--------oldCWL:"+oldCWL);
161 //        ConfigurationWatchList newCWL = oldCWL.buildClone();
162 
163         if (failsafeTopModel == null) {
164             addWarn("No previous configuration to fall back on.");
165             return;
166         } else {
167             addWarn(FALLING_BACK_TO_SAFE_CONFIGURATION);
168             addInfo("Safe model "+failsafeTopModel);
169             try {
170                 loggerContext.reset();
171 //                ConfigurationWatchListUtil.registerConfigurationWatchList(context, newCWL);
172                 ModelUtil.resetForReuse(failsafeTopModel);
173                 joranConfigurator.processModel(failsafeTopModel);
174                 addInfo(RE_REGISTERING_PREVIOUS_SAFE_CONFIGURATION);
175                 joranConfigurator.registerSafeConfiguration(failsafeTopModel);
176                 context.fireConfigurationEvent(newConfigurationEndedSuccessfullyEvent(this));
177             } catch (Exception e) {
178                 addError("Unexpected exception thrown by a configuration considered safe.", e);
179             }
180         }
181     }
182 
183     @Override
184     public String toString() {
185         return "ReconfigureOnChangeTask(born:" + birthdate + ")";
186     }
187 
188     /**
189      * Contains typo. Replaced by {@link #setScheduledFuture(ScheduledFuture)}.
190      * @param aScheduledFuture
191      * @deprecated
192      */
193     @Deprecated
194     public void setScheduredFuture(ScheduledFuture<?> aScheduledFuture) {
195         setScheduledFuture(aScheduledFuture);
196     }
197 
198     /**
199      * Replaces {@link #setScheduredFuture(ScheduledFuture)}
200      * @param aScheduledFuture
201      * @since 1.5.19
202      */
203     public void setScheduledFuture(ScheduledFuture<?> aScheduledFuture) {
204         this.scheduledFuture = aScheduledFuture;
205     }
206 }