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

devonfw / IDEasy / 22284264868

22 Feb 2026 08:00PM UTC coverage: 70.75% (+0.3%) from 70.474%
22284264868

Pull #1714

github

web-flow
Merge 98f01421f into 379acdc9d
Pull Request #1714: #404: #1713: advanced logging

4063 of 6346 branches covered (64.02%)

Branch coverage included in aggregate %.

10636 of 14430 relevant lines covered (73.71%)

3.1 hits per line

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

67.43
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 org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13
import org.slf4j.event.Level;
14

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

34
/**
35
 * Abstract base implementation of {@link ToolRepository}.
36
 */
37
public abstract class AbstractToolRepository implements ToolRepository {
38

39
  private static final Logger LOG = LoggerFactory.getLogger(AbstractToolRepository.class);
4✔
40

41
  private static final int MAX_TEMP_DOWNLOADS = 9;
42

43
  /** The owning {@link IdeContext}. */
44
  protected final IdeContext context;
45

46
  /**
47
   * The constructor.
48
   *
49
   * @param context the owning {@link IdeContext}.
50
   */
51
  public AbstractToolRepository(IdeContext context) {
52

53
    super();
2✔
54
    this.context = context;
3✔
55
  }
1✔
56

57
  /**
58
   * @param tool the name of the tool to download.
59
   * @param edition the edition of the tool to download.
60
   * @param version the {@link VersionIdentifier} to download.
61
   * @param toolCommandlet the {@link ToolCommandlet}.
62
   * @return the resolved {@link UrlDownloadFileMetadata}.
63
   */
64
  protected abstract UrlDownloadFileMetadata getMetadata(String tool, String edition, VersionIdentifier version, ToolCommandlet toolCommandlet);
65

66

67
  @Override
68
  public Path download(String tool, String edition, VersionIdentifier version, ToolCommandlet toolCommandlet) {
69

70
    UrlDownloadFileMetadata metadata = getMetadata(tool, edition, version, toolCommandlet);
×
71
    return download(metadata);
×
72
  }
73

74
  /**
75
   * @param metadata the {@link UrlDownloadFileMetadata}.
76
   * @return the {@link Path} to the downloaded file.
77
   */
78
  public Path download(UrlDownloadFileMetadata metadata) {
79

80
    Set<String> urlCollection = metadata.getUrls();
3✔
81
    if (urlCollection.isEmpty()) {
3!
82
      throw new IllegalStateException("Invalid download metadata with empty urls file for " + metadata);
×
83
    }
84
    return doDownload(metadata);
4✔
85
  }
86

87
  /**
88
   * @param metadata the {@link UrlDownloadFileMetadata}.
89
   * @return the {@link Path} to the downloaded file.
90
   */
91
  protected Path doDownload(UrlDownloadFileMetadata metadata) {
92
    String downloadFilename = createDownloadFilename(metadata.getTool(), metadata.getEdition(), metadata.getVersion(), metadata.getOs(),
12✔
93
        metadata.getArch(), metadata.getUrls().iterator().next());
6✔
94
    Path downloadCache = this.context.getDownloadPath().resolve(getId());
7✔
95
    this.context.getFileAccess().mkdirs(downloadCache);
5✔
96
    Path target = downloadCache.resolve(downloadFilename);
4✔
97

98
    if (Files.exists(target)) {
5✔
99
      // File is already cached
100
      if (this.context.getNetworkStatus().isOffline()) {
5!
101
        LOG.debug("Using cached download of {} in version {} from {} (offline mode)",
9✔
102
            metadata.getTool(), metadata.getVersion(), target);
11✔
103
      } else {
104
        LOG.info(IdeLogLevel.INTERACTION.getSlf4jMarker(), "Artifact already exists at {}\nTo force update please delete the file and run again.", target);
×
105
      }
106
    } else {
107
      if (this.context.getNetworkStatus().isOffline()) {
5✔
108
        throw CliOfflineException.ofDownloadOfTool(metadata.getTool(), metadata.getEdition(), metadata.getVersion());
8✔
109
      }
110
      target = download(metadata, target);
5✔
111
    }
112
    return target;
2✔
113
  }
114

115
  /**
116
   * @param metadata the {@link UrlDownloadFileMetadata} for the download.
117
   * @param target the expected {@link Path} to download to.
118
   * @return the actual {@link Path} of the downloaded file.
119
   */
120
  private Path download(UrlDownloadFileMetadata metadata, Path target) {
121

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

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

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

194
  /**
195
   * @param url the URL to download from.
196
   * @param target the {@link Path} to the target file to download to.
197
   * @param resolvedVersion the resolved version to download as {@link VersionIdentifier} or {@link String}.
198
   * @param expectedChecksums the {@link UrlChecksums}.
199
   * @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
200
   *     edge-cases.
201
   */
202
  protected Path download(String url, Path target, Object resolvedVersion, UrlChecksums expectedChecksums) {
203

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

230
  private static boolean isLatestVersion(Object resolvedVersion) {
231
    return resolvedVersion.toString().equals("latest");
5✔
232
  }
233

234
  /**
235
   * @param filename the name of the temporary download file.
236
   * @return a {@link Path} to such file that does not yet exist.
237
   */
238
  protected Path createTempDownload(String filename) {
239

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

253
  private void verifyChecksums(Path file, UrlChecksums expectedChecksums, Object version) {
254

255
    if (expectedChecksums == null) {
2✔
256
      return;
1✔
257
    }
258
    boolean checksumVerified = false;
2✔
259
    for (UrlGenericChecksum expectedChecksum : expectedChecksums) {
6!
260
      verifyChecksum(file, expectedChecksum);
×
261
      checksumVerified = true;
×
262
    }
×
263
    if (!checksumVerified) {
2!
264
      Level level = Level.WARN;
2✔
265
      if (isLatestVersion(version)) {
3!
266
        level = Level.DEBUG;
×
267
      }
268
      LOG.atLevel(level).log("No checksum found for {}", file);
6✔
269
    }
270
  }
1✔
271

272
  /**
273
   * Performs the checksum verification.
274
   *
275
   * @param file the downloaded software package to verify.
276
   * @param expectedChecksum the expected SHA-256 checksum.
277
   */
278
  protected void verifyChecksum(Path file, UrlGenericChecksum expectedChecksum) {
279

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

295
  @Override
296
  public VersionIdentifier resolveVersion(String tool, String edition, GenericVersionRange version, ToolCommandlet toolCommandlet) {
297

298
    List<VersionIdentifier> versions = getSortedVersions(tool, edition, toolCommandlet);
6✔
299
    return VersionIdentifier.resolveVersionPattern(version, versions);
4✔
300
  }
301

302
  @Override
303
  public Collection<ToolDependency> findDependencies(String tool, String edition, VersionIdentifier version) {
304

305
    UrlEdition urlEdition = this.context.getUrls().getEdition(tool, edition);
7✔
306
    ToolDependencies dependencies = urlEdition.getDependencyFile().getDependencies();
4✔
307
    if (dependencies == ToolDependencies.getEmpty()) {
3✔
308
      UrlTool urlTool = urlEdition.getParent();
4✔
309
      dependencies = urlTool.getDependencyFile().getDependencies();
4✔
310
    }
311
    if (dependencies != ToolDependencies.getEmpty()) {
3✔
312
      LOG.trace("Found dependencies in {}", dependencies);
4✔
313
    }
314
    return dependencies.findDependencies(version);
4✔
315
  }
316

317
  @Override
318
  public ToolSecurity findSecurity(String tool, String edition) {
319
    UrlEdition urlEdition = this.context.getUrls().getEdition(tool, edition);
7✔
320
    ToolSecurity security = urlEdition.getSecurityFile().getSecurity();
4✔
321
    if (security == ToolSecurity.getEmpty()) {
3!
322
      UrlTool urlTool = urlEdition.getParent();
4✔
323
      security = urlTool.getSecurityFile().getSecurity();
4✔
324
    }
325
    if (security != ToolSecurity.getEmpty()) {
3✔
326
      LOG.trace("Found CVE information in {}", security);
4✔
327
    }
328
    return security;
2✔
329
  }
330

331
}
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