1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.ObjectOutputStream;
27  import java.io.OutputStream;
28  import java.io.Serializable;
29  import java.math.BigInteger;
30  import java.net.URI;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.nio.file.Paths;
34  import java.security.MessageDigest;
35  import java.security.NoSuchAlgorithmException;
36  import java.util.HashSet;
37  import java.util.Locale;
38  import java.util.Objects;
39  import java.util.Properties;
40  import java.util.Set;
41  
42  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
43  import com.puppycrawl.tools.checkstyle.api.Configuration;
44  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
45  
46  /**
47   * This class maintains a persistent(on file-system) store of the files
48   * that have checked ok(no validation events) and their associated
49   * timestamp. It is used to optimize Checkstyle between few launches.
50   * It is mostly useful for plugin and extensions of Checkstyle.
51   * It uses a property file
52   * for storage.  A hashcode of the Configuration is stored in the
53   * cache file to ensure the cache is invalidated when the
54   * configuration has changed.
55   *
56   */
57  public final class PropertyCacheFile {
58  
59      /**
60       * The property key to use for storing the hashcode of the
61       * configuration. To avoid name clashes with the files that are
62       * checked the key is chosen in such a way that it cannot be a
63       * valid file name.
64       */
65      public static final String CONFIG_HASH_KEY = "configuration*?";
66  
67      /**
68       * The property prefix to use for storing the hashcode of an
69       * external resource. To avoid name clashes with the files that are
70       * checked the prefix is chosen in such a way that it cannot be a
71       * valid file name and makes it clear it is a resource.
72       */
73      public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:";
74  
75      /** Size of default byte array for buffer. */
76      private static final int BUFFER_SIZE = 1024;
77  
78      /** Default buffer for reading from streams. */
79      private static final byte[] BUFFER = new byte[BUFFER_SIZE];
80  
81      /** Default number for base 16 encoding. */
82      private static final int BASE_16 = 16;
83  
84      /** The details on files. **/
85      private final Properties details = new Properties();
86  
87      /** Configuration object. **/
88      private final Configuration config;
89  
90      /** File name of cache. **/
91      private final String fileName;
92  
93      /** Generated configuration hash. **/
94      private String configHash;
95  
96      /**
97       * Creates a new {@code PropertyCacheFile} instance.
98       *
99       * @param config the current configuration, not null
100      * @param fileName the cache file
101      */
102     public PropertyCacheFile(Configuration config, String fileName) {
103         if (config == null) {
104             throw new IllegalArgumentException("config can not be null");
105         }
106         if (fileName == null) {
107             throw new IllegalArgumentException("fileName can not be null");
108         }
109         this.config = config;
110         this.fileName = fileName;
111     }
112 
113     /**
114      * Load cached values from file.
115      * @throws IOException when there is a problems with file read
116      */
117     public void load() throws IOException {
118         // get the current config so if the file isn't found
119         // the first time the hash will be added to output file
120         configHash = getHashCodeBasedOnObjectContent(config);
121         final File file = new File(fileName);
122         if (file.exists()) {
123             try (InputStream inStream = Files.newInputStream(file.toPath())) {
124                 details.load(inStream);
125                 final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY);
126                 if (!configHash.equals(cachedConfigHash)) {
127                     // Detected configuration change - clear cache
128                     reset();
129                 }
130             }
131         }
132         else {
133             // put the hash in the file if the file is going to be created
134             reset();
135         }
136     }
137 
138     /**
139      * Cleans up the object and updates the cache file.
140      * @throws IOException  when there is a problems with file save
141      */
142     public void persist() throws IOException {
143         final Path path = Paths.get(fileName);
144         final Path directory = path.getParent();
145         if (directory != null) {
146             Files.createDirectories(directory);
147         }
148         OutputStream out = null;
149         try {
150             out = Files.newOutputStream(path);
151             details.store(out, null);
152         }
153         finally {
154             flushAndCloseOutStream(out);
155         }
156     }
157 
158     /**
159      * Resets the cache to be empty except for the configuration hash.
160      */
161     public void reset() {
162         details.clear();
163         details.setProperty(CONFIG_HASH_KEY, configHash);
164     }
165 
166     /**
167      * Flushes and closes output stream.
168      * @param stream the output stream
169      * @throws IOException  when there is a problems with file flush and close
170      */
171     private static void flushAndCloseOutStream(OutputStream stream) throws IOException {
172         if (stream != null) {
173             stream.flush();
174             stream.close();
175         }
176     }
177 
178     /**
179      * Checks that file is in cache.
180      * @param uncheckedFileName the file to check
181      * @param timestamp the timestamp of the file to check
182      * @return whether the specified file has already been checked ok
183      */
184     public boolean isInCache(String uncheckedFileName, long timestamp) {
185         final String lastChecked = details.getProperty(uncheckedFileName);
186         return Objects.equals(lastChecked, Long.toString(timestamp));
187     }
188 
189     /**
190      * Records that a file checked ok.
191      * @param checkedFileName name of the file that checked ok
192      * @param timestamp the timestamp of the file
193      */
194     public void put(String checkedFileName, long timestamp) {
195         details.setProperty(checkedFileName, Long.toString(timestamp));
196     }
197 
198     /**
199      * Retrieves the hash of a specific file.
200      * @param name The name of the file to retrieve.
201      * @return The has of the file or {@code null}.
202      */
203     public String get(String name) {
204         return details.getProperty(name);
205     }
206 
207     /**
208      * Removed a specific file from the cache.
209      * @param checkedFileName The name of the file to remove.
210      */
211     public void remove(String checkedFileName) {
212         details.remove(checkedFileName);
213     }
214 
215     /**
216      * Calculates the hashcode for the serializable object based on its content.
217      * @param object serializable object.
218      * @return the hashcode for serializable object.
219      */
220     private static String getHashCodeBasedOnObjectContent(Serializable object) {
221         try {
222             final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
223             // in-memory serialization of Configuration
224             serialize(object, outputStream);
225             // Instead of hexEncoding outputStream.toByteArray() directly we
226             // use a message digest here to keep the length of the
227             // hashcode reasonable
228 
229             final MessageDigest digest = MessageDigest.getInstance("SHA-1");
230             digest.update(outputStream.toByteArray());
231 
232             return new BigInteger(1, digest.digest()).toString(BASE_16).toUpperCase(Locale.ROOT);
233         }
234         catch (final IOException | NoSuchAlgorithmException ex) {
235             // rethrow as unchecked exception
236             throw new IllegalStateException("Unable to calculate hashcode.", ex);
237         }
238     }
239 
240     /**
241      * Serializes object to output stream.
242      * @param object object to be serialized
243      * @param outputStream serialization stream
244      * @throws IOException if an error occurs
245      */
246     private static void serialize(Serializable object,
247                                   OutputStream outputStream) throws IOException {
248         final ObjectOutputStream oos = new ObjectOutputStream(outputStream);
249         try {
250             oos.writeObject(object);
251         }
252         finally {
253             flushAndCloseOutStream(oos);
254         }
255     }
256 
257     /**
258      * Puts external resources in cache.
259      * If at least one external resource changed, clears the cache.
260      * @param locations locations of external resources.
261      */
262     public void putExternalResources(Set<String> locations) {
263         final Set<ExternalResource> resources = loadExternalResources(locations);
264         if (areExternalResourcesChanged(resources)) {
265             reset();
266             fillCacheWithExternalResources(resources);
267         }
268     }
269 
270     /**
271      * Loads a set of {@link ExternalResource} based on their locations.
272      * @param resourceLocations locations of external configuration resources.
273      * @return a set of {@link ExternalResource}.
274      */
275     private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) {
276         final Set<ExternalResource> resources = new HashSet<>();
277         for (String location : resourceLocations) {
278             try {
279                 final byte[] content = loadExternalResource(location);
280                 final String contentHashSum = getHashCodeBasedOnObjectContent(content);
281                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
282                         contentHashSum));
283             }
284             catch (CheckstyleException | IOException ex) {
285                 // if exception happened (configuration resource was not found, connection is not
286                 // available, resource is broken, etc), we need to calculate hash sum based on
287                 // exception object content in order to check whether problem is resolved later
288                 // and/or the configuration is changed.
289                 final String contentHashSum = getHashCodeBasedOnObjectContent(ex);
290                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
291                         contentHashSum));
292             }
293         }
294         return resources;
295     }
296 
297     /**
298      * Loads the content of external resource.
299      * @param location external resource location.
300      * @return array of bytes which represents the content of external resource in binary form.
301      * @throws IOException if error while loading occurs.
302      * @throws CheckstyleException if error while loading occurs.
303      */
304     private static byte[] loadExternalResource(String location)
305             throws IOException, CheckstyleException {
306         final URI uri = CommonUtil.getUriByFilename(location);
307 
308         try (InputStream is = uri.toURL().openStream()) {
309             return toByteArray(is);
310         }
311     }
312 
313     /**
314      * Reads all the contents of an input stream and returns it as a byte array.
315      * @param stream The input stream to read from.
316      * @return The resulting byte array of the stream.
317      * @throws IOException if there is an error reading the input stream.
318      */
319     private static byte[] toByteArray(InputStream stream) throws IOException {
320         final ByteArrayOutputStream content = new ByteArrayOutputStream();
321 
322         while (true) {
323             final int size = stream.read(BUFFER);
324             if (size == -1) {
325                 break;
326             }
327 
328             content.write(BUFFER, 0, size);
329         }
330 
331         return content.toByteArray();
332     }
333 
334     /**
335      * Checks whether the contents of external configuration resources were changed.
336      * @param resources a set of {@link ExternalResource}.
337      * @return true if the contents of external configuration resources were changed.
338      */
339     private boolean areExternalResourcesChanged(Set<ExternalResource> resources) {
340         return resources.stream().anyMatch(resource -> {
341             boolean changed = false;
342             if (isResourceLocationInCache(resource.location)) {
343                 final String contentHashSum = resource.contentHashSum;
344                 final String cachedHashSum = details.getProperty(resource.location);
345                 if (!cachedHashSum.equals(contentHashSum)) {
346                     changed = true;
347                 }
348             }
349             else {
350                 changed = true;
351             }
352             return changed;
353         });
354     }
355 
356     /**
357      * Fills cache with a set of {@link ExternalResource}.
358      * If external resource from the set is already in cache, it will be skipped.
359      * @param externalResources a set of {@link ExternalResource}.
360      */
361     private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) {
362         externalResources
363             .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
364     }
365 
366     /**
367      * Checks whether resource location is in cache.
368      * @param location resource location.
369      * @return true if resource location is in cache.
370      */
371     private boolean isResourceLocationInCache(String location) {
372         final String cachedHashSum = details.getProperty(location);
373         return cachedHashSum != null;
374     }
375 
376     /**
377      * Class which represents external resource.
378      */
379     private static class ExternalResource {
380 
381         /** Location of resource. */
382         private final String location;
383         /** Hash sum which is calculated based on resource content. */
384         private final String contentHashSum;
385 
386         /**
387          * Creates an instance.
388          * @param location resource location.
389          * @param contentHashSum content hash sum.
390          */
391         /* package */ ExternalResource(String location, String contentHashSum) {
392             this.location = location;
393             this.contentHashSum = contentHashSum;
394         }
395 
396     }
397 
398 }