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.model.processor;
15  
16  import ch.qos.logback.classic.joran.ReconfigureOnChangeTask;
17  import ch.qos.logback.classic.model.ConfigurationModel;
18  import ch.qos.logback.core.Context;
19  import ch.qos.logback.core.CoreConstants;
20  import ch.qos.logback.core.joran.spi.ConfigurationWatchList;
21  import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
22  import ch.qos.logback.core.model.Model;
23  import ch.qos.logback.core.model.processor.ModelHandlerBase;
24  import ch.qos.logback.core.model.processor.ModelHandlerException;
25  import ch.qos.logback.core.model.processor.ModelInterpretationContext;
26  import ch.qos.logback.core.spi.ConfigurationEvent;
27  import ch.qos.logback.core.util.Duration;
28  import ch.qos.logback.core.util.OptionHelper;
29  
30  import java.util.concurrent.ScheduledExecutorService;
31  import java.util.concurrent.ScheduledFuture;
32  import java.util.concurrent.TimeUnit;
33  
34  /**
35   * This is a subclass of {@link ConfigurationModelHandler} offering configuration reloading support.
36   *
37   * <p>This class is also called by logback-tyler.</p>
38   *
39   */
40  public class ConfigurationModelHandlerFull extends ConfigurationModelHandler {
41  
42      public static final String FAILED_WATCH_PREDICATE_MESSAGE_1 = "Missing watchable .xml or .properties files.";
43      public static final String FAILED_WATCH_PREDICATE_MESSAGE_2 = "Watching .xml files requires that the main configuration file is reachable as a URL";
44  
45      public ConfigurationModelHandlerFull(Context context) {
46          super(context);
47      }
48  
49      static public ModelHandlerBase makeInstance(Context context, ModelInterpretationContext mic) {
50          return new ConfigurationModelHandlerFull(context);
51      }
52  
53      @Override
54      public void postHandle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
55          ConfigurationModel configurationModel = (ConfigurationModel) model;
56  
57          // scanning is disabled
58          if (scanning != Boolean.TRUE) {
59              return;
60          }
61  
62          String scanPeriodStr = mic.subst(configurationModel.getScanPeriodStr());
63          scheduleReconfigureOnChangeTask(scanPeriodStr);
64  
65          ConfigurationWatchList cwl = ConfigurationWatchListUtil.getConfigurationWatchList(getContext());
66          if (cwl != null) {
67              try {
68                  addInfo("Main configuration file URL: " + cwl.getTopURL());
69                  addInfo("FileWatchList= {" + cwl.getFileWatchListAsStr() + "}");
70                  addInfo("URLWatchList= {" + cwl.getUrlWatchListAsStr() + "}");
71              } catch (NoSuchMethodError e) {
72                  addWarn("It looks like the version of logback-classic is more recent than");
73                  addWarn("the version of logback-core. Please align the two versions.");
74              }
75          }
76      }
77  
78  
79      /**
80       * This method is called from logback-tyler version 1.0.4 and earlier.
81       * <p>
82       * This method assumes that the variables scanStr and scanPeriodStr have undergone variable substitution
83       * as applicable to their current environment
84       *
85       * @param scanPeriodStr
86       * @since 1.5.0
87       */
88      public void detachedPostProcess(String scanStr, String scanPeriodStr) {
89          if (OptionHelper.isNullOrEmptyOrAllSpaces(scanStr) || CoreConstants.FALSE_STR.equalsIgnoreCase(scanStr)) {
90              return;
91          }
92  
93          scheduleReconfigureOnChangeTask(scanPeriodStr);
94      }
95  
96      /**
97       * This method is called from logback-tyler version 1.0.x and later.
98       * <p>
99       * This method assumes that the variables scanPeriodStr has undergone variable substitution
100      * as applicable to their current environment
101      *
102      * @param scanPeriodStr
103      * @since 1.5.28
104      */
105     public void detachedPostProcess(String scanPeriodStr) {
106         scheduleReconfigureOnChangeTask(scanPeriodStr);
107     }
108 
109 
110     /**
111      * Schedules a task to monitor configuration changes and reconfigure the application when changes are detected.
112      * The task periodically scans for changes based on the given scan period and reconfigures the system if needed.
113      * This method ensures a valid configuration watch is in place before scheduling the task.
114      *
115      * @param scanPeriodStr the string value representing the scanning period, which will determine how often
116      *                      the task checks for configuration changes. The value is expected to have undergone
117      *                      necessary variable substitution for the current environment.
118      */
119     private void scheduleReconfigureOnChangeTask(String scanPeriodStr) {
120 
121         boolean watchPredicateFulfilled = ConfigurationWatchListUtil.watchPredicateFulfilled(context);
122         if (!watchPredicateFulfilled) {
123             addWarn(FAILED_WATCH_PREDICATE_MESSAGE_1);
124             addWarn(FAILED_WATCH_PREDICATE_MESSAGE_2);
125             return;
126         }
127 
128         ReconfigureOnChangeTask rocTask = new ReconfigureOnChangeTask();
129         rocTask.setContext(context);
130 
131         addInfo("Registering a new ReconfigureOnChangeTask " + rocTask);
132 
133         context.fireConfigurationEvent(ConfigurationEvent.newConfigurationChangeDetectorRegisteredEvent(rocTask));
134 
135         Duration duration = getDurationOfScanPeriodAttribute(scanPeriodStr, SCAN_PERIOD_DEFAULT);
136 
137         ConfigurationWatchList cwl = ConfigurationWatchListUtil.getConfigurationWatchList(context);
138 
139         String fileWatchListAsStr = (cwl != null) ? cwl.getFileWatchListAsStr() : "";
140 
141         addInfo("Will scan for changes in [" + fileWatchListAsStr + "] ");
142         // Given that included files are encountered at a later phase, the complete list
143         // of files to scan can only be determined when the configuration is loaded in full.
144         // However, scan can be active if mainURL is set. Otherwise, when changes are
145         // detected the top level config file cannot be accessed.
146         addInfo("Setting ReconfigureOnChangeTask scanning period to " + duration);
147 
148         ScheduledExecutorService scheduledExecutorService = context.getScheduledExecutorService();
149         ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(rocTask, duration.getMilliseconds(), duration.getMilliseconds(),
150                 TimeUnit.MILLISECONDS);
151         rocTask.setScheduledFuture(scheduledFuture);
152         context.addScheduledFuture(scheduledFuture);
153 
154     }
155 
156     private Duration getDurationOfScanPeriodAttribute(String scanPeriodAttrib, Duration defaultDuration) {
157         Duration duration = null;
158 
159         if (!OptionHelper.isNullOrEmptyOrAllSpaces(scanPeriodAttrib)) {
160             try {
161                 duration = Duration.valueOf(scanPeriodAttrib);
162             } catch (IllegalStateException | IllegalArgumentException e) {
163                 addWarn("Failed to parse 'scanPeriod' attribute [" + scanPeriodAttrib + "]", e);
164                 // default duration will be set below
165             }
166         }
167 
168         if (duration == null) {
169             addInfo("No 'scanPeriod' specified. Defaulting to " + defaultDuration.toString());
170             duration = defaultDuration;
171         }
172         return duration;
173     }
174 }