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 }