001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.ByteArrayOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.ObjectOutputStream;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.math.BigInteger;
029import java.net.URI;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.security.MessageDigest;
033import java.security.NoSuchAlgorithmException;
034import java.util.HashSet;
035import java.util.Locale;
036import java.util.Objects;
037import java.util.Properties;
038import java.util.Set;
039
040import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
041import com.puppycrawl.tools.checkstyle.api.Configuration;
042import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
043import com.puppycrawl.tools.checkstyle.utils.OsSpecificUtil;
044
045/**
046 * This class maintains a persistent(on file-system) store of the files
047 * that have checked ok(no validation events) and their associated
048 * timestamp. It is used to optimize Checkstyle between few launches.
049 * It is mostly useful for plugin and extensions of Checkstyle.
050 * It uses a property file
051 * for storage. A hashcode of the Configuration is stored in the
052 * cache file to ensure the cache is invalidated when the
053 * configuration has changed.
054 *
055 */
056public final class PropertyCacheFile {
057
058 /**
059 * The property key to use for storing the hashcode of the
060 * configuration. To avoid name clashes with the files that are
061 * checked the key is chosen in such a way that it cannot be a
062 * valid file name.
063 */
064 public static final String CONFIG_HASH_KEY = "configuration*?";
065
066 /**
067 * The property prefix to use for storing the hashcode of an
068 * external resource. To avoid name clashes with the files that are
069 * checked the prefix is chosen in such a way that it cannot be a
070 * valid file name and makes it clear it is a resource.
071 */
072 public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:";
073
074 /** Size of default byte array for buffer. */
075 private static final int BUFFER_SIZE = 1024;
076
077 /** Default buffer for reading from streams. */
078 private static final byte[] BUFFER = new byte[BUFFER_SIZE];
079
080 /** Default number for base 16 encoding. */
081 private static final int BASE_16 = 16;
082
083 /** The details on files. **/
084 private final Properties details = new Properties();
085
086 /** Configuration object. **/
087 private final Configuration config;
088
089 /** File name of cache. **/
090 private final String fileName;
091
092 /** Generated configuration hash. **/
093 private String configHash;
094
095 /**
096 * Creates a new {@code PropertyCacheFile} instance.
097 *
098 * @param config the current configuration, not null
099 * @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}