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

devonfw / IDEasy / 7280603633

20 Dec 2023 08:45PM UTC coverage: 47.217% (-0.7%) from 47.873%
7280603633

Pull #160

github

web-flow
Merge af9910eb4 into 1d60d9c17
Pull Request #160: #126: Monitoring for ide-urls

1019 of 2393 branches covered (0.0%)

Branch coverage included in aggregate %.

2714 of 5513 relevant lines covered (49.23%)

2.03 hits per line

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

60.36
cli/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java
1
package com.devonfw.tools.ide.url.updater;
2

3
import java.io.IOException;
4
import java.io.InputStream;
5
import java.net.URI;
6
import java.net.http.HttpClient;
7
import java.net.http.HttpClient.Redirect;
8
import java.net.http.HttpRequest;
9
import java.net.http.HttpResponse;
10
import java.security.MessageDigest;
11
import java.security.NoSuchAlgorithmException;
12
import java.time.Duration;
13
import java.time.Instant;
14
import java.util.Collection;
15
import java.util.Locale;
16
import java.util.Objects;
17
import java.util.Set;
18

19
import org.slf4j.Logger;
20
import org.slf4j.LoggerFactory;
21

22
import com.devonfw.tools.ide.os.OperatingSystem;
23
import com.devonfw.tools.ide.os.SystemArchitecture;
24
import com.devonfw.tools.ide.url.model.UrlErrorReport;
25
import com.devonfw.tools.ide.url.model.UrlErrorState;
26
import com.devonfw.tools.ide.url.model.file.UrlChecksum;
27
import com.devonfw.tools.ide.url.model.file.UrlDownloadFile;
28
import com.devonfw.tools.ide.url.model.file.UrlFile;
29
import com.devonfw.tools.ide.url.model.file.UrlStatusFile;
30
import com.devonfw.tools.ide.url.model.file.json.StatusJson;
31
import com.devonfw.tools.ide.url.model.file.json.UrlStatus;
32
import com.devonfw.tools.ide.url.model.file.json.UrlStatusState;
33
import com.devonfw.tools.ide.url.model.folder.UrlEdition;
34
import com.devonfw.tools.ide.url.model.folder.UrlRepository;
35
import com.devonfw.tools.ide.url.model.folder.UrlTool;
36
import com.devonfw.tools.ide.url.model.folder.UrlVersion;
37
import com.devonfw.tools.ide.util.DateTimeUtil;
38
import com.devonfw.tools.ide.util.HexUtil;
39

40
/**
41
 * Abstract base implementation of {@link UrlUpdater}. Contains methods for retrieving response bodies from URLs,
42
 * updating tool versions, and checking if download URLs work.
43
 */
44
public abstract class AbstractUrlUpdater extends AbstractProcessorWithTimeout implements UrlUpdater {
2✔
45

46
  private static final Duration TWO_DAYS = Duration.ofDays(2);
3✔
47

48
  /** {@link OperatingSystem#WINDOWS}. */
49
  protected static final OperatingSystem WINDOWS = OperatingSystem.WINDOWS;
2✔
50

51
  /** {@link OperatingSystem#MAC}. */
52
  protected static final OperatingSystem MAC = OperatingSystem.MAC;
2✔
53

54
  /** {@link OperatingSystem#LINUX}. */
55
  protected static final OperatingSystem LINUX = OperatingSystem.LINUX;
2✔
56

57
  /** {@link SystemArchitecture#X64}. */
58
  protected static final SystemArchitecture X64 = SystemArchitecture.X64;
2✔
59

60
  /** {@link SystemArchitecture#ARM64}. */
61
  protected static final SystemArchitecture ARM64 = SystemArchitecture.ARM64;
2✔
62

63
  /** List of URL file names dependent on OS which need to be checked for existence */
64
  private static final Set<String> URL_FILENAMES_PER_OS = Set.of("linux_x64.urls", "mac_arm64.urls", "mac_x64.urls",
6✔
65
      "windows_x64.urls");
66

67
  /** List of URL file name independent of OS which need to be checked for existence */
68
  private static final Set<String> URL_FILENAMES_OS_INDEPENDENT = Set.of("urls");
3✔
69

70
  /** The {@link HttpClient} for HTTP requests. */
71
  protected final HttpClient client = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS).build();
6✔
72

