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