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

devonfw / IDEasy / 13344992564

15 Feb 2025 12:25PM UTC coverage: 67.959% (-0.5%) from 68.482%
13344992564

Pull #1021

github

web-flow
Merge 19a27ddc4 into c70643978
Pull Request #1021: #786: support ide upgrade to automatically update to the latest version of IDEasy

2966 of 4793 branches covered (61.88%)

Branch coverage included in aggregate %.

7690 of 10887 relevant lines covered (70.63%)

3.07 hits per line

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

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

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

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

203
  private static boolean isLatestVersion(Object resolvedVersion) {
204
    return resolvedVersion.toString().equals("latest");
5✔
205
  }
206

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

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

226
  private void verifyChecksums(Path file, UrlChecksums expectedChecksums, Object version) {
227

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

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

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

268
  @Override
269
  public VersionIdentifier resolveVersion(String tool, String edition, GenericVersionRange version, ToolCommandlet toolCommandlet) {
270

271
    List<VersionIdentifier> versions = getSortedVersions(tool, edition, toolCommandlet);
6✔
272
    return VersionIdentifier.resolveVersionPattern(version, versions, this.context);
6✔
273
  }
274
}
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