73
  private static final Logger logger = LoggerFactory.getLogger(AbstractUrlUpdater.class);
4✔
74

75
  protected UrlErrorState urlErrorState = UrlErrorReport.getErrorState(getToolWithEdition());
6✔
76

77

78
  /**
79
   * @return the name of the {@link UrlTool tool} handled by this updater.
80
   */
81
  protected abstract String getTool();
82

83
  /**
84
   * @return the name of the {@link UrlEdition edition} handled by this updater.
85
   */
86
  protected String getEdition() {
87

88
    return getTool();
3✔
89
  }
90

91
  /**
92
   * @return the combination of {@link #getTool() tool} and {@link #getEdition() edition} but simplified if both are
93
   *         equal.
94
   */
95
  protected final String getToolWithEdition() {
96

97
    String tool = getTool();
3✔
98
    String edition = getEdition();
3✔
99
    if (tool.equals(edition)) {
4!
100
      return tool;
2✔
101
    }
102
    return tool + "/" + edition;
×
103
  }
104

105
  /**
106
   * Retrieves the response body from a given URL.
107
   *
108
   * @param url the URL to retrieve the response body from.
109
   * @return a string representing the response body.
110
   * @throws IllegalStateException if the response body could not be retrieved.
111
   */
112
  protected String doGetResponseBodyAsString(String url) {
113

114
    try {
115
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
7✔
116
      HttpResponse<String> response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
6✔
117
      if (response.statusCode() == 200) {
4✔
118
        return response.body();
4✔
119
      }
120
      throw new IllegalStateException("Unexpected response code " + response.statusCode() + ":" + response.body());
10✔
121
    } catch (Exception e) {
1✔
122
      throw new IllegalStateException("Failed to retrieve response body from url: " + url, e);
7✔
123
    }
124
  }
125

126
  /**
127
   * @param url the URL of the download file.
128
   * @return the {@link InputStream} of response body.
129
   */
130
  protected HttpResponse<InputStream> doGetResponseAsStream(String url) {
131

132
    try {
133
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
7✔
134
      return this.client.send(request, HttpResponse.BodyHandlers.ofInputStream());
6✔
135
    } catch (Exception e) {
×
136
      throw new IllegalStateException("Failed to retrieve response from url: " + url, e);
×
137
    }
138
  }
139

140
  /**
141
   * Updates a tool version with the given arguments (OS independent).
142
   *
143
   * @param urlVersion the {@link UrlVersion} with the {@link UrlVersion#getName() version-number} to process.
144
   * @param downloadUrl the URL of the download for the tool.
145
   * @return true if the version was successfully updated, false otherwise.
146
   */
147
  protected boolean doAddVersion(UrlVersion urlVersion, String downloadUrl) {
148

149
    return doAddVersion(urlVersion, downloadUrl, null);
6✔
150
  }
151

152
  /**
153
   * Updates a tool version with the given arguments.
154
   *
155
   * @param urlVersion the {@link UrlVersion} with the {@link UrlVersion#getName() version-number} to process.
156
   * @param downloadUrl the URL of the download for the tool.
157
   * @param os the {@link OperatingSystem} for the tool (can be null).
158
   * @return true if the version was successfully updated, false otherwise.
159
   */
160
  protected boolean doAddVersion(UrlVersion urlVersion, String downloadUrl, OperatingSystem os) {
161

162
    return doAddVersion(urlVersion, downloadUrl, os, null);
7✔
163
  }
164

165
  /**
166
   * Updates a tool version with the given arguments.
167
   *
168
   * @param urlVersion the {@link UrlVersion} with the {@link UrlVersion#getName() version-number} to process.
169
   * @param downloadUrl the URL of the download for the tool.
170
   * @param os the {@link OperatingSystem} for the tool (can be null).
171
   * @param architecture the optional {@link SystemArchitecture}.
172
   * @return true if the version was successfully updated, false otherwise.
173
   */
174
  protected boolean doAddVersion(UrlVersion urlVersion, String downloadUrl, OperatingSystem os,
175
      SystemArchitecture architecture) {
176

177
    return doAddVersion(urlVersion, downloadUrl, os, architecture, "");
8✔
178
  }
179

