001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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;
044import com.puppycrawl.tools.checkstyle.utils.OsSpecificUtil;
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     * @throws IllegalArgumentException when either arguments are null
102     */
103    public PropertyCacheFile(Configuration config, String fileName) {
104        if (config == null) {
105            throw new IllegalArgumentException("config can not be null");
106        }
107        if (fileName == null) {
108            throw new IllegalArgumentException("fileName can not be null");
109        }
110        this.config = config;
111        this.fileName = fileName;
112    }
113
114    /**
115     * Load cached values from file.
116     *
117     * @throws IOException when there is a problems with file read
118     */
119    public void load() throws IOException {
120        // get the current config so if the file isn't found
121        // the first time the hash will be added to output file
122        configHash = getHashCodeBasedOnObjectContent(config);
123        final Path path = Path.of(fileName);
124        if (Files.exists(path)) {
125            try (InputStream inStream = Files.newInputStream(path)) {
126                details.load(inStream);
127                final String cachedConfigHash = details.getProperty(CONFIG_HASH_KEY);
128                if (!configHash.equals(cachedConfigHash)) {
129                    // Detected configuration change - clear cache
130                    reset();
131                }
132            }
133        }
134        else {
135            // put the hash in the file if the file is going to be created
136            reset();
137        }
138    }
139
140    /**
141     * Cleans up the object and updates the cache file.
142     *
143     * @throws IOException  when there is a problems with file save
144     */
145    public void persist() throws IOException {
146        final Path path = Paths.get(fileName);
147        final Path directory = path.getParent();
148
149        if (directory != null) {
150            OsSpecificUtil.updateDirectory(directory);
151        }
152        try (OutputStream out = Files.newOutputStream(path)) {
153            details.store(out, null);
154        }
155    }
156
157    /**
158     * Resets the cache to be empty except for the configuration hash.
159     */
160    public void reset() {
161        details.clear();
162        details.setProperty(CONFIG_HASH_KEY, configHash);
163    }
164
165    /**
166     * Checks that file is in cache.
167     *
168     * @param uncheckedFileName the file to check
169     * @param timestamp the timestamp of the file to check
170     * @return whether the specified file has already been checked ok
171     */
172    public boolean isInCache(String uncheckedFileName, long timestamp) {
173        final String lastChecked = details.getProperty(uncheckedFileName);
174        return Objects.equals(lastChecked, Long.toString(timestamp));
175    }
176
177    /**
178     * Records that a file checked ok.
179     *
180     * @param checkedFileName name of the file that checked ok
181     * @param timestamp the timestamp of the file
182     */
183    public void put(String checkedFileName, long timestamp) {
184        details.setProperty(checkedFileName, Long.toString(timestamp));
185    }
186
187    /**
188     * Retrieves the hash of a specific file.
189     *
190     * @param name The name of the file to retrieve.
191     * @return The has of the file or {@code null}.
192     */
193    public String get(String name) {
194        return details.getProperty(name);
195    }
196
197    /**
198     * Removed a specific file from the cache.
199     *
200     * @param checkedFileName The name of the file to remove.
201     */
202    public void remove(String checkedFileName) {
203        details.remove(checkedFileName);
204    }
205
206    /**
207     * Calculates the hashcode for the serializable object based on its content.
208     *
209     * @param object serializable object.
210     * @return the hashcode for serializable object.
211     * @throws IllegalStateException when some unexpected happened.
212     */
213    private static String getHashCodeBasedOnObjectContent(Serializable object) {
214        try {
215            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
216            // in-memory serialization of Configuration
217            serialize(object, outputStream);
218            // Instead of hexEncoding outputStream.toByteArray() directly we
219            // use a message digest here to keep the length of the
220            // hashcode reasonable
221
222            final MessageDigest digest = MessageDigest.getInstance("SHA-1");
223            digest.update(outputStream.toByteArray());
224
225            return new BigInteger(1, digest.digest()).toString(BASE_16).toUpperCase(Locale.ROOT);
226        }
227        catch (final IOException | NoSuchAlgorithmException ex) {
228            // rethrow as unchecked exception
229            throw new IllegalStateException("Unable to calculate hashcode.", ex);
230        }
231    }
232
233    /**
234     * Serializes object to output stream.
235     *
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        try (ObjectOutputStream oos = new ObjectOutputStream(outputStream)) {
243            oos.writeObject(object);
244        }
245    }
246
247    /**
248     * Puts external resources in cache.
249     * If at least one external resource changed, clears the cache.
250     *
251     * @param locations locations of external resources.
252     */
253    public void putExternalResources(Set<String> locations) {
254        final Set<ExternalResource> resources = loadExternalResources(locations);
255        if (areExternalResourcesChanged(resources)) {
256            reset();
257            fillCacheWithExternalResources(resources);
258        }
259    }
260
261    /**
262     * Loads a set of {@link ExternalResource} based on their locations.
263     *
264     * @param resourceLocations locations of external configuration resources.
265     * @return a set of {@link ExternalResource}.
266     */
267    private static Set<ExternalResource> loadExternalResources(Set<String> resourceLocations) {
268        final Set<ExternalResource> resources = new HashSet<>();
269        for (String location : resourceLocations) {
270            try {
271                final byte[] content = loadExternalResource(location);
272                final String contentHashSum = getHashCodeBasedOnObjectContent(content);
273                resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
274                        contentHashSum));
275            }
276            catch (CheckstyleException | IOException ex) {
277                // if exception happened (configuration resource was not found, connection is not
278                // available, resource is broken, etc.), we need to calculate hash sum based on
279                // exception object content in order to check whether problem is resolved later
280                // and/or the configuration is changed.
281                final String contentHashSum = getHashCodeBasedOnObjectContent(ex);
282                resources.add(new ExternalResource(EXTERNAL_RESOURCE_KEY_PREFIX + location,
283                        contentHashSum));
284            }
285        }
286        return resources;
287    }
288
289    /**
290     * Loads the content of external resource.
291     *
292     * @param location external resource location.
293     * @return array of bytes which represents the content of external resource in binary form.
294     * @throws IOException if error while loading occurs.
295     * @throws CheckstyleException if error while loading occurs.
296     */
297    private static byte[] loadExternalResource(String location)
298            throws IOException, CheckstyleException {
299        final URI uri = CommonUtil.getUriByFilename(location);
300
301        try (InputStream is = uri.toURL().openStream()) {
302            return toByteArray(is);
303        }
304    }
305
306    /**
307     * Reads all the contents of an input stream and returns it as a byte array.
308     *
309     * @param stream The input stream to read from.
310     * @return The resulting byte array of the stream.
311     * @throws IOException if there is an error reading the input stream.
312     */
313    private static byte[] toByteArray(InputStream stream) throws IOException {
314        final ByteArrayOutputStream content = new ByteArrayOutputStream();
315
316        while (true) {
317            final int size = stream.read(BUFFER);
318            if (size == -1) {
319                break;
320            }
321
322            content.write(BUFFER, 0, size);
323        }
324
325        return content.toByteArray();
326    }
327
328    /**
329     * Checks whether the contents of external configuration resources were changed.
330     *
331     * @param resources a set of {@link ExternalResource}.
332     * @return true if the contents of external configuration resources were changed.
333     */
334    private boolean areExternalResourcesChanged(Set<ExternalResource> resources) {
335        return resources.stream().anyMatch(this::isResourceChanged);
336    }
337
338    /**
339     * Checks whether the resource is changed.
340     *
341     * @param resource resource to check.
342     * @return true if resource is changed.
343     */
344    private boolean isResourceChanged(ExternalResource resource) {
345        boolean changed = false;
346        if (isResourceLocationInCache(resource.location)) {
347            final String contentHashSum = resource.contentHashSum;
348            final String cachedHashSum = details.getProperty(resource.location);
349            if (!cachedHashSum.equals(contentHashSum)) {
350                changed = true;
351            }
352        }
353        else {
354            changed = true;
355        }
356        return changed;
357    }
358
359    /**
360     * Fills cache with a set of {@link ExternalResource}.
361     * If external resource from the set is already in cache, it will be skipped.
362     *
363     * @param externalResources a set of {@link ExternalResource}.
364     */
365    private void fillCacheWithExternalResources(Set<ExternalResource> externalResources) {
366        externalResources
367            .forEach(resource -> details.setProperty(resource.location, resource.contentHashSum));
368    }
369
370    /**
371     * Checks whether resource location is in cache.
372     *
373     * @param location resource location.
374     * @return true if resource location is in cache.
375     */
376    private boolean isResourceLocationInCache(String location) {
377        final String cachedHashSum = details.getProperty(location);
378        return cachedHashSum != null;
379    }
380
381    /**
382     * Class which represents external resource.
383     */
384    private static final class ExternalResource {
385
386        /** Location of resource. */
387        private final String location;
388        /** Hash sum which is calculated based on resource content. */
389        private final String contentHashSum;
390
391        /**
392         * Creates an instance.
393         *
394         * @param location resource location.
395         * @param contentHashSum content hash sum.
396         */
397        private ExternalResource(String location, String contentHashSum) {
398            this.location = location;
399            this.contentHashSum = contentHashSum;
400        }
401
402    }
403
404}