Coverage Report - com.puppycrawl.tools.checkstyle.PropertyCacheFile
 
Classes in this File Line Coverage Branch Coverage Complexity
PropertyCacheFile
100%
95/95
100%
24/24
0
PropertyCacheFile$ExternalResource
100%
5/5
N/A
0
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle;
 21  
 
 22  
 import java.io.BufferedInputStream;
 23  
 import java.io.ByteArrayOutputStream;
 24  
 import java.io.File;
 25  
 import java.io.FileInputStream;
 26  
 import java.io.FileOutputStream;
 27  
 import java.io.IOException;
 28  
 import java.io.ObjectOutputStream;
 29  
 import java.io.OutputStream;
 30  
 import java.io.Serializable;
 31  
 import java.net.URI;
 32  
 import java.nio.file.Files;
 33  
 import java.nio.file.Path;
 34  
 import java.nio.file.Paths;
 35  
 import java.security.MessageDigest;
 36  
 import java.security.NoSuchAlgorithmException;
 37  
 import java.util.HashSet;
 38  
 import java.util.Objects;
 39  
 import java.util.Properties;
 40  
 import java.util.Set;
 41  
 
 42  
 import com.google.common.io.BaseEncoding;
 43  
 import com.google.common.io.ByteStreams;
 44  
 import com.google.common.io.Closeables;
 45  
 import com.google.common.io.Flushables;
 46  
 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
 47  
 import com.puppycrawl.tools.checkstyle.api.Configuration;
 48  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 49  
 
 50  
 /**
 51  
  * This class maintains a persistent(on file-system) store of the files
 52  
  * that have checked ok(no validation events) and their associated
 53  
  * timestamp. It is used to optimize Checkstyle between few launches.
 54  
  * It is mostly useful for plugin and extensions of Checkstyle.
 55  
  * It uses a property file
 56  
  * for storage.  A hashcode of the Configuration is stored in the
 57  
  * cache file to ensure the cache is invalidated when the
 58  
  * configuration has changed.
 59  
  *
 60  
  * @author Oliver Burn
 61  
  * @author Andrei Selkin
 62  
  */
 63  
 final class PropertyCacheFile {
 64  
 
 65  
     /**
 66  
      * The property key to use for storing the hashcode of the
 67  
      * configuration. To avoid name clashes with the files that are
 68  
      * checked the key is chosen in such a way that it cannot be a
 69  
      * valid file name.
 70  
      */
 71  
     public static final String CONFIG_HASH_KEY = "configuration*?";
 72  
 
 73  
     /**
 74  
      * The property prefix to use for storing the hashcode of an
 75  
      * external resource. To avoid name clashes with the files that are
 76  
      * checked the prefix is chosen in such a way that it cannot be a
 77  
      * valid file name and makes it clear it is a resource.
 78  
      */
 79  
     public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:";
 80  
 
 81  
     /** The details on files. **/
 82  47
     private final Properties details = new Properties();
 83  
 
 84  
     /** Configuration object. **/
 85  
     private final Configuration config;
 86  
 
 87  
     /** File name of cache. **/
 88  
     private final String fileName;
 89  
 
 90  
     /** Generated configuration hash. **/
 91  
     private String configHash;
 92  
 
 93  
     /**
 94  
      * Creates a new {@code PropertyCacheFile} instance.
 95  
      *
 96  
      * @param config the current configuration, not null
 97  
      * @param fileName the cache file
 98  
      */
 99  47
     PropertyCacheFile(Configuration config, String fileName) {
 100  47
         if (config == null) {
 101  1
             throw new IllegalArgumentException("config can not be null");
 102  
         }
 103  46
         if (fileName == null) {
 104  1
             throw new IllegalArgumentException("fileName can not be null");
 105  
         }
 106  45
         this.config = config;
 107  45
         this.fileName = fileName;
 108  45
     }
 109  
 
 110  
     /**
 111  
      * Load cached values from file.
 112  
      * @throws IOException when there is a problems with file read
 113  
      */
 114  
     public void load() throws IOException {
 115  
         // get the current config so if the file isn't found
 116  
         // the first time the hash will be added to output file
 117  41
         configHash = getHashCodeBasedOnObjectContent(config);
 118  41
         if (new File(fileName).exists()) {
 119  38
             FileInputStream inStream = null;
 120  
             try {
 121  38
                 inStream = new FileInputStream(fileName);
 122  38
                 details.load(inStream);
 123  38
                 final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY);
 124  38
                 if (!configHash.equals(cachedConfigHash)) {
 125  
                     // Detected configuration change - clear cache
 126  26
                     reset();
 127  
                 }
 128  
             }
 129  
             finally {
 130  38
                 Closeables.closeQuietly(inStream);
 131  38
             }
 132  38
         }
 133  
         else {
 134  
             // put the hash in the file if the file is going to be created
 135  3
             reset();
 136  
         }
 137  41
     }
 138  
 
 139  
     /**
 140  
      * Cleans up the object and updates the cache file.
 141  
      * @throws IOException  when there is a problems with file save
 142  
      */
 143  
     public void persist() throws IOException {
 144  33
         final Path directory = Paths.get(fileName).getParent();
 145  33
         if (directory != null) {
 146  31
             Files.createDirectories(directory);
 147  
         }
 148  33
         FileOutputStream out = null;
 149  
         try {
 150  33
             out = new FileOutputStream(fileName);
 151  32
             details.store(out, null);
 152  
         }
 153  
         finally {
 154  33
             flushAndCloseOutStream(out);
 155  32
         }
 156  32
     }
 157  
 
 158  
     /**
 159  
      * Resets the cache to be empty except for the configuration hash.
 160  
      */
 161  
     public void reset() {
 162  47
         details.clear();
 163  47
         details.setProperty(CONFIG_HASH_KEY, configHash);
 164  47
     }
 165  
 
 166  
     /**
 167  
      * Flushes and closes output stream.
 168  
      * @param stream the output stream
 169  
      * @throws IOException  when there is a problems with file flush and close
 170  
      */
 171  
     private static void flushAndCloseOutStream(OutputStream stream) throws IOException {
 172  99
         if (stream != null) {
 173  98
             Flushables.flush(stream, false);
 174  
         }
 175  99
         Closeables.close(stream, false);
 176  99
     }
 177  
 
 178  
     /**
 179  
      * Checks that file is in cache.
 180  
      * @param uncheckedFileName the file to check
 181  
      * @param timestamp the timestamp of the file to check
 182  
      * @return whether the specified file has already been checked ok
 183  
      */
 184  
     public boolean isInCache(String uncheckedFileName, long timestamp) {
 185  30
         final String lastChecked = details.getProperty(uncheckedFileName);
 186  30
         return Objects.equals(lastChecked, Long.toString(timestamp));
 187  
     }
 188  
 
 189  
     /**
 190  
      * Records that a file checked ok.
 191  
      * @param checkedFileName name of the file that checked ok
 192  
      * @param timestamp the timestamp of the file
 193  
      */
 194  
     public void put(String checkedFileName, long timestamp) {
 195  21
         details.setProperty(checkedFileName, Long.toString(timestamp));
 196  21
     }
 197  
 
 198  
     /**
 199  
      * Retrieves the hash of a specific file.
 200  
      * @param name The name of the file to retrieve.
 201  
      * @return The has of the file or {@code null}.
 202  
      */
 203  
     public String get(String name) {
 204  23
         return details.getProperty(name);
 205  
     }
 206  
 
 207  
     /**
 208  
      * Removed a specific file from the cache.
 209  
      * @param checkedFileName The name of the file to remove.
 210  
      */
 211  
     public void remove(String checkedFileName) {
 212  5
         details.remove(checkedFileName);
 213  5
     }
 214  
 
 215  
     /**
 216  
      * Calculates the hashcode for the serializable object based on its content.
 217  
      * @param object serializable object.
 218  
      * @return the hashcode for serializable object.
 219  
      */
 220  
     private static String getHashCodeBasedOnObjectContent(Serializable object) {
 221  
         try {
 222  66
             final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 223  
             // in-memory serialization of Configuration
 224  66
             serialize(object, outputStream);
 225  
             // Instead of hexEncoding outputStream.toByteArray() directly we
 226  
             // use a message digest here to keep the length of the
 227  
             // hashcode reasonable
 228  
 
 229  66
             final MessageDigest digest = MessageDigest.getInstance("SHA-1");
 230  65
             digest.update(outputStream.toByteArray());
 231  
 
 232  65
             return BaseEncoding.base16().upperCase().encode(digest.digest());
 233  
         }
 234  1
         catch (final IOException | NoSuchAlgorithmException ex) {
 235  
             // rethrow as unchecked exception
 236  1
             throw new IllegalStateException("Unable to calculate hashcode.", ex);
 237  
         }
 238  
     }
 239  
 
 240  
     /**
 241  
      * Serializes object to output stream.
 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) throws IOException {
 248  66
         final ObjectOutputStream oos = new ObjectOutputStream(outputStream);
 249  
         try {
 250  66
             oos.writeObject(object);
 251  
         }
 252  
         finally {
 253  66
             flushAndCloseOutStream(oos);
 254  66
         }
 255  66
     }
 256  
 
 257  
     /**
 258  
      * Puts external resources in cache.
 259  
      * If at least one external resource changed, clears the cache.
 260  
      * @param locations locations of external resources.
 261  
      */
 262  
     public void putExternalResources(Set<String> locations) {
 263  30
         final Set<ExternalResource> resources = loadExternalResources(locations);
 264  30
         if (areExternalResourcesChanged(resources)) {
 265  15
             reset();
 266  
         }
 267  30
         fillCacheWithExternalResources(resources);
 268  30
     }
 269  
 
 270  
     /**
 271  
      * Loads a set of {@link ExternalResource} based on their locations.
 272  
      * @param resourceLocations locations of external configuration resources.
 273  
      * @return a set of {@link ExternalResource}.
 274  
      */
 275  
     private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) {
 276  30
         final Set<ExternalResource> resources = new HashSet<>();
 277  30
         for (String location : resourceLocations) {
 278  24
             String contentHashSum = null;
 279  
             try {
 280  24
                 final byte[] content = loadExternalResource(location);
 281  14
                 contentHashSum = getHashCodeBasedOnObjectContent(content);
 282  
             }
 283  10
             catch (CheckstyleException ex) {
 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  10
                 contentHashSum = getHashCodeBasedOnObjectContent(ex);
 289  
             }
 290  
             finally {
 291  24
                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
 292  
                         contentHashSum));
 293  24
             }
 294  24
         }
 295  30
         return resources;
 296  
     }
 297  
 
 298  
     /**
 299  
      * Loads the content of external resource.
 300  
      * @param location external resource location.
 301  
      * @return array of bytes which represents the content of external resource in binary form.
 302  
      * @throws CheckstyleException if error while loading occurs.
 303  
      */
 304  
     private static byte[] loadExternalResource(String location) throws CheckstyleException {
 305  
         final byte[] content;
 306  24
         final URI uri = CommonUtils.getUriByFilename(location);
 307  
 
 308  
         try {
 309  15
             content = ByteStreams.toByteArray(new BufferedInputStream(uri.toURL().openStream()));
 310  
         }
 311  1
         catch (IOException ex) {
 312  1
             throw new CheckstyleException("Unable to load external resource file " + location, ex);
 313  14
         }
 314  
 
 315  14
         return content;
 316  
     }
 317  
 
 318  
     /**
 319  
      * Checks whether the contents of external configuration resources were changed.
 320  
      * @param resources a set of {@link ExternalResource}.
 321  
      * @return true if the contents of external configuration resources were changed.
 322  
      */
 323  
     private boolean areExternalResourcesChanged(Set<ExternalResource> resources) {
 324  30
         return resources.stream().anyMatch(resource -> {
 325  21
             boolean changed = false;
 326  21
             if (isResourceLocationInCache(resource.location)) {
 327  7
                 final String contentHashSum = resource.contentHashSum;
 328  7
                 final String cachedHashSum = details.getProperty(resource.location);
 329  7
                 if (!cachedHashSum.equals(contentHashSum)) {
 330  1
                     changed = true;
 331  
                 }
 332  7
             }
 333  
             else {
 334  14
                 changed = true;
 335  
             }
 336  21
             return changed;
 337  
         });
 338  
     }
 339  
 
 340  
     /**
 341  
      * Fills cache with a set of {@link ExternalResource}.
 342  
      * If external resource from the set is already in cache, it will be skipped.
 343  
      * @param externalResources a set of {@link ExternalResource}.
 344  
      */
 345  
     private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) {
 346  30
         externalResources.stream()
 347  54
             .filter(resource -> !isResourceLocationInCache(resource.location))
 348  48
             .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
 349  30
     }
 350  
 
 351  
     /**
 352  
      * Checks whether resource location is in cache.
 353  
      * @param location resource location.
 354  
      * @return true if resource location is in cache.
 355  
      */
 356  
     private boolean isResourceLocationInCache(String location) {
 357  45
         final String cachedHashSum = details.getProperty(location);
 358  45
         return cachedHashSum != null;
 359  
     }
 360  
 
 361  
     /**
 362  
      * Class which represents external resource.
 363  
      * @author Andrei Selkin
 364  
      */
 365  95
     private static class ExternalResource {
 366  
         /** Location of resource. */
 367  
         private final String location;
 368  
         /** Hash sum which is calculated based on resource content. */
 369  
         private final String contentHashSum;
 370  
 
 371  
         /**
 372  
          * Creates an instance.
 373  
          * @param location resource location.
 374  
          * @param contentHashSum content hash sum.
 375  
          */
 376  24
         ExternalResource(String location, String contentHashSum) {
 377  24
             this.location = location;
 378  24
             this.contentHashSum = contentHashSum;
 379  24
         }
 380  
     }
 381  
 }