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

devonfw / IDEasy / 14124434400

28 Mar 2025 07:55AM UTC coverage: 67.594% (-0.008%) from 67.602%
14124434400

Pull #1182

github

web-flow
Merge 55e9efb5d into 8bcbfb54c
Pull Request #1182: #692 : "Latest" version of docker is now accepted

3044 of 4934 branches covered (61.69%)

Branch coverage included in aggregate %.

7846 of 11177 relevant lines covered (70.2%)

3.07 hits per line

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

62.14
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.Collections;
7
import java.util.List;
8
import java.util.Set;
9

10
import com.devonfw.tools.ide.cli.CliException;
11
import com.devonfw.tools.ide.cli.CliOfflineException;
12
import com.devonfw.tools.ide.context.IdeContext;
13
import com.devonfw.tools.ide.log.IdeLogLevel;
14
import com.devonfw.tools.ide.os.OperatingSystem;
15
import com.devonfw.tools.ide.os.SystemArchitecture;
16
import com.devonfw.tools.ide.tool.ToolCommandlet;
17
import com.devonfw.tools.ide.url.model.file.UrlChecksums;
18
import com.devonfw.tools.ide.url.model.file.UrlDownloadFileMetadata;
19
import com.devonfw.tools.ide.url.model.file.UrlGenericChecksum;
20
import com.devonfw.tools.ide.util.FilenameUtil;
21
import com.devonfw.tools.ide.version.GenericVersionRange;
22
import com.devonfw.tools.ide.version.VersionIdentifier;
23

24
/**
25
 * Abstract base implementation of {@link ToolRepository}.
26
 */
