ConfigurationWatchList.java

/*
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v2.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.core.joran.spi;

import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.util.MD5Util;

import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;

/**
 * This class manages the list of files and/or urls that are watched for changes.
 *
 * @author Ceki Gülcü
 */
public class ConfigurationWatchList extends ContextAwareBase {

    public static final String HTTPS_PROTOCOL_STR = "https";
    public static final String HTTP_PROTOCOL_STR = "http";
    public static final String FILE_PROTOCOL_STR = "file";

    static final String[] WATCHABLE_PROTOCOLS = new String[] { FILE_PROTOCOL_STR, HTTPS_PROTOCOL_STR, HTTP_PROTOCOL_STR };

    static final byte[] BUF_ZERO = new byte[] { 0 };

    URL topURL;
    List<File> fileWatchList = new ArrayList<>();
    List<URL> urlWatchList = new ArrayList<>();
    List<byte[]> lastHashList = new ArrayList<>();

    List<Long> lastModifiedList = new ArrayList<>();

    public ConfigurationWatchList buildClone() {
        ConfigurationWatchList out = new ConfigurationWatchList();
        out.topURL = this.topURL;
        out.fileWatchList = new ArrayList<File>(this.fileWatchList);
        out.lastModifiedList = new ArrayList<Long>(this.lastModifiedList);
        out.lastHashList = new ArrayList<>(this.lastHashList);
        return out;
    }

    public void clear() {
        this.topURL = null;
        this.lastModifiedList.clear();
        this.fileWatchList.clear();
        this.urlWatchList.clear();
        this.lastHashList.clear();
    }

    /**
     * The topURL for the configuration file. Null values are allowed.
     *
     * @param topURL
     */
    public void setTopURL(URL topURL) {
        // topURL can be null
        this.topURL = topURL;
        if (topURL != null)
            addAsFileToWatch(topURL);
    }

    public boolean watchPredicateFulfilled() {
        if (hasMainURLAndNonEmptyFileList()) {
            return true;
        }

        if(urlListContainsProperties()) {
            return true;
        }

        return fileWatchListContainsProperties();

    }

    private boolean urlListContainsProperties() {
        return urlWatchList.stream().anyMatch(url -> url.toString().endsWith(PROPERTIES_FILE_EXTENSION));
    }

    private boolean hasMainURLAndNonEmptyFileList() {
        return topURL != null && !fileWatchList.isEmpty();
    }

    private boolean fileWatchListContainsProperties() {
        return fileWatchList.stream().anyMatch(file -> file.getName().endsWith(PROPERTIES_FILE_EXTENSION));

    }

    private void addAsFileToWatch(URL url) {
        File file = convertToFile(url);
        if (file != null) {
            fileWatchList.add(file);
            lastModifiedList.add(file.lastModified());
        }
    }


    private boolean isHTTP_Or_HTTPS(URL url) {
        String protocolStr = url.getProtocol();
        return isHTTP_Or_HTTPS(protocolStr);
    }

    private boolean isHTTP_Or_HTTPS(String protocolStr) {
        return (protocolStr.equals(HTTP_PROTOCOL_STR) || protocolStr.equals(HTTPS_PROTOCOL_STR));
    }

    private void addAsHTTP_or_HTTPS_URLToWatch(URL url) {
        if(isHTTP_Or_HTTPS(url)) {
            urlWatchList.add(url);
            lastHashList.add(BUF_ZERO);
        }
    }

    /**
     * Add the url but only if it is file:// or http(s)://
     * @param url should be a file or http(s)
     */
    public void addToWatchList(URL url) {
        // assume that the caller has checked that the protocol is one of {file, https, http}.
        String protocolStr = url.getProtocol();
        if (protocolStr.equals(FILE_PROTOCOL_STR)) {
            addAsFileToWatch(url);
        } else if (isHTTP_Or_HTTPS(protocolStr)) {
            addAsHTTP_or_HTTPS_URLToWatch(url);
        }
    }

    public URL getTopURL() {
        return topURL;
    }