180
  /**
181
   * Updates a tool version with the given arguments.
182
   *
183
   * @param urlVersion the {@link UrlVersion} with the {@link UrlVersion#getName() version-number} to process.
184
   * @param url the URL of the download for the tool.
185
   * @param os the optional {@link OperatingSystem}.
186
   * @param architecture the optional {@link SystemArchitecture}.
187
   * @param checksum String of the checksum to utilize
188
   * @return {@code true} if the version was successfully updated, {@code false} otherwise.
189
   */
190
  protected boolean doAddVersion(UrlVersion urlVersion, String url, OperatingSystem os, SystemArchitecture architecture,
191
      String checksum) {
192

193
    UrlStatusFile status = urlVersion.getStatus();
3✔
194
    if ((status != null) && status.getStatusJson().isManual()) {
6!
195
      return true;
×
196
    }
197
    String version = urlVersion.getName();
3✔
198
    url = url.replace("${version}", version);
5✔
199
    String major = urlVersion.getVersionIdentifier().getStart().getDigits();
5✔
200
    url = url.replace("${major}", major);
5✔
201
    if (os != null) {
2✔
202
      url = url.replace("${os}", os.toString());
6✔
203
    }
204
    if (architecture != null) {
2✔
205
      url = url.replace("${arch}", architecture.toString());
6✔
206
    }
207
    url = url.replace("${edition}", getEdition());
6✔
208

209
    return checkDownloadUrl(url, urlVersion, os, architecture, checksum);
8✔
210

211
  }
212

213
  /**
214
   * @param response the {@link HttpResponse}.
215
   * @return {@code true} if success, {@code false} otherwise.
216
   */
217
  protected boolean isSuccess(HttpResponse<?> response) {
218

219
    if (response == null) {
2!
220
      return false;
×
221
    }
222
    return response.statusCode() == 200;
8✔
223
  }
224

225
  /**
226
   * Checks if the download file checksum is still valid
227
   *
228
   * @param url String of the URL to check
229
   * @param urlVersion the {@link UrlVersion} with the {@link UrlVersion#getName() version-number} to process.
230
   * @param os the {@link OperatingSystem}
231
   * @param architecture the {@link SystemArchitecture}
232
   * @param checksum String of the new checksum to check
233
   * @param tool String of the tool
234
   * @param version String of the version
235
   * @return {@code true} if update of checksum was successful, {@code false} otherwise.
236
   */
237
  private static boolean isChecksumStillValid(String url, UrlVersion urlVersion, OperatingSystem os,
238
      SystemArchitecture architecture, String checksum, String tool, String version) {
239

240
    UrlDownloadFile urlDownloadFile = urlVersion.getOrCreateUrls(os, architecture);
5✔
241
    UrlChecksum urlChecksum = urlVersion.getOrCreateChecksum(urlDownloadFile.getName());
5✔
242
    String oldChecksum = urlChecksum.getChecksum();
3✔
243

244
    if ((oldChecksum != null) && !Objects.equals(oldChecksum, checksum)) {
2!
245
      logger.error("For tool {} and version {} the mirror URL {} points to a different checksum {} but expected {}.",
×
246
          tool, version, url, checksum, oldChecksum);
247
      return false;
×
248
    } else {
249
      urlDownloadFile.addUrl(url);
3✔
250
      urlChecksum.setChecksum(checksum);
3✔
251
    }
252
    return true;
2✔
253
  }
254

255
  /**
256
   * Checks if the content type is valid (not of type text)
257
   *
258
   * @param url String of the url to check
259
   * @param tool String of the tool name
260
   * @param version String of the version
261
   * @param response the {@link HttpResponse}.
262
   * @return {@code true} if the content type is not of type text, {@code false} otherwise.
263
   */
264
  private boolean isValidDownload(String url, String tool, String version, HttpResponse<?> response) {
265

266
    if (isSuccess(response)) {
4✔
267
      String contentType = response.headers().firstValue("content-type").orElse("undefined");
8✔
268
      boolean isValidContentType = isValidContentType(contentType);
4✔
269
      if (!isValidContentType){
2✔
270
        logger.error("For tool {} and version {} the download has an invalid content type {} for URL {}", tool, version,
21✔
271
        contentType, url);
272
        return false;
2✔
273
      }
274
      return true;
2✔
275
    } else {
276
      return false;
2✔
277
    }
278
  }
279

