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.ByteArrayOutputStream;
023import java.io.File;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.ObjectOutputStream;
027import java.io.OutputStream;
028import java.io.Serializable;
029import java.math.BigInteger;
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.Locale;
038import java.util.Objects;
039import java.util.Properties;
040import java.util.Set;
041
042import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
043import com.puppycrawl.tools.checkstyle.api.Configuration;
044import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
045
046/**
047 * This class maintains a persistent(on file-system) store of the files
048 * that have checked ok(no validation events) and their associated
049 * timestamp. It is used to optimize Checkstyle between few launches.
050 * It is mostly useful for plugin and extensions of Checkstyle.
051 * It uses a property file
052 * for storage.  A hashcode of the Configuration is stored in the
053 * cache file to ensure the cache is invalidated when the
054 * configuration has changed.
055 *
056 */
057public final class PropertyCacheFile {
058
059    /**
060     * The property key to use for storing the hashcode of the
061     * configuration. To avoid name clashes with the files that are
062     * checked the key is chosen in such a way that it cannot be a
063     * valid file name.
064     */
065    public static final String CONFIG_HASH_KEY = "configuration*?";
066
067    /**
068     * The property prefix to use for storing the hashcode of an
069     * external resource. To avoid name clashes with the files that are
070     * checked the prefix is chosen in such a way that it cannot be a
071     * valid file name and makes it clear it is a resource.
072     */
073    public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:";
074
075    /** Size of default byte array for buffer. */
076    private static final int BUFFER_SIZE = 1024;
077
078    /** Default buffer for reading from streams. */
079    private static final byte[] BUFFER = new byte[BUFFER_SIZE];
080
081    /** Default number for base 16 encoding. */
082    private static final int BASE_16 = 16;
083
084    /** The details on files. **/
085    private final Properties details = new Properties();
086
087    /** Configuration object. **/
088    private final Configuration config;
089
090    /** File name of cache. **/
091    private final String fileName;
092
093    /** Generated configuration hash. **/
094    private String configHash;
095
096    /**
097     * Creates a new {@code PropertyCacheFile} instance.
098     *
099     * @param config the current configuration, not null
100     * @param fileName the cache file
101     */
102    public PropertyCacheFile(Configuration config, String fileName) {
103        if (config == null) {
104            throw new IllegalArgumentException("config can not be null");
105        }
106        if (fileName == null) {
107            throw new IllegalArgumentException("fileName can not be null");
108        }
109        this.config = config;
110        this.fileName = fileName;
111    }
112
113    /**
114     * Load cached values from file.
115     * @throws IOException when there is a problems with file read
116     */
117    public void load() throws IOException {
118        // get the current config so if the file isn't found
119        // the first time the hash will be added to output file
120        configHash = getHashCodeBasedOnObjectContent(config);
121        final File file = new File(fileName);
122        if (file.exists()) {
123            try (InputStream inStream = Files.newInputStream(file.toPath())) {
124                details.load(inStream);
125                final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY);
126                if (!configHash.equals(cachedConfigHash)) {
127                    // Detected configuration change - clear cache
128                    reset();
129                }
130            }
131        }
132        else {
133            // put the hash in the file if the file is going to be created
134            reset();
135        }
136    }
137
138    /**
139     * Cleans up the object and updates the cache file.
140     * @throws IOException  when there is a problems with file save
141     */
142    public void persist() throws IOException {
143        final Path path = Paths.get(fileName);
144        final Path directory = path.getParent();
145        if (directory != null) {
146            Files.createDirectories(directory);
147        }
148        OutputStream out = null;
149        try {
150            out = Files.newOutputStream(path);
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            stream.flush();
174            stream.close();
175        }
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 new BigInteger(1, digest.digest()).toString(BASE_16).toUpperCase(Locale.ROOT);
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            fillCacheWithExternalResources(resources);
267        }
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            try {
279                final byte[] content = loadExternalResource(location);
280                final String contentHashSum = getHashCodeBasedOnObjectContent(content);
281                resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
282                        contentHashSum));
283            }
284            catch (CheckstyleException | IOException ex) {
285                // if exception happened (configuration resource was not found, connection is not
286                // available, resource is broken, etc), we need to calculate hash sum based on
287                // exception object content in order to check whether problem is resolved later
288                // and/or the configuration is changed.
289                final String contentHashSum = getHashCodeBasedOnObjectContent(ex);
290                resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
291                        contentHashSum));
292            }
293        }
294        return resources;
295    }
296
297    /**
298     * Loads the content of external resource.
299     * @param location external resource location.
300     * @return array of bytes which represents the content of external resource in binary form.
301     * @throws IOException if error while loading occurs.
302     * @throws CheckstyleException if error while loading occurs.
303     */
304    private static byte[] loadExternalResource(String location)
305            throws IOException, CheckstyleException {
306        final URI uri = CommonUtil.getUriByFilename(location);
307
308        try (InputStream is = uri.toURL().openStream()) {
309            return toByteArray(is);
310        }
311    }
312
313    /**
314     * Reads all the contents of an input stream and returns it as a byte array.
315     * @param stream The input stream to read from.
316     * @return The resulting byte array of the stream.
317     * @throws IOException if there is an error reading the input stream.
318     */
319    private static byte[] toByteArray(InputStream stream) throws IOException {
320        final ByteArrayOutputStream content = new ByteArrayOutputStream();
321
322        while (true) {
323            final int size = stream.read(BUFFER);
324            if (size == -1) {
325                break;
326            }
327
328            content.write(BUFFER, 0, size);
329        }
330
331        return content.toByteArray();
332    }
333
334    /**
335     * Checks whether the contents of external configuration resources were changed.
336     * @param resources a set of {@link ExternalResource}.
337     * @return true if the contents of external configuration resources were changed.
338     */
339    private boolean areExternalResourcesChanged(Set<ExternalResource> resources) {
340        return resources.stream().anyMatch(resource -> {
341            boolean changed = false;
342            if (isResourceLocationInCache(resource.location)) {
343                final String contentHashSum = resource.contentHashSum;
344                final String cachedHashSum = details.getProperty(resource.location);
345                if (!cachedHashSum.equals(contentHashSum)) {
346                    changed = true;
347                }
348            }
349            else {
350                changed = true;
351            }
352            return changed;
353        });
354    }
355
356    /**
357     * Fills cache with a set of {@link ExternalResource}.
358     * If external resource from the set is already in cache, it will be skipped.
359     * @param externalResources a set of {@link ExternalResource}.
360     */
361    private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) {
362        externalResources
363            .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
364    }
365
366    /**
367     * Checks whether resource location is in cache.
368     * @param location resource location.
369     * @return true if resource location is in cache.
370     */
371    private boolean isResourceLocationInCache(String location) {
372        final String cachedHashSum = details.getProperty(location);
373        return cachedHashSum != null;
374    }
375
376    /**
377     * Class which represents external resource.
378     */
379    private static class ExternalResource {
380
381        /** Location of resource. */
382        private final String location;
383        /** Hash sum which is calculated based on resource content. */
384        private final String contentHashSum;
385
386        /**
387         * Creates an instance.
388         * @param location resource location.
389         * @param contentHashSum content hash sum.
390         */
391        /* package */ ExternalResource(String location, String contentHashSum) {
392            this.location = location;
393            this.contentHashSum = contentHashSum;
394        }
395
396    }
397
398}