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.core.joran.spi;
15  
16  import ch.qos.logback.core.spi.ContextAwareBase;
17  import ch.qos.logback.core.util.MD5Util;
18  
19  import java.io.File;
20  import java.net.HttpURLConnection;
21  import java.net.URL;
22  import java.net.URLDecoder;
23  import java.security.NoSuchAlgorithmException;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.List;
27  import java.util.stream.Collectors;
28  
29  import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;
30  
31  /**
32   * This class manages the list of files and/or urls that are watched for changes.
33   *
34   * @author Ceki Gülcü
35   */
36  public class ConfigurationWatchList extends ContextAwareBase {
37  
38      public static final String HTTPS_PROTOCOL_STR = "https";
39      public static final String HTTP_PROTOCOL_STR = "http";
40      public static final String FILE_PROTOCOL_STR = "file";
41  
42      static final String[] WATCHABLE_PROTOCOLS = new String[] { FILE_PROTOCOL_STR, HTTPS_PROTOCOL_STR, HTTP_PROTOCOL_STR };
43  
44      static final byte[] BUF_ZERO = new byte[] { 0 };
45  
46      URL topURL;
47      List<File> fileWatchList = new ArrayList<>();
48      List<URL> urlWatchList = new ArrayList<>();
49      List<byte[]> lastHashList = new ArrayList<>();
50  
51      List<Long> lastModifiedList = new ArrayList<>();
52  
53      public ConfigurationWatchList buildClone() {
54          ConfigurationWatchList out = new ConfigurationWatchList();
55          out.topURL = this.topURL;
56          out.fileWatchList = new ArrayList<File>(this.fileWatchList);
57          out.lastModifiedList = new ArrayList<Long>(this.lastModifiedList);
58          out.lastHashList = new ArrayList<>(this.lastHashList);
59          return out;
60      }
61  
62      public void clear() {
63          this.topURL = null;
64          this.lastModifiedList.clear();
65          this.fileWatchList.clear();
66          this.urlWatchList.clear();
67          this.lastHashList.clear();
68      }
69  
70      /**
71       * The topURL for the configuration file. Null values are allowed.
72       *
73       * @param topURL
74       */
75      public void setTopURL(URL topURL) {
76          // topURL can be null
77          this.topURL = topURL;
78          if (topURL != null)
79              addAsFileToWatch(topURL);
80      }
81  
82      public boolean watchPredicateFulfilled() {
83          if (hasMainURLAndNonEmptyFileList()) {
84              return true;
85          }
86  
87          if(urlListContainsProperties()) {
88              return true;
89          }
90  
91          return fileWatchListContainsProperties();
92  
93      }
94  
95      private boolean urlListContainsProperties() {
96          return urlWatchList.stream().anyMatch(url -> url.toString().endsWith(PROPERTIES_FILE_EXTENSION));
97      }
98  
99      private boolean hasMainURLAndNonEmptyFileList() {
100         return topURL != null && !fileWatchList.isEmpty();
101     }
102 
103     private boolean fileWatchListContainsProperties() {
104         return fileWatchList.stream().anyMatch(file -> file.getName().endsWith(PROPERTIES_FILE_EXTENSION));
105 
106     }
107 
108     private void addAsFileToWatch(URL url) {
109         File file = convertToFile(url);
110         if (file != null) {
111             fileWatchList.add(file);
112             lastModifiedList.add(file.lastModified());
113         }
114     }
115 
116 
117     private boolean isHTTP_Or_HTTPS(URL url) {
118         String protocolStr = url.getProtocol();
119         return isHTTP_Or_HTTPS(protocolStr);
120     }
121 
122     private boolean isHTTP_Or_HTTPS(String protocolStr) {
123         return (protocolStr.equals(HTTP_PROTOCOL_STR) || protocolStr.equals(HTTPS_PROTOCOL_STR));
124     }
125 
126     private void addAsHTTP_or_HTTPS_URLToWatch(URL url) {
127         if(isHTTP_Or_HTTPS(url)) {
128             urlWatchList.add(url);
129             lastHashList.add(BUF_ZERO);
130         }
131     }
132 
133     /**
134      * Add the url but only if it is file:// or http(s)://
135      * @param url should be a file or http(s)
136      */
137     public void addToWatchList(URL url) {
138         // assume that the caller has checked that the protocol is one of {file, https, http}.
139         String protocolStr = url.getProtocol();
140         if (protocolStr.equals(FILE_PROTOCOL_STR)) {
141             addAsFileToWatch(url);
142         } else if (isHTTP_Or_HTTPS(protocolStr)) {
143             addAsHTTP_or_HTTPS_URLToWatch(url);
144         }
145     }
146 
147     public URL getTopURL() {
148         return topURL;
149     }
150 
151     public List<File> getCopyOfFileWatchList() {
152         return new ArrayList<File>(fileWatchList);
153     }
154 
155 
156     public boolean emptyWatchLists() {
157         if(fileWatchList != null && !fileWatchList.isEmpty()) {
158             return false;
159         }
160 
161         if(urlWatchList != null && !urlWatchList.isEmpty()) {
162             return false;
163         }
164         return true;
165     }
166 
167 
168     /**
169      *
170      * @deprecated replaced by {@link #changeDetectedInFile()}
171      */
172     public File changeDetected() {
173       return changeDetectedInFile();
174     }
175 
176     /**
177      * Has a changed been detected in one of the files being watched?
178      * @return
179      */
180     public File changeDetectedInFile() {
181         int len = fileWatchList.size();
182 
183         for (int i = 0; i < len; i++) {
184             long lastModified = lastModifiedList.get(i);
185             File file = fileWatchList.get(i);
186             long actualModificationDate = file.lastModified();
187 
188             if (lastModified != actualModificationDate) {
189                 // update modification date in case this instance is reused
190                 lastModifiedList.set(i, actualModificationDate);
191                 return file;
192             }
193         }
194         return null;
195     }
196 
197     public URL changeDetectedInURL() {
198         int len = urlWatchList.size();
199 
200         for (int i = 0; i < len; i++) {
201             byte[] lastHash = this.lastHashList.get(i);
202             URL url = urlWatchList.get(i);
203 
204             HttpUtil httpGetUtil = new HttpUtil(HttpUtil.RequestMethod.GET, url);
205             HttpURLConnection getConnection = httpGetUtil.connectTextTxt();
206             String response = httpGetUtil.readResponse(getConnection);
207 
208             byte[] hash = computeHash(response);
209             if (lastHash == BUF_ZERO) {
210                 this.lastHashList.set(i, hash);
211                 return null;
212             }
213 
214             if (Arrays.equals(lastHash, hash)) {
215                 return null;
216             } else {
217                 this.lastHashList.set(i, hash);
218                 return url;
219             }
220         }
221         return null;
222     }
223 
224     private byte[] computeHash(String response) {
225         if (response == null || response.trim().length() == 0) {
226             return null;
227         }
228 
229         try {
230             MD5Util md5Util = new MD5Util();
231             byte[] hashBytes = md5Util.md5Hash(response);
232             return hashBytes;
233         } catch (NoSuchAlgorithmException e) {
234             addError("missing MD5 algorithm", e);
235             return null;
236         }
237     }
238 
239     @SuppressWarnings("deprecation")
240     File convertToFile(URL url) {
241         String protocol = url.getProtocol();
242         if ("file".equals(protocol)) {
243             return new File(URLDecoder.decode(url.getFile()));
244         } else {
245             addInfo("URL [" + url + "] is not of type file");
246             return null;
247         }
248     }
249 
250     /**
251      * Returns true if there are watchable files, false otherwise.
252      * @return true if there are watchable files,  false otherwise.
253      * @since 1.5.8
254      */
255     public boolean hasAtLeastOneWatchableFile() {
256         return !fileWatchList.isEmpty();
257     }
258 
259     /**
260      * Is protocol for the given URL a protocol that we can watch for.
261      *
262      * @param url
263      * @return true if watchable, false otherwise
264      * @since 1.5.9
265      */
266     static public boolean isWatchableProtocol(URL url) {
267         if (url == null) {
268             return false;
269         }
270         String protocolStr = url.getProtocol();
271         return isWatchableProtocol(protocolStr);
272     }
273 
274     /**
275      * Is the given protocol a protocol that we can watch for.
276      *
277      * @param protocolStr
278      * @return true if watchable, false otherwise
279      * @since 1.5.9
280      */
281     static public boolean isWatchableProtocol(String protocolStr) {
282         return Arrays.stream(WATCHABLE_PROTOCOLS).anyMatch(protocol -> protocol.equalsIgnoreCase(protocolStr));
283     }
284 
285     /**
286      * Returns the urlWatchList field as a String
287      * @return the urlWatchList field as a String
288      * @since 1.5.19
289      */
290     public String getUrlWatchListAsStr() {
291         String urlWatchListStr = urlWatchList.stream().map(URL::toString).collect(Collectors.joining(", "));
292         return urlWatchListStr;
293     }
294 
295     /**
296      * Returns the fileWatchList field as a String
297      * @return the fileWatchList field as a String
298      * @since 1.5.19
299      */
300     public String getFileWatchListAsStr() {
301         return fileWatchList.stream().map(File::getPath).collect(Collectors.joining(", "));
302     }
303 
304 }