280
  /**
281
   * Checks if the content type was not of type text (this method is required because {@link com.devonfw.tools.ide.tool.pip.PipUrlUpdater} returns text and needs to be overridden)
282
   * <p>
283
   * See: <a href="https://github.com/devonfw/ide/issues/1343">#1343</a> for reference.
284
   *
285
   * @param contentType String of the content type
286
   * @return {@code true} if the content type is not of type text, {@code false} otherwise.
287
   */
288
  protected boolean isValidContentType(String contentType) {
289

290
    return !contentType.startsWith("text");
8✔
291
  }
292

293
  /**
294
   * Checks the download URL by checksum or by downloading the file and generating the checksum from it
295
   *
296
   * @param url the URL of the download to check.
297
   * @param urlVersion the {@link UrlVersion} where to store the collected information like status and checksum.
298
   * @param os the {@link OperatingSystem}
299
   * @param architecture the {@link SystemArchitecture}
300
   * @return {@code true} if the download was checked successfully, {@code false} otherwise.
301
   */
302
  private boolean checkDownloadUrl(String url, UrlVersion urlVersion, OperatingSystem os,
303
      SystemArchitecture architecture, String checksum) {
304

305
    HttpResponse<?> response = doCheckDownloadViaHeadRequest(url);
4✔
306
    int statusCode = response.statusCode();
3✔
307
    String tool = getToolWithEdition();
3✔
308
    String version = urlVersion.getName();
3✔
309

310
    boolean success = isValidDownload(url, tool, version, response);
7✔
311

312
    // Checks if checksum for URL is already existing
313
    UrlDownloadFile urlDownloadFile = urlVersion.getUrls(os, architecture);
5✔
314
    if (urlDownloadFile != null) {
2✔
315
      UrlChecksum urlChecksum = urlVersion.getChecksum(urlDownloadFile.getName());
5✔
316
      if (urlChecksum != null) {
2!
317
        logger.warn("Checksum is already existing for: {}, skipping.", url);
4✔
318
        doUpdateStatusJson(success, statusCode, urlVersion, url, true);
7✔
319
        return true;
2✔
320
      }
321
    }
322

323
    if (success) {
2✔
324
      if (checksum == null || checksum.isEmpty()) {
5!
325
        String contentType = response.headers().firstValue("content-type").orElse("undefined");
8✔
326
        checksum = doGenerateChecksum(doGetResponseAsStream(url), url, version, contentType);
9✔
327
      }
328

329
      success = isChecksumStillValid(url, urlVersion, os, architecture, checksum, tool, version);
9✔
330
    }
331

332
    if (success) {
2✔
333
      urlVersion.save();
2✔
334
    }
335

336
    doUpdateStatusJson(success, statusCode, urlVersion, url, false);
7✔
337

338
    return success;
2✔
339
  }
340

341
  /**
342
   * @param response the {@link HttpResponse}.
343
   * @param url the download URL
344
   * @param version the {@link UrlVersion version} identifier.
345
   * @return checksum of input stream as hex string
346
   */
347
  private String doGenerateChecksum(HttpResponse<InputStream> response, String url, String version,
348
      String contentType) {
349

350
    try (InputStream inputStream = response.body()) {
4✔
351
      MessageDigest md = MessageDigest.getInstance(UrlChecksum.HASH_ALGORITHM);
3✔
352

353
      byte[] buffer = new byte[8192];
3✔
354
      int bytesRead;
355
      long size = 0;
2✔
356
      while ((bytesRead = inputStream.read(buffer)) != -1) {
7✔
357
        md.update(buffer, 0, bytesRead);
5✔
358
        size += bytesRead;
6✔
359
      }
360
      if (size == 0) {
4!
361
        throw new IllegalStateException("Download empty for " + url);
×
362
      }
363
      byte[] digestBytes = md.digest();
3✔
364
      String checksum = HexUtil.toHexString(digestBytes);
3✔
365
      logger.info(
8✔
366
          "For tool {} and version {} we received {} bytes with content-type {} and computed SHA256 {} from URL {}",
367
          getToolWithEdition(), version, Long.valueOf(size), contentType, checksum, url);
23✔
368
      return checksum;
4✔
369
    } catch (IOException e) {
×
370
      throw new IllegalStateException("Failed to read body of download " + url, e);
×
371
    } catch (NoSuchAlgorithmException e) {
×
372
      throw new IllegalStateException("No such hash algorithm " + UrlChecksum.HASH_ALGORITHM, e);
×
373
    }
374
  }
