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.MalformedURLException;
022import java.net.URL;
023import java.net.URLDecoder;
024import java.security.NoSuchAlgorithmException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.List;
028import java.util.stream.Collectors;
029
030import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;
031
032/**
033 * @author Ceki Gülcü
034 */
035public class ConfigurationWatchList extends ContextAwareBase {
036
037    public static final String HTTPS_PROTOCOL_STR = "https";
038    public static final String HTTP_PROTOCOL_STR = "http";
039    public static final String FILE_PROTOCOL_STR = "file";
040
041    static final String[] WATCHABLE_PROTOCOLS = new String[] { FILE_PROTOCOL_STR, HTTPS_PROTOCOL_STR, HTTP_PROTOCOL_STR };
042
043    static final byte[] BUF_ZERO = new byte[] { 0 };
044
045    URL mainURL;
046    List<File> fileWatchList = new ArrayList<>();
047    List<URL> urlWatchList = new ArrayList<>();
048    List<byte[]> lastHashList = new ArrayList<>();
049
050    List<Long> lastModifiedList = new ArrayList<>();
051
052    public ConfigurationWatchList buildClone() {
053        ConfigurationWatchList out = new ConfigurationWatchList();
054        out.mainURL = this.mainURL;
055        out.fileWatchList = new ArrayList<File>(this.fileWatchList);
056        out.lastModifiedList = new ArrayList<Long>(this.lastModifiedList);
057        out.lastHashList = new ArrayList<>(this.lastHashList);
058        return out;
059    }
060
061    public void clear() {
062        this.mainURL = null;
063        lastModifiedList.clear();
064        fileWatchList.clear();
065        urlWatchList.clear();
066        lastHashList.clear();
067    }
068
069    /**
070     * The mainURL for the configuration file. Null values are allowed.
071     *
072     * @param mainURL
073     */
074    public void setMainURL(URL mainURL) {
075        // main url can be null
076        this.mainURL = mainURL;
077        if (mainURL != null)
078            addAsFileToWatch(mainURL);
079    }
080
081    public boolean watchPredicateFulfilled() {
082        if (hasMainURLAndNonEmptyFileList()) {
083            return true;
084        }
085
086        if(urlListContainsProperties()) {
087            return true;
088        }
089
090        return fileWatchListContainsProperties();
091
092    }
093
094    private boolean urlListContainsProperties() {
095        return urlWatchList.stream().anyMatch(url -> url.toString().endsWith(PROPERTIES_FILE_EXTENSION));
096    }
097
098    private boolean hasMainURLAndNonEmptyFileList() {
099        return mainURL != null && !fileWatchList.isEmpty();
100    }
101
102    private boolean fileWatchListContainsProperties() {
103        return fileWatchList.stream().anyMatch(file -> file.getName().endsWith(PROPERTIES_FILE_EXTENSION));
104
105    }
106
107    private void addAsFileToWatch(URL url) {
108        File file = convertToFile(url);
109        if (file != null) {
110            fileWatchList.add(file);
111            lastModifiedList.add(file.lastModified());
112        }
113    }
114
115
116    private boolean isHTTP_Or_HTTPS(URL url) {
117        String protocolStr = url.getProtocol();
118        return isHTTP_Or_HTTPS(protocolStr);
119    }
120
121    private boolean isHTTP_Or_HTTPS(String protocolStr) {
122        return (protocolStr.equals(HTTP_PROTOCOL_STR) || protocolStr.equals(HTTPS_PROTOCOL_STR));
123    }
124
125    private void addAsHTTP_or_HTTPS_URLToWatch(URL url) {
126        if(isHTTP_Or_HTTPS(url)) {
127            urlWatchList.add(url);
128            lastHashList.add(BUF_ZERO);
129        }
130    }
131
132    /**
133     * Add the url but only if it is file://.
134     * @param url should be a file
135     */
136
137    public void addToWatchList(URL url) {
138        String protocolStr = url.getProtocol();
139        if (protocolStr.equals(FILE_PROTOCOL_STR)) {
140            addAsFileToWatch(url);
141        } else if (isHTTP_Or_HTTPS(protocolStr)) {
142            addAsHTTP_or_HTTPS_URLToWatch(url);
143        } else {
144            addInfo("Cannot watch ["+url + "] as its protocol is not one of file, http or https.");
145        }
146    }
147
148    public URL getMainURL() {
149        return mainURL;
150    }
151
152    public List<File> getCopyOfFileWatchList() {
153        return new ArrayList<File>(fileWatchList);
154    }
155
156
157    public boolean emptyWatchLists() {
158        if(fileWatchList != null && !fileWatchList.isEmpty()) {
159            return false;
160        }
161
162        if(urlWatchList != null && !urlWatchList.isEmpty()) {
163            return false;
164        }
165        return true;
166    }
167
168
169    /**
170     *
171     * @deprecated replaced by {@link #changeDetectedInFile()}
172     */
173    public File changeDetected() {
174      return changeDetectedInFile();
175    }
176
177    /**
178     * Has a changed been detected in one of the files being watched?
179     * @return
180     */
181    public File changeDetectedInFile() {
182        int len = fileWatchList.size();
183
184        for (int i = 0; i < len; i++) {
185            long lastModified = lastModifiedList.get(i);
186            File file = fileWatchList.get(i);
187            long actualModificationDate = file.lastModified();
188
189            if (lastModified != actualModificationDate) {
190                // update modification date in case this instance is reused
191                lastModifiedList.set(i, actualModificationDate);
192                return file;
193            }
194        }
195        return null;
196    }
197
198    public URL changeDetectedInURL() {
199        int len = urlWatchList.size();
200
201        for (int i = 0; i < len; i++) {
202            byte[] lastHash = this.lastHashList.get(i);
203            URL url = urlWatchList.get(i);
204
205            HttpUtil httpGetUtil = new HttpUtil(HttpUtil.RequestMethod.GET, url);
206            HttpURLConnection getConnection = httpGetUtil.connectTextTxt();
207            String response = httpGetUtil.readResponse(getConnection);
208
209            byte[] hash = computeHash(response);
210            if (lastHash == BUF_ZERO) {
211                this.lastHashList.set(i, hash);
212                return null;
213            }
214
215            if (Arrays.equals(lastHash, hash)) {
216                return null;
217            } else {
218                this.lastHashList.set(i, hash);
219                return url;
220            }
221        }
222        return null;
223    }
224
225    private byte[] computeHash(String response) {
226        if (response == null || response.trim().length() == 0) {
227            return null;
228        }
229
230        try {
231            MD5Util md5Util = new MD5Util();
232            byte[] hashBytes = md5Util.md5Hash(response);
233            return hashBytes;
234        } catch (NoSuchAlgorithmException e) {
235            addError("missing MD5 algorithm", e);
236            return null;
237        }
238    }
239
240    @SuppressWarnings("deprecation")
241    File convertToFile(URL url) {
242        String protocol = url.getProtocol();
243        if ("file".equals(protocol)) {
244            return new File(URLDecoder.decode(url.getFile()));
245        } else {
246            addInfo("URL [" + url + "] is not of type file");
247            return null;
248        }
249    }
250
251    /**
252     * Returns true if there are watchable files, false otherwise.
253     * @return true if there are watchable files,  false otherwise.
254     * @since 1.5.8
255     */
256    public boolean hasAtLeastOneWatchableFile() {
257        return !fileWatchList.isEmpty();
258    }
259
260    /**
261     * Is protocol for the given URL a protocol that we can watch for.
262     *
263     * @param url
264     * @return true if watchable, false otherwise
265     * @since 1.5.9
266     */
267    static public boolean isWatchableProtocol(URL url) {
268        if (url == null) {
269            return false;
270        }
271        String protocolStr = url.getProtocol();
272        return Arrays.stream(WATCHABLE_PROTOCOLS).anyMatch(protocol -> protocol.equalsIgnoreCase(protocolStr));
273    }
274
275    /**
276     * Is the given protocol a protocol that we can watch for.
277     *
278     * @param protocolStr
279     * @return true if watchable, false otherwise
280     * @since 1.5.9
281     */
282    static public boolean isWatchableProtocol(String protocolStr) {
283        return Arrays.stream(WATCHABLE_PROTOCOLS).anyMatch(protocol -> protocol.equalsIgnoreCase(protocolStr));
284    }
285
286    @Override
287    public String toString() {
288
289        String fileWatchListStr = fileWatchList.stream().map(File::getPath).collect(Collectors.joining(", "));
290        String urlWatchListStr = urlWatchList.stream().map(URL::toString).collect(Collectors.joining(", "));
291
292        return "ConfigurationWatchList(" + "mainURL=" + mainURL + ", fileWatchList={" + fileWatchListStr + "}, urlWatchList=[" + urlWatchListStr + "})";
293    }
294}