001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, 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 v2.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 */
014package ch.qos.logback.core.joran.spi;
015
016import ch.qos.logback.core.spi.ContextAwareBase;
017import ch.qos.logback.core.util.MD5Util;
018
019import java.io.File;
020import java.net.HttpURLConnection;
021import java.net.URL;
022import java.net.URLDecoder;
023import java.security.NoSuchAlgorithmException;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027import java.util.stream.Collectors;
028
029import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;
030
031/**
032 * This class manages the list of files and/or urls that are watched for changes.
033 *
034 * @author Ceki Gülcü
035 */
036public class ConfigurationWatchList extends ContextAwareBase {
037
038    public static final String HTTPS_PROTOCOL_STR = "https";
039    public static final String HTTP_PROTOCOL_STR = "http";
040    public static final String FILE_PROTOCOL_STR = "file";
041
042    static final String[] WATCHABLE_PROTOCOLS = new String[] { FILE_PROTOCOL_STR, HTTPS_PROTOCOL_STR, HTTP_PROTOCOL_STR };
043
044    static final byte[] BUF_ZERO = new byte[] { 0 };
045
046    URL topURL;
047    List<File> fileWatchList = new ArrayList<>();
048    List<URL> urlWatchList = new ArrayList<>();
049    List<byte[]> lastHashList = new ArrayList<>();
050
051    List<Long> lastModifiedList = new ArrayList<>();
052
053    public ConfigurationWatchList buildClone() {
054        ConfigurationWatchList out = new ConfigurationWatchList();
055        out.topURL = this.topURL;
056        out.fileWatchList = new ArrayList<File>(this.fileWatchList);
057        out.lastModifiedList = new ArrayList<Long>(this.lastModifiedList);
058        out.lastHashList = new ArrayList<>(this.lastHashList);
059        return out;
060    }
061
062    public void clear() {
063        this.topURL = null;
064        this.lastModifiedList.clear();
065        this.fileWatchList.clear();
066        this.urlWatchList.clear();
067        this.lastHashList.clear();
068    }
069
070    /**
071     * The topURL for the configuration file. Null values are allowed.
072     *
073     * @param topURL
074     */
075    public void setTopURL(URL topURL) {
076        // topURL can be null
077        this.topURL = topURL;
078        if (topURL != null)
079            addAsFileToWatch(topURL);
080    }
081
082    public boolean watchPredicateFulfilled() {
083        if (hasMainURLAndNonEmptyFileList()) {
084            return true;
085        }
086
087        if(urlListContainsProperties()) {
088            return true;
089        }
090
091        return fileWatchListContainsProperties();
092
093    }
094
095    private boolean urlListContainsProperties() {
096        return urlWatchList.stream().anyMatch(url -> url.toString().endsWith(PROPERTIES_FILE_EXTENSION));
097    }
098
099    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     * Has a changed been detected in one of the files being watched?
169     * @return
170     */
171    public File changeDetectedInFile() {
172        int len = fileWatchList.size();
173
174        for (int i = 0; i < len; i++) {
175            long lastModified = lastModifiedList.get(i);
176            File file = fileWatchList.get(i);
177            long actualModificationDate = file.lastModified();
178
179            if (lastModified != actualModificationDate) {
180                // update modification date in case this instance is reused
181                lastModifiedList.set(i, actualModificationDate);
182                return file;
183            }
184        }
185        return null;
186    }
187
188    public URL changeDetectedInURL() {
189        int len = urlWatchList.size();
190
191        for (int i = 0; i < len; i++) {
192            byte[] lastHash = this.lastHashList.get(i);
193            URL url = urlWatchList.get(i);
194
195            HttpUtil httpGetUtil = new HttpUtil(HttpUtil.RequestMethod.GET, url);
196            HttpURLConnection getConnection = httpGetUtil.connectTextTxt();
197            String response = httpGetUtil.readResponse(getConnection);
198
199            byte[] hash = computeHash(response);
200            if (lastHash == BUF_ZERO) {
201                this.lastHashList.set(i, hash);
202                return null;
203            }
204
205            if (Arrays.equals(lastHash, hash)) {
206                return null;
207            } else {
208                this.lastHashList.set(i, hash);
209                return url;
210            }
211        }
212        return null;
213    }
214
215    private byte[] computeHash(String response) {
216        if (response == null || response.trim().length() == 0) {
217            return null;
218        }
219
220        try {
221            MD5Util md5Util = new MD5Util();
222            byte[] hashBytes = md5Util.md5Hash(response);
223            return hashBytes;
224        } catch (NoSuchAlgorithmException e) {
225            addError("missing MD5 algorithm", e);
226            return null;
227        }
228    }
229
230    @SuppressWarnings("deprecation")
231    File convertToFile(URL url) {
232        String protocol = url.getProtocol();
233        if ("file".equals(protocol)) {
234            return new File(URLDecoder.decode(url.getFile()));
235        } else {
236            addInfo("URL [" + url + "] is not of type file");
237            return null;
238        }
239    }
240
241    /**
242     * Returns true if there are watchable files, false otherwise.
243     * @return true if there are watchable files,  false otherwise.
244     * @since 1.5.8
245     */
246    public boolean hasAtLeastOneWatchableFile() {
247        return !fileWatchList.isEmpty();
248    }
249
250    /**
251     * Is protocol for the given URL a protocol that we can watch for.
252     *
253     * @param url
254     * @return true if watchable, false otherwise
255     * @since 1.5.9
256     */
257    static public boolean isWatchableProtocol(URL url) {
258        if (url == null) {
259            return false;
260        }
261        String protocolStr = url.getProtocol();
262        return isWatchableProtocol(protocolStr);
263    }
264
265    /**
266     * Is the given protocol a protocol that we can watch for.
267     *
268     * @param protocolStr
269     * @return true if watchable, false otherwise
270     * @since 1.5.9
271     */
272    static public boolean isWatchableProtocol(String protocolStr) {
273        return Arrays.stream(WATCHABLE_PROTOCOLS).anyMatch(protocol -> protocol.equalsIgnoreCase(protocolStr));
274    }
275
276    /**
277     * Returns the urlWatchList field as a String
278     * @return the urlWatchList field as a String
279     * @since 1.5.19
280     */
281    public String getUrlWatchListAsStr() {
282        String urlWatchListStr = urlWatchList.stream().map(URL::toString).collect(Collectors.joining(", "));
283        return urlWatchListStr;
284    }
285
286    /**
287     * Returns the fileWatchList field as a String
288     * @return the fileWatchList field as a String
289     * @since 1.5.19
290     */
291    public String getFileWatchListAsStr() {
292        return fileWatchList.stream().map(File::getPath).collect(Collectors.joining(", "));
293    }
294
295}