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

devonfw / IDEasy / 13210157275

08 Feb 2025 12:10AM UTC coverage: 68.25% (-0.1%) from 68.379%
13210157275

Pull #1021

github

web-flow
Merge 93d542ee7 into 9c2006bd8
Pull Request #1021: #786: support ide upgrade to automatically update to the latest version of IDEasy

2910 of 4683 branches covered (62.14%)

Branch coverage included in aggregate %.

7563 of 10662 relevant lines covered (70.93%)

3.09 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
  /** The owning {@link IdeContext}. */
30
  protected final IdeContext context;
31

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

39
    super();
2✔
40
    this.context = context;
3✔
41
  }
1✔
42

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

52

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

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

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

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

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

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

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

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

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

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

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

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

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

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

224
  private void verifyChecksums(Path file, UrlChecksums expectedChecksums, Object version) {
225

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

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

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

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

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