375

376
  /**
377
   * Checks if a download URL works and if the file is available for download.
378
   *
379
   * @param url the URL to check.
380
   * @return a URLRequestResult object representing the success or failure of the URL check.
381
   */
382
  protected HttpResponse<?> doCheckDownloadViaHeadRequest(String url) {
383

384
    try {
385
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url))
5✔
386
          .method("HEAD", HttpRequest.BodyPublishers.noBody()).timeout(Duration.ofSeconds(5)).build();
7✔
387

388
      return this.client.send(request, HttpResponse.BodyHandlers.ofString());
6✔
389
    } catch (Exception e) {
×
390
      logger.error("Failed to perform HEAD request of URL {}", url, e);
×
391
      return null;
×
392
    }
393
  }
394

395
  /**
396
   * Creates or refreshes the status JSON file for a given UrlVersion instance based on the URLRequestResult of checking
397
   * if a download URL works.
398
   *
399
   * @param success - {@code true} on successful HTTP response, {@code false} otherwise.
400
   * @param statusCode the HTTP status code of the response.
401
   * @param urlVersion the {@link UrlVersion} instance to create or refresh the status JSON file for.
402
   * @param url the checked download URL.
403
   * @param update - {@code true} in case the URL was updated (verification), {@code false} otherwise (version/URL
404
   *        initially added).
405
   */
406
  @SuppressWarnings("null") // Eclipse is too stupid to check this
407
  private void doUpdateStatusJson(boolean success, int statusCode, UrlVersion urlVersion, String url, boolean update) {
408

409
    UrlStatusFile urlStatusFile = null;
2✔
410
    StatusJson statusJson = null;
2✔
411
    UrlStatus status = null;
2✔
412
    UrlStatusState errorStatus = null;
2✔
413
    Instant errorTimestamp = null;
2✔
414
    UrlStatusState successStatus = null;
2✔
415
    Instant successTimestamp = null;
2✔
416
    if (success || update) {
4✔
417
      urlStatusFile = urlVersion.getOrCreateStatus();
3✔
418
      statusJson = urlStatusFile.getStatusJson();
3✔
419
      status = statusJson.getOrCreateUrlStatus(url);
4✔
420
      errorStatus = status.getError();
3✔
421
      if (errorStatus != null) {
2✔
422
        errorTimestamp = errorStatus.getTimestamp();
3✔
423
      }
424
      successStatus = status.getSuccess();
3✔
425
      if (successStatus != null) {
2✔
426
        successTimestamp = successStatus.getTimestamp();
3✔
427
      }
428
    }
429
    Integer code = Integer.valueOf(statusCode);
3✔
430
    String version = urlVersion.getName();
3✔
431
    String tool = getToolWithEdition();
3✔
432
    boolean modified = false;
2✔
433

434
    if (success) {
2✔
435
      boolean setSuccess = !update;
6✔
436

437
      if (errorStatus != null) {
2!
438
        // we avoid git diff overhead by only updating success timestamp if last check was an error
439
        setSuccess = DateTimeUtil.isAfter(errorTimestamp, successTimestamp);
×
440
      }
441

442
      if (setSuccess) {
2✔
443
        status.setSuccess(new UrlStatusState());
5✔
444
        modified = true;
2✔
445
      }
446

447
      logger.info("For tool {} and version {} the download verification succeeded with status code {} for URL {}.", tool,
21✔
448
          version, code, url);
449

450
      urlErrorState.updateVerifications(true);
4✔
451

452
    } else {
1✔
453
      if (status != null) {
2✔
454
        if (errorStatus == null) {
2✔
455
          modified = true;
3✔
456
        } else {
457
          if (!Objects.equals(code, errorStatus.getCode())) {
5!
458
            logger.warn("For tool {} and version {} the error status-code changed from {} to {} for URL {}.", tool,
20✔
459
                version, code, errorStatus.getCode(), url);
6✔
460
            modified = true;
2✔
461
          }
462
          if (!modified) {
2!
463
            // we avoid git diff overhead by only updating error timestamp if last check was a success
464
            if (DateTimeUtil.isAfter(successTimestamp, errorTimestamp)) {
×
465
              modified = true;
×
466
            }
467
          }
468
        }
469
        if (modified) {
2!
470
          errorStatus = new UrlStatusState();
4✔
471
          errorStatus.setCode(code);
3✔
472
          status.setError(errorStatus);
3✔
473
        }
474
      }
475
      logger.warn("For tool {} and version {} the download verification failed with status code {} for URL {}.", tool,
21✔
476
          version, code, url);
477

478
      urlErrorState.updateVerifications(false);
4✔
479
    }
480
    if (modified) {
2✔
481
      urlStatusFile.setStatusJson(statusJson); // hack to set modified (better solution welcome)
3✔
482
    }
483
  }