27
public abstract class AbstractToolRepository implements ToolRepository {
28

29
  private static final int MAX_TEMP_DOWNLOADS = 9;
30

31
  /** The owning {@link IdeContext}. */
32
  protected final IdeContext context;
33

34
  /**
35
   * The constructor.
36
   *
37
   * @param context the owning {@link IdeContext}.
38
   */
39
  public AbstractToolRepository(IdeContext context) {
40

41
    super();
2✔
42
    this.context = context;
3✔
43
  }
1✔
44

45
  /**
46
   * @param tool the name of the tool to download.
47
   * @param edition the edition of the tool to download.
48
   * @param version the {@link VersionIdentifier} to download.
49
   * @param toolCommandlet the {@link ToolCommandlet}.
50
   * @return the resolved {@link UrlDownloadFileMetadata}.
51
   */
52
  protected abstract UrlDownloadFileMetadata getMetadata(String tool, String edition, VersionIdentifier version, ToolCommandlet toolCommandlet);
53

54

55
  @Override
56
  public Path download(String tool, String edition, VersionIdentifier version, ToolCommandlet toolCommandlet) {
57

58
    UrlDownloadFileMetadata metadata = getMetadata(tool, edition, version, toolCommandlet);
7✔
59
    return download(metadata);
4✔
60
  }
61

62
  /**
63
   * @param metadata the {@link UrlDownloadFileMetadata}.
64
   * @return the {@link Path} to the downloaded file.
65
   */
66
  public Path download(UrlDownloadFileMetadata metadata) {
67

68
    VersionIdentifier version = metadata.getVersion();
3✔
69
    if (context.isOffline()) {
4!
70
      throw CliOfflineException.ofDownloadOfTool(metadata.getTool(), metadata.getEdition(), version);
×
71
    }
72
    Set<String> urlCollection = metadata.getUrls();
3✔
73
    if (urlCollection.isEmpty()) {
3!
74
      throw new IllegalStateException("Invalid download metadata with empty urls file for " + metadata);
×
75
    }
76
    Path target = doDownload(metadata);
4✔
77
    return target;
2✔
78
  }
79

80
  /**
81
   * @param metadata the {@link UrlDownloadFileMetadata}.
82
   * @return the {@link Path} to the downloaded file.
83
   */
84
  protected Path doDownload(UrlDownloadFileMetadata metadata) {
85
    String downloadFilename = createDownloadFilename(metadata.getTool(), metadata.getEdition(), metadata.getVersion(), metadata.getOs(),
12✔
86
        metadata.getArch(), metadata.getUrls().iterator().next());
6✔
87
    Path downloadCache = this.context.getDownloadPath().resolve(getId());
7✔
88
    this.context.getFileAccess().mkdirs(downloadCache);
5✔
89
    Path target = downloadCache.resolve(downloadFilename);
4✔
90
    if (Files.exists(target)) {
5!
91
      this.context.interaction("Artifact already exists at {}\nTo force update please delete the file and run again.", target);
×
92
    } else {
93
      target = download(metadata, target);
5✔
94
    }
95
    return target;
2✔
96
  }
97

98
  /**
99
   * @param metadata the {@link UrlDownloadFileMetadata} for the download.
100
   * @param target the expected {@link Path} to download to.
101
   * @return the actual {@link Path} of the downloaded file.
102
   */
103
  private Path download(UrlDownloadFileMetadata metadata, Path target) {
104

105
    VersionIdentifier resolvedVersion = metadata.getVersion();
3✔
106
    List<String> urlList = new ArrayList<>(metadata.getUrls());
6✔
107
    if (urlList.size() > 1) {
4!
108
      Collections.shuffle(urlList);
×
109
    }
110
    UrlChecksums checksums = metadata.getChecksums();
3✔
111
    for (String url : urlList) {
10!
112
      try {
113
        return download(url, target, resolvedVersion, checksums);
7✔
114
      } catch (Exception e) {
×
115
        this.context.error(e, "Failed to download from " + url);
×
116
      }
117
    }
×
118
    throw new CliException("Download of " + target.getFileName() + " failed after trying " + urlList.size() + " URL(s).");
×
119
  }
120

121
  /**
122
   * Computes the normalized filename of the download package. It uses the schema {@code «tool»-«version»[-«edition»][-«os»][-«arch»].«ext»}.
123
   *
124
   * @param tool the name of the tool to download.
125
   * @param edition the edition of the tool to download.
126
   * @param version the resolved {@link VersionIdentifier} of the tool to download.
127
   * @param os the specific {@link OperatingSystem} or {@code null} if the download is OS-agnostic.
128
   * @param arc the specific {@link SystemArchitecture} or {@code null} if the download is arc-agnostic.
129
   * @param url the download URL used to determine the file-extension ({@code «ext»}).
130
   * @return the computed filename.
131
   */
132
  protected String createDownloadFilename(String tool, String edition, VersionIdentifier version, OperatingSystem os,
133
      SystemArchitecture arc, String url) {
134

135
    StringBuilder sb = new StringBuilder(32);
5✔
136
    sb.append(tool);
4✔
137
    sb.append("-");
4✔
138
    if (VersionIdentifier.LATEST.equals(version)) {
4!
139
      sb.append("latest");
×
140
    } else {
141
      sb.append(version);
4✔
142
    }
143
    if (!edition.equals(tool)) {
4!
144
      sb.append("-");
×
145
      sb.append(edition);
×
146
    }
147
    if (os != null) {
2!
148
      sb.append("-");
4✔
149
      sb.append(os);
4✔
150
    }
151
    if (arc != null) {
2!
152
      sb.append("-");
4✔
153
      sb.append(arc);
4✔
154
    }
155
    String extension = FilenameUtil.getExtension(url);
3✔
156
    if (extension == null) {
2!
157
      // legacy fallback - should never happen
158
      if (this.context.getSystemInfo().isLinux()) {
5!
159
        extension = "tgz";
3✔
160
      } else {
161
        extension = "zip";
×
162
      }
163
      this.context.warning("Could not determine file extension from URL {} - guess was {} but may be incorrect.", url,
14✔
164
          extension);
165
    }
166
    sb.append(".");
4✔
167
    sb.append(extension);
4✔
168
    return sb.toString();
3✔
169
  }
170

171
  /**
172
   * @param url the URL to download from.
173
   * @param target the {@link Path} to the target file to download to.
174
   * @param resolvedVersion the resolved version to download as {@link VersionIdentifier} or {@link String}.
175
   * @param expectedChecksums the {@link UrlChecksums}.
176
   * @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
177
   *     edge-cases.
178
   */
179
  protected Path download(String url, Path target, Object resolvedVersion, UrlChecksums expectedChecksums) {
180

181
    String downloadFilename = target.getFileName().toString();
4✔
182
    Path tmpDownloadFile = createTempDownload(downloadFilename);
4✔
183
    Path result;
184
    try {
185
      this.context.getFileAccess().download(url, tmpDownloadFile);
6✔
186
      verifyChecksums(tmpDownloadFile, expectedChecksums, resolvedVersion);
5✔
187
      if (isLatestVersion(resolvedVersion)) {
3!
188
        // Some software vendors violate best-practices and provide the latest version only under a fixed URL.
189
        // Therefore, if a newer version of that file gets released, the same URL suddenly leads to a different
190
        // download file with a newer version and a different checksum.
191
        // In order to still support such tools we had to implement this workaround so we cannot move the file in the
192
        // download cache for later reuse, cannot verify its checksum and also delete the downloaded file on exit
193
        // (after we assume it has been extracted) so we always ensure to get the LATEST version when requested.
194
        tmpDownloadFile.toFile().deleteOnExit();
×
195
        result = tmpDownloadFile;
×
196
      } else {
197
        this.context.getFileAccess().move(tmpDownloadFile, target);
8✔
198
        result = target;
2✔
199
      }
200
    } catch (RuntimeException e) {
×
201
      this.context.getFileAccess().delete(tmpDownloadFile);
×
202
      throw e;
×
203
    }
1✔
204
    return result;
2✔
205
  }
206

207
  private static boolean isLatestVersion(Object resolvedVersion) {
208
    return resolvedVersion.toString().equals("latest");
5✔
209
  }
210

211
  /**
212
   * @param filename the name of the temporary download file.
213
   * @return a {@link Path} to such file that does not yet exist.
214
   */
215
  protected Path createTempDownload(String filename) {
216

217
    Path tmpDownloads = this.context.getTempDownloadPath();
4✔
218
    Path tmpDownloadFile = tmpDownloads.resolve(filename);
4✔
219
    int i = 2;
2✔
220
    while (Files.exists(tmpDownloadFile)) {
5!
221
      tmpDownloadFile = tmpDownloads.resolve(filename + "." + i);
×
222
      i++;
×
223
      if (i > MAX_TEMP_DOWNLOADS) {
×
224
        throw new IllegalStateException("Too many downloads of the same file: " + tmpDownloadFile);
×
225
      }
226
    }
227
    return tmpDownloadFile;
2✔
228
  }
229

230
  private void verifyChecksums(Path file, UrlChecksums expectedChecksums, Object version) {
231

232
    if (expectedChecksums == null) {
2!
233
      return;
×
234
    }
235
    boolean checksumVerified = false;
2✔
236
    for (UrlGenericChecksum expectedChecksum : expectedChecksums) {
6!
237
      verifyChecksum(file, expectedChecksum);
×
238
      checksumVerified = true;
×
239
    }
×
240
    if (!checksumVerified) {
2!
241
      IdeLogLevel level = IdeLogLevel.WARNING;
2✔
242
      if (isLatestVersion(version)) {
3!
243
        level = IdeLogLevel.DEBUG;
×
244
      }
245
      this.context.level(level).log("No checksum found for {}", file);
13✔
246
    }
247
  }
1✔
248

249
  /**
250
   * Performs the checksum verification.
251
   *
252
   * @param file the downloaded software package to verify.
253
   * @param expectedChecksum the expected SHA-256 checksum.
254
   */
255
  protected void verifyChecksum(Path file, UrlGenericChecksum expectedChecksum) {
256

257
    String hashAlgorithm = expectedChecksum.getHashAlgorithm();
×
258
    String actualChecksum = this.context.getFileAccess().checksum(file, hashAlgorithm);
×
259
    if (expectedChecksum.getChecksum().equals(actualChecksum)) {
×
260
      this.context.success("{} checksum {} is correct.", hashAlgorithm, actualChecksum);
×
261
    } else {
262
      throw new CliException("Downloaded file " + file + " has the wrong " + hashAlgorithm + " checksum!\n" //
×
263
          + "Expected " + expectedChecksum + "\n" //
264
          + "Download " + actualChecksum + "\n" //
265
          + "This could be a man-in-the-middle-attack, a download failure, or a release that has been updated afterwards.\n" //
266
          + "Please review carefully.\n" //
267
          + "Expected checksum can be found at " + expectedChecksum + ".\n" //
268
          + "Installation was aborted for security reasons!");
269
    }
270
  }
×
271

272
  @Override
273
  public VersionIdentifier resolveVersion(String tool, String edition, GenericVersionRange version, ToolCommandlet toolCommandlet) {
274

275
    List<VersionIdentifier> versions = getSortedVersions(tool, edition, toolCommandlet);
6✔
276
    return VersionIdentifier.resolveVersionPattern(version, versions, this.context);
6✔
277
  }
278
}
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

© 2025 Coveralls, Inc