View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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 ex) {
227             // rethrow as unchecked exception
228             throw new IllegalStateException("Unable to calculate hashcode.", ex);
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) throws IOException {
241         try (ObjectOutputStream oos = new ObjectOutputStream(outputStream)) {
242             oos.writeObject(object);
243         }
244     }
245 
246     /**
247      * Puts external resources in cache.
248      * If at least one external resource changed, clears the cache.
249      *
250      * @param locations locations of external resources.
251      */
252     public void putExternalResources(Set<String> locations) {
253         final Set<ExternalResource> resources = loadExternalResources(locations);
254         if (areExternalResourcesChanged(resources)) {
255             reset();
256             fillCacheWithExternalResources(resources);
257         }
258     }
259 
260     /**
261      * Loads a set of {@link ExternalResource} based on their locations.
262      *
263      * @param resourceLocations locations of external configuration resources.
264      * @return a set of {@link ExternalResource}.
265      */
266     private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) {
267         final Set<ExternalResource> resources = new HashSet<>();
268         for (String location : resourceLocations) {
269             try {
270                 final byte[] content = loadExternalResource(location);
271                 final String contentHashSum = getHashCodeBasedOnObjectContent(content);
272                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
273                         contentHashSum));
274             }
275             catch (CheckstyleException | IOException ex) {
276                 // if exception happened (configuration resource was not found, connection is not
277                 // available, resource is broken, etc.), we need to calculate hash sum based on
278                 // exception object content in order to check whether problem is resolved later
279                 // and/or the configuration is changed.
280                 final String contentHashSum = getHashCodeBasedOnObjectContent(ex);
281                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
282                         contentHashSum));
283             }
284         }
285         return resources;
286     }
287 
288     /**
289      * Loads the content of external resource.
290      *
291      * @param location external resource location.
292      * @return array of bytes which represents the content of external resource in binary form.
293      * @throws IOException if error while loading occurs.
294      * @throws CheckstyleException if error while loading occurs.
295      */
296     private static byte[] loadExternalResource(String location)
297             throws IOException, CheckstyleException {
298         final URI uri = CommonUtil.getUriByFilename(location);
299 
300         try (InputStream is = uri.toURL().openStream()) {
301             return toByteArray(is);
302         }
303     }
304 
305     /**
306      * Reads all the contents of an input stream and returns it as a byte array.
307      *
308      * @param stream The input stream to read from.
309      * @return The resulting byte array of the stream.
310      * @throws IOException if there is an error reading the input stream.
311      */
312     private static byte[] toByteArray(InputStream stream) throws IOException {
313         final ByteArrayOutputStream content = new ByteArrayOutputStream();
314 
315         while (true) {
316             final int size = stream.read(BUFFER);
317             if (size == -1) {
318                 break;
319             }
320 
321             content.write(BUFFER, 0, size);
322         }
323 
324         return content.toByteArray();
325     }
326 
327     /**
328      * Checks whether the contents of external configuration resources were changed.
329      *
330      * @param resources a set of {@link ExternalResource}.
331      * @return true if the contents of external configuration resources were changed.
332      */
333     private boolean areExternalResourcesChanged(Set<ExternalResource> resources) {
334         return resources.stream().anyMatch(this::isResourceChanged);
335     }
336 
337     /**
338      * Checks whether the resource is changed.
339      *
340      * @param resource resource to check.
341      * @return true if resource is changed.
342      */
343     private boolean isResourceChanged(ExternalResource resource) {
344         boolean changed = false;
345         if (isResourceLocationInCache(resource.location)) {
346             final String contentHashSum = resource.contentHashSum;
347             final String cachedHashSum = details.getProperty(resource.location);
348             if (!cachedHashSum.equals(contentHashSum)) {
349                 changed = true;
350             }
351         }
352         else {
353             changed = true;
354         }
355         return changed;
356     }
357 
358     /**
359      * Fills cache with a set of {@link ExternalResource}.
360      * If external resource from the set is already in cache, it will be skipped.
361      *
362      * @param externalResources a set of {@link ExternalResource}.
363      */
364     private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) {
365         externalResources
366             .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
367     }
368 
369     /**
370      * Checks whether resource location is in cache.
371      *
372      * @param location resource location.
373      * @return true if resource location is in cache.
374      */
375     private boolean isResourceLocationInCache(String location) {
376         final String cachedHashSum = details.getProperty(location);
377         return cachedHashSum != null;
378     }
379 
380     /**
381      * Class which represents external resource.
382      */
383     private static final class ExternalResource {
384 
385         /** Location of resource. */
386         private final String location;
387         /** Hash sum which is calculated based on resource content. */
388         private final String contentHashSum;
389 
390         /**
391          * Creates an instance.
392          *
393          * @param location resource location.
394          * @param contentHashSum content hash sum.
395          */
396         private ExternalResource(String location, String contentHashSum) {
397             this.location = location;
398             this.contentHashSum = contentHashSum;
399         }
400 
401     }
402 
403 }