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 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) 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 exc) {
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(exc);
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 * @param location resource location.
384 * @param contentHashSum content hash sum.
385 */
386 private record ExternalResource(String location,
387 String contentHashSum) {
388 }
389
390 }