1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2015, 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 v1.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.MalformedURLException;
22  import java.net.URL;
23  import java.net.URLDecoder;
24  import java.security.NoSuchAlgorithmException;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.List;
28  import java.util.stream.Collectors;
29  
30  import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;
31  
32  /**
33   * @author Ceki Gülcü
34   */
35  public class ConfigurationWatchList extends ContextAwareBase {
36  
37      public static final String HTTPS_PROTOCOL_STR = "https";
38      public static final String HTTP_PROTOCOL_STR = "http";
39      public static final String FILE_PROTOCOL_STR = "file";
40  
41      static final String[] WATCHABLE_PROTOCOLS = new String[] { FILE_PROTOCOL_STR, HTTPS_PROTOCOL_STR, HTTP_PROTOCOL_STR };
42  
43      static final byte[] BUF_ZERO = new byte[] { 0 };
44  
45      URL mainURL;
46      List<File> fileWatchList = new ArrayList<>();
47      List<URL> urlWatchList = new ArrayList<>();
48      List<byte[]> lastHashList = new ArrayList<>();
49  
50      List<Long> lastModifiedList = new ArrayList<>();
51  
52      public ConfigurationWatchList buildClone() {
53          ConfigurationWatchList out = new ConfigurationWatchList();
54          out.mainURL = this.mainURL;
55          out.fileWatchList = new ArrayList<File>(this.fileWatchList);
56          out.lastModifiedList = new ArrayList<Long>(this.lastModifiedList);
57          out.lastHashList = new ArrayList<>(this.lastHashList);
58          return out;
59      }
60  
61      public void clear() {
62          this.mainURL = null;
63          lastModifiedList.clear();
64          fileWatchList.clear();
65          urlWatchList.clear();
66          lastHashList.clear();
67      }
68  
69      /**
70       * The mainURL for the configuration file. Null values are allowed.
71       *
72       * @param mainURL
73       */
74      public void setMainURL(URL mainURL) {
75          // main url can be null
76          this.mainURL = mainURL;
77          if (mainURL != null)
78              addAsFileToWatch(mainURL);
79      }
80  
81      public boolean watchPredicateFulfilled() {
82          if (hasMainURLAndNonEmptyFileList()) {
83              return true;
84          }
85  
86          if(urlListContainsProperties()) {
87              return true;
88          }
89  
90          return fileWatchListContainsProperties();
91  
92      }
93  
94      private boolean urlListContainsProperties() {
95          return urlWatchList.stream().anyMatch(url -> url.toString().endsWith(PROPERTIES_FILE_EXTENSION));
96      }
97  
98      private boolean hasMainURLAndNonEmptyFileList() {
99          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 }