diff --git a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java index 854b3374..9bf17f9e 100644 --- a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java +++ b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java @@ -44,6 +44,12 @@ public class ExtensionProcessor implements AutoCloseable { private static final String[] README = { "extension/README.md", "extension/README", "extension/README.txt" }; private static final String[] CHANGELOG = { "extension/CHANGELOG.md", "extension/CHANGELOG", "extension/CHANGELOG.txt" }; + private static final String MANIFEST_METADATA = "Metadata"; + private static final String MANIFEST_IDENTITY = "Identity"; + private static final String MANIFEST_PROPERTIES = "Properties"; + private static final String MANIFEST_PROPERTY = "Property"; + private static final String MANIFEST_VALUE = "Value"; + protected final Logger logger = LoggerFactory.getLogger(ExtensionProcessor.class); private final TempFile extensionFile; @@ -91,7 +97,7 @@ private void loadPackageJson() { // Read package.json try (var entryFile = ArchiveUtil.readEntry(zipFile, PACKAGE_JSON)) { if (entryFile == null) { - throw new ErrorResultException("Entry not found: " + PACKAGE_JSON); + throw new ErrorResultException(entryNotFoundMessage(PACKAGE_JSON)); } var mapper = new ObjectMapper(); @@ -114,7 +120,7 @@ private void loadVsixManifest() { // Read extension.vsixmanifest try (var entryFile = ArchiveUtil.readEntry(zipFile, VSIX_MANIFEST)) { if (entryFile == null) { - throw new ErrorResultException("Entry not found: " + VSIX_MANIFEST); + throw new ErrorResultException(entryNotFoundMessage(VSIX_MANIFEST)); } var mapper = new XmlMapper(); @@ -127,6 +133,10 @@ private void loadVsixManifest() { } } + private String entryNotFoundMessage(String file) { + return "Entry not found: " + file; + } + private JsonNode findByIdInArray(Iterable iter, String id) { for(JsonNode node : iter){ var idNode = node.get("Id"); @@ -139,84 +149,84 @@ private JsonNode findByIdInArray(Iterable iter, String id) { public String getExtensionName() { loadVsixManifest(); - return vsixManifest.path("Metadata").path("Identity").path("Id").asText(); + return vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_IDENTITY).path("Id").asText(); } public String getNamespace() { loadVsixManifest(); - return vsixManifest.path("Metadata").path("Identity").path("Publisher").asText(); + return vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_IDENTITY).path("Publisher").asText(); } public List getExtensionDependencies() { loadVsixManifest(); - var extDepenNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Code.ExtensionDependencies"); - return asStringList(extDepenNode.path("Value").asText(), ","); + var extDepenNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Code.ExtensionDependencies"); + return asStringList(extDepenNode.path(MANIFEST_VALUE).asText(), ","); } public List getBundledExtensions() { loadVsixManifest(); - var extPackNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Code.ExtensionPack"); - return asStringList(extPackNode.path("Value").asText(), ","); + var extPackNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Code.ExtensionPack"); + return asStringList(extPackNode.path(MANIFEST_VALUE).asText(), ","); } public List getExtensionKinds() { loadVsixManifest(); - var extKindNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Code.ExtensionKind"); - return asStringList(extKindNode.path("Value").asText(), ","); + var extKindNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Code.ExtensionKind"); + return asStringList(extKindNode.path(MANIFEST_VALUE).asText(), ","); } public String getHomepage() { loadVsixManifest(); - var extKindNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Services.Links.Learn"); - return extKindNode.path("Value").asText(); + var extKindNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Services.Links.Learn"); + return extKindNode.path(MANIFEST_VALUE).asText(); } public String getRepository() { loadVsixManifest(); - var sourceNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Services.Links.Source"); - return sourceNode.path("Value").asText(); + var sourceNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Services.Links.Source"); + return sourceNode.path(MANIFEST_VALUE).asText(); } public String getBugs() { loadVsixManifest(); - var supportNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Services.Links.Support"); - return supportNode.path("Value").asText(); + var supportNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Services.Links.Support"); + return supportNode.path(MANIFEST_VALUE).asText(); } public String getGalleryColor() { loadVsixManifest(); - var colorNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Services.Branding.Color"); - return colorNode.path("Value").asText(); + var colorNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Services.Branding.Color"); + return colorNode.path(MANIFEST_VALUE).asText(); } public String getGalleryTheme() { loadVsixManifest(); - var themeNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Services.Branding.Theme"); - return themeNode.path("Value").asText(); + var themeNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Services.Branding.Theme"); + return themeNode.path(MANIFEST_VALUE).asText(); } public List getLocalizedLanguages() { loadVsixManifest(); - var languagesNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Code.LocalizedLanguages"); - return asStringList(languagesNode.path("Value").asText(), ","); + var languagesNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Code.LocalizedLanguages"); + return asStringList(languagesNode.path(MANIFEST_VALUE).asText(), ","); } public boolean isPreview() { loadVsixManifest(); - var galleryFlags = vsixManifest.path("Metadata").path("GalleryFlags"); + var galleryFlags = vsixManifest.path(MANIFEST_METADATA).path("GalleryFlags"); return asStringList(galleryFlags.asText(), " ").contains("Preview"); } public boolean isPreRelease() { loadVsixManifest(); - var preReleaseNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Code.PreRelease"); - return preReleaseNode.path("Value").asBoolean(false); + var preReleaseNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Code.PreRelease"); + return preReleaseNode.path(MANIFEST_VALUE).asBoolean(false); } public String getSponsorLink() { loadVsixManifest(); - var sponsorLinkNode = findByIdInArray(vsixManifest.path("Metadata").path("Properties").path("Property"), "Microsoft.VisualStudio.Code.SponsorLink"); - return sponsorLinkNode.path("Value").asText(); + var sponsorLinkNode = findByIdInArray(vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_PROPERTIES).path(MANIFEST_PROPERTY), "Microsoft.VisualStudio.Code.SponsorLink"); + return sponsorLinkNode.path(MANIFEST_VALUE).asText(); } public ExtensionVersion getMetadata() { @@ -227,10 +237,10 @@ public ExtensionVersion getMetadata() { extVersion.setTargetPlatform(getTargetPlatform()); extVersion.setPreview(isPreview()); extVersion.setPreRelease(isPreRelease()); - extVersion.setDisplayName(vsixManifest.path("Metadata").path("DisplayName").asText()); - extVersion.setDescription(vsixManifest.path("Metadata").path("Description").path("").asText()); + extVersion.setDisplayName(vsixManifest.path(MANIFEST_METADATA).path("DisplayName").asText()); + extVersion.setDescription(vsixManifest.path(MANIFEST_METADATA).path("Description").path("").asText()); extVersion.setEngines(getEngines(packageJson.path("engines"))); - extVersion.setCategories(asStringList(vsixManifest.path("Metadata").path("Categories").asText(), ",")); + extVersion.setCategories(asStringList(vsixManifest.path(MANIFEST_METADATA).path("Categories").asText(), ",")); extVersion.setExtensionKind(getExtensionKinds()); extVersion.setTags(getTags()); extVersion.setLicense(packageJson.path("license").textValue()); @@ -248,11 +258,11 @@ public ExtensionVersion getMetadata() { } public String getVersion() { - return vsixManifest.path("Metadata").path("Identity").path("Version").asText(); + return vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_IDENTITY).path("Version").asText(); } private String getTargetPlatform() { - var targetPlatform = vsixManifest.path("Metadata").path("Identity").path("TargetPlatform").asText(); + var targetPlatform = vsixManifest.path(MANIFEST_METADATA).path(MANIFEST_IDENTITY).path("TargetPlatform").asText(); if (targetPlatform.isEmpty()) { targetPlatform = TargetPlatform.NAME_UNIVERSAL; } @@ -261,7 +271,7 @@ private String getTargetPlatform() { } private List getTags() { - var tags = vsixManifest.path("Metadata").path("Tags").asText(); + var tags = vsixManifest.path(MANIFEST_METADATA).path("Tags").asText(); return asStringList(tags, ",").stream() .collect(Collectors.groupingBy(String::toLowerCase)) .entrySet().stream() @@ -356,7 +366,7 @@ protected TempFile getManifest(ExtensionVersion extVersion) throws IOException { readInputStream(); var entryFile = ArchiveUtil.readEntry(zipFile, PACKAGE_JSON); if (entryFile == null) { - throw new ErrorResultException("Entry not found: " + PACKAGE_JSON); + throw new ErrorResultException(entryNotFoundMessage(PACKAGE_JSON)); } var manifest = new FileResource(); manifest.setExtension(extVersion); @@ -403,7 +413,7 @@ public TempFile getLicense(ExtensionVersion extVersion) throws IOException { var entryFile = ArchiveUtil.readEntry(zipFile, assetPath); if(entryFile == null) { - throw new ErrorResultException("Entry not found: " + assetPath); + throw new ErrorResultException(entryNotFoundMessage(assetPath)); } var lastSegmentIndex = assetPath.lastIndexOf('/'); @@ -418,7 +428,7 @@ private TempFile readFromVsixPackage(String assetType, String[] alternateNames) if(StringUtils.isNotEmpty(assetPath)) { var entryFile = ArchiveUtil.readEntry(zipFile, assetPath); if(entryFile == null) { - throw new ErrorResultException("Entry not found: " + assetPath); + throw new ErrorResultException(entryNotFoundMessage(assetPath)); } var lastSegmentIndex = assetPath.lastIndexOf('/'); @@ -451,7 +461,7 @@ private TempFile readFromAlternateNames(String[] names) throws IOException { private String tryGetLicensePath() { loadVsixManifest(); - var licensePath = vsixManifest.path("Metadata").path("License").asText(); + var licensePath = vsixManifest.path(MANIFEST_METADATA).path("License").asText(); return licensePath.isEmpty() ? tryGetAssetPath(ExtensionQueryResult.ExtensionFile.FILE_LICENSE) : licensePath; @@ -486,7 +496,7 @@ protected TempFile getIcon(ExtensionVersion extVersion) throws IOException { var entryFile = ArchiveUtil.readEntry(zipFile, iconPath); if (entryFile == null) { - throw new ErrorResultException("Entry not found: " + iconPath); + throw new ErrorResultException(entryNotFoundMessage(iconPath)); } var icon = new FileResource(); @@ -511,7 +521,7 @@ public TempFile getVsixManifest(ExtensionVersion extVersion) throws IOException var entryFile = ArchiveUtil.readEntry(zipFile, VSIX_MANIFEST); if(entryFile == null) { - throw new ErrorResultException("Entry not found: " + VSIX_MANIFEST); + throw new ErrorResultException(entryNotFoundMessage(VSIX_MANIFEST)); } entryFile.setResource(vsixManifest); diff --git a/server/src/main/java/org/eclipse/openvsx/ExtensionValidator.java b/server/src/main/java/org/eclipse/openvsx/ExtensionValidator.java index a4832014..acd46781 100644 --- a/server/src/main/java/org/eclipse/openvsx/ExtensionValidator.java +++ b/server/src/main/java/org/eclipse/openvsx/ExtensionValidator.java @@ -49,17 +49,27 @@ public Optional validateNamespace(String namespace) { return Optional.of(new Issue("Invalid namespace name: " + namespace)); } if (namespace.length() > DEFAULT_STRING_SIZE) { - return Optional.of(new Issue("The namespace name exceeds the current limit of " + DEFAULT_STRING_SIZE + " characters.")); + return Optional.of(new Issue(charactersExceededMessage("namespace name", DEFAULT_STRING_SIZE))); } return Optional.empty(); } + private void validateDisplayName(String displayName, int limit, List issues) { + var field = "displayName"; + checkCharacters(displayName, field, issues); + checkFieldSize(displayName, limit, field, issues); + } + + private void validateDescription(String description, int limit, List issues) { + var field = "description"; + checkCharacters(description, field, issues); + checkFieldSize(description, limit, field, issues); + } + public List validateNamespaceDetails(NamespaceDetailsJson json) { var issues = new ArrayList(); - checkCharacters(json.getDisplayName(), "displayName", issues); - checkFieldSize(json.getDisplayName(), 32, "displayName", issues); - checkCharacters(json.getDescription(), "description", issues); - checkFieldSize(json.getDescription(), DEFAULT_STRING_SIZE, "description", issues); + validateDisplayName(json.getDisplayName(), 32, issues); + validateDescription(json.getDescription(), DEFAULT_STRING_SIZE, issues); checkURL(json.getWebsite(), "website", issues); checkURL(json.getSupportLink(), "supportLink", issues); @@ -87,7 +97,7 @@ public Optional validateExtensionName(String name) { return Optional.of(new Issue("Invalid extension name: " + name)); } if (name.length() > DEFAULT_STRING_SIZE) { - return Optional.of(new Issue("The extension name exceeds the current limit of " + DEFAULT_STRING_SIZE + " characters.")); + return Optional.of(new Issue(charactersExceededMessage("extension name", DEFAULT_STRING_SIZE))); } return Optional.empty(); } @@ -104,10 +114,8 @@ public List validateMetadata(ExtensionVersion extVersion) { var issues = new ArrayList(); checkVersion(extVersion.getVersion(), issues); checkTargetPlatform(extVersion.getTargetPlatform(), issues); - checkCharacters(extVersion.getDisplayName(), "displayName", issues); - checkFieldSize(extVersion.getDisplayName(), DEFAULT_STRING_SIZE, "displayName", issues); - checkCharacters(extVersion.getDescription(), "description", issues); - checkFieldSize(extVersion.getDescription(), DESCRIPTION_SIZE, "description", issues); + validateDisplayName(extVersion.getDisplayName(), DEFAULT_STRING_SIZE, issues); + validateDescription(extVersion.getDescription(), DESCRIPTION_SIZE, issues); checkCharacters(extVersion.getCategories(), "categories", issues); checkFieldSize(extVersion.getCategories(), DEFAULT_STRING_SIZE, "categories", issues); checkCharacters(extVersion.getTags(), "keywords", issues); @@ -181,10 +189,14 @@ private void checkCharacters(List values, String field, List issu private void checkFieldSize(String value, int limit, String field, List issues) { if (value != null && value.length() > limit) { - issues.add(new Issue("The field '" + field + "' exceeds the current limit of " + limit + " characters.")); + issues.add(new Issue(charactersExceededMessage("field '" + field + "'", limit))); } } + private String charactersExceededMessage(String name, int limit) { + return "The " + name + " exceeds the current limit of " + limit + " characters."; + } + private void checkFieldSize(List values, int limit, String field, List issues) { if (values == null) { return; diff --git a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java index 37fa6f28..ec1c2834 100644 --- a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java +++ b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java @@ -42,14 +42,16 @@ import static org.eclipse.openvsx.cache.CacheService.*; import static org.eclipse.openvsx.entities.FileResource.*; -import static org.eclipse.openvsx.util.UrlUtil.createApiUrl; -import static org.eclipse.openvsx.util.UrlUtil.createApiVersionUrl; +import static org.eclipse.openvsx.util.UrlUtil.*; @Component public class LocalRegistryService implements IExtensionRegistry { protected final Logger logger = LoggerFactory.getLogger(LocalRegistryService.class); + private static final String RESTRICTED_ACCESS = "restricted"; + private static final String ACCESS_TOKEN_ERROR = "Invalid access token."; + private final EntityManager entityManager; private final RepositoryService repositories; private final ExtensionService extensions; @@ -109,7 +111,7 @@ public NamespaceJson getNamespace(String namespaceName) { } json.setExtensions(extensions); json.setVerified(repositories.hasMemberships(namespace, NamespaceMembership.ROLE_OWNER)); - json.setAccess("restricted"); + json.setAccess(RESTRICTED_ACCESS); return json; } @@ -599,7 +601,7 @@ private Map> getMemberships(Collection(); if (latest != null) @@ -883,7 +885,7 @@ public ExtensionJson toExtensionVersionJson(ExtensionVersion extVersion, String var versionBaseUrl = UrlUtil.createApiVersionBaseUrl(serverUrl, json.getNamespace(), json.getName(), targetPlatform); allVersions.addAll(repositories.findVersionStringsSorted(extension, targetPlatform, onlyActive)); - json.setAllVersionsUrl(UrlUtil.createAllVersionsUrl(json.getNamespace(), json.getName(), targetPlatform, "versions")); + json.setAllVersionsUrl(UrlUtil.createAllVersionsUrl(json.getNamespace(), json.getName(), targetPlatform)); var allVersionsJson = Maps.newLinkedHashMapWithExpectedSize(allVersions.size()); for (var version : allVersions) { allVersionsJson.put(version, createApiUrl(versionBaseUrl, version)); @@ -922,12 +924,12 @@ public ExtensionJson toExtensionVersionJson( var json = extVersion.toExtensionJson(); json.setPreview(preview); json.setVerified(isVerified(extVersion, membershipsByNamespaceId)); - json.setNamespaceAccess("restricted"); + json.setNamespaceAccess(RESTRICTED_ACCESS); json.setUnrelatedPublisher(!json.getVerified()); json.setReviewCount(reviewCount); var serverUrl = UrlUtil.getBaseUrl(); json.setNamespaceUrl(createApiUrl(serverUrl, "api", json.getNamespace())); - json.setReviewsUrl(createApiUrl(serverUrl, "api", json.getNamespace(), json.getName(), "reviews")); + json.setReviewsUrl(createApiReviewsUrl(serverUrl, json.getNamespace(), json.getName())); var extension = extVersion.getExtension(); if(extension.getReplacement() != null) { @@ -965,7 +967,7 @@ public ExtensionJson toExtensionVersionJson( allVersions.addAll(versions); } - json.setAllVersionsUrl(UrlUtil.createAllVersionsUrl(json.getNamespace(), json.getName(), targetPlatformParam, "versions")); + json.setAllVersionsUrl(UrlUtil.createAllVersionsUrl(json.getNamespace(), json.getName(), targetPlatformParam)); var allVersionsJson = Maps.newLinkedHashMapWithExpectedSize(allVersions.size()); var versionBaseUrl = UrlUtil.createApiVersionBaseUrl(serverUrl, json.getNamespace(), json.getName(), targetPlatformParam); for(var version : allVersions) { @@ -1013,12 +1015,12 @@ public ExtensionJson toExtensionVersionJsonV2( var json = extVersion.toExtensionJson(); json.setPreview(preview); json.setVerified(isVerified(extVersion, membershipsByNamespaceId)); - json.setNamespaceAccess("restricted"); + json.setNamespaceAccess(RESTRICTED_ACCESS); json.setUnrelatedPublisher(!json.getVerified()); json.setReviewCount(reviewCount); var serverUrl = UrlUtil.getBaseUrl(); json.setNamespaceUrl(createApiUrl(serverUrl, "api", json.getNamespace())); - json.setReviewsUrl(createApiUrl(serverUrl, "api", json.getNamespace(), json.getName(), "reviews")); + json.setReviewsUrl(createApiReviewsUrl(serverUrl, json.getNamespace(), json.getName())); json.setUrl(createApiVersionUrl(serverUrl, json)); var extension = extVersion.getExtension(); @@ -1058,7 +1060,7 @@ public ExtensionJson toExtensionVersionJsonV2( allVersions.addAll(versions); } - json.setAllVersionsUrl(UrlUtil.createAllVersionsUrl(json.getNamespace(), json.getName(), targetPlatformParam, "versions")); + json.setAllVersionsUrl(UrlUtil.createAllVersionsUrl(json.getNamespace(), json.getName(), targetPlatformParam)); if(!allVersions.isEmpty()) { var allVersionsJson = Maps.newLinkedHashMapWithExpectedSize(allVersions.size()); var versionBaseUrl = UrlUtil.createApiVersionBaseUrl(serverUrl, json.getNamespace(), json.getName(), targetPlatformParam); diff --git a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java index 65d714c1..74b835ca 100644 --- a/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java @@ -46,6 +46,7 @@ public class RegistryAPI { private final static int REVIEW_TITLE_SIZE = 255; private final static int REVIEW_COMMENT_SIZE = 2048; private final static String VERSION_PATH_PARAM_REGEX = "(?:" + SemanticVersion.VERSION_PATH_PARAM_REGEX + ")|latest|pre-release"; + private final static String NO_JSON_INPUT = "No JSON input."; protected final Logger logger = LoggerFactory.getLogger(RegistryAPI.class); @@ -166,10 +167,30 @@ public ResponseEntity getNamespaceDetails( // Try the next registry } } - var json = NamespaceDetailsJson.error("Namespace not found: " + namespace); + var json = NamespaceDetailsJson.error(namespaceNotFoundMessage(namespace)); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } + private String extensionNotFoundMessage(String extension) { + return "Extension not found: " + extension; + } + + private String namespaceNotFoundMessage(String namespace) { + return "Namespace not found: " + namespace; + } + + private String negativeSizeMessage() { + return negativeParameterMessage("size"); + } + + private String negativeOffsetMessage() { + return negativeParameterMessage("offset"); + } + + private String negativeParameterMessage(String field) { + return "The parameter '" + field + "' must not be negative."; + } + @GetMapping( path = "/api/{namespace}/logo/{fileName}", produces = { MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE } @@ -231,7 +252,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + NamingUtil.toExtensionId(namespace, extension)); + var json = ExtensionJson.error(extensionNotFoundMessage(NamingUtil.toExtensionId(namespace, extension))); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -278,7 +299,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, targetPlatform.toString(), null)); + var json = ExtensionJson.error(extensionNotFoundMessage(NamingUtil.toLogFormat(namespace, extension, targetPlatform.toString(), null))); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -314,7 +335,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, version)); + var json = ExtensionJson.error(extensionNotFoundMessage(NamingUtil.toLogFormat(namespace, extension, version))); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -363,7 +384,7 @@ public ResponseEntity getExtension( // Try the next registry } } - var json = ExtensionJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, targetPlatform, version)); + var json = ExtensionJson.error(extensionNotFoundMessage(NamingUtil.toLogFormat(namespace, extension, targetPlatform, version))); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -442,11 +463,11 @@ public ResponseEntity getVersions( private ResponseEntity handleGetVersions(String namespace, String extension, String targetPlatform, int size, int offset) { if (size < 0) { - var json = VersionsJson.error("The parameter 'size' must not be negative."); + var json = VersionsJson.error(negativeSizeMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } if (offset < 0) { - var json = VersionsJson.error("The parameter 'offset' must not be negative."); + var json = VersionsJson.error(negativeOffsetMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } for (var registry : getRegistries()) { @@ -458,7 +479,7 @@ private ResponseEntity handleGetVersions(String namespace, String // Try the next registry } } - var json = VersionsJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, targetPlatform)); + var json = VersionsJson.error(extensionNotFoundMessage(NamingUtil.toLogFormat(namespace, extension, targetPlatform))); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -537,11 +558,11 @@ public ResponseEntity getVersionReferences( private ResponseEntity handleGetVersionReferences(String namespace, String extension, String targetPlatform, int size, int offset) { if (size < 0) { - var json = VersionReferencesJson.error("The parameter 'size' must not be negative."); + var json = VersionReferencesJson.error(negativeSizeMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } if (offset < 0) { - var json = VersionReferencesJson.error("The parameter 'offset' must not be negative."); + var json = VersionReferencesJson.error(negativeOffsetMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } for (var registry : getRegistries()) { @@ -553,7 +574,7 @@ private ResponseEntity handleGetVersionReferences(String // Try the next registry } } - var json = VersionReferencesJson.error("Extension not found: " + NamingUtil.toLogFormat(namespace, extension, targetPlatform)); + var json = VersionReferencesJson.error(extensionNotFoundMessage(NamingUtil.toLogFormat(namespace, extension, targetPlatform))); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -684,7 +705,7 @@ public ResponseEntity getReviews( // Try the next registry } } - var json = ReviewListJson.error("Extension not found: " + NamingUtil.toExtensionId(namespace, extension)); + var json = ReviewListJson.error(extensionNotFoundMessage(NamingUtil.toExtensionId(namespace, extension))); return new ResponseEntity<>(json, HttpStatus.NOT_FOUND); } @@ -743,11 +764,11 @@ public ResponseEntity search( boolean includeAllVersions ) { if (size < 0) { - var json = SearchResultJson.error("The parameter 'size' must not be negative."); + var json = SearchResultJson.error(negativeSizeMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } if (offset < 0) { - var json = SearchResultJson.error("The parameter 'offset' must not be negative."); + var json = SearchResultJson.error(negativeOffsetMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } @@ -860,11 +881,11 @@ public ResponseEntity getQueryV2( int offset ) { if (size < 0) { - var json = QueryResultJson.error("The parameter 'size' must not be negative."); + var json = QueryResultJson.error(negativeSizeMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } if (offset < 0) { - var json = QueryResultJson.error("The parameter 'offset' must not be negative."); + var json = QueryResultJson.error(negativeOffsetMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } if(!List.of("true", "false", "links").contains(includeAllVersions)) { @@ -977,11 +998,11 @@ public ResponseEntity getQuery( int offset ) { if (size < 0) { - var json = QueryResultJson.error("The parameter 'size' must not be negative."); + var json = QueryResultJson.error(negativeSizeMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } if (offset < 0) { - var json = QueryResultJson.error("The parameter 'offset' must not be negative."); + var json = QueryResultJson.error(negativeOffsetMessage()); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } @@ -1100,7 +1121,7 @@ public ResponseEntity createNamespace( String token ) { if (namespace == null) { - return ResponseEntity.ok(ResultJson.error("No JSON input.")); + return ResponseEntity.ok(ResultJson.error(NO_JSON_INPUT)); } if (StringUtils.isEmpty(namespace.getName())) { return ResponseEntity.ok(ResultJson.error("Missing required property 'name'.")); @@ -1164,7 +1185,7 @@ public ResponseEntity createNamespace( } if (namespace == null) { - return ResponseEntity.ok(ResultJson.error("No JSON input.")); + return ResponseEntity.ok(ResultJson.error(NO_JSON_INPUT)); } if (StringUtils.isEmpty(namespace.getName())) { return ResponseEntity.ok(ResultJson.error("Missing required property 'name'.")); @@ -1293,7 +1314,7 @@ public ResponseEntity postReview( @PathVariable String extension ) { if (review == null) { - var json = ResultJson.error("No JSON input."); + var json = ResultJson.error(NO_JSON_INPUT); return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST); } if (review.getRating() < 0 || review.getRating() > 5) { diff --git a/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java b/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java index 3fd4878a..0910c093 100644 --- a/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java +++ b/server/src/main/java/org/eclipse/openvsx/UpstreamRegistryService.java @@ -38,6 +38,16 @@ @Component public class UpstreamRegistryService implements IExtensionRegistry { + private static final String VAR_NAMESPACE = "namespace"; + private static final String VAR_EXTENSION = "extension"; + private static final String VAR_TARGET = "targetPlatform"; + private static final String VAR_OFFSET = "offset"; + private static final String VAR_SIZE = "size"; + private static final String VAR_ALL_VERSIONS = "includeAllVersions"; + private static final String URL_EXTENSION_FRAGMENT = "/api/{namespace}/{extension}"; + private static final String URL_TARGET_FRAGMENT = "/{targetPlatform}"; + + protected final Logger logger = LoggerFactory.getLogger(UpstreamRegistryService.class); private final RestTemplate restTemplate; @@ -61,7 +71,7 @@ public boolean isValid() { @Override public NamespaceJson getNamespace(String namespace) { var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}"; - var uriVariables = Map.of("namespace", namespace); + var uriVariables = Map.of(VAR_NAMESPACE, namespace); try { var json = restTemplate.getForObject(urlTemplate, NamespaceJson.class, uriVariables); return proxy != null ? proxy.rewriteUrls(json) : json; @@ -77,7 +87,7 @@ public NamespaceJson getNamespace(String namespace) { @Override public NamespaceDetailsJson getNamespaceDetails(String namespace) { var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/details"; - var uriVariables = Map.of("namespace", namespace); + var uriVariables = Map.of(VAR_NAMESPACE, namespace); try { return restTemplate.getForObject(urlTemplate, NamespaceDetailsJson.class, uriVariables); } catch (RestClientException exc) { @@ -89,19 +99,19 @@ public NamespaceDetailsJson getNamespaceDetails(String namespace) { @Override public ResponseEntity getNamespaceLogo(String namespaceName, String fileName) { var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/logo/{file}"; - var uriVariables = Map.of("namespace", namespaceName, "file", fileName); + var uriVariables = Map.of(VAR_NAMESPACE, namespaceName, "file", fileName); return getFile(urlTemplate, uriVariables); } @Override public ExtensionJson getExtension(String namespace, String extension, String targetPlatform) { - var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/{extension}"; + var urlTemplate = urlConfigService.getUpstreamUrl() + URL_EXTENSION_FRAGMENT; var uriVariables = new HashMap(); - uriVariables.put("namespace", namespace); - uriVariables.put("extension", extension); + uriVariables.put(VAR_NAMESPACE, namespace); + uriVariables.put(VAR_EXTENSION, extension); if(targetPlatform != null) { - urlTemplate += "/{targetPlatform}"; - uriVariables.put("targetPlatform", targetPlatform); + urlTemplate += URL_TARGET_FRAGMENT; + uriVariables.put(VAR_TARGET, targetPlatform); } try { @@ -119,13 +129,13 @@ public ExtensionJson getExtension(String namespace, String extension, String tar @Override public ExtensionJson getExtension(String namespace, String extension, String targetPlatform, String version) { - var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/{extension}"; + var urlTemplate = urlConfigService.getUpstreamUrl() + URL_EXTENSION_FRAGMENT; var uriVariables = new HashMap(); - uriVariables.put("namespace", namespace); - uriVariables.put("extension", extension); + uriVariables.put(VAR_NAMESPACE, namespace); + uriVariables.put(VAR_EXTENSION, extension); if(targetPlatform != null) { - urlTemplate += "/{targetPlatform}"; - uriVariables.put("targetPlatform", targetPlatform); + urlTemplate += URL_TARGET_FRAGMENT; + uriVariables.put(VAR_TARGET, targetPlatform); } if(version != null) { urlTemplate += "/{version}"; @@ -147,18 +157,18 @@ public ExtensionJson getExtension(String namespace, String extension, String tar @Override public VersionsJson getVersions(String namespace, String extension, String targetPlatform, int size, int offset) { - var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/{extension}"; + var urlTemplate = urlConfigService.getUpstreamUrl() + URL_EXTENSION_FRAGMENT; var uriVariables = new HashMap(); - uriVariables.put("namespace", namespace); - uriVariables.put("extension", extension); + uriVariables.put(VAR_NAMESPACE, namespace); + uriVariables.put(VAR_EXTENSION, extension); if(targetPlatform != null) { - urlTemplate += "/{targetPlatform}"; - uriVariables.put("targetPlatform", targetPlatform); + urlTemplate += URL_TARGET_FRAGMENT; + uriVariables.put(VAR_TARGET, targetPlatform); } urlTemplate = "/versions?offset={offset}&size={size}"; - uriVariables.put("offset", String.valueOf(offset)); - uriVariables.put("size", String.valueOf(size)); + uriVariables.put(VAR_OFFSET, String.valueOf(offset)); + uriVariables.put(VAR_SIZE, String.valueOf(size)); try { var json = restTemplate.getForObject(urlTemplate, VersionsJson.class, uriVariables); @@ -174,18 +184,18 @@ public VersionsJson getVersions(String namespace, String extension, String targe @Override public VersionReferencesJson getVersionReferences(String namespace, String extension, String targetPlatform, int size, int offset) { - var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/{extension}"; + var urlTemplate = urlConfigService.getUpstreamUrl() + URL_EXTENSION_FRAGMENT; var uriVariables = new HashMap(); - uriVariables.put("namespace", namespace); - uriVariables.put("extension", extension); + uriVariables.put(VAR_NAMESPACE, namespace); + uriVariables.put(VAR_EXTENSION, extension); if(targetPlatform != null) { - urlTemplate += "/{targetPlatform}"; - uriVariables.put("targetPlatform", targetPlatform); + urlTemplate += URL_TARGET_FRAGMENT; + uriVariables.put(VAR_TARGET, targetPlatform); } urlTemplate = "/version-references?offset={offset}&size={size}"; - uriVariables.put("offset", String.valueOf(offset)); - uriVariables.put("size", String.valueOf(size)); + uriVariables.put(VAR_OFFSET, String.valueOf(offset)); + uriVariables.put(VAR_SIZE, String.valueOf(size)); try { var json = restTemplate.getForObject(urlTemplate, VersionReferencesJson.class, uriVariables); @@ -201,16 +211,16 @@ public VersionReferencesJson getVersionReferences(String namespace, String exten @Override public ResponseEntity getFile(String namespace, String extension, String targetPlatform, String version, String fileName) { - var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/{extension}"; + var urlTemplate = urlConfigService.getUpstreamUrl() + URL_EXTENSION_FRAGMENT; var uriVariables = new HashMap(); - uriVariables.put("namespace", namespace); - uriVariables.put("extension", extension); + uriVariables.put(VAR_NAMESPACE, namespace); + uriVariables.put(VAR_EXTENSION, extension); if(TargetPlatform.isUniversal(targetPlatform)) { targetPlatform = null; } if(targetPlatform != null) { - urlTemplate += "/{targetPlatform}"; - uriVariables.put("targetPlatform", targetPlatform); + urlTemplate += URL_TARGET_FRAGMENT; + uriVariables.put(VAR_TARGET, targetPlatform); } urlTemplate += "/{version}/file/{fileName}"; @@ -259,8 +269,8 @@ public ResponseEntity extractData(ClientHttpResponse resp public ReviewListJson getReviews(String namespace, String extension) { var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/{namespace}/{extension}/reviews"; var uriVariables = new HashMap(); - uriVariables.put("namespace", namespace); - uriVariables.put("extension", extension); + uriVariables.put(VAR_NAMESPACE, namespace); + uriVariables.put(VAR_EXTENSION, extension); try { return restTemplate.getForObject(urlTemplate, ReviewListJson.class, uriVariables); @@ -278,14 +288,14 @@ public ReviewListJson getReviews(String namespace, String extension) { public SearchResultJson search(ISearchService.Options options) { var urlTemplate = urlConfigService.getUpstreamUrl() + "/api/-/search"; var uriVariables = new HashMap(); - uriVariables.put("size", Integer.toString(options.requestedSize())); - uriVariables.put("offset", Integer.toString(options.requestedOffset())); - uriVariables.put("includeAllVersions", Boolean.toString(options.includeAllVersions())); + uriVariables.put(VAR_SIZE, Integer.toString(options.requestedSize())); + uriVariables.put(VAR_OFFSET, Integer.toString(options.requestedOffset())); + uriVariables.put(VAR_ALL_VERSIONS, Boolean.toString(options.includeAllVersions())); uriVariables.put("query", options.queryString()); uriVariables.put("category", options.category()); uriVariables.put("sortOrder", options.sortOrder()); uriVariables.put("sortBy", options.sortBy()); - uriVariables.put("targetPlatform", options.targetPlatform()); + uriVariables.put(VAR_TARGET, options.targetPlatform()); var queryString = uriVariables.entrySet().stream() .filter(entry -> !StringUtils.isEmpty(entry.getValue())) @@ -318,10 +328,10 @@ public QueryResultJson query(QueryRequest request) { queryParams.put("extensionId", request.extensionId()); queryParams.put("extensionUuid", request.extensionUuid()); queryParams.put("namespaceUuid", request.namespaceUuid()); - queryParams.put("includeAllVersions", String.valueOf(request.includeAllVersions())); - queryParams.put("targetPlatform", request.targetPlatform()); - queryParams.put("size", String.valueOf(request.size())); - queryParams.put("offset", String.valueOf(request.offset())); + queryParams.put(VAR_ALL_VERSIONS, String.valueOf(request.includeAllVersions())); + queryParams.put(VAR_TARGET, request.targetPlatform()); + queryParams.put(VAR_SIZE, String.valueOf(request.size())); + queryParams.put(VAR_OFFSET, String.valueOf(request.offset())); var queryString = queryParams.entrySet().stream() .filter(entry -> !StringUtils.isEmpty(entry.getValue())) @@ -354,10 +364,10 @@ public QueryResultJson queryV2(QueryRequestV2 request) { queryParams.put("extensionId", request.extensionId()); queryParams.put("extensionUuid", request.extensionUuid()); queryParams.put("namespaceUuid", request.namespaceUuid()); - queryParams.put("includeAllVersions", String.valueOf(request.includeAllVersions())); - queryParams.put("targetPlatform", request.targetPlatform()); - queryParams.put("size", String.valueOf(request.size())); - queryParams.put("offset", String.valueOf(request.offset())); + queryParams.put(VAR_ALL_VERSIONS, String.valueOf(request.includeAllVersions())); + queryParams.put(VAR_TARGET, request.targetPlatform()); + queryParams.put(VAR_SIZE, String.valueOf(request.size())); + queryParams.put(VAR_OFFSET, String.valueOf(request.offset())); var queryString = queryParams.entrySet().stream() .filter(entry -> !StringUtils.isEmpty(entry.getValue())) 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 8d2c0f7d..19a3d72c 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java @@ -49,6 +49,11 @@ public class LocalVSCodeService implements IVSCodeService { private final ExtensionVersionIntegrityService integrityService; private final WebResourceService webResources; + private final StreamingResponseBody builtinExtensionResponse = outputStream -> { + var message = "Built-in extension namespace '" + BuiltInExtensionUtil.getBuiltInNamespace() + "' not allowed"; + outputStream.write(message.getBytes(StandardCharsets.UTF_8)); + }; + @Value("${ovsx.webui.url:}") String webuiUrl; @@ -270,10 +275,7 @@ public ResponseEntity getAsset( String restOfTheUrl ) { if(BuiltInExtensionUtil.isBuiltIn(namespace)) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(outputStream -> { - var message = "Built-in extension namespace '" + BuiltInExtensionUtil.getBuiltInNamespace() + "' not allowed"; - outputStream.write(message.getBytes(StandardCharsets.UTF_8)); - }); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(builtinExtensionResponse); } var asset = (restOfTheUrl != null && !restOfTheUrl.isEmpty()) ? (assetType + "/" + restOfTheUrl) : assetType; @@ -328,10 +330,14 @@ public ResponseEntity getAsset( throw new NotFoundException(); } + private String builtinExtensionMessage() { + return "Built-in extension namespace '" + BuiltInExtensionUtil.getBuiltInNamespace() + "' not allowed"; + } + @Override public String getItemUrl(String namespaceName, String extensionName) { if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Built-in extension namespace '" + BuiltInExtensionUtil.getBuiltInNamespace() + "' not allowed"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, builtinExtensionMessage()); } var extension = repositories.findActiveExtension(extensionName, namespaceName); @@ -345,7 +351,7 @@ public String getItemUrl(String namespaceName, String extensionName) { @Override public String download(String namespaceName, String extensionName, String version, String targetPlatform) { if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Built-in extension namespace '" + BuiltInExtensionUtil.getBuiltInNamespace() + "' not allowed"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, builtinExtensionMessage()); } var resource = repositories.findFileByType(namespaceName, extensionName, targetPlatform, version, FileResource.DOWNLOAD); @@ -372,10 +378,7 @@ public String download(String namespaceName, String extensionName, String versio @Override public ResponseEntity browse(String namespaceName, String extensionName, String version, String path) { if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(outputStream -> { - var message = "Built-in extension namespace '" + BuiltInExtensionUtil.getBuiltInNamespace() + "' not allowed"; - outputStream.write(message.getBytes(StandardCharsets.UTF_8)); - }); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(builtinExtensionResponse); } var file = webResources.getWebResource(namespaceName, extensionName, null, version, path, true); 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 6ab5be5f..3125154e 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java @@ -37,6 +37,9 @@ @Component 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); @@ -88,9 +91,9 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul public ResponseEntity browse(String namespaceName, String extensionName, String version, String path) { var urlBuilder = new StringBuilder(urlConfigService.getUpstreamUrl() + "/vscode/unpkg/{namespace}/{extension}/{version}"); var uriVariables = new HashMap<>(Map.of( - "namespace", namespaceName, - "extension", extensionName, - "version", version + VAR_NAMESPACE, namespaceName, + VAR_EXTENSION, extensionName, + VAR_VERSION, version )); if (path != null && !path.isBlank()) { @@ -145,8 +148,7 @@ public ResponseEntity extractData(ClientHttpResponse resp } } if(statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { - var url = UriComponentsBuilder.fromUriString(urlBuilder.toString()).build(uriVariables); - logger.error("GET {}: {}", url, response); + handleResponseError(urlTemplate, uriVariables, response); } throw new NotFoundException(); @@ -164,9 +166,9 @@ public ResponseEntity extractData(ClientHttpResponse resp public String download(String namespace, String extension, String version, String targetPlatform) { var urlTemplate = urlConfigService.getUpstreamUrl() + "/vscode/gallery/publishers/{namespace}/vsextensions/{extension}/{version}/vspackage?targetPlatform={targetPlatform}"; var uriVariables = Map.of( - "namespace", namespace, - "extension", extension, - "version", version, + VAR_NAMESPACE, namespace, + VAR_EXTENSION, extension, + VAR_VERSION, version, "targetPlatform", targetPlatform ); @@ -188,8 +190,7 @@ public String download(String namespace, String extension, String version, Strin return location.toString(); } if(statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { - var url = UriComponentsBuilder.fromUriString(urlTemplate).build(uriVariables); - logger.error("GET {}: {}", url, response); + handleResponseError(urlTemplate, uriVariables, response); } throw new NotFoundException(); @@ -198,7 +199,7 @@ public String download(String namespace, String extension, String version, Strin @Override public String getItemUrl(String namespace, String extension) { var urlTemplate = urlConfigService.getUpstreamUrl() + "/vscode/item?itemName={namespace}.{extension}"; - var uriVariables = Map.of("namespace", namespace, "extension", extension); + var uriVariables = Map.of(VAR_NAMESPACE, namespace, VAR_EXTENSION, extension); ResponseEntity response; var method = HttpMethod.GET; @@ -218,8 +219,7 @@ public String getItemUrl(String namespace, String extension) { return location.toString(); } if(statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { - var url = UriComponentsBuilder.fromUriString(urlTemplate).build(uriVariables); - logger.error("GET {}: {}", url, response); + handleResponseError(urlTemplate, uriVariables, response); } throw new NotFoundException(); @@ -231,9 +231,9 @@ public ResponseEntity getAsset(String namespace, String e .append(urlConfigService.getUpstreamUrl()) .append("/vscode/asset/{namespace}/{extension}/{version}/{assetType}"); var uriVariables = new HashMap<>(Map.of( - "namespace", namespace, - "extension", extensionName, - "version", version, + VAR_NAMESPACE, namespace, + VAR_EXTENSION, extensionName, + VAR_VERSION, version, "assetType", assetType, "targetPlatform", targetPlatform )); @@ -292,8 +292,7 @@ public ResponseEntity extractData(ClientHttpResponse resp .build(); } if(statusCode.isError() && statusCode != HttpStatus.NOT_FOUND) { - var url = UriComponentsBuilder.fromUriString(urlTemplate).build(uriVariables); - logger.error("GET {}: {}", url, response); + handleResponseError(urlTemplate, uriVariables, response); } throw new NotFoundException(); @@ -307,6 +306,11 @@ public ResponseEntity extractData(ClientHttpResponse resp } } + private void handleResponseError(String urlTemplate, Map uriVariables, Object response) { + var url = UriComponentsBuilder.fromUriString(urlTemplate).build(uriVariables); + logger.error("GET {}: {}", url, response); + } + private NotFoundException propagateRestException(RestClientException exc, HttpMethod method, String urlTemplate, Map uriVariables) { if (exc instanceof HttpStatusCodeException) { diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java index 8c20e05c..6b5bb4c0 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java @@ -242,9 +242,9 @@ public ResponseEntity getNamespace(@PathVariable String namespace admins.checkAdminUser(); var namespace = local.getNamespace(namespaceName); - var serverUrl = UrlUtil.getBaseUrl(); - namespace.setMembersUrl(UrlUtil.createApiUrl(serverUrl, "admin", "namespace", namespace.getName(), "members")); - namespace.setRoleUrl(UrlUtil.createApiUrl(serverUrl, "admin", "namespace", namespace.getName(), "change-member")); + var adminNamespaceUrl = createAdminNamespaceUrl(namespace); + namespace.setMembersUrl(UrlUtil.createApiUrl(adminNamespaceUrl, "members")); + namespace.setRoleUrl(UrlUtil.createApiUrl(adminNamespaceUrl, "change-member")); return ResponseEntity.ok(namespace); } catch (NotFoundException exc) { var json = NamespaceJson.error("Namespace not found: " + namespaceName); @@ -254,6 +254,10 @@ public ResponseEntity getNamespace(@PathVariable String namespace } } + private String createAdminNamespaceUrl(NamespaceJson namespace) { + return UrlUtil.createApiUrl(UrlUtil.getBaseUrl(), "admin", "namespace", namespace.getName()); + } + @PostMapping( path = "/admin/create-namespace", consumes = MediaType.APPLICATION_JSON_VALUE, @@ -263,8 +267,7 @@ public ResponseEntity createNamespace(@RequestBody NamespaceJson nam try { admins.checkAdminUser(); var json = admins.createNamespace(namespace); - var serverUrl = UrlUtil.getBaseUrl(); - var url = UrlUtil.createApiUrl(serverUrl, "admin", "namespace", namespace.getName()); + var url = createAdminNamespaceUrl(namespace); return ResponseEntity.status(HttpStatus.CREATED) .location(URI.create(url)) .body(json); diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java index 15783f46..b29949a4 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java @@ -166,6 +166,10 @@ private void removeExtensionVersion(ExtensionVersion extVersion) { entityManager.remove(extVersion); } + private String userNotFoundMessage(String user) { + return "User not found: " + user; + } + @Transactional(rollbackOn = ErrorResultException.class) public ResultJson editNamespaceMember(String namespaceName, String userName, String provider, String role, UserData admin) throws ErrorResultException { @@ -175,7 +179,7 @@ public ResultJson editNamespaceMember(String namespaceName, String userName, Str } var user = repositories.findUserByLoginName(provider, userName); if (user == null) { - throw new ErrorResultException("User not found: " + provider + "/" + userName); + throw new ErrorResultException(userNotFoundMessage(provider + "/" + userName)); } var result = role.equals("remove") @@ -253,7 +257,7 @@ public void changeNamespace(ChangeNamespaceJson json) { public UserPublishInfoJson getUserPublishInfo(String provider, String loginName) { var user = repositories.findUserByLoginName(provider, loginName); if (user == null) { - throw new ErrorResultException("User not found: " + loginName, HttpStatus.NOT_FOUND); + throw new ErrorResultException(userNotFoundMessage(loginName), HttpStatus.NOT_FOUND); } var userPublishInfo = new UserPublishInfoJson(); @@ -285,7 +289,7 @@ public UserPublishInfoJson getUserPublishInfo(String provider, String loginName) public ResultJson revokePublisherContributions(String provider, String loginName, UserData admin) { var user = repositories.findUserByLoginName(provider, loginName); if (user == null) { - throw new ErrorResultException("User not found: " + loginName, HttpStatus.NOT_FOUND); + throw new ErrorResultException(userNotFoundMessage(loginName), HttpStatus.NOT_FOUND); } // Send a DELETE request to the Eclipse publisher agreement API diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandler.java index 79b37213..fdb34beb 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandler.java @@ -37,70 +37,24 @@ public void run(AdminStatisticsJobRequest jobRequest) throws Exception { var year = jobRequest.getYear(); var month = jobRequest.getMonth(); - LOGGER.info(">> ADMIN REPORT STATS {} {}", year, month); - var stopwatch = new StopWatch(); - stopwatch.start("repositories.countActiveExtensions"); var extensions = repositories.countActiveExtensions(); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.downloadsUntil"); var downloadsTotal = repositories.downloadsTotal(); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); var lastDate = LocalDateTime.of(year, month, 1, 0, 0).minusMonths(1); var lastAdminStatistics = repositories.findAdminStatisticsByYearAndMonth(lastDate.getYear(), lastDate.getMonthValue()); var lastDownloadsTotal = lastAdminStatistics != null ? lastAdminStatistics.getDownloadsTotal() : 0; var downloads = downloadsTotal - lastDownloadsTotal; - - stopwatch.start("repositories.countActiveExtensionPublishers"); var publishers = repositories.countActiveExtensionPublishers(); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.averageNumberOfActiveReviewsPerActiveExtension"); var averageReviewsPerExtension = repositories.averageNumberOfActiveReviewsPerActiveExtension(); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.countPublishersThatClaimedNamespaceOwnership"); var namespaceOwners = repositories.countPublishersThatClaimedNamespaceOwnership(); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.countActiveExtensionsGroupedByExtensionReviewRating"); var extensionsByRating = repositories.countActiveExtensionsGroupedByExtensionReviewRating(); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.countActiveExtensionPublishersGroupedByExtensionsPublished"); var publishersByExtensionsPublished = repositories.countActiveExtensionPublishersGroupedByExtensionsPublished(); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); var limit = 10; - - stopwatch.start("repositories.topMostActivePublishingUsers"); var topMostActivePublishingUsers = repositories.topMostActivePublishingUsers(limit); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.topNamespaceExtensions"); var topNamespaceExtensions = repositories.topNamespaceExtensions(limit); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.topNamespaceExtensionVersions"); var topNamespaceExtensionVersions = repositories.topNamespaceExtensionVersions(limit); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - - stopwatch.start("repositories.topMostDownloadedExtensions"); var topMostDownloadedExtensions = repositories.topMostDownloadedExtensions(limit); - stopwatch.stop(); - LOGGER.info("{} took {} ms", stopwatch.getLastTaskName(), stopwatch.getLastTaskTimeMillis()); - LOGGER.info("<< ADMIN REPORT STATS {} {}", year, month); var statistics = new AdminStatistics(); statistics.setYear(year); diff --git a/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java b/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java index fc8cfdfd..875c300f 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/ChangeNamespaceJobRequestHandler.java @@ -33,6 +33,8 @@ @Component public class ChangeNamespaceJobRequestHandler implements JobRequestHandler { + private static final String EXT_PACKAGE = ".vsix"; + private static final Logger LOGGER = LoggerFactory.getLogger(ChangeNamespaceJobRequestHandler.class); private static final List RENAME_TYPES = List.of(DOWNLOAD, DOWNLOAD_SHA256, DOWNLOAD_SIG); @@ -172,10 +174,10 @@ private String getNewResourceName(FileResource resource, Map newBi : resource.getName(); if(resource.getType().equals(DOWNLOAD_SHA256)) { - name = name.replace(".vsix", ".sha256"); + name = name.replace(EXT_PACKAGE, ".sha256"); } if(resource.getType().equals(DOWNLOAD_SIG)) { - name = name.replace(".vsix", ".sigzip"); + name = name.replace(EXT_PACKAGE, ".sigzip"); } LOGGER.info("New resource name: {}", name); @@ -188,7 +190,7 @@ private String newBinaryName(Namespace newNamespace, ExtensionVersion extVersion extVersion.getExtension().getName(), extVersion.getTargetPlatform(), extVersion.getVersion(), - ".vsix" + EXT_PACKAGE ); } } diff --git a/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java index b910df18..14d471d1 100644 --- a/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java +++ b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java @@ -48,6 +48,8 @@ @Component public class EclipseService { + private static final String VAR_PERSON_ID = "personId"; + public static final DateTimeFormatter CUSTOM_DATE_TIME = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(DateTimeFormatter.ISO_LOCAL_DATE) @@ -126,7 +128,7 @@ public void checkPublisherAgreement(UserData user) { public EclipseProfile getPublicProfile(String personId) { checkApiUrl(); var urlTemplate = eclipseApiUrl + "account/profile/{personId}"; - var uriVariables = Map.of("personId", personId); + var uriVariables = Map.of(VAR_PERSON_ID, personId); var headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); var request = new HttpEntity(headers); @@ -289,7 +291,7 @@ public PublisherAgreement getPublisherAgreement(UserData user) { } checkApiUrl(); var urlTemplate = eclipseApiUrl + "openvsx/publisher_agreement/{personId}"; - var uriVariables = Map.of("personId", personId); + var uriVariables = Map.of(VAR_PERSON_ID, personId); var headers = new HttpHeaders(); headers.setBearerAuth(eclipseToken.accessToken()); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); @@ -425,7 +427,7 @@ public void revokePublisherAgreement(UserData user, UserData admin) { headers.setBearerAuth(eclipseToken.accessToken()); var request = new HttpEntity<>(headers); var urlTemplate = eclipseApiUrl + "openvsx/publisher_agreement/{personId}"; - var uriVariables = Map.of("personId", user.getEclipsePersonId()); + var uriVariables = Map.of(VAR_PERSON_ID, user.getEclipsePersonId()); try { var requestCallback = restTemplate.httpEntityCallback(request); diff --git a/server/src/main/java/org/eclipse/openvsx/storage/AzureBlobStorageService.java b/server/src/main/java/org/eclipse/openvsx/storage/AzureBlobStorageService.java index 8a92dc40..10e07ace 100644 --- a/server/src/main/java/org/eclipse/openvsx/storage/AzureBlobStorageService.java +++ b/server/src/main/java/org/eclipse/openvsx/storage/AzureBlobStorageService.java @@ -74,6 +74,14 @@ protected BlobContainerClient getContainerClient() { return containerClient; } + private String missingEndpointMessage(String name) { + return missingEndpointMessage("Cannot determine location of file", name); + } + + private String missingEndpointMessage(String action, String name) { + return action + " " + name + ": missing Azure blob service endpoint"; + } + @Override public void uploadFile(TempFile tempFile) { var resource = tempFile.getResource(); @@ -90,8 +98,7 @@ public void uploadNamespaceLogo(TempFile logoFile) { protected void uploadFile(TempFile file, String fileName, String blobName) { if (StringUtils.isEmpty(serviceEndpoint)) { - throw new IllegalStateException("Cannot upload file " - + blobName + ": missing Azure blob service endpoint"); + throw new IllegalStateException(missingEndpointMessage("Cannot upload file", blobName)); } var blobClient = getContainerClient().getBlobClient(blobName); @@ -120,8 +127,7 @@ public void removeNamespaceLogo(Namespace namespace) { private void removeFile(String blobName) { if (StringUtils.isEmpty(serviceEndpoint)) { - throw new IllegalStateException("Cannot remove file " - + blobName + ": missing Azure blob service endpoint"); + throw new IllegalStateException(missingEndpointMessage("Cannot remove file", blobName)); } try { @@ -139,8 +145,7 @@ private void removeFile(String blobName) { public URI getLocation(FileResource resource) { var blobName = getBlobName(resource); if (StringUtils.isEmpty(serviceEndpoint)) { - throw new IllegalStateException("Cannot determine location of file " - + blobName + ": missing Azure blob service endpoint"); + throw new IllegalStateException(missingEndpointMessage(blobName)); } if (!serviceEndpoint.endsWith("/")) { throw new IllegalStateException("The Azure blob service endpoint URL must end with a slash."); @@ -166,8 +171,7 @@ protected String getBlobName(FileResource resource) { public URI getNamespaceLogoLocation(Namespace namespace) { var blobName = getBlobName(namespace); if (StringUtils.isEmpty(serviceEndpoint)) { - throw new IllegalStateException("Cannot determine location of file " - + blobName + ": missing Azure blob service endpoint"); + throw new IllegalStateException(missingEndpointMessage(blobName)); } if (!serviceEndpoint.endsWith("/")) { throw new IllegalStateException("The Azure blob service endpoint URL must end with a slash."); @@ -179,8 +183,7 @@ public URI getNamespaceLogoLocation(Namespace namespace) { public TempFile downloadFile(FileResource resource) throws IOException { var blobName = getBlobName(resource); if (StringUtils.isEmpty(serviceEndpoint)) { - throw new IllegalStateException("Cannot determine location of file " - + blobName + ": missing Azure blob service endpoint"); + throw new IllegalStateException(missingEndpointMessage(blobName)); } var tempFile = new TempFile("temp_file_", ""); @@ -217,8 +220,7 @@ public void copyFiles(List> pairs) { public Path getCachedFile(FileResource resource) throws IOException { var blobName = getBlobName(resource); if (StringUtils.isEmpty(serviceEndpoint)) { - throw new IllegalStateException("Cannot determine location of file " - + blobName + ": missing Azure blob service endpoint"); + throw new IllegalStateException(missingEndpointMessage(blobName)); } var path = Files.createTempFile("cached_file", null); diff --git a/server/src/main/java/org/eclipse/openvsx/storage/GoogleCloudStorageService.java b/server/src/main/java/org/eclipse/openvsx/storage/GoogleCloudStorageService.java index 2bbb678f..d5ec61ec 100644 --- a/server/src/main/java/org/eclipse/openvsx/storage/GoogleCloudStorageService.java +++ b/server/src/main/java/org/eclipse/openvsx/storage/GoogleCloudStorageService.java @@ -72,8 +72,7 @@ public void uploadFile(TempFile tempFile) { var resource = tempFile.getResource(); var objectId = getObjectId(resource); if (StringUtils.isEmpty(bucketId)) { - throw new IllegalStateException("Cannot upload file " - + objectId + ": missing Google bucket id"); + throw new IllegalStateException(missingBucketIdMessage("Cannot upload file", resource.getName())); } uploadFile(tempFile, resource.getName(), objectId); @@ -84,8 +83,7 @@ public void uploadNamespaceLogo(TempFile logoFile) { var namespace = logoFile.getNamespace(); var objectId = getObjectId(namespace); if (StringUtils.isEmpty(bucketId)) { - throw new IllegalStateException("Cannot upload file " - + objectId + ": missing Google bucket id"); + throw new IllegalStateException(missingBucketIdMessage("Cannot upload file", objectId)); } uploadFile(logoFile, namespace.getLogoName(), objectId); @@ -127,8 +125,7 @@ public void removeNamespaceLogo(Namespace namespace) { private void removeFile(String objectId) { if (StringUtils.isEmpty(bucketId)) { - throw new IllegalStateException("Cannot remove file " - + objectId + ": missing Google bucket id"); + throw new IllegalStateException(missingBucketIdMessage("Cannot remove file", objectId)); } getStorage().delete(BlobId.of(bucketId, objectId)); @@ -137,8 +134,7 @@ private void removeFile(String objectId) { @Override public URI getLocation(FileResource resource) { if (StringUtils.isEmpty(bucketId)) { - throw new IllegalStateException("Cannot determine location of file " - + resource.getName() + ": missing Google bucket id"); + throw new IllegalStateException(missingBucketIdMessage(resource.getName())); } return URI.create(BASE_URL + bucketId + "/" + getObjectId(resource)); } @@ -160,8 +156,7 @@ protected String getObjectId(FileResource resource) { @Override public URI getNamespaceLogoLocation(Namespace namespace) { if (StringUtils.isEmpty(bucketId)) { - throw new IllegalStateException("Cannot determine location of file " - + namespace.getLogoName() + ": missing Google bucket id"); + throw new IllegalStateException(missingBucketIdMessage(namespace.getLogoName())); } return URI.create(BASE_URL + bucketId + "/" + getObjectId(namespace)); } @@ -169,8 +164,7 @@ public URI getNamespaceLogoLocation(Namespace namespace) { @Override public TempFile downloadFile(FileResource resource) throws IOException { if (StringUtils.isEmpty(bucketId)) { - throw new IllegalStateException("Cannot determine location of file " - + resource.getName() + ": missing Google bucket id"); + throw new IllegalStateException(missingBucketIdMessage(resource.getName())); } var tempFile = new TempFile("temp_file_", ""); @@ -184,6 +178,14 @@ protected String getObjectId(Namespace namespace) { return UrlUtil.createApiUrl("", namespace.getName(), "logo", namespace.getLogoName()).substring(1); // remove first '/' } + private String missingBucketIdMessage(String name) { + return missingBucketIdMessage("Cannot determine location of file", name); + } + + private String missingBucketIdMessage(String action, String name) { + return action + " " + name + ": missing Google bucket id"; + } + @Override public void copyFiles(List> pairs) { for(var pair : pairs) { @@ -202,8 +204,7 @@ public void copyFiles(List> pairs) { @Cacheable(value = CACHE_EXTENSION_FILES, keyGenerator = GENERATOR_FILES) public Path getCachedFile(FileResource resource) throws IOException { if (StringUtils.isEmpty(bucketId)) { - throw new IllegalStateException("Cannot determine location of file " - + resource.getName() + ": missing Google bucket id"); + throw new IllegalStateException(missingBucketIdMessage(resource.getName())); } var path = Files.createTempFile("cached_file", null); 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 f84480f8..9563eb30 100644 --- a/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java +++ b/server/src/main/java/org/eclipse/openvsx/util/UrlUtil.java @@ -109,6 +109,10 @@ public static String createApiUrl(String baseUrl, String... segments) { return baseUrl + path; } + public static String createApiReviewsUrl(String serverUrl, String namespace, String extension) { + return createApiUrl(serverUrl, "api", namespace, extension, "reviews"); + } + /** * Add a query to a URL. The parameters array must contain a sequence of key and * value pairs, so its length is expected to be even. @@ -242,6 +246,10 @@ public static String getPublicKeyUrl(String publicId) { return createApiUrl(getBaseUrl(), "api", "-", "public-key", publicId); } + public static String createAllVersionsUrl(String namespaceName, String extensionName, String targetPlatform) { + return createAllVersionsUrl(namespaceName, extensionName, targetPlatform, "versions"); + } + public static String createAllVersionsUrl(String namespaceName, String extensionName, String targetPlatform, String versionsSegment) { var segments = new String[]{ "api", namespaceName, extensionName }; if(targetPlatform != null) {