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 }