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

devonfw / IDEasy / 13160073324

05 Feb 2025 02:52PM UTC coverage: 68.252% (-0.1%) from 68.379%
13160073324

Pull #1002

github

web-flow
Merge 61eb7829f into 62fa12bac
Pull Request #1002: #786: Upgrade commandlet

2901 of 4667 branches covered (62.16%)

Branch coverage included in aggregate %.

7504 of 10578 relevant lines covered (70.94%)

3.09 hits per line

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

60.87
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.url.model.file.UrlDownloadFileMetadata;
17
import com.devonfw.tools.ide.util.FilenameUtil;
18
import com.devonfw.tools.ide.version.VersionIdentifier;
19

20
/**
21
 * Abstract base implementation of {@link ToolRepository}.
22
 */
23
public abstract class AbstractToolRepository implements ToolRepository {
24

25
  /** The owning {@link IdeContext}. */
26
  protected final IdeContext context;
27

28
  /**
29
   * The constructor.
30
   *
31
   * @param context the owning {@link IdeContext}.
32
   */
33
  public AbstractToolRepository(IdeContext context) {
34

35
    super();
2✔
36
    this.context = context;
3✔
37
  }
1✔
38

39
  /**
40
   * @param tool the name of the tool to download.
41
   * @param edition the edition of the tool to download.
42
   * @param version the {@link VersionIdentifier} to download.
43
   * @return the resolved {@link UrlDownloadFileMetadata}.
44
   */
45
  protected abstract UrlDownloadFileMetadata getMetadata(String tool, String edition, VersionIdentifier version);
46

47

48
  @Override
49
  public Path download(String tool, String edition, VersionIdentifier version) {
50

51
    UrlDownloadFileMetadata metadata = getMetadata(tool, edition, version);
6✔
52
    VersionIdentifier resolvedVersion = metadata.getVersion();
3✔
53
    Set<String> urlCollection = metadata.getUrls();
3✔
54
    if (context.isOffline()) {
4!
55
      throw CliOfflineException.ofDownloadOfTool(tool, edition, version);
×
56
    }
57
    if (urlCollection.isEmpty()) {
3!
58
      throw new IllegalStateException("Invalid download metadata with empty urls file for " + metadata);
×
59
    }
60
    String downloadFilename = createDownloadFilename(tool, edition, resolvedVersion, metadata.getOs(),
9✔
61
        metadata.getArch(), urlCollection.iterator().next());
5✔
62
    Path downloadCache = this.context.getDownloadPath().resolve(getId());
7✔
63
    this.context.getFileAccess().mkdirs(downloadCache);
5✔
64
    Path target = downloadCache.resolve(downloadFilename);
4✔
65
    if (Files.exists(target)) {
5!
66
      this.context.interaction("Artifact already exists at {}\nTo force update please delete the file and run again.",
×
67
          target);
68
    } else {
69
      target = download(metadata, downloadFilename, target);
6✔
70
    }
71
    String expectedChecksum = metadata.getChecksum();
3✔
72
    if (expectedChecksum == null) {
2!
73
      IdeLogLevel level = IdeLogLevel.WARNING;
2✔
74
      if (resolvedVersion.toString().equals("latest")) {
5!
75
        level = IdeLogLevel.DEBUG;
×
76
      }
77
      this.context.level(level).log("No checksum found for {}", metadata);
13✔
78
    } else {
1✔
79
      verifyChecksum(target, expectedChecksum, metadata);
×
80
    }
81
    return target;
2✔
82
  }
83

84
  /**
85
   * @param metadata the {@link UrlDownloadFileMetadata} for the download.
86
   * @param downloadFilename the computed filename of the file to download.
87
   * @param target the expected {@link Path} to download to.
88
   * @return the actual {@link Path} of the downloaded file.
89
   */
90
  private Path download(UrlDownloadFileMetadata metadata, String downloadFilename, Path target) {
91

92
    VersionIdentifier resolvedVersion = metadata.getVersion();
3✔
93
    List<String> urlList = new ArrayList<>(metadata.getUrls());
6✔
94
    if (urlList.size() > 1) {
4!
95
      Collections.shuffle(urlList);
×
96
    }
97
    for (String url : urlList) {
10!
98
      try {
99
        return download(url, target, downloadFilename, resolvedVersion);
7✔
100
      } catch (Exception e) {
×
101
        this.context.error(e, "Failed to download from " + url);
×
102
      }
103
    }
×
104
    throw new CliException("Download of " + downloadFilename + " failed after trying " + urlList.size() + " URL(s).");
×
105
  }
106

107
  /**
108
   * Computes the normalized filename of the download package. It uses the schema {@code «tool»-«version»[-«edition»][-«os»][-«arch»].«ext»}.
109
   *
110
   * @param tool the name of the tool to download.
111
   * @param edition the edition of the tool to download.
112
   * @param version the resolved {@link VersionIdentifier} of the tool to download.
113
   * @param os the specific {@link OperatingSystem} or {@code null} if the download is OS-agnostic.
114
   * @param arc the specific {@link SystemArchitecture} or {@code null} if the download is arc-agnostic.
115
   * @param url the download URL used to determine the file-extension ({@code «ext»}).
116
   * @return the computed filename.
117
   */
118
  protected String createDownloadFilename(String tool, String edition, VersionIdentifier version, OperatingSystem os,
119
      SystemArchitecture arc, String url) {
120

121
    StringBuilder sb = new StringBuilder(32);
5✔
122
    sb.append(tool);
4✔
123
    sb.append("-");
4✔
124
    sb.append(version);
4✔
125
    if (!edition.equals(tool)) {
4!
126
      sb.append("-");
×
127
      sb.append(edition);
×
128
    }
129
    if (os != null) {
2!
130
      sb.append("-");
4✔
131
      sb.append(os);
4✔
132
    }
133
    if (arc != null) {
2!
134
      sb.append("-");
4✔
135
      sb.append(arc);
4✔
136
    }
137
    String extension = FilenameUtil.getExtension(url);
3✔
138
    if (extension == null) {
2!
139
      // legacy fallback - should never happen
140
      if (this.context.getSystemInfo().isLinux()) {
5!
141
        extension = "tgz";
3✔
142
      } else {
143
        extension = "zip";
×
144
      }
145
      this.context.warning("Could not determine file extension from URL {} - guess was {} but may be incorrect.", url,
14✔
146
          extension);
147
    }
148
    sb.append(".");
4✔
149
    sb.append(extension);
4✔
150
    return sb.toString();
3✔
151
  }
152

153
  /**
154
   * @param url the URL to download from.
155
   * @param target the {@link Path} to the target file to download to.
156
   * @param downloadFilename the filename of the download file.
157
   * @param resolvedVersion the resolved {@link VersionIdentifier} to download.
158
   * @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
159
   *     edge-cases.
160
   */
161
  protected Path download(String url, Path target, String downloadFilename, VersionIdentifier resolvedVersion) {
162

163
    Path tmpDownloadFile = createTempDownload(downloadFilename);
4✔
164
    try {
165
      this.context.getFileAccess().download(url, tmpDownloadFile);
6✔
166
      if (resolvedVersion.toString().equals("latest")) {
5!
167
        // Some software vendors violate best-practices and provide the latest version only under a fixed URL.
168
        // Therefore, if a newer version of that file gets released, the same URL suddenly leads to a different
169
        // download file with a newer version and a different checksum.
170
        // In order to still support such tools we had to implement this workaround so we cannot move the file in the
171
        // download cache for later reuse, cannot verify its checksum and also delete the downloaded file on exit
172
        // (after we assume it has been extracted) so we always ensure to get the LATEST version when requested.
173
        tmpDownloadFile.toFile().deleteOnExit();
×
174
        return tmpDownloadFile;
×
175
      } else {
176
        this.context.getFileAccess().move(tmpDownloadFile, target);
6✔
177
        return target;
2✔
178
      }
179
    } catch (RuntimeException e) {
×
180
      this.context.getFileAccess().delete(tmpDownloadFile);
×
181
      throw e;
×
182
    }
183
  }
184

185
  /**
186
   * @param filename the name of the temporary download file.
187
   * @return a {@link Path} to such file that does not yet exist.
188
   */
189
  protected Path createTempDownload(String filename) {
190

191
    Path tmpDownloads = this.context.getTempDownloadPath();
4✔
192
    Path tmpDownloadFile = tmpDownloads.resolve(filename);
4✔
193
    int i = 2;
2✔
194
    while (Files.exists(tmpDownloadFile)) {
5!
195
      tmpDownloadFile = tmpDownloads.resolve(filename + "." + i);
×
196
      i++;
×
197
      if (i > 5) {
×
198
        throw new IllegalStateException("Too many downloads of the same file: " + tmpDownloadFile);
×
199
      }
200
    }
201
    return tmpDownloadFile;
2✔
202
  }
203

204
  /**
205
   * Performs the checksum verification.
206
   *
207
   * @param file the downloaded software package to verify.
208
   * @param expectedChecksum the expected SHA-256 checksum.
209
   * @param expectedChecksumSource the source of the expected checksum (e.g. an URL or Path).
210
   */
211
  protected void verifyChecksum(Path file, String expectedChecksum, Object expectedChecksumSource) {
212

213
    String actualChecksum = this.context.getFileAccess().checksum(file);
×
214
    if (expectedChecksum.equals(actualChecksum)) {
×
215
      this.context.success("Checksum {} is correct.", actualChecksum);
×
216
    } else {
217
      throw new CliException("Downloaded file " + file + " has the wrong checksum!\n" //
×
218
          + "Expected " + expectedChecksum + "\n" //
219
          + "Download " + actualChecksum + "\n" //
220
          + "This could be a man-in-the-middle-attack, a download failure, or a release that has been updated afterwards.\n" //
221
          + "Please review carefully.\n" //
222
          + "Expected checksum can be found at " + expectedChecksumSource + ".\n" //
223
          + "Actual checksum (sha256sum) was computed from file " + file + "\n" //
224
          + "Installation was aborted for security reasons!");
225
    }
226
  }
×
227

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