1✔
484

485
  /**
486
   * @return Set of URL file names (dependency on OS file names can be overridden with isOsDependent())
487
   */
488
  protected Set<String> getUrlFilenames() {
489

490
    if (isOsDependent()) {
3!
491
      return URL_FILENAMES_PER_OS;
2✔
492
    } else {
493
      return URL_FILENAMES_OS_INDEPENDENT;
×
494
    }
495
  }
496

497
  /**
498
   * Checks if we are dependent on OS URL file names, can be overridden to disable OS dependency
499
   *
500
   * @return true if we want to check for missing OS URL file names, false if not
501
   */
502
  protected boolean isOsDependent() {
503

504
    return true;
2✔
505
  }
506

507
  /**
508
   * Checks if an OS URL file name was missing in {@link UrlVersion}
509
   *
510
   * @param urlVersion the {@link UrlVersion} to check
511
   * @return true if an OS type was missing, false if not
512
   */
513
  public boolean isMissingOs(UrlVersion urlVersion) {
514

515
    Set<String> childNames = urlVersion.getChildNames();
3✔
516
    Set<String> osTypes = getUrlFilenames();
3✔
517
    // invert result of containsAll to avoid negative condition
518
    return !childNames.containsAll(osTypes);
8✔
519
  }
520

521
  /**
522
   * Updates the tool's versions in the URL repository.
523
   *
524
   * @param urlRepository the {@link UrlRepository} to update
525
   */
526
  @Override
527
  public void update(UrlRepository urlRepository) {
528

529
    UrlTool tool = urlRepository.getOrCreateChild(getTool());
6✔
530
    UrlEdition edition = tool.getOrCreateChild(getEdition());
6✔
531
    //updateExistingVersions(edition);
532
    Set<String> versions = getVersions();
3✔
533
    String toolWithEdition = getToolWithEdition();
3✔
534
    logger.info("For tool {} we found the following versions : {}", toolWithEdition, versions);
5✔
535

536
    for (String version : versions) {
10✔
537

538
      if (isTimeoutExpired()) {
3!
539
        break;
×
540
      }
541

542
      UrlVersion urlVersion = edition.getChild(version);
5✔
543
      if (urlVersion == null || isMissingOs(urlVersion)) {
6✔
544
        try {
545
          urlVersion = edition.getOrCreateChild(version);
5✔
546
          addVersion(urlVersion);
3✔
547
          urlVersion.save();
2✔
548
          urlErrorState.updateAdditions(true);
4✔
549
        } catch (Exception e) {
×
550
          logger.error("For tool {} we failed to add version {}.", toolWithEdition, version, e);
×
551
          urlErrorState.updateAdditions(false);
×
552
        }
1✔
553
      }
554
    }
1✔
555
  }
1✔
556

557
  /**
558
   * Update existing versions of the tool in the URL repository.
559
   *
560
   * @param edition the {@link UrlEdition} to update
561
   */
562
  protected void updateExistingVersions(UrlEdition edition) {
563

564
    Set<String> existingVersions = edition.getChildNames();
3✔
565
    for (String version : existingVersions) {
6!
566
      UrlVersion urlVersion = edition.getChild(version);
×
567
      if (urlVersion != null) {
×
568
        UrlStatusFile urlStatusFile = urlVersion.getOrCreateStatus();
×
569
        StatusJson statusJson = urlStatusFile.getStatusJson();
×
570
        if (statusJson.isManual()) {
×
571
          logger.info("For tool {} the version {} is set to manual, hence skipping update", getToolWithEdition(),
×
572
              version);
573
        } else {
574
          updateExistingVersion(version, urlVersion, statusJson, urlStatusFile);
×
575
          urlVersion.save();
×
576
        }
577
      }
578
    }
×
579
  }
1✔
580

