Skip to content

Commit f2fb48d

Browse files
committed
SCANJLIB-277 Create a separate library for the download cache
1 parent 3303548 commit f2fb48d

28 files changed

+500
-258
lines changed

download-cache/pom.xml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>org.sonarsource.scanner.lib</groupId>
6+
<artifactId>sonar-scanner-java-library-parent</artifactId>
7+
<version>3.4-SNAPSHOT</version>
8+
</parent>
9+
10+
<artifactId>sonar-scanner-download-cache</artifactId>
11+
<name>SonarScanner Download Cache Utility</name>
12+
<description>Provide the utility classes to store downloaded files in a local cache</description>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>org.slf4j</groupId>
17+
<artifactId>slf4j-api</artifactId>
18+
<version>${slf4j.version}</version>
19+
<!-- provided by the bootstrapper plugin context (Maven, Gradle) -->
20+
<scope>provided</scope>
21+
</dependency>
22+
<dependency>
23+
<groupId>com.google.code.findbugs</groupId>
24+
<artifactId>jsr305</artifactId>
25+
<scope>provided</scope>
26+
</dependency>
27+
28+
<!-- unit tests -->
29+
<dependency>
30+
<groupId>org.junit.jupiter</groupId>
31+
<artifactId>junit-jupiter</artifactId>
32+
<scope>test</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.mockito</groupId>
36+
<artifactId>mockito-core</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.mockito</groupId>
41+
<artifactId>mockito-junit-jupiter</artifactId>
42+
<scope>test</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.assertj</groupId>
46+
<artifactId>assertj-core</artifactId>
47+
<scope>test</scope>
48+
</dependency>
49+
<dependency>
50+
<!-- used to compare results -->
51+
<groupId>commons-codec</groupId>
52+
<artifactId>commons-codec</artifactId>
53+
<scope>test</scope>
54+
</dependency>
55+
<dependency>
56+
<groupId>ch.qos.logback</groupId>
57+
<artifactId>logback-classic</artifactId>
58+
<version>1.2.13</version>
59+
<scope>test</scope>
60+
</dependency>
61+
</dependencies>
62+
63+
</project>

lib/src/main/java/org/sonarsource/scanner/lib/internal/cache/CachedFile.java renamed to download-cache/src/main/java/org/sonarsource/scanner/downloadcache/CachedFile.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SonarScanner Java Library
2+
* SonarScanner Download Cache Utility
33
* Copyright (C) 2011-2025 SonarSource SA
44
* mailto:info AT sonarsource DOT com
55
*
@@ -17,28 +17,31 @@
1717
* along with this program; if not, write to the Free Software Foundation,
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
20-
package org.sonarsource.scanner.lib.internal.cache;
20+
package org.sonarsource.scanner.downloadcache;
2121

2222
import java.nio.file.Path;
23-
import javax.annotation.CheckForNull;
24-
import javax.annotation.Nullable;
2523

