From a2816cc89738d72bdda1fef4900326fd4b75cec2 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Wed, 14 Aug 2024 17:44:28 +0200 Subject: [PATCH] implement lists for hosted repository --- .../composer/ComposerHostedFacet.java | 2 ++ .../ComposerHostedDownloadHandler.java | 2 +- .../internal/ComposerHostedFacetImpl.java | 34 +++++++++++++++++++ .../internal/ComposerJsonProcessor.java | 23 +++++++++++++ .../recipe/ComposerHostedRecipe.groovy | 13 +++++++ .../ComposerHostedDownloadHandlerTest.java | 33 ++++++++++++------ .../internal/ComposerHostedFacetImplTest.java | 24 +++++++++++++ 7 files changed, 120 insertions(+), 11 deletions(-) diff --git a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/ComposerHostedFacet.java b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/ComposerHostedFacet.java index be96e0d9..89cdf445 100644 --- a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/ComposerHostedFacet.java +++ b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/ComposerHostedFacet.java @@ -33,6 +33,8 @@ FluentAsset upload(String vendor, String project, String version, String sourceT Content getPackagesJson() throws IOException; + Content getListJson(String filter) throws IOException; + Content getProviderJson(String vendor, String project) throws IOException; Content getPackageJson(String vendor, String project) throws IOException; diff --git a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandler.java b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandler.java index 80b5c5d7..f5c64abe 100644 --- a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandler.java +++ b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandler.java @@ -48,7 +48,7 @@ public Response handle(@Nonnull final Context context) throws Exception { case PACKAGES: return HttpResponses.ok(hostedFacet.getPackagesJson()); case LIST: - throw new IllegalStateException("Unsupported assetKind: " + assetKind); + return responseFor(hostedFacet.getListJson(context.getRequest().getParameters().get("filter"))); case PROVIDER: return responseFor(hostedFacet.getProviderJson(getVendorToken(context), getProjectToken(context))); case PACKAGE: diff --git a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImpl.java b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImpl.java index 97dd2d2d..b1c71cc8 100644 --- a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImpl.java +++ b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImpl.java @@ -27,6 +27,8 @@ import javax.inject.Named; import java.io.IOException; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static com.google.common.base.Preconditions.checkNotNull; @@ -38,6 +40,8 @@ public class ComposerHostedFacetImpl extends FacetSupport implements ComposerHostedFacet { + private static final Pattern FILTER_PATTERN = Pattern.compile("\\s*(?[*a-zA-Z0-9_.-]+)/(?[*a-zA-Z0-9_.-]+)\\s*"); + private final ComposerJsonProcessor composerJsonProcessor; @Inject @@ -69,6 +73,18 @@ public Content getPackagesJson() throws IOException { return composerJsonProcessor.generatePackagesFromComponents(getRepository(), content().components()); } + @Override + public Content getListJson(String filter) throws IOException { + FluentQuery components; + if (filter == null || filter.isEmpty()) { + components = content().components(); + } else { + components = queryComponents(filter); + } + + return composerJsonProcessor.generateListFromComponents(components); + } + @Override public Content getProviderJson(final String vendor, final String project) throws IOException { Optional content = content().get(ComposerPathUtils.buildProviderPath(vendor, project)); @@ -125,6 +141,24 @@ private FluentQuery queryComponents(final String vendor, final ); } + private FluentQuery queryComponents(final String filter) { + Matcher m = FILTER_PATTERN.matcher(filter); + if (m.matches()) { + String vendor = m.group("vendor").replaceAll("\\*+", "%"); + String project = m.group("project").replaceAll("\\*+", "%"); + + return content() + .components() + .byFilter( + "namespace LIKE #{filterParams.vendor} AND name LIKE #{filterParams.project}", + ImmutableMap.of("vendor", vendor, "project", project) + ); + } else { + // invalid filter pattern + return null; + } + } + private ComposerContentFacet content() { return getRepository().facet(ComposerContentFacet.class); } diff --git a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java index ec09675e..76ec33d8 100644 --- a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java +++ b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/ComposerJsonProcessor.java @@ -12,6 +12,7 @@ */ package org.sonatype.nexus.repository.composer.internal; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.hash.Hashing; @@ -31,6 +32,7 @@ import org.sonatype.nexus.repository.view.Payload; import org.sonatype.nexus.repository.view.payloads.StringPayload; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; @@ -195,6 +197,27 @@ private Content buildPackagesJson(final Repository repository, final Set return new Content(new StringPayload(mapper.writeValueAsString(packagesJson), ContentTypes.APPLICATION_JSON)); } + /** + * Generates a list.json file based on the components provided. + * + * @param components Components to process + * @return JSON list with package names + */ + public Content generateListFromComponents(@Nullable final FluentQuery components) throws IOException { + Set packages = new HashSet<>(); + if (components != null) { + Continuation comps = components.browse(PAGE_SIZE, null); + while (!comps.isEmpty()) { + comps.stream().map(comp -> comp.namespace() + "/" + comp.name()).forEach(packages::add); + comps = components.browse(PAGE_SIZE, comps.nextContinuationToken()); + } + } + + Map packagesJson = singletonMap(PACKAGE_NAMES_KEY, packages.stream().sorted().collect(Collectors.toList())); + + return new Content(new StringPayload(mapper.writeValueAsString(packagesJson), ContentTypes.APPLICATION_JSON)); + } + /** * Rewrites the provider JSON so that source entries are removed and dist entries are pointed back to Nexus. */ diff --git a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/recipe/ComposerHostedRecipe.groovy b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/recipe/ComposerHostedRecipe.groovy index 78a1ace0..ea49a9fd 100644 --- a/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/recipe/ComposerHostedRecipe.groovy +++ b/nexus-repository-composer/src/main/java/org/sonatype/nexus/repository/composer/internal/recipe/ComposerHostedRecipe.groovy @@ -33,6 +33,7 @@ import org.sonatype.nexus.repository.view.ConfigurableViewFacet import org.sonatype.nexus.repository.view.Router import org.sonatype.nexus.repository.view.ViewFacet +import static org.sonatype.nexus.repository.composer.AssetKind.LIST import static org.sonatype.nexus.repository.composer.AssetKind.PACKAGE import static org.sonatype.nexus.repository.composer.AssetKind.PACKAGES import static org.sonatype.nexus.repository.composer.AssetKind.PROVIDER @@ -94,6 +95,18 @@ class ComposerHostedRecipe .handler(downloadHandler) .create()) + builder.route(listMatcher() + .handler(timingHandler) + .handler(assetKindHandler.rcurry(LIST)) + .handler(securityHandler) + .handler(exceptionHandler) + .handler(handlerContributor) + .handler(conditionalRequestHandler) + .handler(partialFetchHandler) + .handler(contentHeadersHandler) + .handler(downloadHandler) + .create()) + builder.route(providerMatcher() .handler(timingHandler) .handler(assetKindHandler.rcurry(PROVIDER)) diff --git a/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandlerTest.java b/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandlerTest.java index 3242e26d..c3274ece 100644 --- a/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandlerTest.java +++ b/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedDownloadHandlerTest.java @@ -19,10 +19,7 @@ import org.sonatype.nexus.repository.Repository; import org.sonatype.nexus.repository.composer.AssetKind; import org.sonatype.nexus.repository.composer.ComposerHostedFacet; -import org.sonatype.nexus.repository.view.Content; -import org.sonatype.nexus.repository.view.Context; -import org.sonatype.nexus.repository.view.Payload; -import org.sonatype.nexus.repository.view.Response; +import org.sonatype.nexus.repository.view.*; import org.sonatype.nexus.repository.view.matchers.token.TokenMatcher; import org.junit.Before; @@ -32,8 +29,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.when; import static org.sonatype.nexus.repository.composer.AssetKind.*; import static org.sonatype.nexus.repository.composer.internal.recipe.ComposerRecipeSupport.NAME_TOKEN; @@ -73,7 +68,10 @@ public class ComposerHostedDownloadHandlerTest private Content content; @Mock - private Payload payload; + private Request request; + + @Mock + private Parameters parameters; @Mock private AttributesMap attributes; @@ -85,6 +83,8 @@ public void setUp() { when(repository.facet(ComposerHostedFacet.class)).thenReturn(composerHostedFacet); when(context.getRepository()).thenReturn(repository); when(context.getAttributes()).thenReturn(attributes); + when(context.getRequest()).thenReturn(request); + when(request.getParameters()).thenReturn(parameters); when(attributes.require(TokenMatcher.State.class)).thenReturn(state); when(state.getTokens()).thenReturn(tokens); } @@ -142,11 +142,24 @@ public void testHandlePackageAbsent() throws Exception { assertThat(response.getPayload(), is(nullValue())); } + @Test - public void testHandleList() { + public void testHandleList() throws Exception { when(attributes.require(AssetKind.class)).thenReturn(LIST); - IllegalStateException e = assertThrows(IllegalStateException.class, () -> underTest.handle(context)); - assertEquals("Unsupported assetKind: " + LIST, e.getMessage()); + + // No filter + when(parameters.get("filter")).thenReturn(null); + when(composerHostedFacet.getListJson(null)).thenReturn(content); + Response response = underTest.handle(context); + assertThat(response.getStatus().getCode(), is(200)); + assertThat(response.getPayload(), is(content)); + + // With filter + when(parameters.get("filter")).thenReturn("test/*"); + when(composerHostedFacet.getListJson("test/*")).thenReturn(content); + response = underTest.handle(context); + assertThat(response.getStatus().getCode(), is(200)); + assertThat(response.getPayload(), is(content)); } @Test diff --git a/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImplTest.java b/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImplTest.java index 1c5223ba..964ad7cb 100644 --- a/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImplTest.java +++ b/nexus-repository-composer/src/test/java/org/sonatype/nexus/repository/composer/internal/ComposerHostedFacetImplTest.java @@ -23,6 +23,7 @@ import org.sonatype.nexus.repository.content.fluent.FluentComponent; import org.sonatype.nexus.repository.content.fluent.FluentComponents; import org.sonatype.nexus.repository.content.fluent.FluentQuery; +import org.sonatype.nexus.repository.content.fluent.internal.FluentComponentQueryImpl; import org.sonatype.nexus.repository.view.Content; import org.sonatype.nexus.repository.view.Payload; @@ -99,6 +100,29 @@ public void testGetPackagesJson() throws Exception { assertThat(underTest.getPackagesJson(), is(content)); } + @Test + public void testGetListJson() throws Exception { + // Without filter + when(composerJsonProcessor.generateListFromComponents(components)).thenReturn(content); + assertThat(underTest.getListJson(null), is(content)); + + // With filter + FluentQuery query = mock(FluentComponentQueryImpl.class); + when(components.byFilter("namespace LIKE #{filterParams.vendor} AND name LIKE #{filterParams.project}", + ImmutableMap.of("vendor", "test", "project", "%"))).thenReturn(query); + when(composerJsonProcessor.generateListFromComponents(query)).thenReturn(content); + assertThat(underTest.getListJson("test/*"), is(content)); + + when(components.byFilter("namespace LIKE #{filterParams.vendor} AND name LIKE #{filterParams.project}", + ImmutableMap.of("vendor", "%abc%", "project", "pr0_j3cT"))).thenReturn(query); + when(composerJsonProcessor.generateListFromComponents(query)).thenReturn(content); + assertThat(underTest.getListJson("*abc**/pr0_j3cT"), is(content)); + + // Invalid filter + when(composerJsonProcessor.generateListFromComponents(null)).thenReturn(content); + assertThat(underTest.getListJson("In\\al1d"), is(content)); + } + @Test public void testGetProviderJson() throws Exception { when(composerContentFacet.get(PROVIDER_PATH)).thenReturn(Optional.of(content));