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.BufferedInputStream;
023import java.io.ByteArrayOutputStream;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.ObjectOutputStream;
028import java.io.OutputStream;
029import java.io.Serializable;
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.Objects;
038import java.util.Properties;
039import java.util.Set;
040
041import com.google.common.io.BaseEncoding;
042import com.google.common.io.ByteStreams;
043import com.google.common.io.Closeables;
044import com.google.common.io.Flushables;
045import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
046import com.puppycrawl.tools.checkstyle.api.Configuration;
047import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
048
049/**
050 * This class maintains a persistent(on file-system) store of the files
051 * that have checked ok(no validation events) and their associated
052 * timestamp. It is used to optimize Checkstyle between few launches.
053 * It is mostly useful for plugin and extensions of Checkstyle.
054 * It uses a property file
055 * for storage.  A hashcode of the Configuration is stored in the
056 * cache file to ensure the cache is invalidated when the
057 * configuration has changed.
058 *
059 */
060public final class PropertyCacheFile {
061
062    /**
063     * The property key to use for storing the hashcode of the
064     * configuration. To avoid name clashes with the files that are
065     * checked the key is chosen in such a way that it cannot be a
066     * valid file name.
067     */
068    public static final String CONFIG_HASH_KEY = "configuration*?";
069
070    /**
071     * The property prefix to use for storing the hashcode of an
072     * external resource. To avoid name clashes with the files that are
073     * checked the prefix is chosen in such a way that it cannot be a
074     * valid file name and makes it clear it is a resource.
075     */
076    public static final String EXTERNAL_RESOURCE_KEY_PREFIX = "module-resource*?:";
077
078    /** The details on files. **/
079    private final Properties details = new Properties();
080
081    /** Configuration object. **/
082    private final Configuration config;
083
084    /** File name of cache. **/
085    private final String fileName;
086
087    /** Generated configuration hash. **/
088    private String configHash;
089
090    /**
091     * Creates a new {@code PropertyCacheFile} instance.
092     *
093     * @param config the current configuration, not null
094     * @param fileName the cache file
095     */
096    public PropertyCacheFile(Configuration config, String fileName) {
097        if (config == null) {
098            throw new IllegalArgumentException("config can not be null");
099        }
100        if (fileName == null) {
101            throw new IllegalArgumentException("fileName can not be null");
102        }
103        this.config = config;
104        this.fileName = fileName;
105    }
106
107    /**
108     * Load cached values from file.
109     * @throws IOException when there is a problems with file read
110     */
111    public void load() throws IOException {
112        // get the current config so if the file isn't found
113        // the first time the hash will be added to output file
114        configHash = getHashCodeBasedOnObjectContent(config);
115        final File file = new File(fileName);
116        if (file.exists()) {
117            try (InputStream inStream = Files.newInputStream(file.toPath())) {
118                details.load(inStream);
119                final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY);
120                if (!configHash.equals(cachedConfigHash)) {
121                    // Detected configuration change - clear cache
122                    reset();
123                }
124            }
125        }
126        else {
127            // put the hash in the file if the file is going to be created
128            reset();
129        }
130    }
131
132    /**
133     * Cleans up the object and updates the cache file.
134     * @throws IOException  when there is a problems with file save
135     */
136    public void persist() throws IOException {
137        final Path path = Paths.get(fileName);
138        final Path directory = path.getParent();
139        if (directory != null) {
140            Files.createDirectories(directory);
141        }
142        OutputStream out = null;
143        try {
144            out = Files.newOutputStream(path);
145            details.store(out, null);
146        }
147        finally {
148            flushAndCloseOutStream(out);
149        }
150    }
151
152    /**
153     * Resets the cache to be empty except for the configuration hash.
154     */
155    public void reset() {
156        details.clear();
157        details.setProperty(CONFIG_HASH_KEY, configHash);
158    }
159
160    /**
161     * Flushes and closes output stream.
162     * @param stream the output stream
163     * @throws IOException  when there is a problems with file flush and close
164     */
165    private static void flushAndCloseOutStream(OutputStream stream) throws IOException {
166        if (stream != null) {
167            Flushables.flush(stream, false);
168        }
169        Closeables.close(stream, false);
170    }
171
172    /**
173     * Checks that file is in cache.
174     * @param uncheckedFileName the file to check
175     * @param timestamp the timestamp of the file to check
176     * @return whether the specified file has already been checked ok
177     */
178    public boolean isInCache(String uncheckedFileName, long timestamp) {
179        final String lastChecked = details.getProperty(uncheckedFileName);
180        return Objects.equals(lastChecked, Long.toString(timestamp));
181    }
182
183    /**
184     * Records that a file checked ok.
185     * @param checkedFileName name of the file that checked ok
186     * @param timestamp the timestamp of the file
187     */
188    public void put(String checkedFileName, long timestamp) {
189        details.setProperty(checkedFileName, Long.toString(timestamp));
190    }
191
192    /**
193     * Retrieves the hash of a specific file.
194     * @param name The name of the file to retrieve.
195     * @return The has of the file or {@code null}.
196     */
197    public String get(String name) {
198        return details.getProperty(name);
199    }
200
201    /**
202     * Removed a specific file from the cache.
203     * @param checkedFileName The name of the file to remove.
204     */
205    public void remove(String checkedFileName) {
206        details.remove(checkedFileName);
207    }
208
209    /**
210     * Calculates the hashcode for the serializable object based on its content.
211     * @param object serializable object.
212     * @return the hashcode for serializable object.
213     */
214    private static String getHashCodeBasedOnObjectContent(Serializable object) {
215        try {
216            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
217            // in-memory serialization of Configuration
218            serialize(object, outputStream);
219            // Instead of hexEncoding outputStream.toByteArray() directly we
220            // use a message digest here to keep the length of the
221            // hashcode reasonable
222
223            final MessageDigest digest = MessageDigest.getInstance("SHA-1");
224            digest.update(outputStream.toByteArray());
225
226            return BaseEncoding.base16().upperCase().encode(digest.digest());
227        }
228        catch (final IOException | NoSuchAlgorithmException ex) {
229            // rethrow as unchecked exception
230            throw new IllegalStateException("Unable to calculate hashcode.", ex);
231        }
232    }
233
234    /**
235     * Serializes object to output stream.
236     * @param object object to be serialized
237     * @param outputStream serialization stream
238     * @throws IOException if an error occurs
239     */
240    private static void serialize(Serializable object,
241                                  OutputStream outputStream) throws IOException {
242        final ObjectOutputStream oos = new ObjectOutputStream(outputStream);
243        try {
244            oos.writeObject(object);
245        }
246        finally {
247            flushAndCloseOutStream(oos);
248        }
249    }
250
251    /**
252     * Puts external resources in cache.
253     * If at least one external resource changed, clears the cache.
254     * @param locations locations of external resources.
255     */
256    public void putExternalResources(Set<String> locations) {
257        final Set<ExternalResource> resources = loadExternalResources(locations);
258        if (areExternalResourcesChanged(resources)) {
259            reset();
260            fillCacheWithExternalResources(resources);
261        }
262    }
263
264    /**
265     * Loads a set of {@link ExternalResource} based on their locations.
266     * @param resourceLocations locations of external configuration resources.
267     * @return a set of {@link ExternalResource}.
268     */
269    private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) {
270        final Set<ExternalResource> resources = new HashSet<>();
271        for (String location : resourceLocations) {
272            try {
273                final byte[] content = loadExternalResource(location);
274                final String contentHashSum = getHashCodeBasedOnObjectContent(content);
275                resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
276                        contentHashSum));
277            }
278            catch (CheckstyleException ex) {
279                // if exception happened (configuration resource was not found, connection is not
280                // available, resource is broken, etc), we need to calculate hash sum based on
281                // exception object content in order to check whether problem is resolved later
282                // and/or the configuration is changed.
283                final String contentHashSum = getHashCodeBasedOnObjectContent(ex);
284                resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
285                        contentHashSum));
286            }
287        }
288        return resources;
289    }
290
291    /**
292     * Loads the content of external resource.
293     * @param location external resource location.
294     * @return array of bytes which represents the content of external resource in binary form.
295     * @throws CheckstyleException if error while loading occurs.
296     */
297    private static byte[] loadExternalResource(String location) throws CheckstyleException {
298        final byte[] content;
299        final URI uri = CommonUtil.getUriByFilename(location);
300
301        try {
302            content = ByteStreams.toByteArray(new BufferedInputStream(uri.toURL().openStream()));
303        }
304        catch (IOException ex) {
305            throw new CheckstyleException("Unable to load external resource file " + location, ex);
306        }
307
308        return content;
309    }
310
311    /**
312     * Checks whether the contents of external configuration resources were changed.
313     * @param resources a set of {@link ExternalResource}.
314     * @return true if the contents of external configuration resources were changed.
315     */
316    private boolean areExternalResourcesChanged(Set<ExternalResource> resources) {
317        return resources.stream().anyMatch(resource -> {
318            boolean changed = false;
319            if (isResourceLocationInCache(resource.location)) {
320                final String contentHashSum = resource.contentHashSum;
321                final String cachedHashSum = details.getProperty(resource.location);
322                if (!cachedHashSum.equals(contentHashSum)) {
323                    changed = true;
324                }
325            }
326            else {
327                changed = true;
328            }
329            return changed;
330        });
331    }
332
333    /**
334     * Fills cache with a set of {@link ExternalResource}.
335     * If external resource from the set is already in cache, it will be skipped.
336     * @param externalResources a set of {@link ExternalResource}.
337     */
338    private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) {
339        externalResources
340            .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
341    }
342
343    /**
344     * Checks whether resource location is in cache.
345     * @param location resource location.
346     * @return true if resource location is in cache.
347     */
348    private boolean isResourceLocationInCache(String location) {
349        final String cachedHashSum = details.getProperty(location);
350        return cachedHashSum != null;
351    }
352
353    /**
354     * Class which represents external resource.
355     */
356    private static class ExternalResource {
357
358        /** Location of resource. */
359        private final String location;
360        /** Hash sum which is calculated based on resource content. */
361        private final String contentHashSum;
362
363        /**
364         * Creates an instance.
365         * @param location resource location.
366         * @param contentHashSum content hash sum.
367         */
368        ExternalResource(String location, String contentHashSum) {
369            this.location = location;
370            this.contentHashSum = contentHashSum;
371        }
372
373    }
374
375}