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