View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2018 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      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      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      * @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         configHash = getHashCodeBasedOnObjectContent(config);
118         if (new File(fileName).exists()) {
119             FileInputStream inStream = null;
120             try {
121                 inStream = new FileInputStream(fileName);
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             finally {
130                 Closeables.closeQuietly(inStream);
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      * @throws IOException  when there is a problems with file save
142      */
143     public void persist() throws IOException {
144         final Path directory = Paths.get(fileName).getParent();
145         if (directory != null) {
146             Files.createDirectories(directory);
147         }
148         FileOutputStream out = null;
149         try {
150             out = new FileOutputStream(fileName);
151             details.store(out, null);
152         }
153         finally {
154             flushAndCloseOutStream(out);
155         }
156     }
157 
158     /**
159      * Resets the cache to be empty except for the configuration hash.
160      */
161     public void reset() {
162         details.clear();
163         details.setProperty(CONFIG_HASH_KEY, configHash);
164     }
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         if (stream != null) {
173             Flushables.flush(stream, false);
174         }
175         Closeables.close(stream, false);
176     }
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         final String lastChecked = details.getProperty(uncheckedFileName);
186         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         details.setProperty(checkedFileName, Long.toString(timestamp));
196     }
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         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         details.remove(checkedFileName);
213     }
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             final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
223             // in-memory serialization of Configuration
224             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             final MessageDigest digest = MessageDigest.getInstance("SHA-1");
230             digest.update(outputStream.toByteArray());
231 
232             return BaseEncoding.base16().upperCase().encode(digest.digest());
233         }
234         catch (final IOException | NoSuchAlgorithmException ex) {
235             // rethrow as unchecked exception
236             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         final ObjectOutputStream oos = new ObjectOutputStream(outputStream);
249         try {
250             oos.writeObject(object);
251         }
252         finally {
253             flushAndCloseOutStream(oos);
254         }
255     }
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         final Set<ExternalResource> resources = loadExternalResources(locations);
264         if (areExternalResourcesChanged(resources)) {
265             reset();
266         }
267         fillCacheWithExternalResources(resources);
268     }
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         final Set<ExternalResource> resources = new HashSet<>();
277         for (String location : resourceLocations) {
278             String contentHashSum = null;
279             try {
280                 final byte[] content = loadExternalResource(location);
281                 contentHashSum = getHashCodeBasedOnObjectContent(content);
282             }
283             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                 contentHashSum = getHashCodeBasedOnObjectContent(ex);
289             }
290             finally {
291                 resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
292                         contentHashSum));
293             }
294         }
295         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         final URI uri = CommonUtils.getUriByFilename(location);
307 
308         try {
309             content = ByteStreams.toByteArray(new BufferedInputStream(uri.toURL().openStream()));
310         }
311         catch (IOException ex) {
312             throw new CheckstyleException("Unable to load external resource file " + location, ex);
313         }
314 
315         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         return resources.stream().anyMatch(resource -> {
325             boolean changed = false;
326             if (isResourceLocationInCache(resource.location)) {
327                 final String contentHashSum = resource.contentHashSum;
328                 final String cachedHashSum = details.getProperty(resource.location);
329                 if (!cachedHashSum.equals(contentHashSum)) {
330                     changed = true;
331                 }
332             }
333             else {
334                 changed = true;
335             }
336             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         externalResources.stream()
347             .filter(resource -> !isResourceLocationInCache(resource.location))
348             .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
349     }
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         final String cachedHashSum = details.getProperty(location);
358         return cachedHashSum != null;
359     }
360 
361     /**
362      * Class which represents external resource.
363      * @author Andrei Selkin
364      */
365     private static class ExternalResource {
366 
367         /** Location of resource. */
368         private final String location;
369         /** Hash sum which is calculated based on resource content. */
370         private final String contentHashSum;
371 
372         /**
373          * Creates an instance.
374          * @param location resource location.
375          * @param contentHashSum content hash sum.
376          */
377         ExternalResource(String location, String contentHashSum) {
378             this.location = location;
379             this.contentHashSum = contentHashSum;
380         }
381 
382     }
383 
384 }