001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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 for storage. A hashcode of the Configuration is stored in the 051 * cache file to ensure the cache is invalidated when the configuration has changed. 052 * 053 */ 054public final class PropertyCacheFile { 055 056 /** 057 * The property key to use for storing the hashcode of the 058 * configuration. To avoid name clashes with the files that are 059 * checked the key is chosen in such a way that it cannot be a valid file name. 060 */ 061 public static final String CONFIG_HASH_KEY = "configuration*?"; 062 063 /** 064 * The property prefix to use for storing the hashcode of an 065 * external resource. To avoid name clashes with the files that are 066 * checked the prefix is chosen in such a way that it cannot be a 067 * valid file name and makes it clear it is a resource. 068 */ 069 public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:"; 070 071 /** Size of default byte array for buffer. */ 072 private static final int BUFFER_SIZE = 1024; 073 074 /** Default buffer for reading from streams. */ 075 private static final byte[] BUFFER = new byte[BUFFER_SIZE]; 076 077 /** Default number for base 16 encoding. */ 078 private static final int BASE_16 = 16; 079 080 /** The details on files. */ 081 private final Properties details = new Properties(); 082 083 /** Configuration object. */ 084 private final Configuration config; 085 086 /** File name of cache. */ 087 private final String fileName; 088 089 /** Generated configuration hash. */ 090 private String configHash; 091 092 /** 093 * Creates a new {@code PropertyCacheFile} instance. 094 * 095 * @param config the current configuration, not null 096 * @param fileName the cache file 097 * @throws IllegalArgumentException when either arguments are null 098 */ 099 public PropertyCacheFile(Configuration config, String fileName) { 100 if (config == null) { 101 throw new IllegalArgumentException("config can not be null"); 102 } 103 if (fileName == null) { 104 throw new IllegalArgumentException("fileName can not be null"); 105 } 106 this.config = config; 107 this.fileName = fileName; 108 } 109 110 /** 111 * Load cached values from file. 112 * 113 * @throws IOException when there is a problems with file read 114 */ 115 public void load() throws IOException { 116 // get the current config so if the file isn't found 117 // the first time the hash will be added to output file 118 configHash = getHashCodeBasedOnObjectContent(config); 119 final Path path = Path.of(fileName); 120 if (Files.exists(path)) { 121 try (InputStream inStream = Files.newInputStream(path)) { 122 details.load(inStream); 123 final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY); 124 if (!configHash.equals(cachedConfigHash)) { 125 // Detected configuration change - clear cache 126 reset(); 127 } 128 } 129 } 130 else { 131 // put the hash in the file if the file is going to be created 132 reset(); 133 } 134 } 135 136 /** 137 * Cleans up the object and updates the cache file. 138 * 139 * @throws IOException when there is a problems with file save 140 */ 141 public void persist() throws IOException { 142 final Path path = Path.of(fileName); 143 final Path directory = path.getParent(); 144 145 if (directory != null) { 146 OsSpecificUtil.updateDirectory(directory); 147 } 148 try (OutputStream out = Files.newOutputStream(path)) { 149 details.store(out, null); 150 } 151 } 152 153 /** 154 * Resets the cache to be empty except for the configuration hash. 155 */ 156 public void reset() { 157 details.clear(); 158 details.setProperty(CONFIG_HASH_KEY, configHash); 159 } 160 161 /** 162 * Checks that file is in cache. 163 * 164 * @param uncheckedFileName the file to check 165 * @param timestamp the timestamp of the file to check 166 * @return whether the specified file has already been checked ok 167 */ 168 public boolean isInCache(String uncheckedFileName, long timestamp) { 169 final String lastChecked = details.getProperty(uncheckedFileName); 170 return Objects.equals(lastChecked, Long.toString(timestamp)); 171 } 172 173 /** 174 * Records that a file checked ok. 175 * 176 * @param checkedFileName name of the file that checked ok 177 * @param timestamp the timestamp of the file 178 */ 179 public void put(String checkedFileName, long timestamp) { 180 details.setProperty(checkedFileName, Long.toString(timestamp)); 181 } 182 183 /** 184 * Retrieves the hash of a specific file. 185 * 186 * @param name The name of the file to retrieve. 187 * @return The has of the file or {@code null}. 188 */ 189 public String get(String name) { 190 return details.getProperty(name); 191 } 192 193 /** 194 * Removed a specific file from the cache. 195 * 196 * @param checkedFileName The name of the file to remove. 197 */ 198 public void remove(String checkedFileName) { 199 details.remove(checkedFileName); 200 } 201 202 /** 203 * Returns the cache file name. 204 * 205 * @return the cache file name. 206 */ 207 public String getFileName() { 208 return fileName; 209 } 210 211 /** 212 * Calculates the hashcode for the serializable object based on its content. 213 * 214 * @param object serializable object. 215 * @return the hashcode for serializable object. 216 * @throws IllegalStateException when some unexpected happened. 217 */ 218 private static String getHashCodeBasedOnObjectContent(Serializable object) { 219 try { 220 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 221 // in-memory serialization of Configuration 222 serialize(object, outputStream); 223 // Instead of hexEncoding outputStream.toByteArray() directly we 224 // use a message digest here to keep the length of the 225 // hashcode reasonable 226 227 final MessageDigest digest = MessageDigest.getInstance("SHA-1"); 228 digest.update(outputStream.toByteArray()); 229 230 return new BigInteger(1, digest.digest()).toString(BASE_16).toUpperCase(Locale.ROOT); 231 } 232 catch (final IOException | NoSuchAlgorithmException exc) { 233 // rethrow as unchecked exception 234 throw new IllegalStateException("Unable to calculate hashcode for " 235 + object.getClass().getName(), exc); 236 } 237 } 238 239 /** 240 * Serializes object to output stream. 241 * 242 * @param object object to be serialized 243 * @param outputStream serialization stream 244 * @throws IOException if an error occurs 245 */ 246 private static void serialize(Serializable object, 247 OutputStream outputStream) 248 throws IOException { 249 try (ObjectOutputStream oos = new ObjectOutputStream(outputStream)) { 250 oos.writeObject(object); 251 } 252 } 253 254 /** 255 * Puts external resources in cache. 256 * If at least one external resource changed, clears the cache. 257 * 258 * @param locations locations of external resources. 259 */ 260 public void putExternalResources(Set<String> locations) { 261 final Set<ExternalResource> resources = loadExternalResources(locations); 262 if (areExternalResourcesChanged(resources)) { 263 reset(); 264 fillCacheWithExternalResources(resources); 265 } 266 } 267 268 /** 269 * Loads a set of {@link ExternalResource} based on their locations. 270 * 271 * @param resourceLocations locations of external configuration resources. 272 * @return a set of {@code ExternalResource}. 273 */ 274 private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) { 275 final Set<ExternalResource> resources = new HashSet<>(); 276 for (String location : resourceLocations) { 277 try { 278 final byte[] content = loadExternalResource(location); 279 final String contentHashSum = getHashCodeBasedOnObjectContent(content); 280 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location, 281 contentHashSum)); 282 } 283 catch (CheckstyleException | IOException exc) { 284 // if exception happened (configuration resource was not found, connection is not 285 // available, resource is broken, etc.), we need to calculate hash sum based on 286 // exception object content in order to check whether problem is resolved later 287 // and/or the configuration is changed. 288 final String contentHashSum = getHashCodeBasedOnObjectContent(exc); 289 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location, 290 contentHashSum)); 291 } 292 } 293 return resources; 294 } 295 296 /** 297 * Loads the content of external resource. 298 * 299 * @param location external resource location. 300 * @return array of bytes which represents the content of external resource in binary form. 301 * @throws CheckstyleException if error while loading occurs. 302 * @throws IOException if error while loading occurs. 303 */ 304 private static byte[] loadExternalResource(String location) 305 throws IOException, CheckstyleException { 306 final URI uri = CommonUtil.getUriByFilename(location); 307 308 try (InputStream is = uri.toURL().openStream()) { 309 return toByteArray(is); 310 } 311 } 312 313 /** 314 * Reads all the contents of an input stream and returns it as a byte array. 315 * 316 * @param stream The input stream to read from. 317 * @return The resulting byte array of the stream. 318 * @throws IOException if there is an error reading the input stream. 319 */ 320 private static byte[] toByteArray(InputStream stream) throws IOException { 321 final ByteArrayOutputStream content = new ByteArrayOutputStream(); 322 323 while (true) { 324 final int size = stream.read(BUFFER); 325 if (size == -1) { 326 break; 327 } 328 329 content.write(BUFFER, 0, size); 330 } 331 332 return content.toByteArray(); 333 } 334 335 /** 336 * Checks whether the contents of external configuration resources were changed. 337 * 338 * @param resources a set of {@link ExternalResource}. 339 * @return true if the contents of external configuration resources were changed. 340 */ 341 private boolean areExternalResourcesChanged(Set<ExternalResource> resources) { 342 return resources.stream().anyMatch(this::isResourceChanged); 343 } 344 345 /** 346 * Checks whether the resource is changed. 347 * 348 * @param resource resource to check. 349 * @return true if resource is changed. 350 */ 351 private boolean isResourceChanged(ExternalResource resource) { 352 boolean changed = false; 353 if (isResourceLocationInCache(resource.location)) { 354 final String contentHashSum = resource.contentHashSum; 355 final String cachedHashSum = details.getProperty(resource.location); 356 if (!cachedHashSum.equals(contentHashSum)) { 357 changed = true; 358 } 359 } 360 else { 361 changed = true; 362 } 363 return changed; 364 } 365 366 /** 367 * Fills cache with a set of {@link ExternalResource}. 368 * If external resource from the set is already in cache, it will be skipped. 369 * 370 * @param externalResources a set of {@code ExternalResource}. 371 */ 372 private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) { 373 externalResources 374 .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum)); 375 } 376 377 /** 378 * Checks whether resource location is in cache. 379 * 380 * @param location resource location. 381 * @return true if resource location is in cache. 382 */ 383 private boolean isResourceLocationInCache(String location) { 384 final String cachedHashSum = details.getProperty(location); 385 return cachedHashSum != null; 386 } 387 388 /** 389 * Class which represents external resource. 390 * 391 * @param location resource location. 392 * @param contentHashSum content hash sum. 393 */ 394 private record ExternalResource(String location, String contentHashSum) { 395 } 396 397}