001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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.BufferedInputStream; 023import java.io.ByteArrayOutputStream; 024import java.io.File; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.ObjectOutputStream; 028import java.io.OutputStream; 029import java.io.Serializable; 030import java.net.URI; 031import java.nio.file.Files; 032import java.nio.file.Path; 033import java.nio.file.Paths; 034import java.security.MessageDigest; 035import java.security.NoSuchAlgorithmException; 036import java.util.HashSet; 037import java.util.Objects; 038import java.util.Properties; 039import java.util.Set; 040 041import com.google.common.io.BaseEncoding; 042import com.google.common.io.ByteStreams; 043import com.google.common.io.Closeables; 044import com.google.common.io.Flushables; 045import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 046import com.puppycrawl.tools.checkstyle.api.Configuration; 047import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 048 049/** 050 * This class maintains a persistent(on file-system) store of the files 051 * that have checked ok(no validation events) and their associated 052 * timestamp. It is used to optimize Checkstyle between few launches. 053 * It is mostly useful for plugin and extensions of Checkstyle. 054 * It uses a property file 055 * for storage. A hashcode of the Configuration is stored in the 056 * cache file to ensure the cache is invalidated when the 057 * configuration has changed. 058 * 059 */ 060public final class PropertyCacheFile { 061 062 /** 063 * The property key to use for storing the hashcode of the 064 * configuration. To avoid name clashes with the files that are 065 * checked the key is chosen in such a way that it cannot be a 066 * valid file name. 067 */ 068 public static final String CONFIG_HASH_KEY = "configuration*?"; 069 070 /** 071 * The property prefix to use for storing the hashcode of an 072 * external resource. To avoid name clashes with the files that are 073 * checked the prefix is chosen in such a way that it cannot be a 074 * valid file name and makes it clear it is a resource. 075 */ 076 public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:"; 077 078 /** The details on files. **/ 079 private final Properties details = new Properties(); 080 081 /** Configuration object. **/ 082 private final Configuration config; 083 084 /** File name of cache. **/ 085 private final String fileName; 086 087 /** Generated configuration hash. **/ 088 private String configHash; 089 090 /** 091 * Creates a new {@code PropertyCacheFile} instance. 092 * 093 * @param config the current configuration, not null 094 * @param fileName the cache file 095 */ 096 public PropertyCacheFile(Configuration config, String fileName) { 097 if (config == null) { 098 throw new IllegalArgumentException("config can not be null"); 099 } 100 if (fileName == null) { 101 throw new IllegalArgumentException("fileName can not be null"); 102 } 103 this.config = config; 104 this.fileName = fileName; 105 } 106 107 /** 108 * Load cached values from file. 109 * @throws IOException when there is a problems with file read 110 */ 111 public void load() throws IOException { 112 // get the current config so if the file isn't found 113 // the first time the hash will be added to output file 114 configHash = getHashCodeBasedOnObjectContent(config); 115 final File file = new File(fileName); 116 if (file.exists()) { 117 try (InputStream inStream = Files.newInputStream(file.toPath())) { 118 details.load(inStream); 119 final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY); 120 if (!configHash.equals(cachedConfigHash)) { 121 // Detected configuration change - clear cache 122 reset(); 123 } 124 } 125 } 126 else { 127 // put the hash in the file if the file is going to be created 128 reset(); 129 } 130 } 131 132 /** 133 * Cleans up the object and updates the cache file. 134 * @throws IOException when there is a problems with file save 135 */ 136 public void persist() throws IOException { 137 final Path path = Paths.get(fileName); 138 final Path directory = path.getParent(); 139 if (directory != null) { 140 Files.createDirectories(directory); 141 } 142 OutputStream out = null; 143 try { 144 out = Files.newOutputStream(path); 145 details.store(out, null); 146 } 147 finally { 148 flushAndCloseOutStream(out); 149 } 150 } 151 152 /** 153 * Resets the cache to be empty except for the configuration hash. 154 */ 155 public void reset() { 156 details.clear(); 157 details.setProperty(CONFIG_HASH_KEY, configHash); 158 } 159 160 /** 161 * Flushes and closes output stream. 162 * @param stream the output stream 163 * @throws IOException when there is a problems with file flush and close 164 */ 165 private static void flushAndCloseOutStream(OutputStream stream) throws IOException { 166 if (stream != null) { 167 Flushables.flush(stream, false); 168 } 169 Closeables.close(stream, false); 170 } 171 172 /** 173 * Checks that file is in cache. 174 * @param uncheckedFileName the file to check 175 * @param timestamp the timestamp of the file to check 176 * @return whether the specified file has already been checked ok 177 */ 178 public boolean isInCache(String uncheckedFileName, long timestamp) { 179 final String lastChecked = details.getProperty(uncheckedFileName); 180 return Objects.equals(lastChecked, Long.toString(timestamp)); 181 } 182 183 /** 184 * Records that a file checked ok. 185 * @param checkedFileName name of the file that checked ok 186 * @param timestamp the timestamp of the file 187 */ 188 public void put(String checkedFileName, long timestamp) { 189 details.setProperty(checkedFileName, Long.toString(timestamp)); 190 } 191 192 /** 193 * Retrieves the hash of a specific file. 194 * @param name The name of the file to retrieve. 195 * @return The has of the file or {@code null}. 196 */ 197 public String get(String name) { 198 return details.getProperty(name); 199 } 200 201 /** 202 * Removed a specific file from the cache. 203 * @param checkedFileName The name of the file to remove. 204 */ 205 public void remove(String checkedFileName) { 206 details.remove(checkedFileName); 207 } 208 209 /** 210 * Calculates the hashcode for the serializable object based on its content. 211 * @param object serializable object. 212 * @return the hashcode for serializable object. 213 */ 214 private static String getHashCodeBasedOnObjectContent(Serializable object) { 215 try { 216 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 217 // in-memory serialization of Configuration 218 serialize(object, outputStream); 219 // Instead of hexEncoding outputStream.toByteArray() directly we 220 // use a message digest here to keep the length of the 221 // hashcode reasonable 222 223 final MessageDigest digest = MessageDigest.getInstance("SHA-1"); 224 digest.update(outputStream.toByteArray()); 225 226 return BaseEncoding.base16().upperCase().encode(digest.digest()); 227 } 228 catch (final IOException | NoSuchAlgorithmException ex) { 229 // rethrow as unchecked exception 230 throw new IllegalStateException("Unable to calculate hashcode.", ex); 231 } 232 } 233 234 /** 235 * Serializes object to output stream. 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 final ObjectOutputStream oos = new ObjectOutputStream(outputStream); 243 try { 244 oos.writeObject(object); 245 } 246 finally { 247 flushAndCloseOutStream(oos); 248 } 249 } 250 251 /** 252 * Puts external resources in cache. 253 * If at least one external resource changed, clears the cache. 254 * @param locations locations of external resources. 255 */ 256 public void putExternalResources(Set<String> locations) { 257 final Set<ExternalResource> resources = loadExternalResources(locations); 258 if (areExternalResourcesChanged(resources)) { 259 reset(); 260 fillCacheWithExternalResources(resources); 261 } 262 } 263 264 /** 265 * Loads a set of {@link ExternalResource} based on their locations. 266 * @param resourceLocations locations of external configuration resources. 267 * @return a set of {@link ExternalResource}. 268 */ 269 private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) { 270 final Set<ExternalResource> resources = new HashSet<>(); 271 for (String location : resourceLocations) { 272 try { 273 final byte[] content = loadExternalResource(location); 274 final String contentHashSum = getHashCodeBasedOnObjectContent(content); 275 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location, 276 contentHashSum)); 277 } 278 catch (CheckstyleException ex) { 279 // if exception happened (configuration resource was not found, connection is not 280 // available, resource is broken, etc), we need to calculate hash sum based on 281 // exception object content in order to check whether problem is resolved later 282 // and/or the configuration is changed. 283 final String contentHashSum = getHashCodeBasedOnObjectContent(ex); 284 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location, 285 contentHashSum)); 286 } 287 } 288 return resources; 289 } 290 291 /** 292 * Loads the content of external resource. 293 * @param location external resource location. 294 * @return array of bytes which represents the content of external resource in binary form. 295 * @throws CheckstyleException if error while loading occurs. 296 */ 297 private static byte[] loadExternalResource(String location) throws CheckstyleException { 298 final byte[] content; 299 final URI uri = CommonUtil.getUriByFilename(location); 300 301 try { 302 content = ByteStreams.toByteArray(new BufferedInputStream(uri.toURL().openStream())); 303 } 304 catch (IOException ex) { 305 throw new CheckstyleException("Unable to load external resource file " + location, ex); 306 } 307 308 return content; 309 } 310 311 /** 312 * Checks whether the contents of external configuration resources were changed. 313 * @param resources a set of {@link ExternalResource}. 314 * @return true if the contents of external configuration resources were changed. 315 */ 316 private boolean areExternalResourcesChanged(Set<ExternalResource> resources) { 317 return resources.stream().anyMatch(resource -> { 318 boolean changed = false; 319 if (isResourceLocationInCache(resource.location)) { 320 final String contentHashSum = resource.contentHashSum; 321 final String cachedHashSum = details.getProperty(resource.location); 322 if (!cachedHashSum.equals(contentHashSum)) { 323 changed = true; 324 } 325 } 326 else { 327 changed = true; 328 } 329 return changed; 330 }); 331 } 332 333 /** 334 * Fills cache with a set of {@link ExternalResource}. 335 * If external resource from the set is already in cache, it will be skipped. 336 * @param externalResources a set of {@link ExternalResource}. 337 */ 338 private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) { 339 externalResources 340 .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum)); 341 } 342 343 /** 344 * Checks whether resource location is in cache. 345 * @param location resource location. 346 * @return true if resource location is in cache. 347 */ 348 private boolean isResourceLocationInCache(String location) { 349 final String cachedHashSum = details.getProperty(location); 350 return cachedHashSum != null; 351 } 352 353 /** 354 * Class which represents external resource. 355 */ 356 private static class ExternalResource { 357 358 /** Location of resource. */ 359 private final String location; 360 /** Hash sum which is calculated based on resource content. */ 361 private final String contentHashSum; 362 363 /** 364 * Creates an instance. 365 * @param location resource location. 366 * @param contentHashSum content hash sum. 367 */ 368 ExternalResource(String location, String contentHashSum) { 369 this.location = location; 370 this.contentHashSum = contentHashSum; 371 } 372 373 } 374 375}