• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 20815527833

08 Jan 2026 11:38AM UTC coverage: 69.937% (+0.04%) from 69.902%
20815527833

Pull #1671

github

web-flow
Merge 279ae0013 into b9e7b47d8
Pull Request #1671: #1602: Being offline can block ide startup

3989 of 6280 branches covered (63.52%)

Branch coverage included in aggregate %.

10202 of 14011 relevant lines covered (72.81%)

3.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.24
cli/src/main/java/com/devonfw/tools/ide/tool/repository/AbstractToolRepository.java
1
package com.devonfw.tools.ide.tool.repository;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.ArrayList;
6
import java.util.Collection;
7
import java.util.Collections;
8
import java.util.List;
9
import java.util.Set;
10

11
import com.devonfw.tools.ide.cli.CliException;
12
import com.devonfw.tools.ide.cli.CliOfflineException;
13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.log.IdeLogLevel;
15
import com.devonfw.tools.ide.os.OperatingSystem;
16
import com.devonfw.tools.ide.os.SystemArchitecture;
17
import com.devonfw.tools.ide.tool.ToolCommandlet;
18
import com.devonfw.tools.ide.url.model.file.UrlChecksums;
19
import com.devonfw.tools.ide.url.model.file.UrlDownloadFileMetadata;
20
import com.devonfw.tools.ide.url.model.file.UrlGenericChecksum;
21
import com.devonfw.tools.ide.url.model.file.json.ToolDependencies;
22
import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
23
import com.devonfw.tools.ide.url.model.file.json.ToolSecurity;
24
import com.devonfw.tools.ide.url.model.folder.UrlEdition;
25
import com.devonfw.tools.ide.url.model.folder.UrlTool;
26
import com.devonfw.tools.ide.util.FilenameUtil;
27
import com.devonfw.tools.ide.version.GenericVersionRange;
28
import com.devonfw.tools.ide.version.VersionIdentifier;
29

30
/**
31
 * Abstract base implementation of {@link ToolRepository}.
32
 */
