001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, 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 v1.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 mainURL;
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.mainURL = this.mainURL;
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.mainURL = null;
064        lastModifiedList.clear();
065        fileWatchList.clear();
066        urlWatchList.clear();
067        lastHashList.clear();
068    }
069
070    /**
071     * The mainURL for the configuration file. Null values are allowed.
072     *
073     * @param mainURL
074     */
075    public void setMainURL(URL mainURL) {
076        // main url can be null
077        this.mainURL = mainURL;
078        if (mainURL != null)
079            addAsFileToWatch(mainURL);
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 mainURL != 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 getMainURL() {
148        return mainURL;
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}