From 0064ccadf955b66ba2b7b1460eccf0a31f3f175e Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Fri, 24 Apr 2026 14:22:13 +0200 Subject: [PATCH 1/7] cleanups --- .../DefaultExtensionQueryRequestHandler.java | 38 ++++---- .../IExtensionQueryRequestHandler.java | 1 - .../openvsx/adapter/IVSCodeService.java | 1 - .../openvsx/adapter/LocalVSCodeService.java | 81 ++++++++-------- .../eclipse/openvsx/adapter/PublicIds.java | 3 +- .../adapter/UpstreamVSCodeService.java | 22 +++-- .../eclipse/openvsx/adapter/VSCodeAPI.java | 16 ++-- .../openvsx/adapter/VSCodeIdService.java | 23 ++--- .../adapter/VSCodeIdUpdateService.java | 95 ++++++++++--------- .../openvsx/adapter/WebResourceService.java | 21 ++-- .../org/eclipse/openvsx/util/UrlUtil.java | 7 +- 11 files changed, 156 insertions(+), 152 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java index 699b5e972..266500d04 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/DefaultExtensionQueryRequestHandler.java @@ -18,12 +18,26 @@ public class DefaultExtensionQueryRequestHandler implements IExtensionQueryRequestHandler { - private LocalVSCodeService local; - private UpstreamVSCodeService upstream; + private final LocalVSCodeService local; + private final UpstreamVSCodeService upstream; + private final List registries; public DefaultExtensionQueryRequestHandler(LocalVSCodeService local, UpstreamVSCodeService upstream) { this.local = local; this.upstream = upstream; + this.registries = setupRegistries(); + } + + private List setupRegistries() { + if (upstream.isValid()) { + return List.of(local, upstream); + } else { + return List.of(local); + } + } + + private Iterable getVSCodeServices() { + return registries; } @Override @@ -38,7 +52,7 @@ public ExtensionQueryResult getResult(ExtensionQueryParam param, int pageSize, i var service = services.next(); if(extensions.isEmpty()) { var subResult = service.extensionQuery(param, defaultPageSize); - var subExtensions = subResult.results().get(0).extensions(); + var subExtensions = subResult.results().getFirst().extensions(); if(subExtensions != null) { extensions.addAll(subExtensions); } @@ -47,7 +61,7 @@ public ExtensionQueryResult getResult(ExtensionQueryParam param, int pageSize, i } else { var extensionCount = extensions.size(); var subResult = service.extensionQuery(param, defaultPageSize); - var subExtensions = subResult.results().get(0).extensions(); + var subExtensions = subResult.results().getFirst().extensions(); var subExtensionsCount = subExtensions != null ? subExtensions.size() : 0; if (subExtensionsCount > 0) { int limit = pageSize - extensionCount; @@ -66,18 +80,10 @@ public ExtensionQueryResult getResult(ExtensionQueryParam param, int pageSize, i return local.toQueryResult(extensions, totalCount); } - private Iterable getVSCodeServices() { - var registries = new ArrayList(); - registries.add(local); - if (upstream.isValid()) - registries.add(upstream); - return registries; - } - private void mergeExtensionQueryResults(List extensions, Set extensionIds, List subExtensions, int limit) { if(extensionIds.isEmpty() && !extensions.isEmpty()) { var extensionIdSet = extensions.stream() - .map(extension -> NamingUtil.toExtensionId(extension)) + .map(NamingUtil::toExtensionId) .collect(Collectors.toSet()); extensionIds.addAll(extensionIdSet); @@ -95,15 +101,15 @@ private void mergeExtensionQueryResults(List ext } private long getTotalCount(ExtensionQueryResult subResult) { - return subResult.results().get(0).resultMetadata().stream() + return subResult.results().getFirst().resultMetadata().stream() .filter(metadata -> metadata.metadataType().equals("ResultCount")) .findFirst() - .map(metadata -> metadata.metadataItems()) + .map(ExtensionQueryResult.ResultMetadata::metadataItems) .orElseGet(Collections::emptyList) .stream() .filter(item -> item.name().equals("TotalCount")) .findFirst() - .map(item -> item.count()) + .map(ExtensionQueryResult.ResultMetadataItem::count) .orElse(0L); } } diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/IExtensionQueryRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/adapter/IExtensionQueryRequestHandler.java index bccd6246c..2f53e7848 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/IExtensionQueryRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/IExtensionQueryRequestHandler.java @@ -10,6 +10,5 @@ package org.eclipse.openvsx.adapter; public interface IExtensionQueryRequestHandler { - ExtensionQueryResult getResult(ExtensionQueryParam param, int pageSize, int defaultPageSize); } diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java index 84778d2ad..435689514 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java @@ -13,7 +13,6 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; public interface IVSCodeService { - ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaultPageSize); ResponseEntity browse(String namespaceName, String extensionName, String version, String path); diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java index 854661ff7..28dec3310 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java @@ -28,7 +28,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @@ -48,9 +48,9 @@ import static org.eclipse.openvsx.adapter.ExtensionQueryResult.Statistic.*; import static org.eclipse.openvsx.entities.FileResource.*; -@Component +@Service public class LocalVSCodeService implements IVSCodeService { - protected final Logger logger = LoggerFactory.getLogger(LocalVSCodeService.class); + private final Logger logger = LoggerFactory.getLogger(LocalVSCodeService.class); private final RepositoryService repositories; private final VersionService versions; @@ -101,7 +101,7 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul extensionIds = Collections.emptySet(); extensionNames = Collections.emptySet(); } else { - var filter = param.filters().get(0); + var filter = param.filters().getFirst(); extensionIds = new HashSet<>(filter.findCriteria(FILTER_EXTENSION_ID)); extensionNames = new HashSet<>(filter.findCriteria(FILTER_EXTENSION_NAME)); @@ -157,7 +157,7 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul throw new ResponseStatusException(HttpStatus.BAD_REQUEST, exc.getMessage(), exc); } } - if(totalCount == null) { + if (totalCount == null) { totalCount = (long) extensionsList.size(); } @@ -195,7 +195,7 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul Map> fileResources; if (test(flags, FLAG_INCLUDE_FILES) && !extensionVersionsMap.isEmpty()) { var types = new ArrayList<>(List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, CHANGELOG, VSIXMANIFEST)); - if(integrityService.isEnabled()) { + if (integrityService.isEnabled()) { types.add(DOWNLOAD_SIG); } @@ -235,11 +235,11 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul } private String createFileUrl(List singleResource, String fileBaseUrl) { - if(singleResource == null || singleResource.isEmpty()) { + if (singleResource == null || singleResource.isEmpty()) { return null; } - return createFileUrl(singleResource.get(0), fileBaseUrl); + return createFileUrl(singleResource.getFirst(), fileBaseUrl); } private String createFileUrl(FileResource resource, String fileBaseUrl) { @@ -258,24 +258,23 @@ public ExtensionQueryResult toQueryResult(List e } private String getSortBy(int sortBy) { - switch (sortBy) { - case 4: // InstallCount - return SortBy.DOWNLOADS; - case 5: // PublishedDate - return SortBy.TIMESTAMP; - case 6: // AverageRating - return SortBy.RATING; - default: - return SortBy.RELEVANCE; - } + return switch (sortBy) { + // InstallCount + case 4 -> SortBy.DOWNLOADS; + // PublishedDate + case 5 -> SortBy.TIMESTAMP; + // AverageRating + case 6 -> SortBy.RATING; + default -> SortBy.RELEVANCE; + }; } private String getSortOrder(int sortOrder) { - switch (sortOrder) { - case 1: // Ascending - return "asc"; - default: - return "desc"; + if (sortOrder == 1) { + // Ascending + return "asc"; + } else { + return "desc"; } } @@ -285,18 +284,18 @@ public ResponseEntity getAsset( String namespace, String extensionName, String version, String assetType, String targetPlatform, String restOfTheUrl ) { - if(BuiltInExtensionUtil.isBuiltIn(namespace)) { + if (BuiltInExtensionUtil.isBuiltIn(namespace)) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(builtinExtensionResponse()); } var asset = (restOfTheUrl != null && !restOfTheUrl.isEmpty()) ? (assetType + "/" + restOfTheUrl) : assetType; - if((asset.equals(FILE_PUBLIC_KEY) || asset.equals(FILE_SIGNATURE)) && !integrityService.isEnabled()) { + if ((asset.equals(FILE_PUBLIC_KEY) || asset.equals(FILE_SIGNATURE)) && !integrityService.isEnabled()) { throw new NotFoundException(); } - if(asset.equals(FILE_PUBLIC_KEY)) { + if (asset.equals(FILE_PUBLIC_KEY)) { var publicId = repositories.findSignatureKeyPairPublicId(namespace, extensionName, targetPlatform, version); - if(publicId == null) { + if (publicId == null) { throw new NotFoundException(); } else { return ResponseEntity @@ -318,7 +317,7 @@ public ResponseEntity getAsset( ); var type = assets.get(assetType); - if(type != null) { + if (type != null) { var resource = repositories.findFileByType(namespace, extensionName, targetPlatform, version, type); if (resource == null) { throw new NotFoundException(); @@ -328,11 +327,11 @@ public ResponseEntity getAsset( } return storageUtil.getFileResponse(resource); - } else if(asset.startsWith(FILE_WEB_RESOURCES + "/extension/")) { + } else if (asset.startsWith(FILE_WEB_RESOURCES + "/extension/")) { var name = asset.substring((FILE_WEB_RESOURCES.length() + 1)); var extensionDownloadPath = webResources.getExtensionDownload(namespace, extensionName, targetPlatform, version); var file = extensionDownloadPath != null ? getWebResource(namespace, extensionName, targetPlatform, version, name, extensionDownloadPath) : null; - if(file != null) { + if (file != null) { return storageUtil.getFileResponse(file); } } @@ -342,7 +341,7 @@ public ResponseEntity getAsset( private Path getWebResource(String namespaceName, String extensionName, String targetPlatform, String version, String name, Path extensionDownloadPath) { var file = webResources.getWebResource(namespaceName, extensionName, targetPlatform, version, name, extensionDownloadPath); - if(file != null && !Files.exists(file)) { + if (file != null && !Files.exists(file)) { logger.error("File doesn't exist {}", file); cache.evictWebResourceFile(namespaceName, extensionName, targetPlatform, version, name); file = null; @@ -360,7 +359,7 @@ private StreamingResponseBody builtinExtensionResponse() { @Override public String getItemUrl(String namespaceName, String extensionName) { - if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { + if (BuiltInExtensionUtil.isBuiltIn(namespaceName)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, builtinExtensionMessage()); } @@ -374,7 +373,7 @@ public String getItemUrl(String namespaceName, String extensionName) { @Override public String download(String namespaceName, String extensionName, String version, String targetPlatform) { - if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { + if (BuiltInExtensionUtil.isBuiltIn(namespaceName)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, builtinExtensionMessage()); } @@ -383,12 +382,12 @@ public String download(String namespaceName, String extensionName, String versio throw new NotFoundException(); } - if(resource.getStorageType().equals(STORAGE_LOCAL)) { + if (resource.getStorageType().equals(STORAGE_LOCAL)) { var extVersion = resource.getExtension(); var extension = extVersion.getExtension(); var namespace = extension.getNamespace(); var apiUrl = UrlUtil.createApiUrl(UrlUtil.getBaseUrl(), "vscode", "asset", namespace.getName(), extension.getName(), extVersion.getVersion(), FILE_VSIX); - if(!TargetPlatform.isUniversal(extVersion.getTargetPlatform())) { + if (!TargetPlatform.isUniversal(extVersion.getTargetPlatform())) { apiUrl = UrlUtil.addQuery(apiUrl, "targetPlatform", extVersion.getTargetPlatform()); } @@ -402,22 +401,22 @@ public String download(String namespaceName, String extensionName, String versio @Observed @Override public ResponseEntity browse(String namespaceName, String extensionName, String version, String path) { - if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { + if (BuiltInExtensionUtil.isBuiltIn(namespaceName)) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(builtinExtensionResponse()); } var extensionDownloadPath = webResources.getExtensionDownload(namespaceName, extensionName, null, version); - if(extensionDownloadPath == null) { + if (extensionDownloadPath == null) { throw new NotFoundException(); } var file = getWebResource(namespaceName, extensionName, null, version, path, extensionDownloadPath); - if(file != null) { + if (file != null) { return storageUtil.getFileResponse(file); } var node = webResources.browseExtensionPackage(namespaceName, extensionName, null, version, path, extensionDownloadPath); - if(node != null) { + if (node != null) { return storageUtil.getFileResponse(node); } @@ -507,7 +506,7 @@ private ExtensionQueryResult.ExtensionVersion toQueryVersion( } List files = null; - if(fileResources.containsKey(extVer.getId())) { + if (fileResources.containsKey(extVer.getId())) { var resourcesByType = fileResources.get(extVer.getId()).stream() .collect(Collectors.groupingBy(FileResource::getType)); @@ -522,7 +521,7 @@ private ExtensionQueryResult.ExtensionVersion toQueryVersion( addQueryExtensionVersionFile(files, FILE_CHANGELOG, createFileUrl(resourcesByType.get(CHANGELOG), fileBaseUrl)); addQueryExtensionVersionFile(files, FILE_VSIXMANIFEST, createFileUrl(resourcesByType.get(VSIXMANIFEST), fileBaseUrl)); addQueryExtensionVersionFile(files, FILE_SIGNATURE, createFileUrl(resourcesByType.get(DOWNLOAD_SIG), fileBaseUrl)); - if(resourcesByType.containsKey(DOWNLOAD_SIG)) { + if (resourcesByType.containsKey(DOWNLOAD_SIG)) { addQueryExtensionVersionFile(files, FILE_PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVer)); } } diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/PublicIds.java b/server/src/main/java/org/eclipse/openvsx/adapter/PublicIds.java index cb25d857d..ea0f20358 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/PublicIds.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/PublicIds.java @@ -9,5 +9,4 @@ * ****************************************************************************** */ package org.eclipse.openvsx.adapter; -public record PublicIds(String namespace, String extension) { -} +public record PublicIds(String namespace, String extension) {} diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java index 21f2b458a..a824cdc3d 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java @@ -21,7 +21,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.*; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.ResponseExtractor; import org.springframework.web.client.RestClientException; @@ -36,13 +36,13 @@ import java.util.Map; import java.util.Optional; -@Component +@Service public class UpstreamVSCodeService implements IVSCodeService { private static final String VAR_NAMESPACE = "namespace"; private static final String VAR_EXTENSION = "extension"; private static final String VAR_VERSION = "version"; - protected final Logger logger = LoggerFactory.getLogger(UpstreamVSCodeService.class); + private final Logger logger = LoggerFactory.getLogger(UpstreamVSCodeService.class); private final RestTemplate restTemplate; private UpstreamProxyService proxy; @@ -77,11 +77,11 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul } var statusCode = response.getStatusCode(); - if(statusCode.is2xxSuccessful()) { + if (statusCode.is2xxSuccessful()) { var json = response.getBody(); return proxy != null ? proxy.rewriteUrls(json) : json; } - if(statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { + if (statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { logger.error("POST {}: {}", urlTemplate, response); } @@ -241,9 +241,13 @@ public ResponseEntity getAsset(String namespace, String e @Override public ResponseEntity extractData(ClientHttpResponse response) throws IOException { var statusCode = response.getStatusCode(); - if(statusCode.is3xxRedirection()) { + if (statusCode.is3xxRedirection()) { var location = response.getHeaders().getLocation(); - if(proxy != null) { + if (location == null) { + return ResponseEntity.internalServerError().build(); + } + + if (proxy != null) { location = proxy.rewriteUrl(location); } @@ -251,9 +255,9 @@ public ResponseEntity extractData(ClientHttpResponse resp .headers(response.getHeaders()) .location(location) .build(); - } else if(statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { + } else if (statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { handleResponseError(urlTemplate, uriVariables, response); - } else if(!statusCode.is2xxSuccessful()) { + } else if (!statusCode.is2xxSuccessful()) { throw new NotFoundException(); } diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java index 4e931a99d..2e8d25c3b 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java @@ -34,7 +34,6 @@ import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -45,12 +44,12 @@ @RestController public class VSCodeAPI { - private static final int DEFAULT_PAGE_SIZE = 20; private static final Logger logger = LoggerFactory.getLogger(VSCodeAPI.class); private final LocalVSCodeService local; private final UpstreamVSCodeService upstream; + private final List registries; private final IExtensionQueryRequestHandler extensionQueryRequestHandler; public VSCodeAPI( @@ -60,14 +59,19 @@ public VSCodeAPI( ) { this.local = local; this.upstream = upstream; + this.registries = setupRegistries(); this.extensionQueryRequestHandler = extensionQueryRequestHandler; } + private List setupRegistries() { + if (upstream.isValid()) { + return List.of(local, upstream); + } else { + return List.of(local); + } + } + private Iterable getVSCodeServices() { - var registries = new ArrayList(); - registries.add(local); - if (upstream.isValid()) - registries.add(upstream); return registries; } diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java index 09baea1ad..e45c5d768 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java @@ -18,28 +18,24 @@ import org.eclipse.openvsx.util.UrlUtil; import org.jobrunr.scheduling.JobRequestScheduler; import org.jobrunr.scheduling.cron.Cron; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.event.EventListener; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.ZoneId; import java.util.List; import java.util.UUID; -@Component +@Service public class VSCodeIdService { - + // TODO: check if this version is still valid when connecting to the VSC Marketplace private static final String API_VERSION = "3.0-preview.1"; - protected final Logger logger = LoggerFactory.getLogger(VSCodeIdService.class); - private final RestTemplate vsCodeIdRestTemplate; private final UrlConfigService urlConfigService; private final JobRequestScheduler scheduler; @@ -65,10 +61,10 @@ public VSCodeIdService( @EventListener public void applicationStarted(ApplicationStartedEvent event) { - if(mirrorEnabled) { + if (mirrorEnabled) { return; } - if(updateOnStart) { + if (updateOnStart) { scheduler.schedule(TimeUtil.getCurrentUTC().plusSeconds(delay), new HandlerJobRequest<>(VSCodeIdDailyUpdateJobRequestHandler.class)); } @@ -106,9 +102,9 @@ private ExtensionQueryResult.Extension getUpstreamExtension(Extension extension) headers.set(HttpHeaders.ACCEPT, "application/json;api-version=" + API_VERSION); var result = vsCodeIdRestTemplate.postForObject(requestUrl, new HttpEntity<>(requestData, headers), ExtensionQueryResult.class); if (result != null && result.results() != null && !result.results().isEmpty()) { - var item = result.results().get(0); + var item = result.results().getFirst(); if (item.extensions() != null && !item.extensions().isEmpty()) { - return item.extensions().get(0); + return item.extensions().getFirst(); } } @@ -127,9 +123,6 @@ private ExtensionQueryParam createRequestData(Extension extension) { ) ); - return new ExtensionQueryParam( - List.of(new ExtensionQueryParam.Filter(criteria, 1, 1, 0, 0)), - 0 - ); + return new ExtensionQueryParam(List.of(new ExtensionQueryParam.Filter(criteria, 1, 1, 0, 0)), 0); } } diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java index 8c4192365..aed4c7692 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java @@ -11,19 +11,20 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.entities.Extension; +import org.eclipse.openvsx.entities.Namespace; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.util.BuiltInExtensionUtil; import org.eclipse.openvsx.util.NamingUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import java.util.*; import java.util.stream.Collectors; -@Component +@Service public class VSCodeIdUpdateService { - private static final Logger LOGGER = LoggerFactory.getLogger(VSCodeIdUpdateService.class); + private final Logger logger = LoggerFactory.getLogger(VSCodeIdUpdateService.class); private final RepositoryService repositories; private final VSCodeIdService service; @@ -34,8 +35,8 @@ public VSCodeIdUpdateService(RepositoryService repositories, VSCodeIdService ser } public void update(String namespaceName, String extensionName) { - if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { - LOGGER.atDebug() + if (BuiltInExtensionUtil.isBuiltIn(namespaceName)) { + logger.atDebug() .setMessage("SKIP BUILT-IN EXTENSION {}") .addArgument(() -> NamingUtil.toExtensionId(namespaceName, extensionName)) .log(); @@ -45,56 +46,56 @@ public void update(String namespaceName, String extensionName) { var extension = repositories.findPublicId(namespaceName, extensionName); var extensionUpdates = new HashMap(); updateExtensionPublicId(extension, extensionUpdates, false); - if(!extensionUpdates.isEmpty()) { + if (!extensionUpdates.isEmpty()) { repositories.updateExtensionPublicIds(extensionUpdates); } var namespaceUpdates = new HashMap(); updateNamespacePublicId(extension, namespaceUpdates, false); - if(!namespaceUpdates.isEmpty()) { + if (!namespaceUpdates.isEmpty()) { repositories.updateNamespacePublicIds(namespaceUpdates); } } private void updateExtensionPublicId(Extension extension, Map updates, boolean mustUpdate) { - LOGGER.atDebug() + logger.atDebug() .setMessage("updateExtensionPublicId: {}") .addArgument(() -> NamingUtil.toExtensionId(extension)) .log(); var oldPublicId = extension.getPublicId(); var newPublicId = service.getUpstreamPublicIds(extension).extension(); - if(newPublicId == null || (mustUpdate && newPublicId.equals(oldPublicId))) { + if (newPublicId == null || (mustUpdate && newPublicId.equals(oldPublicId))) { do { newPublicId = service.getRandomPublicId(); - LOGGER.debug("RANDOM EXTENSION PUBLIC ID: {}", newPublicId); - } while(updates.containsValue(newPublicId) || repositories.extensionPublicIdExists(newPublicId)); - LOGGER.debug("RANDOM PUT UPDATE: {} - {}", extension.getId(), newPublicId); + logger.debug("RANDOM EXTENSION PUBLIC ID: {}", newPublicId); + } while (updates.containsValue(newPublicId) || repositories.extensionPublicIdExists(newPublicId)); + logger.debug("RANDOM PUT UPDATE: {} - {}", extension.getId(), newPublicId); updates.put(extension.getId(), newPublicId); } else if (!newPublicId.equals(oldPublicId)) { - LOGGER.debug("UPSTREAM PUT UPDATE: {} - {}", extension.getId(), newPublicId); + logger.debug("UPSTREAM PUT UPDATE: {} - {}", extension.getId(), newPublicId); updates.put(extension.getId(), newPublicId); var duplicatePublicId = repositories.findPublicId(newPublicId); - if(duplicatePublicId != null) { + if (duplicatePublicId != null) { updateExtensionPublicId(duplicatePublicId, updates, true); } } } private void updateNamespacePublicId(Extension extension, Map updates, boolean mustUpdate) { - LOGGER.debug("updateNamespacePublicId: {}", extension.getNamespace().getName()); + logger.debug("updateNamespacePublicId: {}", extension.getNamespace().getName()); var oldPublicId = extension.getNamespace().getPublicId(); var newPublicId = service.getUpstreamPublicIds(extension).namespace(); var id = extension.getNamespace().getId(); - if(newPublicId == null || (mustUpdate && newPublicId.equals(oldPublicId))) { + if (newPublicId == null || (mustUpdate && newPublicId.equals(oldPublicId))) { do { newPublicId = service.getRandomPublicId(); - LOGGER.debug("RANDOM NAMESPACE PUBLIC ID: {}", newPublicId); + logger.debug("RANDOM NAMESPACE PUBLIC ID: {}", newPublicId); } while(updates.containsValue(newPublicId) || repositories.namespacePublicIdExists(newPublicId)); - LOGGER.debug("RANDOM PUT UPDATE: {} - {}", id, newPublicId); + logger.debug("RANDOM PUT UPDATE: {} - {}", id, newPublicId); updates.put(id, newPublicId); - } else if(!newPublicId.equals(oldPublicId)) { - LOGGER.debug("UPSTREAM PUT UPDATE: {} - {}", id, newPublicId); + } else if (!newPublicId.equals(oldPublicId)) { + logger.debug("UPSTREAM PUT UPDATE: {} - {}", id, newPublicId); updates.put(id, newPublicId); var duplicatePublicId = repositories.findNamespacePublicId(newPublicId); if(duplicatePublicId != null) { @@ -104,64 +105,64 @@ private void updateNamespacePublicId(Extension extension, Map upda } public void updateAll() { - LOGGER.debug("DAILY UPDATE ALL"); + logger.debug("DAILY UPDATE ALL"); var extensions = repositories.findAllPublicIds(); var extensionPublicIdsMap = extensions.stream() .filter(e -> StringUtils.isNotEmpty(e.getPublicId())) - .collect(Collectors.toMap(e -> e.getId(), e -> e.getPublicId())); + .collect(Collectors.toMap(Extension::getId, Extension::getPublicId)); var namespacePublicIdsMap = extensions.stream() - .map(e -> e.getNamespace()) + .map(Extension::getNamespace) .filter(n -> StringUtils.isNotEmpty(n.getPublicId())) - .collect(Collectors.toMap(n -> n.getId(), n -> n.getPublicId(), (id1, id2) -> id1)); + .collect(Collectors.toMap(Namespace::getId, Namespace::getPublicId, (id1, _) -> id1)); var upstreamExtensionPublicIds = new HashMap(); var upstreamNamespacePublicIds = new HashMap(); for(var extension : extensions) { if(BuiltInExtensionUtil.isBuiltIn(extension)) { - LOGGER.atTrace() + logger.atTrace() .setMessage("SKIP BUILT-IN EXTENSION {}") .addArgument(() -> NamingUtil.toExtensionId(extension)) .log(); continue; } - LOGGER.atTrace() + logger.atTrace() .setMessage("GET UPSTREAM PUBLIC ID: {} | {}") .addArgument(extension::getId) .addArgument(() -> NamingUtil.toExtensionId(extension)) .log(); var publicIds = service.getUpstreamPublicIds(extension); - if(upstreamExtensionPublicIds.get(extension.getId()) == null) { - LOGGER.trace("ADD EXTENSION PUBLIC ID: {} - {}", extension.getId(), publicIds.extension()); + if (upstreamExtensionPublicIds.get(extension.getId()) == null) { + logger.trace("ADD EXTENSION PUBLIC ID: {} - {}", extension.getId(), publicIds.extension()); upstreamExtensionPublicIds.put(extension.getId(), publicIds.extension()); } var namespace = extension.getNamespace(); - if(upstreamNamespacePublicIds.get(namespace.getId()) == null) { - LOGGER.trace("ADD NAMESPACE PUBLIC ID: {} - {}", namespace.getId(), publicIds.namespace()); + if (upstreamNamespacePublicIds.get(namespace.getId()) == null) { + logger.trace("ADD NAMESPACE PUBLIC ID: {} - {}", namespace.getId(), publicIds.namespace()); upstreamNamespacePublicIds.put(namespace.getId(), publicIds.namespace()); } } var changedExtensionPublicIds = getChangedPublicIds(upstreamExtensionPublicIds, extensionPublicIdsMap); - LOGGER.debug("UPSTREAM EXTENSIONS: {}", upstreamExtensionPublicIds.size()); - LOGGER.debug("CHANGED EXTENSIONS: {}", changedExtensionPublicIds.size()); - if(!changedExtensionPublicIds.isEmpty()) { - LOGGER.debug("CHANGED EXTENSION PUBLIC IDS"); - for(var entry : changedExtensionPublicIds.entrySet()) { - LOGGER.debug("{}: {}", entry.getKey(), entry.getValue()); + logger.debug("UPSTREAM EXTENSIONS: {}", upstreamExtensionPublicIds.size()); + logger.debug("CHANGED EXTENSIONS: {}", changedExtensionPublicIds.size()); + if (!changedExtensionPublicIds.isEmpty()) { + logger.debug("CHANGED EXTENSION PUBLIC IDS"); + for (var entry : changedExtensionPublicIds.entrySet()) { + logger.debug("{}: {}", entry.getKey(), entry.getValue()); } repositories.updateExtensionPublicIds(changedExtensionPublicIds); } var changedNamespacePublicIds = getChangedPublicIds(upstreamNamespacePublicIds, namespacePublicIdsMap); - LOGGER.debug("UPSTREAM NAMESPACES: {}", upstreamNamespacePublicIds.size()); - LOGGER.debug("CHANGED NAMESPACES: {}", changedNamespacePublicIds.size()); - if(!changedNamespacePublicIds.isEmpty()) { - LOGGER.debug("CHANGED NAMESPACE PUBLIC IDS"); - for(var entry : changedNamespacePublicIds.entrySet()) { - LOGGER.debug("{}: {}", entry.getKey(), entry.getValue()); + logger.debug("UPSTREAM NAMESPACES: {}", upstreamNamespacePublicIds.size()); + logger.debug("CHANGED NAMESPACES: {}", changedNamespacePublicIds.size()); + if (!changedNamespacePublicIds.isEmpty()) { + logger.debug("CHANGED NAMESPACE PUBLIC IDS"); + for (var entry : changedNamespacePublicIds.entrySet()) { + logger.debug("{}: {}", entry.getKey(), entry.getValue()); } repositories.updateNamespacePublicIds(changedNamespacePublicIds); @@ -174,7 +175,7 @@ private Map getChangedPublicIds(Map upstreamPublicId .filter(e -> !Objects.equals(currentPublicIds.get(e.getKey()), e.getValue())) .forEach(e -> changedPublicIds.put(e.getKey(), e.getValue())); - if(!changedPublicIds.isEmpty()) { + if (!changedPublicIds.isEmpty()) { var newPublicIds = new HashSet<>(upstreamPublicIds.values()); updatePublicIdNulls(changedPublicIds, newPublicIds, currentPublicIds); } @@ -187,7 +188,7 @@ private void updatePublicIdNulls(Map changedPublicIds, Set changedPublicIds.entrySet().removeIf(e -> { var publicId = e.getValue() == null ? publicIdMap.get(e.getKey()) : null; var remove = publicId != null && !newPublicIds.contains(publicId); - if(remove) { + if (remove) { newPublicIds.add(publicId); } @@ -195,15 +196,15 @@ private void updatePublicIdNulls(Map changedPublicIds, Set }); // put random public ids where upstream public id is missing - for(var entry : changedPublicIds.entrySet()) { + for (var entry : changedPublicIds.entrySet()) { if(entry.getValue() != null) { continue; } String publicId = null; - while(newPublicIds.contains(publicId)) { + while (newPublicIds.contains(publicId)) { publicId = service.getRandomPublicId(); - LOGGER.debug("NEW PUBLIC ID - {}: '{}'", entry.getKey(), publicId); + logger.debug("NEW PUBLIC ID - {}: '{}'", entry.getKey(), publicId); } entry.setValue(publicId); diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/WebResourceService.java b/server/src/main/java/org/eclipse/openvsx/adapter/WebResourceService.java index ed848da84..68ae89892 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/WebResourceService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/WebResourceService.java @@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import java.io.IOException; import java.io.UncheckedIOException; @@ -38,10 +38,9 @@ import static org.eclipse.openvsx.cache.CacheService.*; -@Component +@Service public class WebResourceService { - - protected final Logger logger = LoggerFactory.getLogger(WebResourceService.class); + private final Logger logger = LoggerFactory.getLogger(WebResourceService.class); private final StorageUtilService storageUtil; private final RepositoryService repositories; @@ -62,12 +61,12 @@ public WebResourceService( public Path getExtensionDownload(String namespace, String extension, String targetPlatform, String version) { var download = repositories.findFileByType(namespace, extension, targetPlatform, version, FileResource.DOWNLOAD); - if(download == null) { + if (download == null) { return null; } var path = storageUtil.getCachedFile(download); - if(path != null && !Files.exists(path)) { + if (path != null && !Files.exists(path)) { logger.error("File doesn't exist {}", path); cache.evictExtensionFile(download); path = null; @@ -79,9 +78,9 @@ public Path getExtensionDownload(String namespace, String extension, String targ @Observed @Cacheable(value = CACHE_WEB_RESOURCE_FILES, keyGenerator = GENERATOR_FILES, cacheManager = "fileCacheManager", sync = true) public Path getWebResource(String namespace, String extension, String targetPlatform, String version, String name, Path extensionDownloadPath) { - try(var zip = new ZipFile(extensionDownloadPath.toFile())) { + try (var zip = new ZipFile(extensionDownloadPath.toFile())) { var fileEntry = zip.getEntry(name); - if(fileEntry != null) { + if (fileEntry != null) { var fileExt = getFileExtension(fileEntry); var file = filesCacheKeyGenerator.generateCachedWebResourcePath(namespace, extension, targetPlatform, version, name, fileExt); writeBinaryFile(file, zip, fileEntry); @@ -100,13 +99,13 @@ public Path getWebResource(String namespace, String extension, String targetPlat @Cacheable(value = CACHE_BROWSE_EXTENSION_FILES, keyGenerator = GENERATOR_FILES, cacheManager = "fileCacheManager") public ArrayNode browseExtensionPackage(String namespace, String extension, String targetPlatform, String version, String name, Path extensionDownloadPath) { - try(var zip = new ZipFile(extensionDownloadPath.toFile())) { + try (var zip = new ZipFile(extensionDownloadPath.toFile())) { var dirName = getDirectoryName(name); var dirEntries = zip.stream() .filter(entry -> entry.getName().startsWith(dirName)) .map(entry -> getFileInDirectory(dirName, entry)) .collect(Collectors.toSet()); - if(dirEntries.isEmpty()) { + if (dirEntries.isEmpty()) { return null; } @@ -136,7 +135,7 @@ private void writeBinaryFile(Path file, ZipFile zip, ZipEntry fileEntry) { FileUtil.writeSync(file, p -> { try (var in = zip.getInputStream(fileEntry)) { Files.copy(in, p); - } catch(IOException e) { + } catch (IOException e) { throw new UncheckedIOException(e); } }); diff --git a/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java b/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java index 194052f11..a36b8c20a 100644 --- a/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java +++ b/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java @@ -9,6 +9,7 @@ ********************************************************************************/ package org.eclipse.openvsx.util; +import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; @@ -94,9 +95,9 @@ private static String[] createApiVersionSegments(String namespaceName, String ex /** * Create a URL pointing to an API path. */ - public static String createApiUrl(String baseUrl, String... segments) { - if(Arrays.stream(segments).anyMatch(Objects::isNull)) { - return null; + public static @Nonnull String createApiUrl(String baseUrl, String... segments) { + if (Arrays.stream(segments).anyMatch(Objects::isNull)) { + throw new IllegalArgumentException("Argument to createApiUrl has been null"); } var path = Arrays.stream(segments) From 831961d0d96a3466c44b6b30120d575d51348f57 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 27 Apr 2026 14:40:27 +0200 Subject: [PATCH 2/7] fix: vscode api latest endpoint shall return the latest versions by platform only (including preRelease versions) --- .../eclipse/openvsx/UpstreamProxyService.java | 4 ++ .../openvsx/adapter/IVSCodeService.java | 2 + .../openvsx/adapter/LocalVSCodeService.java | 45 ++++++++++++++ .../adapter/UpstreamVSCodeService.java | 29 +++++++++ .../eclipse/openvsx/adapter/VSCodeAPI.java | 34 +++++----- .../eclipse/openvsx/cache/CacheConfig.java | 20 ++++++ .../eclipse/openvsx/cache/CacheService.java | 41 +++++++++++- ...testExtensionVersionCacheKeyGenerator.java | 2 +- ...onVersionsByPlatformCacheKeyGenerator.java | 46 ++++++++++++++ .../ExtensionVersionJooqRepository.java | 62 ++++++++++++++++++- .../repositories/RepositoryService.java | 4 ++ .../org/eclipse/openvsx/util/NamingUtil.java | 6 +- .../eclipse/openvsx/util/VersionService.java | 28 +++++++-- .../org/eclipse/openvsx/RegistryAPITest.java | 4 +- .../openvsx/adapter/VSCodeAPITest.java | 4 +- .../eclipse/openvsx/admin/AdminAPITest.java | 4 +- .../RepositoryServiceSmokeTest.java | 1 + .../openvsx/util/VersionServiceTest.java | 10 ++- 18 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionsByPlatformCacheKeyGenerator.java diff --git a/server/src/main/java/org/eclipse/openvsx/UpstreamProxyService.java b/server/src/main/java/org/eclipse/openvsx/UpstreamProxyService.java index 7fa7153b4..45c02df44 100644 --- a/server/src/main/java/org/eclipse/openvsx/UpstreamProxyService.java +++ b/server/src/main/java/org/eclipse/openvsx/UpstreamProxyService.java @@ -73,6 +73,10 @@ public ExtensionQueryResult rewriteUrls(ExtensionQueryResult json) { .toList()); } + public ExtensionQueryResult.Extension rewriteUrls(ExtensionQueryResult.Extension json) { + return rewriteExtensionUrls(List.of(json)).getFirst(); + } + private List rewriteExtensionUrls(List extensions) { return extensions.stream() .map(extension -> new ExtensionQueryResult.Extension( diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java index 435689514..60f4d4848 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java @@ -15,6 +15,8 @@ public interface IVSCodeService { ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaultPageSize); + ExtensionQueryResult.Extension latest(String namespaceName, String extensionName); + ResponseEntity browse(String namespaceName, String extensionName, String version, String path); String download(String namespace, String extension, String version, String targetPlatform); diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java index 28dec3310..6276aba49 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -39,6 +40,7 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.eclipse.openvsx.adapter.ExtensionQueryParam.Criterion.*; import static org.eclipse.openvsx.adapter.ExtensionQueryParam.*; @@ -278,6 +280,49 @@ private String getSortOrder(int sortOrder) { } } + @Override + @Cacheable(value = CacheService.CACHE_LATEST_EXTENSION_VERSION_VSCODE) + public ExtensionQueryResult.Extension latest(String namespaceName, String extensionName) { + if (BuiltInExtensionUtil.isBuiltIn(namespaceName)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, builtinExtensionMessage()); + } + + var extension = repositories.findActiveExtension(extensionName, namespaceName); + if (extension == null) { + throw new NotFoundException(); + } + + var latestRegularVersions = versions.getLatestByTargetPlatform(extension, false); + var latestPrereleaseVersions = versions.getLatestByTargetPlatform(extension, true); + + var versions = Stream.concat(latestRegularVersions.stream(), latestPrereleaseVersions.stream()).toList(); + if (versions.isEmpty()) { + throw new NotFoundException(); + } + + var fileTypes = new ArrayList<>(List.of(MANIFEST, README, LICENSE, ICON, DOWNLOAD, CHANGELOG, VSIXMANIFEST)); + if (integrityService.isEnabled()) { + fileTypes.add(DOWNLOAD_SIG); + } + + var fileResources = + repositories.findFileResourcesByExtensionVersionIdAndType( + versions.stream().map(ExtensionVersion::getId).toList(), fileTypes) + .stream() + .collect(Collectors.groupingBy(fr -> fr.getExtension().getId())); + + // get the latest published version, giving regular releases precedence + var latestVersion = versions.stream() + .max(Comparator.comparing(ExtensionVersion::isPreRelease).reversed().thenComparing(ExtensionVersion::getTimestamp)); + + var queryVersions = versions.stream() + .sorted(ExtensionVersion.SORT_COMPARATOR.reversed()) + .map(ev -> toQueryVersion(ev, fileResources, FLAG_INCLUDE_ASSET_URI | FLAG_INCLUDE_VERSION_PROPERTIES)) + .toList(); + + return toQueryExtension(extension, latestVersion.get(), queryVersions, FLAG_INCLUDE_STATISTICS); + } + @Observed @Override public ResponseEntity getAsset( diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java index a824cdc3d..57295fe73 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java @@ -88,6 +88,35 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul throw new NotFoundException(); } + @Override + public ExtensionQueryResult.Extension latest(String namespaceName, String extensionName) { + var urlTemplate = urlConfigService.getUpstreamUrl() + "/vscode/gallery/{namespace}/{extension}/latest"; + var uriVariables = new HashMap<>(Map.of( + VAR_NAMESPACE, namespaceName, + VAR_EXTENSION, extensionName + )); + + URI url = restTemplate.getUriTemplateHandler().expand(urlTemplate, uriVariables); + var request = new RequestEntity<>(HttpHeadersUtil.getForwardedHeaders(), HttpMethod.GET, url); + ResponseEntity response; + try { + response = restTemplate.exchange(request, ExtensionQueryResult.Extension.class); + } catch (RestClientException exc) { + throw propagateRestException(exc, request.getMethod(), url.toString(), null); + } + + var statusCode = response.getStatusCode(); + if (statusCode.is2xxSuccessful()) { + var json = response.getBody(); + return proxy != null ? proxy.rewriteUrls(json) : json; + } + if (statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { + logger.error("GET {}: {}", urlTemplate, response); + } + + throw new NotFoundException(); + } + @Override public ResponseEntity browse(String namespaceName, String extensionName, String version, String path) { var urlBuilder = new StringBuilder(urlConfigService.getUpstreamUrl() + "/vscode/unpkg/{namespace}/{extension}/{version}"); diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java index 2e8d25c3b..43c8ac3d5 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java @@ -347,23 +347,19 @@ public ResponseEntity getLatest( @PathVariable @Parameter(description = "Extension namespace", example = "malloydata") String namespaceName, @PathVariable @Parameter(description = "Extension name", example = "malloy-vscode") String extensionName ) { - var extensionId = String.join(".", namespaceName, extensionName); - var criterion = new ExtensionQueryParam.Criterion(ExtensionQueryParam.Criterion.FILTER_EXTENSION_NAME, extensionId); - var filter = new ExtensionQueryParam.Filter(List.of(criterion), 0, 0, 0, 0); - int flags = FLAG_INCLUDE_VERSIONS | FLAG_INCLUDE_ASSET_URI | FLAG_INCLUDE_VERSION_PROPERTIES | FLAG_INCLUDE_FILES | FLAG_INCLUDE_STATISTICS; - var param = new ExtensionQueryParam(List.of(filter), flags); - var result = extensionQueryRequestHandler.getResult(param, 1, DEFAULT_PAGE_SIZE); - var extension = Optional.of(result) - .filter(r -> !r.results().isEmpty()) - .map(r -> r.results().getFirst().extensions()) - .filter(e -> !e.isEmpty()) - .map(List::getFirst) - .orElse(null); - - return extension != null - ? ResponseEntity.ok() - .cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic()) - .body(extension) - : ResponseEntity.notFound().build(); + try { + for (var service : getVSCodeServices()) { + try { + var result = service.latest(namespaceName, extensionName); + return ResponseEntity.ok(result); + } catch (NotFoundException exc) { + // Try the next registry + } + } + return ResponseEntity.notFound().build(); + } catch (ErrorResultException ex) { + logger.error(ex.getMessage()); + return ResponseEntity.internalServerError().build(); + } } -} \ No newline at end of file +} diff --git a/server/src/main/java/org/eclipse/openvsx/cache/CacheConfig.java b/server/src/main/java/org/eclipse/openvsx/cache/CacheConfig.java index e89b61d7f..aa8ed0468 100644 --- a/server/src/main/java/org/eclipse/openvsx/cache/CacheConfig.java +++ b/server/src/main/java/org/eclipse/openvsx/cache/CacheConfig.java @@ -19,6 +19,7 @@ import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration; import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; import io.micrometer.common.util.StringUtils; +import org.eclipse.openvsx.adapter.ExtensionQueryResult; import org.eclipse.openvsx.entities.ExtensionVersion; import org.eclipse.openvsx.json.ExtensionJson; import org.eclipse.openvsx.json.NamespaceDetailsJson; @@ -48,6 +49,7 @@ import java.net.URI; import java.time.Duration; +import java.util.List; import java.util.OptionalLong; import java.util.Properties; import java.util.stream.Collectors; @@ -158,6 +160,10 @@ public JCacheCacheManager caffeineCacheManager( @Value("${ovsx.caching.extension-json.max-size:1024}") long extensionJsonMaxSize, @Value("${ovsx.caching.latest-extension-version.ttl:PT1H}") Duration latestExtensionVersionTtl, @Value("${ovsx.caching.latest-extension-version.max-size:1024}") long latestExtensionVersionMaxSize, + @Value("${ovsx.caching.latest-extension-version-vscode.ttl:PT1H}") Duration latestExtensionVersionVscodeTtl, + @Value("${ovsx.caching.latest-extension-version-vscode.max-size:1024}") long latestExtensionVersionVscodeMaxSize, + @Value("${ovsx.caching.latest-extension-versions-by-platform.ttl:PT1H}") Duration latestExtensionVersionsByPlatformTtl, + @Value("${ovsx.caching.latest-extension-versions-by-platform.max-size:1024}") long latestExtensionVersionsByPlatformMaxSize, @Value("${ovsx.caching.sitemap.ttl:PT1H}") Duration sitemapTtl, @Value("${ovsx.caching.sitemap.max-size:1}") long sitemapMaxSize, @Value("${ovsx.caching.malicious-extensions.ttl:P3D}") Duration maliciousExtensionsTtl, @@ -172,6 +178,8 @@ public JCacheCacheManager caffeineCacheManager( var databaseSearchCache = createCaffeineConfiguration(databaseSearchTtl, databaseSearchMaxSize, false); var extensionJsonCache = createCaffeineConfiguration(extensionJsonTtl, extensionJsonMaxSize, false); var latestExtensionVersionCache = createCaffeineConfiguration(latestExtensionVersionTtl, latestExtensionVersionMaxSize, false); + var latestExtensionVersionVscodeCache = createCaffeineConfiguration(latestExtensionVersionVscodeTtl, latestExtensionVersionVscodeMaxSize, false); + var latestExtensionVersionsByPlatformCache = createCaffeineConfiguration(latestExtensionVersionsByPlatformTtl, latestExtensionVersionsByPlatformMaxSize, false); var sitemapCache = createCaffeineConfiguration(sitemapTtl, sitemapMaxSize, false); var maliciousExtensionsCache = createCaffeineConfiguration(maliciousExtensionsTtl, maliciousExtensionsMaxSize, false); var rateLimitingCache = createCaffeineConfiguration(rateLimitingTti, rateLimitingMaxSize, true); @@ -189,6 +197,8 @@ public JCacheCacheManager caffeineCacheManager( cacheManager.createCache(CACHE_DATABASE_SEARCH, databaseSearchCache); cacheManager.createCache(CACHE_EXTENSION_JSON, extensionJsonCache); cacheManager.createCache(CACHE_LATEST_EXTENSION_VERSION, latestExtensionVersionCache); + cacheManager.createCache(CACHE_LATEST_EXTENSION_VERSION_VSCODE, latestExtensionVersionVscodeCache); + cacheManager.createCache(CACHE_LATEST_EXTENSION_VERSIONS_BY_PLATFORM, latestExtensionVersionsByPlatformCache); cacheManager.createCache(CACHE_SITEMAP, sitemapCache); cacheManager.createCache(CACHE_MALICIOUS_EXTENSIONS, maliciousExtensionsCache); cacheManager.createCache(rateLimitingCacheName, rateLimitingCache); @@ -217,6 +227,8 @@ public CacheManager redisCacheManager( @Value("${ovsx.caching.database-search.ttl:PT1H}") Duration databaseSearchTtl, @Value("${ovsx.caching.extension-json.ttl:PT1H}") Duration extensionJsonTtl, @Value("${ovsx.caching.latest-extension-version.ttl:PT1H}") Duration latestExtensionVersionTtl, + @Value("${ovsx.caching.latest-extension-version-vscode.ttl:PT1H}") Duration latestExtensionVersionVscodeTtl, + @Value("${ovsx.caching.latest-extension-versions-by-platform.ttl:PT1H}") Duration latestExtensionVersionsByPlatformTtl, @Value("${ovsx.caching.sitemap.ttl:PT1H}") Duration sitemapTtl, @Value("${ovsx.caching.malicious-extensions.ttl:P3D}") Duration maliciousExtensionsTtl ) { @@ -242,10 +254,18 @@ public CacheManager redisCacheManager( CACHE_EXTENSION_JSON, redisCacheConfig(new Jackson2JsonRedisSerializer<>(ExtensionJson.class), extensionJsonTtl) ) + .withCacheConfiguration( + CACHE_LATEST_EXTENSION_VERSION_VSCODE, + redisCacheConfig(new Jackson2JsonRedisSerializer<>(ExtensionQueryResult.Extension.class), latestExtensionVersionVscodeTtl) + ) .withCacheConfiguration( CACHE_LATEST_EXTENSION_VERSION, redisCacheConfig(new Jackson2JsonRedisSerializer<>(extensionVersionMapper, ExtensionVersion.class), latestExtensionVersionTtl) ) + .withCacheConfiguration( + CACHE_LATEST_EXTENSION_VERSIONS_BY_PLATFORM, + redisCacheConfig(new Jackson2JsonRedisSerializer<>(extensionVersionMapper, List.class), latestExtensionVersionsByPlatformTtl) + ) .withCacheConfiguration( CACHE_SITEMAP, redisCacheConfig(new StringRedisSerializer(), sitemapTtl) diff --git a/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java b/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java index 8768db9d6..d1ecaa7de 100644 --- a/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java +++ b/server/src/main/java/org/eclipse/openvsx/cache/CacheService.java @@ -16,6 +16,7 @@ import org.eclipse.openvsx.util.VersionAlias; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.SimpleKey; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.stereotype.Component; @@ -31,6 +32,8 @@ public class CacheService { public static final String CACHE_EXTENSION_FILES = "files.extension"; public static final String CACHE_EXTENSION_JSON = "extension.json"; public static final String CACHE_LATEST_EXTENSION_VERSION = "latest.extension.version"; + public static final String CACHE_LATEST_EXTENSION_VERSION_VSCODE = "latest.extension.version.vscode"; + public static final String CACHE_LATEST_EXTENSION_VERSIONS_BY_PLATFORM = "latest.extension.versions.by.platform"; public static final String CACHE_NAMESPACE_DETAILS_JSON = "namespace.details.json"; public static final String CACHE_AVERAGE_REVIEW_RATING = "average.review.rating"; public static final String CACHE_SITEMAP = "sitemap"; @@ -38,6 +41,7 @@ public class CacheService { public static final String GENERATOR_EXTENSION_JSON = "extensionJsonCacheKeyGenerator"; public static final String GENERATOR_LATEST_EXTENSION_VERSION = "latestExtensionVersionCacheKeyGenerator"; + public static final String GENERATOR_LATEST_EXTENSION_VERSIONS_BY_PLATFORM = "latestExtensionVersionsByPlatformCacheKeyGenerator"; public static final String GENERATOR_FILES = "filesCacheKeyGenerator"; private final CacheManager cacheManager; @@ -45,6 +49,7 @@ public class CacheService { private final RepositoryService repositories; private final ExtensionJsonCacheKeyGenerator extensionJsonCacheKey; private final LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKey; + private final LatestExtensionVersionsByPlatformCacheKeyGenerator latestExtensionVersionsByPlatformCacheKeyGenerator; private final FilesCacheKeyGenerator filesCacheKeyGenerator; public CacheService( @@ -53,6 +58,7 @@ public CacheService( RepositoryService repositories, ExtensionJsonCacheKeyGenerator extensionJsonCacheKey, LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKey, + LatestExtensionVersionsByPlatformCacheKeyGenerator latestExtensionVersionsByPlatformCacheKeyGenerator, FilesCacheKeyGenerator filesCacheKeyGenerator ) { this.cacheManager = cacheManager; @@ -60,6 +66,7 @@ public CacheService( this.repositories = repositories; this.extensionJsonCacheKey = extensionJsonCacheKey; this.latestExtensionVersionCacheKey = latestExtensionVersionCacheKey; + this.latestExtensionVersionsByPlatformCacheKeyGenerator = latestExtensionVersionsByPlatformCacheKeyGenerator; this.filesCacheKeyGenerator = filesCacheKeyGenerator; } @@ -151,11 +158,19 @@ public void evictExtensionJsons(ExtensionVersion extVersion) { public void evictLatestExtensionVersions() { invalidateCache(CACHE_LATEST_EXTENSION_VERSION); + invalidateCache(CACHE_LATEST_EXTENSION_VERSIONS_BY_PLATFORM); + invalidateCache(CACHE_LATEST_EXTENSION_VERSION_VSCODE); } public void evictLatestExtensionVersion(Extension extension) { + evictInternalLatestExtensionVersion(extension); + evictInternalLatestExtensionVersionsByPlatform(extension); + evictInternalLatestExtensionVersionVSCode(extension); + } + + private void evictInternalLatestExtensionVersion(Extension extension) { var cache = cacheManager.getCache(CACHE_LATEST_EXTENSION_VERSION); - if(cache == null) { + if (cache == null) { return; } @@ -172,7 +187,7 @@ public void evictLatestExtensionVersion(Extension extension) { for (var targetPlatform : targetPlatforms) { for (var preRelease : List.of(true, false)) { for (var onlyActive : List.of(true, false)) { - for(var type : ExtensionVersion.Type.values()) { + for (var type : ExtensionVersion.Type.values()) { var key = latestExtensionVersionCacheKey.generate(extension, targetPlatform, preRelease, onlyActive, type); cache.evictIfPresent(key); } @@ -181,6 +196,28 @@ public void evictLatestExtensionVersion(Extension extension) { } } + private void evictInternalLatestExtensionVersionsByPlatform(Extension extension) { + var cache = cacheManager.getCache(CACHE_LATEST_EXTENSION_VERSIONS_BY_PLATFORM); + if (cache == null) { + return; + } + + for (var preRelease : List.of(true, false)) { + var key = latestExtensionVersionsByPlatformCacheKeyGenerator.generate(extension, preRelease); + cache.evictIfPresent(key); + } + } + + private void evictInternalLatestExtensionVersionVSCode(Extension extension) { + var cache = cacheManager.getCache(CACHE_LATEST_EXTENSION_VERSION_VSCODE); + if (cache == null) { + return; + } + + var key = new SimpleKey(extension.getNamespace().getName(), extension.getName()); + cache.evictIfPresent(key); + } + private void invalidateCache(String cacheName) { var cache = cacheManager.getCache(cacheName); if(cache == null) { diff --git a/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java b/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java index 02216f3d8..7dfa3d025 100644 --- a/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java +++ b/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionCacheKeyGenerator.java @@ -37,7 +37,7 @@ public Object generate(Object target, Method method, Object... params) { onlyActive = (boolean) params[3]; } else { var versions = (List) params[0]; - var firstVersion = versions.get(0); + var firstVersion = versions.getFirst(); extension = firstVersion.getExtension(); type = firstVersion.getType(); var groupedByTargetPlatform = (boolean) params[1]; diff --git a/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionsByPlatformCacheKeyGenerator.java b/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionsByPlatformCacheKeyGenerator.java new file mode 100644 index 000000000..5b3b38e6f --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/cache/LatestExtensionVersionsByPlatformCacheKeyGenerator.java @@ -0,0 +1,46 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.cache; + +import org.eclipse.openvsx.entities.Extension; +import org.eclipse.openvsx.util.NamingUtil; +import org.eclipse.openvsx.util.VersionAlias; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +@Component +public class LatestExtensionVersionsByPlatformCacheKeyGenerator implements KeyGenerator { + + @Override + public Object generate(Object target, Method method, Object... params) { + Extension extension; + var preRelease = false; + + if (params[0] instanceof Extension) { + extension = (Extension) params[0]; + preRelease = (boolean) params[1]; + } else { + throw new IllegalStateException("unexpected method parameters"); + } + + return generate(extension, preRelease); + } + + public String generate(Extension extension, boolean preReleases) { + var extensionName = extension.getName(); + var namespaceName = extension.getNamespace().getName(); + return NamingUtil.toFileFormat(namespaceName, extensionName, null, VersionAlias.LATEST) + ",pre-releases=" + preReleases; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java index e2a483238..3595eb8c3 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java @@ -516,7 +516,7 @@ private ExtensionVersion toExtensionVersionCommon( extVersion.setSponsorLink(row.get(extensionVersionMapper.map(EXTENSION_VERSION.SPONSOR_LINK))); extVersion.setPotentiallyMalicious(row.get(extensionVersionMapper.map(EXTENSION_VERSION.POTENTIALLY_MALICIOUS))); - if(extension == null) { + if (extension == null) { var namespace = new Namespace(); namespace.setId(row.get(NAMESPACE.ID)); namespace.setName(row.get(NAMESPACE.NAME)); @@ -1197,6 +1197,66 @@ public ExtensionVersion findLatest(UserData user, String namespace, String exten }); } + public List findLatestVersionByTargetPlatform( + Extension extension, + boolean preReleases, + boolean onlyActive + ) { + var query = dsl.selectQuery(); + query.addDistinctOn(EXTENSION_VERSION.TARGET_PLATFORM); + query.addFrom(EXTENSION_VERSION); + + query.addJoin(EXTENSION, EXTENSION.ID.eq(EXTENSION_VERSION.EXTENSION_ID)); + query.addJoin(NAMESPACE, NAMESPACE.ID.eq(EXTENSION.NAMESPACE_ID)); + query.addJoin(SIGNATURE_KEY_PAIR, JoinType.LEFT_OUTER_JOIN, SIGNATURE_KEY_PAIR.ID.eq(EXTENSION_VERSION.SIGNATURE_KEY_PAIR_ID)); + + query.addConditions(EXTENSION_VERSION.PRE_RELEASE.eq(preReleases)); + + if (onlyActive) { + query.addConditions(EXTENSION_VERSION.ACTIVE.eq(true)); + } + + query.addOrderBy( + EXTENSION_VERSION.TARGET_PLATFORM, + EXTENSION_VERSION.SEMVER_MAJOR.desc(), + EXTENSION_VERSION.SEMVER_MINOR.desc(), + EXTENSION_VERSION.SEMVER_PATCH.desc(), + EXTENSION_VERSION.TIMESTAMP.desc() + ); + + query.addConditions(EXTENSION_VERSION.EXTENSION_ID.eq(extension.getId())); + + query.addSelect( + NAMESPACE.ID, + NAMESPACE.NAME, + EXTENSION.ID, + EXTENSION.NAME, + EXTENSION_VERSION.ID, + EXTENSION_VERSION.VERSION, + EXTENSION_VERSION.POTENTIALLY_MALICIOUS, + EXTENSION_VERSION.TARGET_PLATFORM, + EXTENSION_VERSION.PREVIEW, + EXTENSION_VERSION.PRE_RELEASE, + EXTENSION_VERSION.TIMESTAMP, + EXTENSION_VERSION.DISPLAY_NAME, + EXTENSION_VERSION.DESCRIPTION, + EXTENSION_VERSION.ENGINES, + EXTENSION_VERSION.CATEGORIES, + EXTENSION_VERSION.TAGS, + EXTENSION_VERSION.EXTENSION_KIND, + EXTENSION_VERSION.REPOSITORY, + EXTENSION_VERSION.SPONSOR_LINK, + EXTENSION_VERSION.GALLERY_COLOR, + EXTENSION_VERSION.GALLERY_THEME, + EXTENSION_VERSION.LOCALIZED_LANGUAGES, + EXTENSION_VERSION.DEPENDENCIES, + EXTENSION_VERSION.BUNDLED_EXTENSIONS, + SIGNATURE_KEY_PAIR.PUBLIC_ID + ); + + return query.fetch().map(this::toExtensionVersion); + } + public ExtensionVersion findLatestForAllUrls( Extension extension, String targetPlatform, diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java index 906100d8b..4a0fcbc12 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -625,6 +625,10 @@ public ExtensionVersion findLatestVersion(Extension extension, String targetPlat return extensionVersionJooqRepo.findLatest(extension, targetPlatform, onlyPreRelease, onlyActive); } + public List findLatestVersionByTargetPlatform(Extension extension, boolean preReleases, boolean onlyActive) { + return extensionVersionJooqRepo.findLatestVersionByTargetPlatform(extension, preReleases, onlyActive); + } + public ExtensionVersion findLatestVersion(String namespaceName, String extensionName, String targetPlatform, boolean onlyPreRelease, boolean onlyActive) { return extensionVersionJooqRepo.findLatest(namespaceName, extensionName, targetPlatform, onlyPreRelease, onlyActive); } diff --git a/server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java b/server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java index 6d83970ae..381b4ce78 100644 --- a/server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java +++ b/server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java @@ -38,7 +38,7 @@ public static String toFileFormat(String namespace, String extension, String tar public static String toFileFormat(String namespace, String extension, String targetPlatform, String version) { var name = toExtensionId(namespace, extension) + "-" + version; - if(!TargetPlatform.isUniversal(targetPlatform)) { + if (targetPlatform != null && !TargetPlatform.isUniversal(targetPlatform)) { name += "@" + targetPlatform; } @@ -61,10 +61,10 @@ public static String toLogFormat(String namespace, String extension, String vers public static String toLogFormat(String namespace, String extension, String targetPlatform, String version) { var name = toExtensionId(namespace, extension); - if(!StringUtils.isEmpty(version)) { + if (!StringUtils.isEmpty(version)) { name += " " + version; } - if(!StringUtils.isEmpty(targetPlatform) && !TargetPlatform.isUniversal(targetPlatform)) { + if (!StringUtils.isEmpty(targetPlatform) && !TargetPlatform.isUniversal(targetPlatform)) { name += " (" + targetPlatform + ")"; } diff --git a/server/src/main/java/org/eclipse/openvsx/util/VersionService.java b/server/src/main/java/org/eclipse/openvsx/util/VersionService.java index 3df3eeeb4..867b2a59b 100644 --- a/server/src/main/java/org/eclipse/openvsx/util/VersionService.java +++ b/server/src/main/java/org/eclipse/openvsx/util/VersionService.java @@ -9,18 +9,38 @@ * ****************************************************************************** */ package org.eclipse.openvsx.util; +import org.eclipse.openvsx.entities.Extension; import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.repositories.RepositoryService; import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import java.util.List; -import static org.eclipse.openvsx.cache.CacheService.CACHE_LATEST_EXTENSION_VERSION; -import static org.eclipse.openvsx.cache.CacheService.GENERATOR_LATEST_EXTENSION_VERSION; +import static org.eclipse.openvsx.cache.CacheService.*; -@Component +@Service public class VersionService { + private final RepositoryService repositories; + + public VersionService(RepositoryService repositories) { + this.repositories = repositories; + } + + /** + * This returns the latest version of an {@code Extension}, grouped by target platform. + * + * @param extension the extension to query + * @param preReleases whether pre-release or regular versions should be considered, this is an exclusive or + * parameter, either only pre-releases are considered or not at all + * @return a list of the latest {@code ExtensionVersion} for the given {@code Extension} grouped by target platform + */ + @Cacheable(value = CACHE_LATEST_EXTENSION_VERSIONS_BY_PLATFORM, keyGenerator = GENERATOR_LATEST_EXTENSION_VERSIONS_BY_PLATFORM) + public List getLatestByTargetPlatform(Extension extension, boolean preReleases) { + return repositories.findLatestVersionByTargetPlatform(extension, preReleases, true); + } + // groupedByTargetPlatform is used by cache key generator, don't remove this parameter @Cacheable(value = CACHE_LATEST_EXTENSION_VERSION, keyGenerator = GENERATOR_LATEST_EXTENSION_VERSION) public ExtensionVersion getLatest(List versions, boolean groupedByTargetPlatform) { diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index e8a8ac056..47378d0e9 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -2725,8 +2725,8 @@ LocalStorageService localStorageService() { ExtensionJsonCacheKeyGenerator extensionJsonCacheKeyGenerator() { return new ExtensionJsonCacheKeyGenerator(); } @Bean - VersionService versionService() { - return new VersionService(); + VersionService versionService(RepositoryService repositoryService) { + return new VersionService(repositoryService); } @Bean diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java index 7bed165ce..c34366321 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java @@ -1123,8 +1123,8 @@ LocalStorageService localStorage() { } @Bean - VersionService versionService() { - return new VersionService(); + VersionService versionService(RepositoryService repositoryService) { + return new VersionService(repositoryService); } @Bean diff --git a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java index ffdd05740..b6ddd51af 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -1770,8 +1770,8 @@ LocalStorageService localStorage() { } @Bean - VersionService versionService() { - return new VersionService(); + VersionService versionService(RepositoryService repositoryService) { + return new VersionService(repositoryService); } @Bean diff --git a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java index 02dce203c..2271f1044 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -246,6 +246,7 @@ void testExecuteQueries() { () -> repositories.findLatestVersion(extension, "targetPlatform", false, false), () -> repositories.findLatestVersions(namespace), () -> repositories.findLatestVersions(userData), + () -> repositories.findLatestVersionByTargetPlatform(extension, true, true), () -> repositories.findExtensionTargetPlatforms(extension), () -> repositories.isNamespaceOwner(userData, namespace), () -> repositories.findMembershipsForOwner(userData,"namespaceName"), diff --git a/server/src/test/java/org/eclipse/openvsx/util/VersionServiceTest.java b/server/src/test/java/org/eclipse/openvsx/util/VersionServiceTest.java index 707b53931..f0f31e561 100644 --- a/server/src/test/java/org/eclipse/openvsx/util/VersionServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/util/VersionServiceTest.java @@ -10,13 +10,16 @@ package org.eclipse.openvsx.util; import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.repositories.RepositoryService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.Comparator; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -24,6 +27,9 @@ @ExtendWith(SpringExtension.class) class VersionServiceTest { + @MockitoBean + RepositoryService repositories; + @Autowired VersionService versions; @@ -128,8 +134,8 @@ void testGetLatestNoPreRelease() { @TestConfiguration static class TestConfig { @Bean - VersionService versionService() { - return new VersionService(); + VersionService versionService(RepositoryService repositoryService) { + return new VersionService(repositoryService); } } } \ No newline at end of file From 71c20b8c26211b9441c28abc9dc8144c9756c777 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 27 Apr 2026 14:58:58 +0200 Subject: [PATCH 3/7] add validation for user-controlled path variables --- .../openvsx/adapter/UpstreamVSCodeService.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java index 57295fe73..a24c42084 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipse.openvsx.ExtensionValidator; import org.eclipse.openvsx.UpstreamProxyService; import org.eclipse.openvsx.UrlConfigService; import org.eclipse.openvsx.util.HttpHeadersUtil; @@ -48,17 +49,20 @@ public class UpstreamVSCodeService implements IVSCodeService { private UpstreamProxyService proxy; private final RestTemplate nonRedirectingRestTemplate; private final UrlConfigService urlConfigService; + private final ExtensionValidator extensionValidator; public UpstreamVSCodeService( RestTemplate restTemplate, Optional upstreamProxyService, RestTemplate nonRedirectingRestTemplate, - UrlConfigService urlConfigService + UrlConfigService urlConfigService, + ExtensionValidator extensionValidator ) { this.restTemplate = restTemplate; upstreamProxyService.ifPresent(service -> this.proxy = service); this.nonRedirectingRestTemplate = nonRedirectingRestTemplate; this.urlConfigService = urlConfigService; + this.extensionValidator = extensionValidator; } public boolean isValid() { @@ -90,6 +94,13 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul @Override public ExtensionQueryResult.Extension latest(String namespaceName, String extensionName) { + // Check that the user-provided namespace and extension parameters are actually valid before + // making a request to the upstream server. + if (extensionValidator.validateNamespace(namespaceName).isPresent() || + extensionValidator.validateExtensionName(extensionName).isPresent()) { + throw new NotFoundException(); + } + var urlTemplate = urlConfigService.getUpstreamUrl() + "/vscode/gallery/{namespace}/{extension}/latest"; var uriVariables = new HashMap<>(Map.of( VAR_NAMESPACE, namespaceName, From 6a10f49bafd876e61ada21c38e0eaf99ca0220f0 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 27 Apr 2026 18:02:13 +0200 Subject: [PATCH 4/7] add previous cache control settings --- .../src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java index 43c8ac3d5..71cbd8a00 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java @@ -351,7 +351,9 @@ public ResponseEntity getLatest( for (var service : getVSCodeServices()) { try { var result = service.latest(namespaceName, extensionName); - return ResponseEntity.ok(result); + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic().mustRevalidate()) + .body(result); } catch (NotFoundException exc) { // Try the next registry } From 8991a654d4eb2ad0583b7692c52721201d385b23 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 27 Apr 2026 20:57:58 +0200 Subject: [PATCH 5/7] add logging for testing --- server/src/main/java/org/eclipse/openvsx/RegistryAPI.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java index b272a706b..11ca1a61e 100644 --- a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java @@ -252,6 +252,8 @@ public ResponseEntity getExtension( .body(registry.getExtension(namespace, extension, null)); } catch (NotFoundException exc) { // Try the next registry + } finally { + logger.info("done getting extension {}.{}", namespace, extension); } } var json = ExtensionJson.error(extensionNotFoundMessage(NamingUtil.toExtensionId(namespace, extension))); From 26b09c8e741b6c2b2517ee1cd8d58a0a3b7d5795 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 27 Apr 2026 22:16:11 +0200 Subject: [PATCH 6/7] remove logging call --- server/src/main/java/org/eclipse/openvsx/RegistryAPI.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java index 11ca1a61e..b272a706b 100644 --- a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java @@ -252,8 +252,6 @@ public ResponseEntity getExtension( .body(registry.getExtension(namespace, extension, null)); } catch (NotFoundException exc) { // Try the next registry - } finally { - logger.info("done getting extension {}.{}", namespace, extension); } } var json = ExtensionJson.error(extensionNotFoundMessage(NamingUtil.toExtensionId(namespace, extension))); From 9e08fa56b9757cc01c6843bbc74386cf043a0346 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Tue, 28 Apr 2026 08:54:32 +0200 Subject: [PATCH 7/7] add logging --- .../eclipse/openvsx/adapter/UpstreamVSCodeService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java index a24c42084..5cd535fb1 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java @@ -96,8 +96,13 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul public ExtensionQueryResult.Extension latest(String namespaceName, String extensionName) { // Check that the user-provided namespace and extension parameters are actually valid before // making a request to the upstream server. - if (extensionValidator.validateNamespace(namespaceName).isPresent() || - extensionValidator.validateExtensionName(extensionName).isPresent()) { + if (extensionValidator.validateNamespace(namespaceName).isPresent()) { + logger.debug("Received request to get latest extension data for invalid namespace {}", namespaceName); + throw new NotFoundException(); + } + + if (extensionValidator.validateExtensionName(extensionName).isPresent()) { + logger.debug("Received request to get latest extension data for invalid extension {}", extensionName); throw new NotFoundException(); }