33
public abstract class AbstractToolRepository implements ToolRepository {
34

35
  private static final int MAX_TEMP_DOWNLOADS = 9;
36

37
  /** The owning {@link IdeContext}. */
38
  protected final IdeContext context;
39

40
  /**
41
   * The constructor.
42
   *
43
   * @param context the owning {@link IdeContext}.
44
   */
45
  public AbstractToolRepository(IdeContext context) {
46

47
    super();
2✔
48
    this.context = context;
3✔
49
  }
1✔
50

51
  /**
52
   * @param tool the name of the tool to download.
53
   * @param edition the edition of the tool to download.
54
   * @param version the {@link VersionIdentifier} to download.
55
   * @param toolCommandlet the {@link ToolCommandlet}.
56
   * @return the resolved {@link UrlDownloadFileMetadata}.
57
   */
58
  protected abstract UrlDownloadFileMetadata getMetadata(String tool, String edition, VersionIdentifier version, ToolCommandlet toolCommandlet);
59

60

61
  @Override
62
  public Path download(String tool, String edition, VersionIdentifier version, ToolCommandlet toolCommandlet) {
63

64
    UrlDownloadFileMetadata metadata = getMetadata(tool, edition, version, toolCommandlet);
×
65
    return download(metadata);
×
66
  }
67

68
  /**
69
   * @param metadata the {@link UrlDownloadFileMetadata}.
70
   * @return the {@link Path} to the downloaded file.
71
   */
72
  public Path download(UrlDownloadFileMetadata metadata) {
73

74
    Set<String> urlCollection = metadata.getUrls();
3✔
75
    if (urlCollection.isEmpty()) {
3!
76
      throw new IllegalStateException("Invalid download metadata with empty urls file for " + metadata);
×
77
    }
78
    return doDownload(metadata);
4✔
79
  }
80

81
  /**
82
   * @param metadata the {@link UrlDownloadFileMetadata}.
83
   * @return the {@link Path} to the downloaded file.
84
   */
85
  protected Path doDownload(UrlDownloadFileMetadata metadata) {
86
    String downloadFilename = createDownloadFilename(metadata.getTool(), metadata.getEdition(), metadata.getVersion(), metadata.getOs(),
12✔
87
        metadata.getArch(), metadata.getUrls().iterator().next());
6✔
88
    Path downloadCache = this.context.getDownloadPath().resolve(getId());
7✔
89
    this.context.getFileAccess().mkdirs(downloadCache);
5✔
90
    Path target = downloadCache.resolve(downloadFilename);
4✔
91
    
92
    if (Files.exists(target)) {
5✔
93
      // File is already cached
94
      if (this.context.getNetworkStatus().isOffline()) {
5!
95
        this.context.debug("Using cached download of {} in version {} from {} (offline mode)", 
10✔
96
            metadata.getTool(), metadata.getVersion(), target);
11✔
97
      } else {
98
        this.context.interaction("Artifact already exists at {}\nTo force update please delete the file and run again.", target);
×
99
      }
100
    } else {
101
      if (this.context.getNetworkStatus().isOffline()) {
5✔
102
        throw CliOfflineException.ofDownloadOfTool(metadata.getTool(), metadata.getEdition(), metadata.getVersion());
8✔
103
      }
104
      target = download(metadata, target);
5✔
105
    }
106
    return target;
2✔
107
  }
108

109
  /**
110
   * @param metadata the {@link UrlDownloadFileMetadata} for the download.
111
   * @param target the expected {@link Path} to download to.
112
   * @return the actual {@link Path} of the downloaded file.
113
   */
114
  private Path download(UrlDownloadFileMetadata metadata, Path target) {
115

116
    VersionIdentifier resolvedVersion = metadata.getVersion();
3✔
117
    List<String> urlList = new ArrayList<>(metadata.getUrls());
6✔
118
    int size = urlList.size();
3✔
119
    int max = size - 1;
4✔
120
    if (size > 1) {
3!
121
      Collections.shuffle(urlList);
×
122
    }
123
    UrlChecksums checksums = metadata.getChecksums();
3✔
124
    Exception error = null;
2✔
125
    for (int i = 0; i < size; i++) {
5!
126
      String url = urlList.get(i);
5✔
127
      try {
128
        return download(url, target, resolvedVersion, checksums);
7✔
129
      } catch (Exception e) {
×
130
        error = e;
×
131
      }
132
      if (i < max) {
×
133
        this.context.error(error, "Failed to download from " + url);
×
134
      }
135
    }
136
    throw new IllegalStateException("Download of " + target.getFileName() + " failed after trying " + size + " URL(s).", error);
×
137
  }
138

139
  /**
140
   * Computes the normalized filename of the download package. It uses the schema {@code «tool»-«version»[-«edition»][-«os»][-«arch»].«ext»}.
141
   *
142
   * @param tool the name of the tool to download.
143
   * @param edition the edition of the tool to download.
144
   * @param version the resolved {@link VersionIdentifier} of the tool to download.
145
   * @param os the specific {@link OperatingSystem} or {@code null} if the download is OS-agnostic.
146
   * @param arc the specific {@link SystemArchitecture} or {@code null} if the download is arc-agnostic.
147
   * @param url the download URL used to determine the file-extension ({@code «ext»}).
148
   * @return the computed filename.
149
   */
150
  protected String createDownloadFilename(String tool, String edition, VersionIdentifier version, OperatingSystem os,
151
      SystemArchitecture arc, String url) {
152

153
    StringBuilder sb = new StringBuilder(32);
5✔
154
    sb.append(tool);
4✔
155
    sb.append("-");
4✔
156
    if (VersionIdentifier.LATEST.equals(version)) {
4!
157
      sb.append("latest");
×
158
    } else {
159
      sb.append(version);
4✔
160
    }
161
    if (!edition.equals(tool)) {
4!
162
      sb.append("-");
×
163
      sb.append(edition);
×
164
    }
165
    if (os != null) {
2✔
166
      sb.append("-");
4✔
167
      sb.append(os);
4✔
168
    }
169
    if (arc != null) {
2✔
170
      sb.append("-");
4✔
171
      sb.append(arc);
4✔
172
    }
173
    String extension = FilenameUtil.getExtension(url);
3✔
174
    if (extension == null) {
2!
175
      // legacy fallback - should never happen
176
      if (this.context.getSystemInfo().isLinux()) {
×
177
        extension = "tgz";
×
178
      } else {
179
        extension = "zip";
×
180
      }
181
      this.context.warning("Could not determine file extension from URL {} - guess was {} but may be incorrect.", url,
×
182
          extension);
183
    }
184
    sb.append(".");
4✔
185
    sb.append(extension);
4✔
186
    return sb.toString();
3✔
187
  }
188

189
  /**
190
   * @param url the URL to download from.
191
   * @param target the {@link Path} to the target file to download to.
192
   * @param resolvedVersion the resolved version to download as {@link VersionIdentifier} or {@link String}.
193
   * @param expectedChecksums the {@link UrlChecksums}.
194
   * @return the actual {@link Path} where the file was downloaded to. Typically the given {@link Path} {@code target} but may also be a different file in
195
   *     edge-cases.
196
   */
197
  protected Path download(String url, Path target, Object resolvedVersion, UrlChecksums expectedChecksums) {
198

199
    String downloadFilename = target.getFileName().toString();
4✔
200
    Path tmpDownloadFile = createTempDownload(downloadFilename);
4✔
201
    Path result;
202
    try {
203
      this.context.getFileAccess().download(url, tmpDownloadFile);
6✔
204
      verifyChecksums(tmpDownloadFile, expectedChecksums, resolvedVersion);
5✔
205
      if (isLatestVersion(resolvedVersion)) {
3!
206
        // Some software vendors violate best-practices and provide the latest version only under a fixed URL.
207
        // Therefore, if a newer version of that file gets released, the same URL suddenly leads to a different
208
        // download file with a newer version and a different checksum.
209
        // In order to still support such tools we had to implement this workaround so we cannot move the file in the
210
        // download cache for later reuse, cannot verify its checksum and also delete the downloaded file on exit
211
        // (after we assume it has been extracted) so we always ensure to get the LATEST version when requested.
212
        tmpDownloadFile.toFile().deleteOnExit();
×
213
        result = tmpDownloadFile;
×
214
      } else {
215
        this.context.getFileAccess().move(tmpDownloadFile, target);
8✔
216
        result = target;
2✔
217
      }
218
    } catch (RuntimeException e) {
×
219
      this.context.getFileAccess().delete(tmpDownloadFile);
×
220
      throw e;
×
221
    }
1✔
222
    return result;
2✔
223
  }
224

225
  private static boolean isLatestVersion(Object resolvedVersion) {
226
    return resolvedVersion.toString().equals("latest");
5✔
227
  }
228

229
  /**
230
   * @param filename the name of the temporary download file.
231
   * @return a {@link Path} to such file that does not yet exist.
232
   */
233
  protected Path createTempDownload(String filename) {
234

235
    Path tmpDownloads = this.context.getTempDownloadPath();
4✔
236
    Path tmpDownloadFile = tmpDownloads.resolve(filename);
4✔
237
    int i = 2;
2✔
238
    while (Files.exists(tmpDownloadFile)) {
5!
239
      tmpDownloadFile = tmpDownloads.resolve(filename + "." + i);
×
240
      i++;
×
241
      if (i > MAX_TEMP_DOWNLOADS) {
×
242
        throw new IllegalStateException("Too many downloads of the same file: " + tmpDownloadFile);
×
243
      }
244
    }
245
    return tmpDownloadFile;
2✔
246
  }
247

248
  private void verifyChecksums(Path file, UrlChecksums expectedChecksums, Object version) {
249

250
    if (expectedChecksums == null) {
2✔
251
      return;
1✔
252
    }
253
    boolean checksumVerified = false;
2✔
254
    for (UrlGenericChecksum expectedChecksum : expectedChecksums) {
6!
255
      verifyChecksum(file, expectedChecksum);
×
256
      checksumVerified = true;
×
257
    }
×
258
    if (!checksumVerified) {
2!
259
      IdeLogLevel level = IdeLogLevel.WARNING;
2✔
260
      if (isLatestVersion(version)) {
3!
261
        level = IdeLogLevel.DEBUG;
×
262
      }
263
      this.context.level(level).log("No checksum found for {}", file);
13✔
264
    }
265
  }
1✔
266

267
  /**
268
   * Performs the checksum verification.
269
   *
270
   * @param file the downloaded software package to verify.
271
   * @param expectedChecksum the expected SHA-256 checksum.
272
   */
273
  protected void verifyChecksum(Path file, UrlGenericChecksum expectedChecksum) {
274

275
    String hashAlgorithm = expectedChecksum.getHashAlgorithm();
×
276
    String actualChecksum = this.context.getFileAccess().checksum(file, hashAlgorithm);
×
277
    if (expectedChecksum.getChecksum().equals(actualChecksum)) {
×
278
      this.context.success("{} checksum {} is correct.", hashAlgorithm, actualChecksum);
×
279
    } else {
280
      throw new CliException("Downloaded file " + file + " has the wrong " + hashAlgorithm + " checksum!\n" //
×
281
          + "Expected " + expectedChecksum + "\n" //
282
          + "Download " + actualChecksum + "\n" //
283
          + "This could be a man-in-the-middle-attack, a download failure, or a release that has been updated afterwards.\n" //
284
          + "Please review carefully.\n" //
285
          + "Expected checksum can be found at " + expectedChecksum + ".\n" //
286
          + "Installation was aborted for security reasons!");
287
    }
288
  }
×
289

290
  @Override
291
  public VersionIdentifier resolveVersion(String tool, String edition, GenericVersionRange version, ToolCommandlet toolCommandlet) {
292

293
    List<VersionIdentifier> versions = getSortedVersions(tool, edition, toolCommandlet);
6✔
294
    return VersionIdentifier.resolveVersionPattern(version, versions, this.context);
6✔
295
  }
296

297
  @Override
298
  public Collection<ToolDependency> findDependencies(String tool, String edition, VersionIdentifier version) {
299

300
    UrlEdition urlEdition = this.context.getUrls().getEdition(tool, edition);
7✔
301
    ToolDependencies dependencies = urlEdition.getDependencyFile().getDependencies();
4✔
302
    if (dependencies == ToolDependencies.getEmpty()) {
3✔
303
      UrlTool urlTool = urlEdition.getParent();
4✔
304
      dependencies = urlTool.getDependencyFile().getDependencies();
4✔
305
    }
306
    if (dependencies != ToolDependencies.getEmpty()) {
3✔
307
      this.context.trace("Found dependencies in {}", dependencies);
10✔
308
    }
309
    return dependencies.findDependencies(version, this.context);
6✔
310
  }
311

312
  @Override
313
  public ToolSecurity findSecurity(String tool, String edition) {
314
    UrlEdition urlEdition = this.context.getUrls().getEdition(tool, edition);
7✔
315
    ToolSecurity security = urlEdition.getSecurityFile().getSecurity();
4✔
316
    if (security == ToolSecurity.getEmpty()) {
3!
317
      UrlTool urlTool = urlEdition.getParent();
4✔
318
      security = urlTool.getSecurityFile().getSecurity();
4✔
319
    }
320
    if (security != ToolSecurity.getEmpty()) {
3✔
321
      this.context.trace("Found dependencies in {}", security);
10✔
322
    }
323
    return security;
2✔
324
  }
325

326
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc