View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2015 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  package com.puppycrawl.tools.checkstyle;
20  
21  import java.io.FileInputStream;
22  import java.io.FileNotFoundException;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.ByteArrayOutputStream;
26  import java.io.ObjectOutputStream;
27  import java.io.OutputStream;
28  import java.io.Serializable;
29  import java.util.Properties;
30  import java.security.MessageDigest;
31  
32  
33  import com.puppycrawl.tools.checkstyle.api.Configuration;
34  import com.puppycrawl.tools.checkstyle.api.Utils;
35  
36  /**
37   * This class maintains a persistent(on file-system) store of the files
38   * that have checked ok(no validation events) and their associated
39   * timestamp. It is used to optimize Checkstyle between few launches.
40   * It is mostly useful for plugin and extensions of Checkstyle.
41   * It uses a property file
42   * for storage.  A hashcode of the Configuration is stored in the
43   * cache file to ensure the cache is invalidated when the
44   * configuration has changed.
45   *
46   * @author Oliver Burn
47   */
48  final class PropertyCacheFile
49  {
50      /**
51       * The property key to use for storing the hashcode of the
52       * configuration. To avoid nameclashes with the files that are
53       * checked the key is chosen in such a way that it cannot be a
54       * valid file name.
55       */
56      private static final String CONFIG_HASH_KEY = "configuration*?";
57  
58      /** name of file to store details **/
59      private final String detailsFile;
60      /** the details on files **/
61      private final Properties details = new Properties();
62  
63      /**
64       * Creates a new <code>PropertyCacheFile</code> instance.
65       *
66       * @param currentConfig the current configuration, not null
67       * @param fileName the cache file
68       */
69      PropertyCacheFile(Configuration currentConfig, String fileName)
70      {
71          boolean setInActive = true;
72          if (fileName != null) {
73              FileInputStream inStream = null;
74              // get the current config so if the file isn't found
75              // the first time the hash will be added to output file
76              final String currentConfigHash = getConfigHashCode(currentConfig);
77              try {
78                  inStream = new FileInputStream(fileName);
79                  details.load(inStream);
80                  final String cachedConfigHash =
81                      details.getProperty(CONFIG_HASH_KEY);
82                  setInActive = false;
83                  if (cachedConfigHash == null
84                      || !cachedConfigHash.equals(currentConfigHash))
85                  {
86                      // Detected configuration change - clear cache
87                      details.clear();
88                      details.put(CONFIG_HASH_KEY, currentConfigHash);
89                  }
90              }
91              catch (final FileNotFoundException e) {
92                  // Ignore, the cache does not exist
93                  setInActive = false;
94                  // put the hash in the file if the file is going to be created
95                  details.put(CONFIG_HASH_KEY, currentConfigHash);
96              }
97              catch (final IOException e) {
98                  Utils.getExceptionLogger()
99                      .debug("Unable to open cache file, ignoring.", e);
100             }
101             finally {
102                 Utils.closeQuietly(inStream);
103             }
104         }
105         detailsFile = setInActive ? null : fileName;
106     }
107 
108     /** Cleans up the object and updates the cache file. **/
109     void destroy()
110     {
111         if (detailsFile != null) {
112             FileOutputStream out = null;
113             try {
114                 out = new FileOutputStream(detailsFile);
115                 details.store(out, null);
116             }
117             catch (final IOException e) {
118                 Utils.getExceptionLogger()
119                     .debug("Unable to save cache file.", e);
120             }
121             finally {
122                 this.flushAndCloseOutStream(out);
123             }
124         }
125     }
126 
127     /**
128      * Flushes and closes output stream.
129      * @param stream the output stream
130      */
131     private void flushAndCloseOutStream(OutputStream stream)
132     {
133         if (stream != null) {
134             try {
135                 stream.flush();
136             }
137             catch (final IOException ex) {
138                 Utils.getExceptionLogger()
139                     .debug("Unable to flush output stream.", ex);
140             }
141             finally {
142                 Utils.closeQuietly(stream);
143             }
144         }
145     }
146 
147     /**
148      * @return whether the specified file has already been checked ok
149      * @param fileName the file to check
150      * @param timestamp the timestamp of the file to check
151      */
152     boolean alreadyChecked(String fileName, long timestamp)
153     {
154         final String lastChecked = details.getProperty(fileName);
155         return lastChecked != null
156             && lastChecked.equals(Long.toString(timestamp));
157     }
158 
159     /**
160      * Records that a file checked ok.
161      * @param fileName name of the file that checked ok
162      * @param timestamp the timestamp of the file
163      */
164     void checkedOk(String fileName, long timestamp)
165     {
166         details.put(fileName, Long.toString(timestamp));
167     }
168 
169     /**
170      * Calculates the hashcode for a GlobalProperties.
171      *
172      * @param configuration the GlobalProperties
173      * @return the hashcode for <code>configuration</code>
174      */
175     private String getConfigHashCode(Serializable configuration)
176     {
177         try {
178             // im-memory serialization of Configuration
179 
180             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
181             ObjectOutputStream oos = null;
182             try {
183                 oos = new ObjectOutputStream(baos);
184                 oos.writeObject(configuration);
185             }
186             finally {
187                 this.flushAndCloseOutStream(oos);
188             }
189 
190             // Instead of hexEncoding baos.toByteArray() directly we
191             // use a message digest here to keep the length of the
192             // hashcode reasonable
193 
194             final MessageDigest md = MessageDigest.getInstance("SHA");
195             md.update(baos.toByteArray());
196 
197             return hexEncode(md.digest());
198         }
199         catch (final Exception ex) { // IO, NoSuchAlgorithm
200             Utils.getExceptionLogger()
201                 .debug("Unable to calculate hashcode.", ex);
202             return "ALWAYS FRESH: " + System.currentTimeMillis();
203         }
204     }
205 
206     /** hex digits */
207     private static final char[] HEX_CHARS = {
208         '0', '1', '2', '3', '4', '5', '6', '7',
209         '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
210     };
211 
212     /** mask for last byte */
213     private static final int MASK_0X0F = 0x0F;
214 
215     /** bit shift */
216     private static final int SHIFT_4 = 4;
217 
218     /**
219      * Hex-encodes a byte array.
220      * @param byteArray the byte array
221      * @return hex encoding of <code>byteArray</code>
222      */
223     private static String hexEncode(byte[] byteArray)
224     {
225         final StringBuilder buf = new StringBuilder(2 * byteArray.length);
226         for (final byte b : byteArray) {
227             final int low = b & MASK_0X0F;
228             final int high = b >> SHIFT_4 & MASK_0X0F;
229             buf.append(HEX_CHARS[high]);
230             buf.append(HEX_CHARS[low]);
231         }
232         return buf.toString();
233     }
234 }