View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.security.MessageDigest;
33  import java.security.NoSuchAlgorithmException;
34  import java.util.HashSet;
35  import java.util.Locale;
36  import java.util.Objects;
37  import java.util.Properties;
38  import java.util.Set;
39  
40  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
41  import com.puppycrawl.tools.checkstyle.api.Configuration;
42  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
43  import com.puppycrawl.tools.checkstyle.utils.OsSpecificUtil;
44  
45  /**
46   * This class maintains a persistent(on file-system) store of the files
47   * that have checked ok(no validation events) and their associated
48   * timestamp. It is used to optimize Checkstyle between few launches.
49   * It is mostly useful for plugin and extensions of Checkstyle.
50   * It uses a property file
51   * for storage.  A hashcode of the Configuration is stored in the
52   * cache file to ensure the cache is invalidated when the
53   * configuration has changed.
54   *
55   */
56  public final class PropertyCacheFile {
57  
58      /**
59       * The property key to use for storing the hashcode of the
60       * configuration. To avoid name clashes with the files that are
61       * checked the key is chosen in such a way that it cannot be a
62       * valid file name.
63       */
64      public static final String CONFIG_HASH_KEY = "configuration*?";
65  
66      /**
67       * The property prefix to use for storing the hashcode of an
68       * external resource. To avoid name clashes with the files that are
69       * checked the prefix is chosen in such a way that it cannot be a
70       * valid file name and makes it clear it is a resource.
71       */
72      public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:";
73  
74      /** Size of default byte array for buffer. */
75      private static final int BUFFER_SIZE = 1024;
76  
77      /** Default buffer for reading from streams. */
78      private static final byte[] BUFFER = new byte[BUFFER_SIZE];
79  
80      /** Default number for base 16 encoding. */
81      private static final int BASE_16 = 16;
82  
83      /** The details on files. */
84      private final Properties details = new Properties();
85  
86      /** Configuration object. */
87      private final Configuration config;
88  
89      /** File name of cache. */
90      private final String fileName;
91  
92      /** Generated configuration hash. */
93      private String configHash;
94  
95      /**
96       * Creates a new {@code PropertyCacheFile} instance.
97       *
98       * @param config the current configuration, not null
99       * @param fileName the cache file
100      * @throws IllegalArgumentException when either arguments are null
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      *
116      * @throws IOException when there is a problems with file read
117      */
118     public void load() throws IOException {
119         // get the current config so if the file isn't found
120         // the first time the hash will be added to output file
121         configHash = getHashCodeBasedOnObjectContent(config);
122         final Path path = Path.of(fileName);
123         if (Files.exists(path)) {
124             try (InputStream inStream = Files.newInputStream(path)) {
125                 details.load(inStream);
126                 final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY);
127                 if (!configHash.equals(cachedConfigHash)) {
128                     // Detected configuration change - clear cache
129                     reset();
130                 }
131             }
132         }
133         else {
134             // put the hash in the file if the file is going to be created
135             reset();
136         }
137     }
138 
139     /**
140      * Cleans up the object and updates the cache file.
141      *
142      * @throws IOException  when there is a problems with file save
143      */
144     public void persist() throws IOException {
145         final Path path = Path.of(fileName);
146         final Path directory = path.getParent();
147 
148         if (directory != null) {
149             OsSpecificUtil.updateDirectory(directory);
150         }
151         try (OutputStream out = Files.newOutputStream(path)) {
152             details.store(out, null);
153         }
154     }
155 
156     /**
157      * Resets the cache to be empty except for the configuration hash.
158      */
159     public void reset() {
160         details.clear();
161         details.setProperty(CONFIG_HASH_KEY, configHash);
162     }
163 
164     /**
165      * Checks that file is in cache.
166      *
167      * @param uncheckedFileName the file to check
168      * @param timestamp the timestamp of the file to check
169      * @return whether the specified file has already been checked ok
170      */
171     public boolean isInCache(String uncheckedFileName, long timestamp) {
172         final String lastChecked = details.getProperty(uncheckedFileName);
173         return Objects.equals(lastChecked, Long.toString(timestamp));
174     }
175 
176     /**
177      * Records that a file checked ok.
178      *
179      * @param checkedFileName name of the file that checked ok
180      * @param timestamp the timestamp of the file
181      */
182     public void put(String checkedFileName, long timestamp) {
183         details.setProperty(checkedFileName, Long.toString(timestamp));
184     }
185 
186     /**
187      * Retrieves the hash of a specific file.
188      *
189      * @param name The name of the file to retrieve.
190      * @return The has of the file or {@code null}.
191      */
192     public String get(String name) {
193         return details.getProperty(name);
194     }
195 
196     /**
197      * Removed a specific file from the cache.
198      *
199      * @param checkedFileName The name of the file to remove.
200      */
201     public void remove(String checkedFileName) {
202         details.remove(checkedFileName);
203     }
204 
205     /**
206      * Calculates the hashcode for the serializable object based on its content.
207      *
208      * @param object serializable object.
209      * @return the hashcode for serializable object.
210      * @throws IllegalStateException when some unexpected happened.
211      */
212     private static String getHashCodeBasedOnObjectContent(Serializable object) {
213         try {
214             final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
215             // in-memory serialization of Configuration
216             serialize(object, outputStream);
217             // Instead of hexEncoding outputStream.toByteArray() directly we
218             // use a message digest here to keep the length of the
219             // hashcode reasonable
220 
221             final MessageDigest digest = MessageDigest.getInstance("SHA-1");
222             digest.update(outputStream.toByteArray());
223 
224             return new BigInteger(1, digest.digest()).toString(BASE_16).toUpperCase(Locale.ROOT);
225         }
226         catch (final IOException | NoSuchAlgorithmException exc) {
227             // rethrow as unchecked exception
228             throw new IllegalStateException("Unable to calculate hashcode.", exc);
229         }
230     }
231 
232     /**
233      * Serializes object to output stream.
234      *
235      * @param object object to be serialized
236      * @param outputStream serialization stream
237      * @throws IOException if an error occurs
238      */
239     private static void serialize(Serializable object,
240                                   OutputStream outputStream)
241             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 {@code 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 exc) {
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(exc);
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 CheckstyleException if error while loading occurs.
295      * @throws IOException 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 {@code 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      * @param location       resource location.
385      * @param contentHashSum content hash sum.
386      */
387     private record ExternalResource(String location,
388                                     String contentHashSum) {
389     }
390 
391 }