24+
/**
25+
* A file from the cache
26+
*/
2627
public class CachedFile {
2728

28-
private final Path pathInCache;
29-
private final Boolean cacheHit;
29+
private final Path path;
30+
private final boolean didCacheHit;
3031

31-
public CachedFile(Path pathInCache, @Nullable Boolean cacheHit) {
32-
this.pathInCache = pathInCache;
33-
this.cacheHit = cacheHit;
32+
public CachedFile(Path path, boolean didCacheHit) {
33+
this.path = path;
34+
this.didCacheHit = didCacheHit;
3435
}
3536

36-
public Path getPathInCache() {
37-
return pathInCache;
37+
public Path getPath() {
38+
return path;
3839
}
3940

40-
@CheckForNull
41-
public Boolean getCacheHit() {
42-
return cacheHit;
41+
/**
42+
* @return true if the file was previously in the cache, false if it was freshly downloaded
43+
*/
44+
public boolean didCacheHit() {
45+
return didCacheHit;
4346
}
4447
}

lib/src/main/java/org/sonarsource/scanner/lib/internal/cache/FileCache.java renamed to download-cache/src/main/java/org/sonarsource/scanner/downloadcache/DownloadCache.java

Lines changed: 50 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SonarScanner Java Library
2+
* SonarScanner Download Cache Utility
33
* Copyright (C) 2011-2025 SonarSource SA
44
* mailto:info AT sonarsource DOT com
55
*
@@ -17,81 +17,72 @@
1717
* along with this program; if not, write to the Free Software Foundation,
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
20-
package org.sonarsource.scanner.lib.internal.cache;
20+
package org.sonarsource.scanner.downloadcache;
2121

2222
import java.io.IOException;
2323
import java.nio.file.AtomicMoveNotSupportedException;
2424
import java.nio.file.FileAlreadyExistsException;
2525
import java.nio.file.Files;
2626
import java.nio.file.Path;
2727
import java.nio.file.StandardCopyOption;
28-
import javax.annotation.CheckForNull;
28+
import java.util.Optional;
2929
import org.slf4j.Logger;
3030
import org.slf4j.LoggerFactory;
3131

3232
/**
33-
* This class is responsible for managing Sonar batch file cache. You can put file into cache and
34-
* later try to retrieve them. The checksum is used to differentiate files (name is not secure as files may come
35-
* from different Sonar servers and have same name but be actually different, and same for SNAPSHOTs).
33+
* This class is responsible for managing Sonar Scanner download cache. You can download files into the cache and
34+
* later try to retrieve them. The file hash is used as the cache index (name is not reliable as files may come
35+
* from different SonarQube servers and have the same name but be actually different).
3636
*/
37-
public class FileCache {
37+
public class DownloadCache {
3838

39-
private static final Logger LOG = LoggerFactory.getLogger(FileCache.class);
39+
private static final Logger LOG = LoggerFactory.getLogger(DownloadCache.class);
4040

41-
private final Path dir;
41+
private final Path baseDir;
4242
private final Path tmpDir;
4343
private final FileHashes hashes;
4444

45-
FileCache(Path dir, FileHashes fileHashes) {
45+
DownloadCache(Path baseDir, FileHashes fileHashes) {
46+
LOG.debug("Download cache base directory: {}", baseDir);
4647
this.hashes = fileHashes;
47-
this.dir = createDir(dir, "user cache: ");
48-
LOG.info("User cache: {}", dir);
49-
this.tmpDir = createDir(dir.resolve("_tmp"), "temp dir");
48+
this.baseDir = mkdirs(baseDir);
49+
this.tmpDir = mkdirs(baseDir.resolve("_tmp"));
5050
}
5151

52-
public static FileCache create(Path sonarUserHome) {
53-
var dir = sonarUserHome.resolve("cache");
54-
return new FileCache(dir, new FileHashes());
52+
public DownloadCache(Path baseDir) {
53+
this(baseDir, new FileHashes());
5554
}
5655

57-
public Path getDir() {
58-
return dir;
56+
public Path getBaseDir() {
57+
return baseDir;
5958
}
6059

6160
/**
62-
* Look for a file in the cache by its filename and checksum. If the file is not
63-
* present then return null.
61+
* Look for a file in the cache by its filename and hash. If the file is not
62+
* present, then return empty.
6463
*/
65-
@CheckForNull
66-
public Path get(String filename, String hash) {
67-
Path cachedFile = dir.resolve(hash).resolve(filename);
64+
public Optional<Path> get(String filename, String hash) {
65+
Path cachedFile = hashDir(hash).resolve(filename);
6866
if (Files.exists(cachedFile)) {
69-
return cachedFile;
67+
return Optional.of(cachedFile);
7068
}
71-
LOG.debug("No file found in the cache with name {} and hash {}", filename, hash);
72-
return null;
73-
}
74-
75-
@FunctionalInterface
76-
public interface Downloader {
77-
void download(String filename, Path toFile) throws IOException;
69+
return Optional.empty();
7870
}
7971

80-
public CachedFile getOrDownload(String filename, String hash, String hashAlgorithm, Downloader downloader) {
72+
public CachedFile getOrDownload(String filename, String expectedFileHash, String hashAlgorithm, Downloader downloader) throws HashMismatchException {
8173
// Does not fail if another process tries to create the directory at the same time.
82-
Path hashDir = hashDir(hash);
74+
Path hashDir = hashDir(expectedFileHash);
8375
Path targetFile = hashDir.resolve(filename);
8476
if (Files.exists(targetFile)) {
8577
return new CachedFile(targetFile, true);
8678
}
87-
Path tempFile = newTempFile();
79+
Path tempFile = newTempFile(filename);
8880
download(downloader, filename, tempFile);
89-
String downloadedHash = hashes.of(tempFile.toFile(), hashAlgorithm);
90-
if (!hash.equals(downloadedHash)) {
91-
throw new HashMismatchException("INVALID HASH: File " + tempFile.toAbsolutePath() + " was expected to have hash " + hash
92-
+ " but was downloaded with hash " + downloadedHash);
81+
String downloadedFileHash = hashes.of(tempFile.toFile(), hashAlgorithm);
82+
if (!expectedFileHash.equals(downloadedFileHash)) {
83+
throw new HashMismatchException(expectedFileHash, downloadedFileHash, tempFile.toAbsolutePath());
9384
}
94-
mkdirQuietly(hashDir);
85+
mkdirs(hashDir);
9586
renameQuietly(tempFile, targetFile);
9687
return new CachedFile(targetFile, false);
9788
}
@@ -113,41 +104,42 @@ private static void renameQuietly(Path sourceFile, Path targetFile) {
113104
try {
114105
Files.move(sourceFile, targetFile);
115106
} catch (IOException e) {
116-
throw new IllegalStateException("Fail to move " + sourceFile.toAbsolutePath() + " to " + targetFile, e);
107+
throw new IllegalStateException("Failed to move " + sourceFile.toAbsolutePath() + " to " + targetFile, e);
117108
}
118109
} catch (FileAlreadyExistsException e) {
119-
// File was probably cached by another process in the mean time
110+
// File was probably cached by another process in the meantime
120111
} catch (IOException e) {
121-
throw new IllegalStateException("Fail to move " + sourceFile.toAbsolutePath() + " to " + targetFile, e);
112+
throw new IllegalStateException("Failed to move " + sourceFile.toAbsolutePath() + " to " + targetFile, e);
122113
}
123114
}
124115

125116
private Path hashDir(String hash) {
126-
return dir.resolve(hash);
117+
return baseDir.resolve(hash);
127118
}
128119

129-
private static void mkdirQuietly(Path hashDir) {
130-
try {
131-
Files.createDirectories(hashDir);
132-
} catch (IOException e) {
133-
throw new IllegalStateException("Fail to create cache directory: " + hashDir, e);
120+
private Path newTempFile(String filename) {
121+
int dotLocation = filename.lastIndexOf(".");
122+
String prefix = filename;
123+
String suffix = null;
124+
if (dotLocation > 0) {
125+
prefix = filename.substring(0, dotLocation);
126+
suffix = filename.substring(dotLocation + 1);
134127
}
135-
}
136-
137-
private Path newTempFile() {
138128
try {
139-
return Files.createTempFile(tmpDir, "fileCache", null);
129+
return Files.createTempFile(tmpDir, prefix, suffix);
140130
} catch (IOException e) {
141131
throw new IllegalStateException("Fail to create temp file in " + tmpDir, e);
142132
}
143133
}
144134

145-
private static Path createDir(Path dir, String debugTitle) {
146-
LOG.debug("Create: {}", dir);
147-
try {
148-
Files.createDirectories(dir);
149-
} catch (IOException e) {
150-
throw new IllegalStateException("Unable to create " + debugTitle + dir.toString(), e);
135+
private static Path mkdirs(Path dir) {
136+
if (!Files.isDirectory(dir)) {
137+
LOG.debug("Create: {}", dir);
138+
try {
139+
Files.createDirectories(dir);
140+
} catch (IOException e) {
141+
throw new IllegalStateException("Unable to create directory: " + dir, e);
142+
}
151143
}
152144
return dir;
153145
}

lib/src/main/java/org/sonarsource/scanner/lib/internal/cache/HashMismatchException.java renamed to download-cache/src/main/java/org/sonarsource/scanner/downloadcache/Downloader.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SonarScanner Java Library
2+
* SonarScanner Download Cache Utility
33
* Copyright (C) 2011-2025 SonarSource SA
44
* mailto:info AT sonarsource DOT com
55
*
@@ -17,11 +17,14 @@
1717
* along with this program; if not, write to the Free Software Foundation,
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
20-
package org.sonarsource.scanner.lib.internal.cache;
20+
package org.sonarsource.scanner.downloadcache;
2121

22-
public class HashMismatchException extends RuntimeException {
22+
import java.io.IOException;
23+
import java.nio.file.Path;
2324

24-
public HashMismatchException(String message) {
25-
super(message);
26-
}
25+
/**
26+
* Provide the logic to download the file when not found in the cache.
27+
*/
28+
public interface Downloader {
29+
void download(String filename, Path destination) throws IOException;
2730
}

lib/src/main/java/org/sonarsource/scanner/lib/internal/cache/FileHashes.java renamed to download-cache/src/main/java/org/sonarsource/scanner/downloadcache/FileHashes.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SonarScanner Java Library
2+
* SonarScanner Download Cache Utility
33
* Copyright (C) 2011-2025 SonarSource SA
44
* mailto:info AT sonarsource DOT com
55
*
@@ -17,7 +17,7 @@
1717
* along with this program; if not, write to the Free Software Foundation,
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
20-
package org.sonarsource.scanner.lib.internal.cache;
20+
package org.sonarsource.scanner.downloadcache;
2121

2222
import java.io.File;
2323
import java.io.FileInputStream;
@@ -28,8 +28,6 @@
2828

2929
/**
3030
* Hashes used to store files in the cache directory.
31-
*
32-
* @since 3.5
3331
*/
3432
class FileHashes {
3533

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* SonarScanner Download Cache Utility
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.scanner.downloadcache;
21+
22+
import java.nio.file.Path;
23+
24+
public class HashMismatchException extends Exception {
25+
26+
private final String expectedFileHash;
27+
private final String downloadedFileHash;
28+
private final Path downloadedFile;
29+
30+
public HashMismatchException(String expectedFileHash, String downloadedFileHash, Path downloadedFile) {
31+
super("Hash mismatch for file " + downloadedFile + ". Expected hash: " + expectedFileHash + ", actual hash: " + downloadedFileHash);
32+
this.expectedFileHash = expectedFileHash;
33+
this.downloadedFileHash = downloadedFileHash;
34+
this.downloadedFile = downloadedFile;
35+
}
36+
37+
public String getExpectedFileHash() {
38+
return expectedFileHash;
39+
}
40+
41+
public Path getDownloadedFile() {
42+
return downloadedFile;
43+
}
44+
45+
public String getDownloadedFileHash() {
46+
return downloadedFileHash;
47+
}
48+
}

0 commit comments

Comments
 (0)