001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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.nio.file.Paths; 033import java.security.MessageDigest; 034import java.security.NoSuchAlgorithmException; 035import java.util.HashSet; 036import java.util.Locale; 037import java.util.Objects; 038import java.util.Properties; 039import java.util.Set; 040 041import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 042import com.puppycrawl.tools.checkstyle.api.Configuration; 043import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 044import com.puppycrawl.tools.checkstyle.utils.OsSpecificUtil; 045 046/** 047 * This class maintains a persistent(on file-system) store of the files 048 * that have checked ok(no validation events) and their associated 049 * timestamp. It is used to optimize Checkstyle between few launches. 050 * It is mostly useful for plugin and extensions of Checkstyle. 051 * It uses a property file 052 * for storage. A hashcode of the Configuration is stored in the 053 * cache file to ensure the cache is invalidated when the 054 * configuration has changed. 055 * 056 */ 057public final class PropertyCacheFile { 058 059 /** 060 * The property key to use for storing the hashcode of the 061 * configuration. To avoid name clashes with the files that are 062 * checked the key is chosen in such a way that it cannot be a 063 * valid file name. 064 */ 065 public static final String CONFIG_HASH_KEY = "configuration*?"; 066 067 /** 068 * The property prefix to use for storing the hashcode of an 069 * external resource. To avoid name clashes with the files that are 070 * checked the prefix is chosen in such a way that it cannot be a 071 * valid file name and makes it clear it is a resource. 072 */ 073 public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:"; 074 075 /** Size of default byte array for buffer. */ 076 private static final int BUFFER_SIZE = 1024; 077 078 /** Default buffer for reading from streams. */ 079 private static final byte[] BUFFER = new byte[BUFFER_SIZE]; 080 081 /** Default number for base 16 encoding. */ 082 private static final int BASE_16 = 16; 083 084 /** The details on files. **/ 085 private final Properties details = new Properties(); 086 087 /** Configuration object. **/ 088 private final Configuration config; 089 090 /** File name of cache. **/ 091 private final String fileName; 092 093 /** Generated configuration hash. **/ 094 private String configHash; 095 096 /** 097 * Creates a new {@code PropertyCacheFile} instance. 098 * 099 * @param config the current configuration, not null 100 * @param fileName the cache file 101 * @throws IllegalArgumentException when either arguments are null 102 */ 103 public PropertyCacheFile(Configuration config, String fileName) { 104 if (config == null) { 105 throw new IllegalArgumentException("config can not be null"); 106 } 107 if (fileName == null) { 108 throw new IllegalArgumentException("fileName can not be null"); 109 } 110 this.config = config; 111 this.fileName = fileName; 112 } 113 114 /** 115 * Load cached values from file. 116 * 117 * @throws IOException when there is a problems with file read 118 */ 119 public void load() throws IOException { 120 // get the current config so if the file isn't found 121 // the first time the hash will be added to output file 122 configHash = getHashCodeBasedOnObjectContent(config); 123 final Path path = Path.of(fileName); 124 if (Files.exists(path)) { 125 try (InputStream inStream = Files.newInputStream(path)) { 126 details.load(inStream); 127 final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY); 128 if (!configHash.equals(cachedConfigHash)) { 129 // Detected configuration change - clear cache 130 reset(); 131 } 132 } 133 } 134 else { 135 // put the hash in the file if the file is going to be created 136 reset(); 137 } 138 } 139 140 /** 141 * Cleans up the object and updates the cache file. 142 * 143 * @throws IOException when there is a problems with file save 144 */ 145 public void persist() throws IOException { 146 final Path path = Paths.get(fileName); 147 final Path directory = path.getParent(); 148 149 if (directory != null) { 150 OsSpecificUtil.updateDirectory(directory); 151 } 152 try (OutputStream out = Files.newOutputStream(path)) { 153 details.store(out, null); 154 } 155 } 156 157 /** 158 * Resets the cache to be empty except for the configuration hash. 159 */ 160 public void reset() { 161 details.clear(); 162 details.setProperty(CONFIG_HASH_KEY, configHash); 163 } 164 165 /** 166 * Checks that file is in cache. 167 * 168 * @param uncheckedFileName the file to check 169 * @param timestamp the timestamp of the file to check 170 * @return whether the specified file has already been checked ok 171 */ 172 public boolean isInCache(String uncheckedFileName, long timestamp) { 173 final String lastChecked = details.getProperty(uncheckedFileName); 174 return Objects.equals(lastChecked, Long.toString(timestamp)); 175 } 176 177 /** 178 * Records that a file checked ok. 179 * 180 * @param checkedFileName name of the file that checked ok 181 * @param timestamp the timestamp of the file 182 */ 183 public void put(String checkedFileName, long timestamp) { 184 details.setProperty(checkedFileName, Long.toString(timestamp)); 185 } 186 187 /** 188 * Retrieves the hash of a specific file. 189 * 190 * @param name The name of the file to retrieve. 191 * @return The has of the file or {@code null}. 192 */ 193 public String get(String name) { 194 return details.getProperty(name); 195 } 196 197 /** 198 * Removed a specific file from the cache. 199 * 200 * @param checkedFileName The name of the file to remove. 201 */ 202 public void remove(String checkedFileName) { 203 details.remove(checkedFileName); 204 } 205 206 /** 207 * Calculates the hashcode for the serializable object based on its content. 208 * 209 * @param object serializable object. 210 * @return the hashcode for serializable object. 211 * @throws IllegalStateException when some unexpected happened. 212 */ 213 private static String getHashCodeBasedOnObjectContent(Serializable object) { 214 try { 215 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 216 // in-memory serialization of Configuration 217 serialize(object, outputStream); 218 // Instead of hexEncoding outputStream.toByteArray() directly we 219 // use a message digest here to keep the length of the 220 // hashcode reasonable 221 222 final MessageDigest digest = MessageDigest.getInstance("SHA-1"); 223 digest.update(outputStream.toByteArray()); 224 225 return new BigInteger(1, digest.digest()).toString(BASE_16).toUpperCase(Locale.ROOT); 226 } 227 catch (final IOException | NoSuchAlgorithmException ex) { 228 // rethrow as unchecked exception 229 throw new IllegalStateException("Unable to calculate hashcode.", ex); 230 } 231 } 232 233 /** 234 * Serializes object to output stream. 235 * 236 * @param object object to be serialized 237 * @param outputStream serialization stream 238 * @throws IOException if an error occurs 239 */ 240 private static void serialize(Serializable object, 241 OutputStream outputStream) 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 {@link 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 ex) { 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(ex); 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 IOException if error while loading occurs. 295 * @throws CheckstyleException 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 {@link 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 private static final class ExternalResource { 385 386 /** Location of resource. */ 387 private final String location; 388 /** Hash sum which is calculated based on resource content. */ 389 private final String contentHashSum; 390 391 /** 392 * Creates an instance. 393 * 394 * @param location resource location. 395 * @param contentHashSum content hash sum. 396 */ 397 private ExternalResource(String location, String contentHashSum) { 398 this.location = location; 399 this.contentHashSum = contentHashSum; 400 } 401 402 } 403 404}