581
  private void updateExistingVersion(String version, UrlVersion urlVersion, StatusJson statusJson,
582
      UrlStatusFile urlStatusFile) {
583

584
    boolean modified = false;
×
585
    String toolWithEdition = getToolWithEdition();
×
586
    Instant now = Instant.now();
×
587
    for (UrlFile<?> child : urlVersion.getChildren()) {
×
588
      if (child instanceof UrlDownloadFile) {
×
589
        Set<String> urls = ((UrlDownloadFile) child).getUrls();
×
590
        for (String url : urls) {
×
591
          if (shouldVerifyDownloadUrl(version, statusJson, toolWithEdition, now)) {
×
592
            HttpResponse<?> response = doCheckDownloadViaHeadRequest(url);
×
593
            doUpdateStatusJson(isSuccess(response), response.statusCode(), urlVersion, url, true);
×
594
            modified = true;
×
595
          }
596
        }
×
597
      }
598
    }
×
599
    if (modified) {
×
600
      urlStatusFile.save();
×
601
    }
602
  }
×
603

604
  private boolean shouldVerifyDownloadUrl(String url, StatusJson statusJson, String toolWithEdition, Instant now) {
605

606
    UrlStatus urlStatus = statusJson.getOrCreateUrlStatus(url);
×
607
    UrlStatusState success = urlStatus.getSuccess();
×
608
    if (success != null) {
×
609
      Instant timestamp = success.getTimestamp();
×
610
      Integer delta = DateTimeUtil.compareDuration(timestamp, now, TWO_DAYS);
×
611
      if ((delta != null) && (delta.intValue() <= 0)) {
×
612
        logger.debug("For tool {} the URL {} has already been checked recently on {}", toolWithEdition, url, timestamp);
×
613
        return false;
×
614
      }
615
    }
616
    return true;
×
617
  }
618

619
  /**
620
   * Finds all currently available versions of the {@link UrlEdition tool edition}.
621
   *
622
   * @return the {@link Set} with all current versions.
623
   */
624
  protected abstract Set<String> getVersions();
625

626
  /**
627
   * @param version the original version (e.g. "v1.0").
628
   * @return the transformed version (e.g. "1.0") or {@code null} to filter and omit the given version.
629
   */
630
  protected String mapVersion(String version) {
631

632
    String prefix = getVersionPrefixToRemove();
×
633
    if ((prefix != null) && version.startsWith(prefix)) {
×
634
      version = version.substring(prefix.length());
×
635
    }
636
    String vLower = version.toLowerCase(Locale.ROOT);
×
637
    if (vLower.contains("alpha") || vLower.contains("beta") || vLower.contains("dev") || vLower.contains("snapshot")
×
638
        || vLower.contains("preview") || vLower.contains("test") || vLower.contains("tech-preview") //
×
639
        || vLower.contains("-pre") || vLower.startsWith("ce-")
×
640
        // vscode nonsense
641
        || vLower.startsWith("bad") || vLower.contains("vsda-") || vLower.contains("translation/")
×
642
        || vLower.contains("-insiders")) {
×
643
      return null;
×
644
    }
645
    return version;
×
646
  }
647

648
  /**
649
   * @return the optional version prefix that has to be removed (e.g. "v").
650
   */
651
  protected String getVersionPrefixToRemove() {
652

653
    return null;
×
654
  }
655

656
  /**
657
   * @param version the version to add (e.g. "1.0").
658
   * @param versions the {@link Collection} with the versions to collect.
659
   */
660
  protected final void addVersion(String version, Collection<String> versions) {
661

662
    String mappedVersion = mapVersion(version);
×
663
    if ((mappedVersion == null) || mappedVersion.isBlank()) {
×
664
      logger.debug("Filtered version {}", version);
×
665
      return;
×
666
    } else if (!mappedVersion.equals(version)) {
×
667
      logger.debug("Mapped version {} to {}", version, mappedVersion);
×
668
    }
669
    boolean added = versions.add(mappedVersion);
×
670
    if (!added) {
×
671
      logger.warn("Duplicate version {}", version);
×
672
    }
673
  }
×
674

675
  /**
676
   * Updates the version of a given URL version.
677
   *
678
   * @param urlVersion the {@link UrlVersion} to be updated
679
   */
680
  protected abstract void addVersion(UrlVersion urlVersion);
681

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