    public List<File> getCopyOfFileWatchList() {
        return new ArrayList<File>(fileWatchList);
    }


    public boolean emptyWatchLists() {
        if(fileWatchList != null && !fileWatchList.isEmpty()) {
            return false;
        }

        if(urlWatchList != null && !urlWatchList.isEmpty()) {
            return false;
        }
        return true;
    }

    /**
     * Has a changed been detected in one of the files being watched?
     * @return
     */
    public File changeDetectedInFile() {
        int len = fileWatchList.size();

        for (int i = 0; i < len; i++) {
            long lastModified = lastModifiedList.get(i);
            File file = fileWatchList.get(i);
            long actualModificationDate = file.lastModified();

            if (lastModified != actualModificationDate) {
                // update modification date in case this instance is reused
                lastModifiedList.set(i, actualModificationDate);
                return file;
            }
        }
        return null;
    }

    public URL changeDetectedInURL() {
        int len = urlWatchList.size();

        for (int i = 0; i < len; i++) {
            byte[] lastHash = this.lastHashList.get(i);
            URL url = urlWatchList.get(i);

            HttpUtil httpGetUtil = new HttpUtil(HttpUtil.RequestMethod.GET, url);
            HttpURLConnection getConnection = httpGetUtil.connectTextTxt();
            String response = httpGetUtil.readResponse(getConnection);

            byte[] hash = computeHash(response);
            if (lastHash == BUF_ZERO) {
                this.lastHashList.set(i, hash);
                return null;
            }

            if (Arrays.equals(lastHash, hash)) {
                return null;
            } else {
                this.lastHashList.set(i, hash);
                return url;
            }
        }
        return null;
    }

    private byte[] computeHash(String response) {
        if (response == null || response.trim().length() == 0) {
            return null;
        }

        try {
            MD5Util md5Util = new MD5Util();
            byte[] hashBytes = md5Util.md5Hash(response);
            return hashBytes;
        } catch (NoSuchAlgorithmException e) {
            addError("missing MD5 algorithm", e);
            return null;
        }
    }

    @SuppressWarnings("deprecation")
    File convertToFile(URL url) {
        String protocol = url.getProtocol();
        if ("file".equals(protocol)) {
            return new File(URLDecoder.decode(url.getFile()));
        } else {
            addInfo("URL [" + url + "] is not of type file");
            return null;
        }
    }

    /**
     * Returns true if there are watchable files, false otherwise.
     * @return true if there are watchable files,  false otherwise.
     * @since 1.5.8
     */
    public boolean hasAtLeastOneWatchableFile() {
        return !fileWatchList.isEmpty();
    }

    /**
     * Is protocol for the given URL a protocol that we can watch for.
     *
     * @param url
     * @return true if watchable, false otherwise
     * @since 1.5.9
     */
    static public boolean isWatchableProtocol(URL url) {
        if (url == null) {
            return false;
        }
        String protocolStr = url.getProtocol();
        return isWatchableProtocol(protocolStr);
    }

    /**
     * Is the given protocol a protocol that we can watch for.
     *
     * @param protocolStr
     * @return true if watchable, false otherwise
     * @since 1.5.9
     */
    static public boolean isWatchableProtocol(String protocolStr) {
        return Arrays.stream(WATCHABLE_PROTOCOLS).anyMatch(protocol -> protocol.equalsIgnoreCase(protocolStr));
    }

    /**
     * Returns the urlWatchList field as a String
     * @return the urlWatchList field as a String
     * @since 1.5.19
     */
    public String getUrlWatchListAsStr() {
        String urlWatchListStr = urlWatchList.stream().map(URL::toString).collect(Collectors.joining(", "));
        return urlWatchListStr;
    }

    /**
     * Returns the fileWatchList field as a String
     * @return the fileWatchList field as a String
     * @since 1.5.19
     */
    public String getFileWatchListAsStr() {
        return fileWatchList.stream().map(File::getPath).collect(Collectors.joining(", "));
    }

}