View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.IOException;
24  import java.io.InputStream;
25  import java.io.ObjectOutputStream;
26  import java.io.OutputStream;
27  import java.io.Serializable;
28  import java.math.BigInteger;
29  import java.net.URI;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.security.MessageDigest;
34  import java.security.NoSuchAlgorithmException;
35  import java.util.HashSet;
36  import java.util.Locale;
37  import java.util.Objects;
38  import java.util.Properties;
39  import java.util.Set;
40  
41  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
42  import com.puppycrawl.tools.checkstyle.api.Configuration;
43  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
44  import com.puppycrawl.tools.checkstyle.utils.OsSpecificUtil;
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      * @throws IllegalArgumentException when either arguments are null
102      */
103     public PropertyCacheFile(Configuration config, String fileName) {
104         if (config == null) {
105             throw new IllegalArgumentException("config can not be null");
106         }
107         if (fileName == null) {
108             throw new IllegalArgumentException("fileName can not be null");
109         }
110         this.config = config;
111         this.fileName = fileName;
112     }
113 
114     /**
115      * Load cached values from file.
116      *
117      * @throws IOException when there is a problems with file read
118      */
119     public void load() throws IOException {
120         // get the current config so if the file isn't found
121         // the first time the hash will be added to output file
122         configHash = getHashCodeBasedOnObjectContent(config);
123         final Path path = Path.of(fileName);
124         if (Files.exists(path)) {
125             try (InputStream inStream = Files.newInputStream(path)) {
126                 details.load(inStream);
127                 final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY);
128                 if (!configHash.equals(cachedConfigHash)) {
129                     // Detected configuration change - clear cache
130                     reset();
131                 }
132             }
133         }
134         else {
135             // put the hash in the file if the file is going to be created
136             reset();
137         }
138     }
139 
140     /**
141      * Cleans up the object and updates the cache file.
142      *
143      * @throws IOException  when there is a problems with file save
144      */
145     public void persist() throws IOException {
146         final Path path = Paths.get(fileName);
147         final Path directory = path.getParent();
148 
149         if (directory != null) {
150             OsSpecificUtil.updateDirectory(directory);
151         }
152         try (OutputStream out = Files.newOutputStream(path)) {
153             details.store(out, null);
154         }
155     }
156 
157     /**
158      * Resets the cache to be empty except for the configuration hash.
159      */
160     public void reset() {
161         details.clear();
162         details.setProperty(CONFIG_HASH_KEY, configHash);
163     }
164 
165     /**
166      * Checks that file is in cache.
167      *
168      * @param uncheckedFileName the file to check
169      * @param timestamp the timestamp of the file to check
170      * @return whether the specified file has already been checked ok
171      */
172     public boolean isInCache(String uncheckedFileName, long timestamp) {
173         final String lastChecked = details.getProperty(uncheckedFileName);
174         return Objects.equals(lastChecked, Long.toString(timestamp));
175     }
176 
177     /**
178      * Records that a file checked ok.
179      *
180      * @param checkedFileName name of the file that checked ok
181      * @param timestamp the timestamp of the file
182      */
183     public void put(String checkedFileName, long timestamp) {
184         details.setProperty(checkedFileName, Long.toString(timestamp));
185     }
186 
187     /**
188      * Retrieves the hash of a specific file.
189      *
190      * @param name The name of the file to retrieve.
191      * @return The has of the file or {@code null}.
192      */
193     public String get(String name) {
194         return details.getProperty(name);
195     }
196 
197     /**
198      * Removed a specific file from the cache.
199      *
200      * @param checkedFileName The name of the file to remove.
201      */
202     public void remove(String checkedFileName) {
203         details.remove(checkedFileName);
204     }
205 
206     /**
207      * Calculates the hashcode for the serializable object based on its content.
208      *
209      * @param object serializable object.
210      * @return the hashcode for serializable object.
211      * @throws IllegalStateException when some unexpected happened.
212      */
213     private static String getHashCodeBasedOnObjectContent(Serializable object) {
214         try {
215             final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
216             // in-memory serialization of Configuration
217             serialize(object, outputStream);
218             // Instead of hexEncoding outputStream.toByteArray() directly we
219             // use a message digest here to keep the length of the
220             // hashcode reasonable
221 
222             final MessageDigest digest = MessageDigest.getInstance("SHA-1");
223             digest.update(outputStream.toByteArray());
224 
225             return new BigInteger(1, digest.digest()).toString(BASE_16).toUpperCase(Locale.ROOT);
226         }
227         catch (final IOException | NoSuchAlgorithmException ex) {
228             // rethrow as unchecked exception
229             throw new IllegalStateException("Unable to calculate hashcode.", ex);
230         }
231     }
232 
233     /**
234      * Serializes object to output stream.
235      *
236      * @param object object to be serialized
237      * @param outputStream serialization stream
238      * @throws IOException if an error occurs
239      */
240     private static void serialize(Serializable object,
241                                   OutputStream outputStream) throws IOException {
242         try (ObjectOutputStream oos = new ObjectOutputStream(outputStream)) {
243             oos.writeObject(object);
244         }
245     }
246 
247     /**
248      * Puts external resources in cache.
249      * If at least one external resource changed, clears the cache.
250      *
251      * @param locations locations of external resources.
252      */
253     public void putExternalResources(Set<String> locations) {
254         final Set<ExternalResource> resources = loadExternalResources(locations);
255         if (areExternalResourcesChanged(resources)) {
256             reset();
257             fillCacheWithExternalResources(resources);
258         }
259     }
260 
261     /**
262      * Loads a set of {@link ExternalResource} based on their locations.
263      *
264      * @param resourceLocations locations of external configuration resources.
265      * @return a set of {@link ExternalResource}.
266      */
267     private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) {
268         final Set<ExternalResource> resources = new HashSet<>();
269         for (String location : resourceLocations) {
270             try {
271                 final byte[] content = loadExternalResource(location);
272                 final String contentHashSum = getHashCodeBasedOnObjectContent(content);
273                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
274                         contentHashSum));
275             }
276             catch (CheckstyleException | IOException ex) {
277                 // if exception happened (configuration resource was not found, connection is not
278                 // available, resource is broken, etc.), we need to calculate hash sum based on
279                 // exception object content in order to check whether problem is resolved later
280                 // and/or the configuration is changed.
281                 final String contentHashSum = getHashCodeBasedOnObjectContent(ex);
282                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
283                         contentHashSum));
284             }
285         }
286         return resources;
287     }
288 
289     /**
290      * Loads the content of external resource.
291      *
292      * @param location external resource location.
293      * @return array of bytes which represents the content of external resource in binary form.
294      * @throws IOException if error while loading occurs.
295      * @throws CheckstyleException if error while loading occurs.
296      */
297     private static byte[] loadExternalResource(String location)
298             throws IOException, CheckstyleException {
299         final URI uri = CommonUtil.getUriByFilename(location);
300 
301         try (InputStream is = uri.toURL().openStream()) {
302             return toByteArray(is);
303         }
304     }
305 
306     /**
307      * Reads all the contents of an input stream and returns it as a byte array.
308      *
309      * @param stream The input stream to read from.
310      * @return The resulting byte array of the stream.
311      * @throws IOException if there is an error reading the input stream.
312      */
313     private static byte[] toByteArray(InputStream stream) throws IOException {
314         final ByteArrayOutputStream content = new ByteArrayOutputStream();
315 
316         while (true) {
317             final int size = stream.read(BUFFER);
318             if (size == -1) {
319                 break;
320             }
321 
322             content.write(BUFFER, 0, size);
323         }
324 
325         return content.toByteArray();
326     }
327 
328     /**
329      * Checks whether the contents of external configuration resources were changed.
330      *
331      * @param resources a set of {@link ExternalResource}.
332      * @return true if the contents of external configuration resources were changed.
333      */
334     private boolean areExternalResourcesChanged(Set<ExternalResource> resources) {
335         return resources.stream().anyMatch(this::isResourceChanged);
336     }
337 
338     /**
339      * Checks whether the resource is changed.
340      *
341      * @param resource resource to check.
342      * @return true if resource is changed.
343      */
344     private boolean isResourceChanged(ExternalResource resource) {
345         boolean changed = false;
346         if (isResourceLocationInCache(resource.location)) {
347             final String contentHashSum = resource.contentHashSum;
348             final String cachedHashSum = details.getProperty(resource.location);
349             if (!cachedHashSum.equals(contentHashSum)) {
350                 changed = true;
351             }
352         }
353         else {
354             changed = true;
355         }
356         return changed;
357     }
358 
359     /**
360      * Fills cache with a set of {@link ExternalResource}.
361      * If external resource from the set is already in cache, it will be skipped.
362      *
363      * @param externalResources a set of {@link ExternalResource}.
364      */
365     private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) {
366         externalResources
367             .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
368     }
369 
370     /**
371      * Checks whether resource location is in cache.
372      *
373      * @param location resource location.
374      * @return true if resource location is in cache.
375      */
376     private boolean isResourceLocationInCache(String location) {
377         final String cachedHashSum = details.getProperty(location);
378         return cachedHashSum != null;
379     }
380 
381     /**
382      * Class which represents external resource.
383      */
384     private static final class ExternalResource {
385 
386         /** Location of resource. */
387         private final String location;
388         /** Hash sum which is calculated based on resource content. */
389         private final String contentHashSum;
390 
391         /**
392          * Creates an instance.
393          *
394          * @param location resource location.
395          * @param contentHashSum content hash sum.
396          */
397         private ExternalResource(String location, String contentHashSum) {
398             this.location = location;
399             this.contentHashSum = contentHashSum;
400         }
401 
402     }
403 
404 }