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 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}