From 20d967989511f3b5028ceb9781180729be3f7533 Mon Sep 17 00:00:00 2001 From: Bartosz Spyrko-Smietanko Date: Tue, 11 Oct 2022 16:40:36 +0100 Subject: [PATCH] [#118,#97,#95] Implementing spec changes - channel/manifest split, blocklist and resolve strategies --- .../java/org/wildfly/channel/Blocklist.java | 141 +++++ .../wildfly/channel/BlocklistCoordinate.java | 80 +++ .../org/wildfly/channel/BlocklistEntry.java | 51 ++ .../java/org/wildfly/channel/Channel.java | 321 +++-------- .../java/org/wildfly/channel/ChannelImpl.java | 335 +++++++++++ .../org/wildfly/channel/ChannelManifest.java | 183 +++++++ .../channel/ChannelManifestCoordinate.java | 84 +++ .../channel/ChannelManifestMapper.java | 144 +++++ .../org/wildfly/channel/ChannelMapper.java | 26 +- .../channel/ChannelMetadataCoordinate.java | 109 ++++ .../org/wildfly/channel/ChannelRecorder.java | 7 +- .../wildfly/channel/ChannelRequirement.java | 74 --- .../org/wildfly/channel/ChannelSession.java | 76 ++- .../channel/CyclicDependencyException.java | 23 + ...a => InvalidChannelMetadataException.java} | 4 +- .../wildfly/channel/ManifestRequirement.java | 97 ++++ .../org/wildfly/channel/MavenCoordinate.java | 92 ++++ .../java/org/wildfly/channel/Repository.java | 77 +++ .../UnresolvedMavenArtifactException.java | 5 + .../UnresolvedRequiredManifestException.java | 37 ++ .../main/java/org/wildfly/channel/Vendor.java | 7 +- .../channel/spi/MavenVersionsResolver.java | 50 +- .../org/wildfly/blocklist/v1.0.0/schema.json | 3 +- .../org/wildfly/channel/v1.0.0/schema.json | 2 +- .../org/wildfly/channel/v2.0.0/schema.json | 2 +- .../org/wildfly/manifest/v1.0.0/schema.json | 4 +- .../org/wildfly/channel/ChannelBuilder.java | 61 +++ .../ChannelManifestMapperTestCase.java | 87 +++ .../channel/ChannelMapperTestCase.java | 44 +- .../channel/ChannelRecorderTestCase.java | 35 +- .../channel/ChannelSessionInitTestCase.java | 379 +++++++++++++ .../channel/ChannelSessionTestCase.java | 381 ++++++++++--- .../channel/ChannelWithBlocklistTestCase.java | 518 ++++++++++++++++++ .../ChannelWithRequirementsTestCase.java | 340 ++++++++---- .../org/wildfly/channel/ManifestBuilder.java | 51 ++ .../channel/StreamResolverTestCase.java | 12 +- .../mapping/ChannelManifestTestCase.java | 118 ++++ .../channel/mapping/ChannelTestCase.java | 54 +- ....java => ManifestRequirementTestCase.java} | 34 +- ...yaml => 2nd-level-requiring-manifest.yaml} | 11 +- .../channels/channel-with-blocklist.yaml | 22 + .../channel-with-unknown-properties.yaml | 15 +- .../resources/channels/invalid-blocklist.yaml | 5 + .../manifest-with-unknown-properties.yaml | 11 + .../channels/multiple-manifests.yaml | 6 + ...hannel-2.yaml => required-manifest-2.yaml} | 2 +- ...ed-channel.yaml => required-manifest.yaml} | 2 +- .../resources/channels/simple-channel.yaml | 13 +- .../resources/channels/simple-manifest.yaml | 9 + .../test-blocklist-with-wildcards.yaml | 6 + .../resources/channels/test-blocklist.yaml | 6 + .../channel/maven/ChannelCoordinate.java | 55 +- .../channel/maven/VersionResolverFactory.java | 166 ++++-- .../maven/VersionResolverFactoryTest.java | 218 +++++++- 54 files changed, 3957 insertions(+), 738 deletions(-) create mode 100644 core/src/main/java/org/wildfly/channel/Blocklist.java create mode 100644 core/src/main/java/org/wildfly/channel/BlocklistCoordinate.java create mode 100644 core/src/main/java/org/wildfly/channel/BlocklistEntry.java create mode 100644 core/src/main/java/org/wildfly/channel/ChannelImpl.java create mode 100644 core/src/main/java/org/wildfly/channel/ChannelManifest.java create mode 100644 core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java create mode 100644 core/src/main/java/org/wildfly/channel/ChannelManifestMapper.java create mode 100644 core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java delete mode 100644 core/src/main/java/org/wildfly/channel/ChannelRequirement.java create mode 100644 core/src/main/java/org/wildfly/channel/CyclicDependencyException.java rename core/src/main/java/org/wildfly/channel/{InvalidChannelException.java => InvalidChannelMetadataException.java} (85%) create mode 100644 core/src/main/java/org/wildfly/channel/ManifestRequirement.java create mode 100644 core/src/main/java/org/wildfly/channel/MavenCoordinate.java create mode 100644 core/src/main/java/org/wildfly/channel/Repository.java create mode 100644 core/src/main/java/org/wildfly/channel/UnresolvedRequiredManifestException.java create mode 100644 core/src/test/java/org/wildfly/channel/ChannelBuilder.java create mode 100644 core/src/test/java/org/wildfly/channel/ChannelManifestMapperTestCase.java create mode 100644 core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java create mode 100644 core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java create mode 100644 core/src/test/java/org/wildfly/channel/ManifestBuilder.java create mode 100644 core/src/test/java/org/wildfly/channel/mapping/ChannelManifestTestCase.java rename core/src/test/java/org/wildfly/channel/mapping/{ChannelRequirementTestCase.java => ManifestRequirementTestCase.java} (67%) rename core/src/test/resources/channels/{2nd-level-requiring-channel.yaml => 2nd-level-requiring-manifest.yaml} (57%) create mode 100644 core/src/test/resources/channels/channel-with-blocklist.yaml create mode 100644 core/src/test/resources/channels/invalid-blocklist.yaml create mode 100644 core/src/test/resources/channels/manifest-with-unknown-properties.yaml create mode 100644 core/src/test/resources/channels/multiple-manifests.yaml rename core/src/test/resources/channels/{required-channel-2.yaml => required-manifest-2.yaml} (78%) rename core/src/test/resources/channels/{required-channel.yaml => required-manifest.yaml} (88%) create mode 100644 core/src/test/resources/channels/simple-manifest.yaml create mode 100644 core/src/test/resources/channels/test-blocklist-with-wildcards.yaml create mode 100644 core/src/test/resources/channels/test-blocklist.yaml diff --git a/core/src/main/java/org/wildfly/channel/Blocklist.java b/core/src/main/java/org/wildfly/channel/Blocklist.java new file mode 100644 index 00000000..464c1fe7 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/Blocklist.java @@ -0,0 +1,141 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +public class Blocklist { + + public static final String SCHEMA_VERSION_1_0_0 = "1.0.0"; + private static final String SCHEMA_1_0_0_FILE = "org/wildfly/blocklist/v1.0.0/schema.json"; + private static final YAMLFactory YAML_FACTORY = new YAMLFactory() + .configure(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR, true); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(YAML_FACTORY) + .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(ORDER_MAP_ENTRIES_BY_KEYS, true); + private static final JsonSchemaFactory SCHEMA_FACTORY = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).objectMapper(OBJECT_MAPPER).build(); + + private static final Map SCHEMAS = new HashMap<>(); + + static { + SCHEMAS.put(SCHEMA_VERSION_1_0_0, SCHEMA_FACTORY.getSchema(ChannelMapper.class.getClassLoader().getResourceAsStream(SCHEMA_1_0_0_FILE))); + } + + private final String schemaVersion; + + private Set entries; + + @JsonCreator + public Blocklist(@JsonProperty(value = "schemaVersion", required = true) String schemaVersion, + @JsonProperty(value = "blocks") Set entries) { + this.schemaVersion = schemaVersion; + this.entries = entries; + } + + public static Blocklist from(URL blocklistUrl) { + requireNonNull(blocklistUrl); + + try { + // QoL improvement + if (blocklistUrl.toString().endsWith("/")) { + blocklistUrl = blocklistUrl.toURI().resolve("blocklist.yaml").toURL(); + } + + List messages = validate(blocklistUrl); + if (!messages.isEmpty()) { + throw new InvalidChannelMetadataException("Invalid blocklist", messages); + } + Blocklist blocklist = OBJECT_MAPPER.readValue(blocklistUrl, Blocklist.class); + return blocklist; + } catch (IOException | URISyntaxException e) { + throw wrapException(e); + } + } + + private static InvalidChannelMetadataException wrapException(Exception e) { + InvalidChannelMetadataException ice = new InvalidChannelMetadataException("Invalid Channel", singletonList(e.getLocalizedMessage())); + ice.initCause(e); + return ice; + } + + public Set getVersionsFor(String groupId, String artifactId) { + Objects.requireNonNull(groupId); + Objects.requireNonNull(artifactId); + + if (entries == null) { + return Collections.emptySet(); + } + for (BlocklistEntry entry : entries) { + if (entry.getGroupId().equals(groupId) && entry.getArtifactId().equals(artifactId)) { + return entry.getVersions(); + } + } + for (BlocklistEntry entry : entries) { + if (entry.getGroupId().equals(groupId) && entry.getArtifactId().equals("*")) { + return entry.getVersions(); + } + } + return Collections.emptySet(); + } + + private static List validate(URL url) throws IOException { + JsonNode node = OBJECT_MAPPER.readTree(url); + JsonSchema schema = getSchema(node); + schema.initializeValidators(); + Set validationMessages = schema.validate(node); + return validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.toList()); + } + + private static JsonSchema getSchema(JsonNode node) { + JsonNode schemaVersion = node.path("schemaVersion"); + String version = schemaVersion.asText(); + if (version == null || version.isEmpty()) { + throw new RuntimeException("The blocklist does not specify a schemaVersion."); + } + JsonSchema schema = SCHEMAS.get(version); + if (schema == null) { + throw new RuntimeException("Unknown schema version " + schemaVersion); + } + return schema; + } +} diff --git a/core/src/main/java/org/wildfly/channel/BlocklistCoordinate.java b/core/src/main/java/org/wildfly/channel/BlocklistCoordinate.java new file mode 100644 index 00000000..56599589 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/BlocklistCoordinate.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import java.net.MalformedURLException; +import java.net.URL; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties({"groupId", "artifactId", "version", "classifier", "extension"}) +public class BlocklistCoordinate extends ChannelMetadataCoordinate { + + public static String CLASSIFIER = "blocklist"; + public static String EXTENSION = "yaml"; + + public BlocklistCoordinate(String groupId, String artifactId) { + super(groupId, artifactId, BlocklistCoordinate.CLASSIFIER, BlocklistCoordinate.EXTENSION); + } + + public BlocklistCoordinate(String groupId, String artifactId, String version) { + super(groupId, artifactId, version, BlocklistCoordinate.CLASSIFIER, BlocklistCoordinate.EXTENSION); + } + + public BlocklistCoordinate(URL url) { + super(url); + } + + public BlocklistCoordinate() { + super(BlocklistCoordinate.CLASSIFIER, BlocklistCoordinate.EXTENSION); + } + + @JsonCreator + public static BlocklistCoordinate create(@JsonProperty(value = "maven") MavenCoordinate coord, + @JsonProperty(value = "url") String url) + throws MalformedURLException { + if (coord != null) { + if (coord.getVersion() == null || coord.getVersion().isEmpty()) { + return new BlocklistCoordinate(coord.getGroupId(), coord.getArtifactId()); + } else { + return new BlocklistCoordinate(coord.getGroupId(), coord.getArtifactId(), coord.getVersion()); + } + } else { + return new BlocklistCoordinate(new URL(url)); + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Override + public URL getUrl() { + return super.getUrl(); + } + + @JsonProperty("maven") + @JsonInclude(JsonInclude.Include.NON_NULL) + public MavenCoordinate getMaven() { + if (getUrl() == null) { + return new MavenCoordinate(getGroupId(), getArtifactId(), getVersion()); + } else { + return null; + } + } +} diff --git a/core/src/main/java/org/wildfly/channel/BlocklistEntry.java b/core/src/main/java/org/wildfly/channel/BlocklistEntry.java new file mode 100644 index 00000000..3ca8069f --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/BlocklistEntry.java @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BlocklistEntry { + + private String groupId; + private String artifactId; + private Set versions; + + @JsonCreator + public BlocklistEntry(@JsonProperty(value = "groupId") String groupId, + @JsonProperty(value = "artifactId") String artifactId, + @JsonProperty(value = "versions") Set versions) { + this.groupId = groupId; + this.artifactId = artifactId; + this.versions = versions; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public Set getVersions() { + return versions; + } +} diff --git a/core/src/main/java/org/wildfly/channel/Channel.java b/core/src/main/java/org/wildfly/channel/Channel.java index 560e2a37..16e6193c 100644 --- a/core/src/main/java/org/wildfly/channel/Channel.java +++ b/core/src/main/java/org/wildfly/channel/Channel.java @@ -1,323 +1,156 @@ -/* - * Copyright 2022 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package org.wildfly.channel; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; -import static java.util.Collections.emptyList; -import static java.util.Objects.requireNonNull; - -import java.io.File; -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; - import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.version.VersionMatcher; -/** - * Java representation of a Channel. - */ -public class Channel implements AutoCloseable { +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; +import java.util.ArrayList; +import static java.util.Collections.emptyList; - private static final String CLASSIFIER="channel"; - private static final String EXTENSION="yaml"; +public class Channel { + + public static final String CLASSIFIER="channel"; + public static final String EXTENSION="yaml"; /** Version of the schema used by this channel. * This is a required field. */ - private final String schemaVersion; + private String schemaVersion; /** * Name of the channel (as an one-line human readable description of the channel). * This is an optional field. */ - private final String name; + private String name; /** * Description of the channel. It can use multiple lines. * This is an optional field. */ - private final String description; + private String description; /** * Vendor of the channel. * This is an optional field. */ - private final Vendor vendor; + private Vendor vendor; + private List repositories = new ArrayList<>(); + private BlocklistCoordinate blocklistCoordinate; + private ChannelManifestCoordinate manifestCoordinate; + private NoStreamStrategy noStreamStrategy = NoStreamStrategy.ORIGINAL; - /** - * Other channels that are required by the channel. - * This is an optional field. - */ - private List channelRequirements; - - /** - * Required channels - */ - private List requiredChannels = Collections.emptyList(); - - /** - * Streams of components that are provided by this channel. - */ - private Set streams; - - private MavenVersionsResolver resolver; + // no-arg constructor for maven plugins + public Channel() { + schemaVersion = ChannelMapper.CURRENT_SCHEMA_VERSION; + } /** * Representation of a Channel resource using the current schema version. * - * @see #Channel(String, String, Vendor, List, Collection) + * @see #Channel(String, String, String, Vendor, List, ChannelManifestCoordinate, BlocklistCoordinate, NoStreamStrategy) */ public Channel(String name, String description, Vendor vendor, - List channelRequirements, - Collection streams) { + List repositories, + ChannelManifestCoordinate manifestCoordinate, + BlocklistCoordinate blocklistCoordinate, + NoStreamStrategy noStreamStrategy){ this(ChannelMapper.CURRENT_SCHEMA_VERSION, name, description, vendor, - channelRequirements, - streams); + repositories, + manifestCoordinate, + blocklistCoordinate, + noStreamStrategy); } - /** - * Representation of a Channel resource - * - * @param schemaVersion the version of the schema to validate this channel resource - required - * @param name the name of the channel - can be {@code null} - * @param description the description of the channel - can be {@code null} - * @param vendor the vendor of the channel - can be {@code null} - * @param channelRequirements the required channels - cane be {@code null} - * @param streams the streams defined by the channel - can be {@code null} - */ @JsonCreator - @JsonPropertyOrder({ "schemaVersion", "name", "description", "vendor", "requires", "streams" }) public Channel(@JsonProperty(value = "schemaVersion", required = true) String schemaVersion, @JsonProperty(value = "name") String name, @JsonProperty(value = "description") String description, @JsonProperty(value = "vendor") Vendor vendor, - @JsonProperty(value = "requires") - @JsonInclude(NON_EMPTY) List channelRequirements, - @JsonProperty(value = "streams") Collection streams) { + @JsonProperty(value = "repositories") + @JsonInclude(NON_EMPTY) List repositories, + @JsonProperty(value = "manifest") ChannelManifestCoordinate manifestCoordinate, + @JsonProperty(value = "blocklist") @JsonInclude(NON_EMPTY) BlocklistCoordinate blocklistCoordinate, + @JsonProperty(value = "resolves-if-no-stream") NoStreamStrategy noStreamStrategy) { this.schemaVersion = schemaVersion; this.name = name; this.description = description; this.vendor = vendor; - this.channelRequirements = (channelRequirements != null) ? channelRequirements : emptyList(); - this.streams = new TreeSet<>(); - if (streams != null) { - this.streams.addAll(streams); - } + this.repositories = (repositories != null) ? repositories : emptyList(); + this.blocklistCoordinate = blocklistCoordinate; + this.manifestCoordinate = manifestCoordinate; + this.noStreamStrategy = (noStreamStrategy != null) ? noStreamStrategy: NoStreamStrategy.ORIGINAL; } - @JsonInclude public String getSchemaVersion() { return schemaVersion; } - @JsonInclude(NON_NULL) + @JsonInclude(JsonInclude.Include.NON_NULL) public String getName() { return name; } - @JsonInclude(NON_NULL) + @JsonInclude(JsonInclude.Include.NON_NULL) public String getDescription() { return description; } - @JsonInclude(NON_NULL) + @JsonInclude(JsonInclude.Include.NON_NULL) public Vendor getVendor() { return vendor; } - @JsonInclude(NON_EMPTY) - @JsonProperty(value = "requires") - public List getChannelRequirements() { - return channelRequirements; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public List getRepositories() { + return repositories; } - @JsonInclude(NON_EMPTY) - public Collection getStreams() { - return streams; + @JsonInclude(JsonInclude.Include.NON_NULL) + public BlocklistCoordinate getBlocklistCoordinate() { + return blocklistCoordinate; } - void addStream(Stream stream) { - Objects.requireNonNull(stream); - this.streams.add(stream); + @JsonInclude(JsonInclude.Include.NON_NULL) + public ChannelManifestCoordinate getManifestCoordinate() { + return manifestCoordinate; } - void init(MavenVersionsResolver.Factory factory) { - resolver = factory.create(); - - if (!channelRequirements.isEmpty()) { - requiredChannels = new ArrayList<>(); - } - for (ChannelRequirement channelRequirement : channelRequirements) { - String groupId = channelRequirement.getGroupId(); - String artifactId = channelRequirement.getArtifactId(); - String version = channelRequirement.getVersion(); - if (version == null) { - Set versions = resolver.getAllVersions(groupId, artifactId, EXTENSION, CLASSIFIER); - Optional latest = VersionMatcher.getLatestVersion(versions); - version = latest.orElseThrow(() -> new RuntimeException(String.format("Can not determine the latest version for Maven artifact %s:%s:%s:%s", - groupId, artifactId, EXTENSION, CLASSIFIER))); - } - try { - final File file; - file = resolver.resolveArtifact(groupId, artifactId, EXTENSION, CLASSIFIER, version); - Channel requiredChannel = ChannelMapper.from(file.toURI().toURL()); - requiredChannel.init(factory); - requiredChannels.add(requiredChannel); - } catch (UnresolvedMavenArtifactException | MalformedURLException e) { - throw new RuntimeException(String.format("Unable to resolve required channel %s:%s", groupId, artifactId, version)); - } - } + @JsonInclude(JsonInclude.Include.NON_NULL) + public NoStreamStrategy getNoStreamStrategy() { + return noStreamStrategy; } - @Override - public void close() { - for (Channel requiredChannel : requiredChannels) { - requiredChannel.close(); - } - this.resolver.close(); - this.resolver = null; - } - - static class ResolveLatestVersionResult { - final String version; - final Channel channel; - - ResolveLatestVersionResult(String version, Channel channel) { - this.version = version; - this.channel = channel; - } - } - - - Optional resolveLatestVersion(String groupId, String artifactId, String extension, String classifier) { - requireNonNull(groupId); - requireNonNull(artifactId); - requireNonNull(resolver); - - // first we find if there is a stream for that given (groupId, artifactId). - Optional foundStream = findStreamFor(groupId, artifactId); - - // no stream for this artifact, let's look into the required channel - if (!foundStream.isPresent()) { - // we return the latest value from the required channels - Map foundVersions = new HashMap<>(); - for (Channel requiredChannel : requiredChannels) { - Optional found = requiredChannel.resolveLatestVersion(groupId, artifactId, extension, classifier); - if (found.isPresent()) { - foundVersions.put(found.get().version, found.get().channel); - } - } - Optional foundVersionInRequiredChannels = foundVersions.keySet().stream().sorted(VersionMatcher.COMPARATOR.reversed()).findFirst(); - if (foundVersionInRequiredChannels.isPresent()) { - return Optional.of(new ResolveLatestVersionResult(foundVersionInRequiredChannels.get(), foundVersions.get(foundVersionInRequiredChannels.get()))); - } - return Optional.empty(); - } - - Stream stream = foundStream.get(); - Optional foundVersion = Optional.empty(); - // there is a stream, let's now check its version - if (stream.getVersion() != null) { - foundVersion = Optional.of(stream.getVersion()); - } else if (stream.getVersionPattern() != null) { - // if there is a version pattern, we resolve all versions from Maven to find the latest one - Set versions = resolver.getAllVersions(groupId, artifactId, extension, classifier); - foundVersion = foundStream.get().getVersionComparator().matches(versions); - } - - if (foundVersion.isPresent()) { - return Optional.of(new ResolveLatestVersionResult(foundVersion.get(), this)); - } - return Optional.empty(); - } - - - static class ResolveArtifactResult { - File file; - Channel channel; - - ResolveArtifactResult(File file, Channel channel) { - this.file = file; - this.channel = channel; - } - } - - ResolveArtifactResult resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) throws UnresolvedMavenArtifactException { - requireNonNull(groupId); - requireNonNull(artifactId); - requireNonNull(version); - requireNonNull(resolver); - - // first we looked into the required channels - for (Channel requiredChannel : requiredChannels) { - try { - return requiredChannel.resolveArtifact(groupId, artifactId, extension, classifier, version); - } catch (UnresolvedMavenArtifactException e) { - // ignore if the required channel are not able to resolve the artifact - } - } - - return new ResolveArtifactResult(resolver.resolveArtifact(groupId, artifactId, extension, classifier, version), this); - } - - List resolveArtifacts(List coordinates) throws UnresolvedMavenArtifactException { - final List resolvedArtifacts = resolver.resolveArtifacts(coordinates); - return resolvedArtifacts.stream().map(f->new ResolveArtifactResult(f, this)).collect(Collectors.toList()); - } - - public Optional findStreamFor(String groupId, String artifactId) { - // first exact match: - Optional stream = streams.stream().filter(s -> s.getGroupId().equals(groupId) && s.getArtifactId().equals(artifactId)).findFirst(); - if (stream.isPresent()) { - return stream; - } - // check if there is a stream for groupId:* - stream = streams.stream().filter(s -> s.getGroupId().equals(groupId) && s.getArtifactId().equals("*")).findFirst(); - return stream; - } - - @Override - public String toString() { - return "Channel{" + - ", name='" + name + '\'' + - ", description='" + description + '\'' + - ", vendor=" + vendor + - ", channelRequirements=" + channelRequirements + - ", streams=" + streams + - '}'; + /** + * Strategies for resolving artifact versions if it is not listed in streams. + *
    + *
  • LATEST - Use the latest version according to {@link VersionMatcher#COMPARATOR}
  • + *
  • ORIGINAL - Use the {@code baseVersion} if provided in the query
  • + *
  • MAVEN_LATEST - Use the value of {@code } in maven-metadata.xml
  • + *
  • MAVEN_RELEASE - Use the value of {@code } in maven-metadata.xml
  • + *
  • NONE - throw {@link UnresolvedMavenArtifactException}
  • + *
+ */ + @JsonFormat(shape = JsonFormat.Shape.STRING) + public enum NoStreamStrategy { + @JsonProperty("latest") + LATEST, + @JsonProperty("original") + ORIGINAL, + @JsonProperty("maven-latest") + MAVEN_LATEST, + @JsonProperty("maven-release") + MAVEN_RELEASE, + @JsonProperty("none") + NONE } } diff --git a/core/src/main/java/org/wildfly/channel/ChannelImpl.java b/core/src/main/java/org/wildfly/channel/ChannelImpl.java new file mode 100644 index 00000000..e34678ce --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/ChannelImpl.java @@ -0,0 +1,335 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import static java.util.Objects.requireNonNull; +import static org.wildfly.channel.version.VersionMatcher.COMPARATOR; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.wildfly.channel.spi.MavenVersionsResolver; +import org.wildfly.channel.version.VersionMatcher; + +/** + * Java representation of a Channel. + */ +class ChannelImpl implements AutoCloseable { + + private Channel channelDefinition; + + private List requiredChannels = Collections.emptyList(); + + private ChannelManifest channelManifest; + + private MavenVersionsResolver resolver; + + // marks an instance of Channel as dependency of another channel + private boolean dependency = false; + + public Optional blocklist = Optional.empty(); + + public ChannelManifest getManifest() { + return channelManifest; + } + + public ChannelImpl(Channel channelDefinition) { + this.channelDefinition = channelDefinition; + } + + + /** + * + * @param factory + * @param channels + * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels + * @throws CyclicDependencyException - if the required manifests form a cyclic dependency + */ + void init(MavenVersionsResolver.Factory factory, List channels) { + if (resolver != null) { + //already initialized + return; + } + + resolver = factory.create(channelDefinition.getRepositories()); + + if (channelDefinition.getManifestCoordinate() != null) { + channelManifest = resolveManifest(channelDefinition.getManifestCoordinate()); + } else { + channelManifest = new ChannelManifest(null, null, null, Collections.emptyList()); + } + + final List manifestRequirements = channelManifest.getManifestRequirements(); + if (!manifestRequirements.isEmpty()) { + requiredChannels = new ArrayList<>(); + } + for (ManifestRequirement manifestRequirement : manifestRequirements) { + ChannelImpl foundChannel = findRequiredChannel(factory, channels, manifestRequirement); + requiredChannels.add(foundChannel); + } + + if (channelDefinition.getBlocklistCoordinate() != null) { + BlocklistCoordinate blocklistCoordinate = channelDefinition.getBlocklistCoordinate(); + final List urls = resolver.resolveChannelMetadata(List.of(blocklistCoordinate)); + this.blocklist = urls.stream() + .map(Blocklist::from) + .findFirst(); + } + } + + private ChannelImpl findRequiredChannel(MavenVersionsResolver.Factory factory, List channels, ManifestRequirement manifestRequirement) { + ChannelImpl foundChannel = null; + for (ChannelImpl c: channels) { + if (c.getManifest() == null) { + c.init(factory, channels); + } + if (manifestRequirement.getId().equals(c.getManifest().getId())) { + foundChannel = c; + break; + } + } + + if (foundChannel == null) { + if (manifestRequirement.getMavenCoordinate() == null) { + throw new UnresolvedRequiredManifestException("Manifest with ID " + manifestRequirement.getId() + " is not available", manifestRequirement.getId()); + } + foundChannel = createNewChannelFromMaven(factory, channels, manifestRequirement); + } + + checkForCycles(foundChannel); + + foundChannel.markAsDependency(); + return foundChannel; + } + + private ChannelImpl createNewChannelFromMaven(MavenVersionsResolver.Factory factory, List channels, ManifestRequirement manifestRequirement) { + String groupId = manifestRequirement.getGroupId(); + String artifactId = manifestRequirement.getArtifactId(); + String version = manifestRequirement.getVersion(); + if (version == null) { + Set versions = resolver.getAllVersions(groupId, artifactId, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER); + Optional latest = VersionMatcher.getLatestVersion(versions); + version = latest.orElseThrow(() -> new RuntimeException(String.format("Can not determine the latest version for Maven artifact %s:%s:%s:%s", + groupId, artifactId, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER))); + } + final ChannelImpl requiredChannel = new ChannelImpl(new Channel(null, null, null, channelDefinition.getRepositories(), + new ChannelManifestCoordinate(groupId, artifactId, version), null, + Channel.NoStreamStrategy.NONE)); + try { + requiredChannel.init(factory, channels); + } catch (UnresolvedMavenArtifactException e) { + throw new UnresolvedRequiredManifestException("Manifest with ID " + manifestRequirement.getId() + " is not available", manifestRequirement.getId(), e); + } + return requiredChannel; + } + + private void checkForCycles(ChannelImpl foundChannel) { + final String manifestId = this.getManifest().getId(); + if (foundChannel.getManifest().getId() != null && foundChannel.getManifest().getId().equals(manifestId)) { + throw new CyclicDependencyException("Illegal manifest dependency: " + manifestId + "->" + foundChannel.getManifest().getId()); + } + if (foundChannel.requiredChannels.stream().map(ChannelImpl::getManifest).map(ChannelManifest::getId).filter((id)->id != null && id.equals(manifestId)).findFirst().isPresent()) { + throw new CyclicDependencyException("Illegal manifest dependency: " + manifestId + "->" + foundChannel.getManifest().getId()); + } + for (ChannelImpl requiredChannel : foundChannel.requiredChannels) { + checkForCycles(requiredChannel); + } + } + + @Override + public void close() { + if (resolver != null) { + for (ChannelImpl requiredChannel : requiredChannels) { + requiredChannel.close(); + } + this.resolver.close(); + this.resolver = null; + } + } + private void markAsDependency() { + this.dependency = true; + } + + boolean isDependency() { + return dependency; + } + + + static class ResolveLatestVersionResult { + final String version; + final ChannelImpl channel; + + ResolveLatestVersionResult(String version, ChannelImpl channel) { + this.version = version; + this.channel = channel; + } + } + + private ChannelManifest resolveManifest(ChannelManifestCoordinate manifestCoordinate) throws UnresolvedMavenArtifactException { + return resolver.resolveChannelMetadata(List.of(manifestCoordinate)) + .stream() + .map(ChannelManifestMapper::from) + .findFirst().orElseThrow(); + } + + Optional resolveLatestVersion(String groupId, String artifactId, String extension, String classifier, String baseVersion) { + requireNonNull(groupId); + requireNonNull(artifactId); + requireNonNull(resolver); + + // first we find if there is a stream for that given (groupId, artifactId). + Optional foundStream = channelManifest.findStreamFor(groupId, artifactId); + // no stream for this artifact, let's look into the required channel + if (!foundStream.isPresent()) { + // we return the latest value from the required channels + Map foundVersions = new HashMap<>(); + for (ChannelImpl requiredChannel : requiredChannels) { + Optional found = requiredChannel.resolveLatestVersion(groupId, artifactId, extension, classifier, baseVersion); + if (found.isPresent()) { + foundVersions.put(found.get().version, found.get().channel); + } + } + Optional foundVersionInRequiredChannels = foundVersions.keySet().stream().sorted(COMPARATOR.reversed()).findFirst(); + if (foundVersionInRequiredChannels.isPresent()) { + return Optional.of(new ResolveLatestVersionResult(foundVersionInRequiredChannels.get(), foundVersions.get(foundVersionInRequiredChannels.get()))); + } + + // finally try the NoStreamStrategy + switch (channelDefinition.getNoStreamStrategy()) { + case ORIGINAL: + return baseVersion == null?Optional.empty():Optional.of(new ResolveLatestVersionResult(baseVersion, this)); + case LATEST: + Set versions = resolver.getAllVersions(groupId, artifactId, extension, classifier); + final Optional latestVersion = versions.stream().sorted(COMPARATOR.reversed()).findFirst(); + if (latestVersion.isPresent()) { + return Optional.of(new ResolveLatestVersionResult(latestVersion.get(), this)); + } else { + return Optional.empty(); + } + case MAVEN_LATEST: + String latestMetadataVersion = resolver.getMetadataLatestVersion(groupId, artifactId); + return Optional.of(new ResolveLatestVersionResult(latestMetadataVersion, this)); + case MAVEN_RELEASE: + String releaseMetadataVersion = resolver.getMetadataReleaseVersion(groupId, artifactId); + return Optional.of(new ResolveLatestVersionResult(releaseMetadataVersion, this)); + default: + return Optional.empty(); + } + } + + Stream stream = foundStream.get(); + Optional foundVersion = Optional.empty(); + // there is a stream, let's now check its version + if (stream.getVersion() != null) { + foundVersion = Optional.of(stream.getVersion()); + } else if (stream.getVersionPattern() != null) { + // if there is a version pattern, we resolve all versions from Maven to find the latest one + Set versions = resolver.getAllVersions(groupId, artifactId, extension, classifier); + if (this.blocklist.isPresent()) { + final Set blocklistedVersions = this.blocklist.get().getVersionsFor(groupId, artifactId); + + versions.removeAll(blocklistedVersions); + } + foundVersion = foundStream.get().getVersionComparator().matches(versions); + } + + if (foundVersion.isPresent()) { + return Optional.of(new ResolveLatestVersionResult(foundVersion.get(), this)); + } + return Optional.empty(); + } + + MavenArtifact resolveDirectMavenArtifact(String groupId, String artifactId, String extension, String classifier, String version) throws UnresolvedMavenArtifactException { + requireNonNull(groupId); + requireNonNull(artifactId); + requireNonNull(version); + + File file = resolver.resolveArtifact(groupId, artifactId, extension, classifier, version); + return new MavenArtifact(groupId, artifactId, extension, classifier, version, file); + } + + List resolveDirectMavenArtifacts(List coordinates) throws UnresolvedMavenArtifactException { + coordinates.stream().forEach(c->{ + requireNonNull(c.getGroupId()); + requireNonNull(c.getArtifactId()); + requireNonNull(c.getVersion()); + }); + final List files = resolver.resolveArtifacts(coordinates); + + final ArrayList res = new ArrayList<>(); + for (int i = 0; i < coordinates.size(); i++) { + final ArtifactCoordinate request = coordinates.get(i); + final MavenArtifact resolvedArtifact = new MavenArtifact(request.getGroupId(), request.getArtifactId(), request.getExtension(), request.getClassifier(), request.getVersion(), files.get(i)); + + res.add(resolvedArtifact); + } + return res; + } + + static class ResolveArtifactResult { + File file; + ChannelImpl channel; + + ResolveArtifactResult(File file, ChannelImpl channel) { + this.file = file; + this.channel = channel; + } + } + + ResolveArtifactResult resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) throws UnresolvedMavenArtifactException { + requireNonNull(groupId); + requireNonNull(artifactId); + requireNonNull(version); + requireNonNull(resolver); + + // first we looked into the required channels + for (ChannelImpl requiredChannel : requiredChannels) { + try { + return requiredChannel.resolveArtifact(groupId, artifactId, extension, classifier, version); + } catch (UnresolvedMavenArtifactException e) { + // ignore if the required channel are not able to resolve the artifact + } + } + + return new ResolveArtifactResult(resolver.resolveArtifact(groupId, artifactId, extension, classifier, version), this); + } + + List resolveArtifacts(List coordinates) throws UnresolvedMavenArtifactException { + final List resolvedArtifacts = resolver.resolveArtifacts(coordinates); + return resolvedArtifacts.stream().map(f->new ResolveArtifactResult(f, this)).collect(Collectors.toList()); + } + + @Override + public String toString() { + return "Channel{" + + "channelDefinition=" + channelDefinition + + ", requiredChannels=" + requiredChannels + + ", channelManifest=" + channelManifest + + ", resolver=" + resolver + + ", dependency=" + dependency + + ", blocklist=" + blocklist + + '}'; + } +} diff --git a/core/src/main/java/org/wildfly/channel/ChannelManifest.java b/core/src/main/java/org/wildfly/channel/ChannelManifest.java new file mode 100644 index 00000000..b70f35e1 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/ChannelManifest.java @@ -0,0 +1,183 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +/** + * Java representation of a Channel Manifest. + */ +public class ChannelManifest { + + public static final String CLASSIFIER="manifest"; + public static final String EXTENSION="yaml"; + + /** + * Version of the schema used by this manifest. + * This is a required field. + */ + private final String schemaVersion; + + /** + * Optional manifest name. + * Short, one-line description of the manifest + */ + private final String name; + + /** + * Optional manifest ID. + * Alphanumeric identifier of manifest. + */ + private final String id; + + /** + * Optional description of the manifest. It can use multiple lines. + */ + private final String description; + + /** + * Streams of components that are provided by this manifest. + */ + private Set streams; + + /** + * Optional list of manifests that should be checked if artifact cannot be found in this manifest. + */ + private List manifestRequirements; + + /** + * Representation of a ChannelManifest resource using the current schema version. + * + * @see #ChannelManifest(String, String, String, String, Collection, Collection) + */ + public ChannelManifest(String name, + String id, + String description, + Collection streams) { + this(ChannelManifestMapper.CURRENT_SCHEMA_VERSION, + name, + id, + description, + Collections.emptyList(), + streams); + } + + /** + * Representation of a ChannelManifest resource using the current schema version. + * + * @see #ChannelManifest(String, String, String, String, Collection, Collection) + */ + public ChannelManifest(String name, + String id, + String description, + Collection manifestRequirements, + Collection streams) { + this(ChannelManifestMapper.CURRENT_SCHEMA_VERSION, + name, + id, + description, + manifestRequirements, + streams); + } + + /** + * Representation of a Channel resource + * + * @param schemaVersion the version of the schema to validate this manifest resource - required + * @param name the name of the manifest - can be {@code null} + * @param description the description of the manifest - can be {@code null} + * @param streams the streams defined by the manifest - can be {@code null} + */ + @JsonCreator + @JsonPropertyOrder({ "schemaVersion", "name", "description", "streams" }) + public ChannelManifest(@JsonProperty(value = "schemaVersion", required = true) String schemaVersion, + @JsonProperty(value = "name") String name, + @JsonProperty(value = "id") String id, + @JsonProperty(value = "description") String description, + @JsonProperty(value = "requires") Collection manifestRequirements, + @JsonProperty(value = "streams") Collection streams) { + this.schemaVersion = schemaVersion; + this.name = name; + this.id = id; + this.description = description; + this.manifestRequirements = new ArrayList<>(); + if (manifestRequirements != null) { + this.manifestRequirements.addAll(manifestRequirements); + } + this.streams = new TreeSet<>(); + if (streams != null) { + this.streams.addAll(streams); + } + } + + @JsonInclude + public String getSchemaVersion() { + return schemaVersion; + } + + @JsonInclude(NON_NULL) + public String getName() { + return name; + } + + @JsonInclude(NON_NULL) + public String getId() { + return id; + } + + @JsonInclude(NON_NULL) + public String getDescription() { + return description; + } + + @JsonInclude(NON_EMPTY) + @JsonProperty(value = "requires") + public List getManifestRequirements() { + return manifestRequirements; + } + + @JsonInclude(NON_EMPTY) + public Collection getStreams() { + return streams; + } + + public Optional findStreamFor(String groupId, String artifactId) { + // first exact match: + Optional stream = streams.stream().filter(s -> s.getGroupId().equals(groupId) && s.getArtifactId().equals(artifactId)).findFirst(); + if (stream.isPresent()) { + return stream; + } + // check if there is a stream for groupId:* + stream = streams.stream().filter(s -> s.getGroupId().equals(groupId) && s.getArtifactId().equals("*")).findFirst(); + return stream; + } +} diff --git a/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java b/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java new file mode 100644 index 00000000..8b4efcc1 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java @@ -0,0 +1,84 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.net.MalformedURLException; +import java.net.URL; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +/** + * A channel manifest coordinate. Uses either a Maven coordinates (groupId, artifactId, version) + * or a URL from which the channel manifest file can be fetched. + */ +@JsonIgnoreProperties(value = {"groupId", "artifactId", "version", "classifier", "extension"}) +public class ChannelManifestCoordinate extends ChannelMetadataCoordinate { + public ChannelManifestCoordinate(String groupId, String artifactId, String version) { + super(groupId, artifactId, version, ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION); + } + + public ChannelManifestCoordinate(String groupId, String artifactId) { + super(groupId, artifactId, ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION); + } + + public ChannelManifestCoordinate(URL url) { + super(url); + } + + public ChannelManifestCoordinate() { + super(ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION); + } + + @JsonCreator + public static ChannelManifestCoordinate create(@JsonProperty(value = "url") String url, @JsonProperty(value = "maven") MavenCoordinate gav) throws MalformedURLException { + if (gav != null) { + if (gav.getVersion() == null || gav.getVersion().isEmpty()) { + return new ChannelManifestCoordinate(gav.getGroupId(), gav.getArtifactId()); + } else { + return new ChannelManifestCoordinate(gav.getGroupId(), gav.getArtifactId(), gav.getVersion()); + } + } else { + return new ChannelManifestCoordinate(new URL(url)); + } + } + + @JsonProperty(value = "maven") + @JsonInclude(NON_NULL) + public MavenCoordinate getMaven() { + if (isEmpty(getGroupId()) || isEmpty(getArtifactId())) { + return null; + } + return new MavenCoordinate(getGroupId(), getArtifactId(), getVersion()); + } + + private boolean isEmpty(String text) { + return text == null || text.isEmpty(); + } + + @JsonProperty(value = "url") + @JsonInclude(NON_NULL) + @Override + public URL getUrl() { + return super.getUrl(); + } +} diff --git a/core/src/main/java/org/wildfly/channel/ChannelManifestMapper.java b/core/src/main/java/org/wildfly/channel/ChannelManifestMapper.java new file mode 100644 index 00000000..0ff62c96 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/ChannelManifestMapper.java @@ -0,0 +1,144 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.fasterxml.jackson.dataformat.yaml.YAMLParser; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** + * Mapper class to transform YAML content (from URL or String) to Channel objects (and vice versa). + * + * YAML input is validated against a schema. + */ +public class ChannelManifestMapper { + + public static final String SCHEMA_VERSION_1_0_0 = "1.0.0"; + public static final String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_1_0_0; + + private static final String SCHEMA_1_0_0_FILE = "org/wildfly/manifest/v1.0.0/schema.json"; + private static final YAMLFactory YAML_FACTORY = new YAMLFactory() + .configure(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR, true); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(YAML_FACTORY) + .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(ORDER_MAP_ENTRIES_BY_KEYS, true); + private static final JsonSchemaFactory SCHEMA_FACTORY = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).objectMapper(OBJECT_MAPPER).build(); + private static final Map SCHEMAS = new HashMap<>(); + + static { + SCHEMAS.put(SCHEMA_VERSION_1_0_0, SCHEMA_FACTORY.getSchema(ChannelManifestMapper.class.getClassLoader().getResourceAsStream(SCHEMA_1_0_0_FILE))); + } + + private static JsonSchema getSchema(JsonNode node) { + JsonNode schemaVersion = node.path("schemaVersion"); + String version = schemaVersion.asText(); + if (version == null || version.isEmpty()) { + throw new RuntimeException("The manifest does not specify a schemaVersion."); + } + JsonSchema schema = SCHEMAS.get(version); + if (schema == null) { + throw new RuntimeException("Unknown schema version " + schemaVersion); + } + return schema; + } + + public static String toYaml(ChannelManifest channelManifest) throws IOException { + Objects.requireNonNull(channelManifest); + StringWriter w = new StringWriter(); + OBJECT_MAPPER.writeValue(w, channelManifest); + return w.toString(); + } + + public static ChannelManifest from(URL manifestURL) throws InvalidChannelMetadataException { + requireNonNull(manifestURL); + + try { + // QoL improvement + if (manifestURL.toString().endsWith("/")) { + manifestURL = manifestURL.toURI().resolve("channel.yaml").toURL(); + } + + List messages = validate(manifestURL); + if (!messages.isEmpty()) { + throw new InvalidChannelMetadataException("Invalid manifest", messages); + } + ChannelManifest channelManifest = OBJECT_MAPPER.readValue(manifestURL, ChannelManifest.class); + return channelManifest; + } catch (IOException | URISyntaxException e) { + throw wrapException(e); + } + } + + public static ChannelManifest fromString(String yamlContent) throws InvalidChannelMetadataException { + requireNonNull(yamlContent); + + try { + List messages = validateString(yamlContent); + if (!messages.isEmpty()) { + throw new InvalidChannelMetadataException("Invalid manifest", messages); + } + + YAMLParser parser = YAML_FACTORY.createParser(yamlContent); + ChannelManifest channelManifest = OBJECT_MAPPER.readValue(parser, ChannelManifest.class); + return channelManifest; + } catch (IOException e) { + throw wrapException(e); + } + } + + private static List validate(URL url) throws IOException { + JsonNode node = OBJECT_MAPPER.readTree(url); + JsonSchema schema = getSchema(node); + Set validationMessages = schema.validate(node); + return validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.toList()); + } + + private static List validateString(String yamlContent) throws IOException { + JsonNode node = OBJECT_MAPPER.readTree(yamlContent); + JsonSchema schema = getSchema(node); + Set validationMessages = schema.validate(node); + return validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.toList()); + } + + private static InvalidChannelMetadataException wrapException(Exception e) { + InvalidChannelMetadataException ice = new InvalidChannelMetadataException("Invalid Manifest", singletonList(e.getLocalizedMessage())); + ice.initCause(e); + return ice; + } +} diff --git a/core/src/main/java/org/wildfly/channel/ChannelMapper.java b/core/src/main/java/org/wildfly/channel/ChannelMapper.java index dd64a816..11f9ca7b 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelMapper.java +++ b/core/src/main/java/org/wildfly/channel/ChannelMapper.java @@ -33,11 +33,8 @@ import java.util.Set; import java.util.stream.Collectors; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import com.fasterxml.jackson.dataformat.yaml.YAMLParser; @@ -54,9 +51,11 @@ public class ChannelMapper { public static final String SCHEMA_VERSION_1_0_0 = "1.0.0"; - public static final String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_1_0_0; + public static final String SCHEMA_VERSION_2_0_0 = "2.0.0"; + public static final String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_2_0_0; private static final String SCHEMA_1_0_0_FILE = "org/wildfly/channel/v1.0.0/schema.json"; + private static final String SCHEMA_2_0_0_FILE = "org/wildfly/channel/v2.0.0/schema.json"; private static final YAMLFactory YAML_FACTORY = new YAMLFactory() .configure(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR, true); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(YAML_FACTORY) @@ -67,6 +66,7 @@ public class ChannelMapper { static { SCHEMAS.put(SCHEMA_VERSION_1_0_0, SCHEMA_FACTORY.getSchema(ChannelMapper.class.getClassLoader().getResourceAsStream(SCHEMA_1_0_0_FILE))); + SCHEMAS.put(SCHEMA_VERSION_2_0_0, SCHEMA_FACTORY.getSchema(ChannelMapper.class.getClassLoader().getResourceAsStream(SCHEMA_2_0_0_FILE))); } private static JsonSchema getSchema(JsonNode node) { @@ -95,7 +95,7 @@ public static String toYaml(List channels) throws IOException { return w.toString(); } - public static Channel from(URL channelURL) throws InvalidChannelException { + public static Channel from(URL channelURL) throws InvalidChannelMetadataException { requireNonNull(channelURL); try { @@ -106,27 +106,25 @@ public static Channel from(URL channelURL) throws InvalidChannelException { List messages = validate(channelURL); if (!messages.isEmpty()) { - throw new InvalidChannelException("Invalid channel", messages); + throw new InvalidChannelMetadataException("Invalid channel", messages); } - Channel channel = OBJECT_MAPPER.readValue(channelURL, Channel.class); - return channel; + return OBJECT_MAPPER.readValue(channelURL, Channel.class); } catch (IOException | URISyntaxException e) { throw wrapException(e); } } - public static List fromString(String yamlContent) throws InvalidChannelException { + public static List fromString(String yamlContent) throws InvalidChannelMetadataException { requireNonNull(yamlContent); try { List messages = validateString(yamlContent); if (!messages.isEmpty()) { - throw new InvalidChannelException("Invalid channel", messages); + throw new InvalidChannelMetadataException("Invalid channel", messages); } YAMLParser parser = YAML_FACTORY.createParser(yamlContent); - List channels = OBJECT_MAPPER.readValues(parser, Channel.class).readAll(); - return channels; + return OBJECT_MAPPER.readValues(parser, Channel.class).readAll(); } catch (IOException e) { throw wrapException(e); } @@ -146,8 +144,8 @@ private static List validateString(String yamlContent) throws IOExceptio return validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.toList()); } - private static InvalidChannelException wrapException(Exception e) { - InvalidChannelException ice = new InvalidChannelException("Invalid Channel", singletonList(e.getLocalizedMessage())); + private static InvalidChannelMetadataException wrapException(Exception e) { + InvalidChannelMetadataException ice = new InvalidChannelMetadataException("Invalid Channel", singletonList(e.getLocalizedMessage())); ice.initCause(e); return ice; } diff --git a/core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java b/core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java new file mode 100644 index 00000000..1d61c045 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java @@ -0,0 +1,109 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import java.net.URL; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * A coordinate of Channel metadata artefacts (channel or manifest). + * Uses either a Maven coordinates (groupId, artifactId, version) + * or a URL from which the metadata file can be fetched. + */ +public class ChannelMetadataCoordinate { + private String groupId; + private String artifactId; + private String version; + private String classifier; + private String extension; + + private URL url; + + protected ChannelMetadataCoordinate() { + } + + public ChannelMetadataCoordinate(String groupId, String artifactId, String version, String classifier, String extension) { + this(groupId, artifactId, version, classifier, extension, null); + requireNonNull(groupId); + requireNonNull(artifactId); + requireNonNull(version); + } + + public ChannelMetadataCoordinate(String groupId, String artifactId, String classifier, String extension) { + this(groupId, artifactId, null, classifier, extension, null); + requireNonNull(groupId); + requireNonNull(artifactId); + } + + public ChannelMetadataCoordinate(URL url) { + this(null, null, null, null, null, url); + requireNonNull(url); + } + + private ChannelMetadataCoordinate(String groupId, String artifactId, String version, String classifier, String extension, URL url) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.classifier = classifier; + this.extension = extension; + this.url = url; + } + + protected ChannelMetadataCoordinate(String classifier, String extension) { + this(null, null, null, classifier, extension, null); + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getVersion() { + return version; + } + + public URL getUrl() { + return url; + } + + public String getClassifier() { + return classifier; + } + + public String getExtension() { + return extension; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChannelMetadataCoordinate that = (ChannelMetadataCoordinate) o; + return Objects.equals(groupId, that.groupId) && Objects.equals(artifactId, that.artifactId) && Objects.equals(version, that.version) && Objects.equals(classifier, that.classifier) && Objects.equals(extension, that.extension) && Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version, classifier, extension, url); + } +} diff --git a/core/src/main/java/org/wildfly/channel/ChannelRecorder.java b/core/src/main/java/org/wildfly/channel/ChannelRecorder.java index c9bf7a27..2eaa1f63 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelRecorder.java +++ b/core/src/main/java/org/wildfly/channel/ChannelRecorder.java @@ -26,10 +26,7 @@ void recordStream(String groupId, String artifactId, String version) { streams.putIfAbsent(groupId + ":" + artifactId + ":" + version, new Stream(groupId, artifactId, version, null)); } - Channel getRecordedChannel() { - return new Channel(null, - null, - null, - null, new ArrayList(streams.values())); + ChannelManifest getRecordedChannel() { + return new ChannelManifest(null, null, null, new ArrayList(streams.values())); } } diff --git a/core/src/main/java/org/wildfly/channel/ChannelRequirement.java b/core/src/main/java/org/wildfly/channel/ChannelRequirement.java deleted file mode 100644 index fe587d4c..00000000 --- a/core/src/main/java/org/wildfly/channel/ChannelRequirement.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2022 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.wildfly.channel; - -import static java.util.Objects.requireNonNull; - -import java.util.Objects; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Java representation of a Channel requirement identified by Maven coordinates. - */ -public class ChannelRequirement { - - private final String groupId; - private final String artifactId; - private final String version; - - /** - * Representation of a channel requirement. - * - * @param groupId groupId of the Maven coordinate - required - * @param artifactId artifactId of the Maven coordinate - required - * @param version version of the Maven coordinate - can be {@code null} - */ - @JsonCreator - public ChannelRequirement(@JsonProperty(value = "groupId", required = true) String groupId, - @JsonProperty(value = "artifactId", required = true) String artifactId, - @JsonProperty(value = "version") String version) { - requireNonNull(groupId); - requireNonNull(artifactId); - - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - } - - public String getGroupId() { - return groupId; - } - - public String getArtifactId() { - return artifactId; - } - - public String getVersion() { - return version; - } - - @Override - public String toString() { - return "ChannelRequirement{" + - "groupId='" + groupId + '\'' + - ", artifactId='" + artifactId + '\'' + - ", version='" + version + '\'' + - '}'; - } -} diff --git a/core/src/main/java/org/wildfly/channel/ChannelSession.java b/core/src/main/java/org/wildfly/channel/ChannelSession.java index 0c477e87..5f75ae17 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelSession.java +++ b/core/src/main/java/org/wildfly/channel/ChannelSession.java @@ -21,9 +21,12 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.version.VersionMatcher; @@ -32,24 +35,34 @@ * A ChannelSession is used to install and resolve Maven Artifacts inside a single scope. */ public class ChannelSession implements AutoCloseable { - private final List channels; + private final List channels; private final ChannelRecorder recorder = new ChannelRecorder(); - private final MavenVersionsResolver resolver; + // resolver used for direct dependencies only. Uses combination of all repositories in the channels. + private final MavenVersionsResolver combinedResolver; /** * Create a ChannelSession. * - * @param channels the list of channels to resolve Maven artifact + * @param channelDefinitions the list of channels to resolve Maven artifact * @param factory Factory to create {@code MavenVersionsResolver} that are performing the actual Maven resolution. + * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels + * @throws CyclicDependencyException - if the required manifests form a cyclic dependency */ - public ChannelSession(List channels, MavenVersionsResolver.Factory factory) { - requireNonNull(channels); + public ChannelSession(List channelDefinitions, MavenVersionsResolver.Factory factory) { + requireNonNull(channelDefinitions); requireNonNull(factory); - this.resolver = factory.create(); - this.channels = channels; - for (Channel channel : channels) { - channel.init(factory); + + final Set repositories = channelDefinitions.stream().flatMap(c -> c.getRepositories().stream()).collect(Collectors.toSet()); + this.combinedResolver = factory.create(repositories); + + List channelList = channelDefinitions.stream().map(ChannelImpl::new).collect(Collectors.toList()); + for (ChannelImpl channel : channelList) { + channel.init(factory, channelList); } + // filter out channels marked as dependency, so that resolution starts only at top level channels + this.channels = channelList.stream().filter(c->!c.isDependency()).collect(Collectors.toList()); + + validateNoDuplicatedManifests(); } /** @@ -75,11 +88,11 @@ public MavenArtifact resolveMavenArtifact(String groupId, String artifactId, Str requireNonNull(artifactId); // baseVersion is not used at the moment but will provide essential to support advanced use cases to determine multiple streams of the same Maven component. - Channel.ResolveLatestVersionResult result = findChannelWithLatestVersion(groupId, artifactId, extension, classifier, baseVersion); + ChannelImpl.ResolveLatestVersionResult result = findChannelWithLatestVersion(groupId, artifactId, extension, classifier, baseVersion); String latestVersion = result.version; - Channel channel = result.channel; + ChannelImpl channel = result.channel; - Channel.ResolveArtifactResult artifact = channel.resolveArtifact(groupId, artifactId, extension, classifier, latestVersion); + ChannelImpl.ResolveArtifactResult artifact = channel.resolveArtifact(groupId, artifactId, extension, classifier, latestVersion); recorder.recordStream(groupId, artifactId, latestVersion); return new MavenArtifact(groupId, artifactId, extension, classifier, latestVersion, artifact.file); } @@ -101,12 +114,12 @@ public MavenArtifact resolveMavenArtifact(String groupId, String artifactId, Str public List resolveMavenArtifacts(List coordinates) throws UnresolvedMavenArtifactException { requireNonNull(coordinates); - Map> channelMap = splitArtifactsPerChannel(coordinates); + Map> channelMap = splitArtifactsPerChannel(coordinates); final ArrayList res = new ArrayList<>(); - for (Channel channel : channelMap.keySet()) { + for (ChannelImpl channel : channelMap.keySet()) { final List requests = channelMap.get(channel); - final List resolveArtifactResults = channel.resolveArtifacts(requests); + final List resolveArtifactResults = channel.resolveArtifacts(requests); for (int i = 0; i < requests.size(); i++) { final ArtifactCoordinate request = requests.get(i); final MavenArtifact resolvedArtifact = new MavenArtifact(request.getGroupId(), request.getArtifactId(), request.getExtension(), request.getClassifier(), request.getVersion(), resolveArtifactResults.get(i).file); @@ -136,7 +149,7 @@ public MavenArtifact resolveDirectMavenArtifact(String groupId, String artifactI requireNonNull(artifactId); requireNonNull(version); - File file = resolver.resolveArtifact(groupId, artifactId, extension, classifier, version); + File file = combinedResolver.resolveArtifact(groupId, artifactId, extension, classifier, version); recorder.recordStream(groupId, artifactId, version); return new MavenArtifact(groupId, artifactId, extension, classifier, version, file); } @@ -156,7 +169,7 @@ public List resolveDirectMavenArtifacts(List requireNonNull(c.getArtifactId()); requireNonNull(c.getVersion()); }); - final List files = resolver.resolveArtifacts(coordinates); + final List files = combinedResolver.resolveArtifacts(coordinates); final ArrayList res = new ArrayList<>(); for (int i = 0; i < coordinates.size(); i++) { @@ -187,10 +200,10 @@ public String findLatestMavenArtifactVersion(String groupId, String artifactId, @Override public void close() { - for (Channel channel : channels) { + for (ChannelImpl channel : channels) { channel.close(); } - resolver.close(); + combinedResolver.close(); } /** @@ -201,17 +214,24 @@ public void close() { * * @return a synthetic Channel. */ - public Channel getRecordedChannel() { + public ChannelManifest getRecordedChannel() { return recorder.getRecordedChannel(); } - private Channel.ResolveLatestVersionResult findChannelWithLatestVersion(String groupId, String artifactId, String extension, String classifier, String baseVersion) throws UnresolvedMavenArtifactException { + private void validateNoDuplicatedManifests() { + final List manifestIds = this.channels.stream().map(c -> c.getManifest().getId()).filter(id -> id != null).collect(Collectors.toList()); + if (manifestIds.size() != new HashSet<>(manifestIds).size()) { + throw new RuntimeException("The same manifest is provided by one or more channels"); + } + } + + private ChannelImpl.ResolveLatestVersionResult findChannelWithLatestVersion(String groupId, String artifactId, String extension, String classifier, String baseVersion) throws UnresolvedMavenArtifactException { requireNonNull(groupId); requireNonNull(artifactId); - Map foundVersions = new HashMap<>(); - for (Channel channel : channels) { - Optional result = channel.resolveLatestVersion(groupId, artifactId, extension, classifier); + Map foundVersions = new HashMap<>(); + for (ChannelImpl channel : channels) { + Optional result = channel.resolveLatestVersion(groupId, artifactId, extension, classifier, baseVersion); if (result.isPresent()) { foundVersions.put(result.get().version, result.get()); } @@ -224,13 +244,13 @@ private Channel.ResolveLatestVersionResult findChannelWithLatestVersion(String g })); } - private Map> splitArtifactsPerChannel(List coordinates) { - Map> channelMap = new HashMap<>(); + private Map> splitArtifactsPerChannel(List coordinates) { + Map> channelMap = new HashMap<>(); for (ArtifactCoordinate coord : coordinates) { - Channel.ResolveLatestVersionResult result = findChannelWithLatestVersion(coord.getGroupId(), coord.getArtifactId(), + ChannelImpl.ResolveLatestVersionResult result = findChannelWithLatestVersion(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), coord.getVersion()); ArtifactCoordinate query = new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), result.version); - Channel channel = result.channel; + ChannelImpl channel = result.channel; if (!channelMap.containsKey(channel)) { channelMap.put(channel, new ArrayList<>()); } diff --git a/core/src/main/java/org/wildfly/channel/CyclicDependencyException.java b/core/src/main/java/org/wildfly/channel/CyclicDependencyException.java new file mode 100644 index 00000000..9bd20f16 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/CyclicDependencyException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +public class CyclicDependencyException extends RuntimeException { + public CyclicDependencyException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/org/wildfly/channel/InvalidChannelException.java b/core/src/main/java/org/wildfly/channel/InvalidChannelMetadataException.java similarity index 85% rename from core/src/main/java/org/wildfly/channel/InvalidChannelException.java rename to core/src/main/java/org/wildfly/channel/InvalidChannelMetadataException.java index 91b78928..5d20ede7 100644 --- a/core/src/main/java/org/wildfly/channel/InvalidChannelException.java +++ b/core/src/main/java/org/wildfly/channel/InvalidChannelMetadataException.java @@ -18,10 +18,10 @@ import java.util.List; -public class InvalidChannelException extends RuntimeException { +public class InvalidChannelMetadataException extends RuntimeException { private final List messages; - public InvalidChannelException(String message, List messages) { + public InvalidChannelMetadataException(String message, List messages) { this.messages = messages; } diff --git a/core/src/main/java/org/wildfly/channel/ManifestRequirement.java b/core/src/main/java/org/wildfly/channel/ManifestRequirement.java new file mode 100644 index 00000000..8c7d1562 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/ManifestRequirement.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * Java representation of a Channel requirement identified by Maven coordinates. + */ +public class ManifestRequirement { + + private final String id; + private final MavenCoordinate mavenCoordinate; + + /** + * Representation of a channel requirement. + * + * @param id - ID of the required manifest + * @param mavenCoordinate - optional {@link MavenCoordinate} of the required manifest + */ + @JsonCreator + public ManifestRequirement(@JsonProperty(value = "id", required = true) String id, + @JsonProperty(value = "maven") MavenCoordinate mavenCoordinate) { + requireNonNull(id); + + this.id = id; + this.mavenCoordinate = mavenCoordinate; + } + + public String getId() { + return id; + } + + @JsonInclude(NON_EMPTY) + @JsonProperty("maven") + public MavenCoordinate getMavenCoordinate() { + return mavenCoordinate; + } + + @JsonIgnore + public String getGroupId() { + return mavenCoordinate == null?null:mavenCoordinate.getGroupId(); + } + + @JsonIgnore + public String getArtifactId() { + return mavenCoordinate == null?null:mavenCoordinate.getArtifactId(); + } + + @JsonIgnore + public String getVersion() { + return mavenCoordinate == null?null:mavenCoordinate.getVersion(); + } + + @Override + public String toString() { + return "ManifestRequirement{" + + "id='" + id + '\'' + + ", mavenCoordinate=" + mavenCoordinate + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ManifestRequirement that = (ManifestRequirement) o; + return Objects.equals(id, that.id) && Objects.equals(mavenCoordinate, that.mavenCoordinate); + } + + @Override + public int hashCode() { + return Objects.hash(id, mavenCoordinate); + } +} diff --git a/core/src/main/java/org/wildfly/channel/MavenCoordinate.java b/core/src/main/java/org/wildfly/channel/MavenCoordinate.java new file mode 100644 index 00000000..6792cf85 --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/MavenCoordinate.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +/** + * Java representation of a Maven Coordinate used to represent a channel artifact. + */ +public class MavenCoordinate { + + /** + * GroupId of the maven artifact. + */ + private String groupId; + + /** + * ArtifactId of the maven artifact. + */ + private String artifactId; + /** + * Optional version of the maven artifact. + */ + private String version; + + @JsonCreator + public MavenCoordinate(@JsonProperty(value = "groupId", required = true) String groupId, + @JsonProperty(value = "artifactId", required = true) String artifactId, + @JsonProperty(value = "version") String version) { + Objects.requireNonNull(groupId); + Objects.requireNonNull(artifactId); + + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + @JsonInclude(NON_NULL) + public String getVersion() { + return version; + } + + @Override + public String toString() { + return "MavenCoordinate{" + + "groupId='" + groupId + '\'' + + ", artifactId='" + artifactId + '\'' + + ", version='" + version + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MavenCoordinate that = (MavenCoordinate) o; + return Objects.equals(groupId, that.groupId) && Objects.equals(artifactId, that.artifactId) && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version); + } +} diff --git a/core/src/main/java/org/wildfly/channel/Repository.java b/core/src/main/java/org/wildfly/channel/Repository.java new file mode 100644 index 00000000..a0f4b69e --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/Repository.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * Java representation of Repository + */ +public class Repository { + /** + * ID of the repository. + * Can be used to identify repository mirrors and proxies. + */ + private String id; + /** + * URL of the repository. + * Used when the client doesn't provide alternative URLs for a repository. + */ + private String url; + + @JsonCreator + public Repository(@JsonProperty(value = "id", required = true) String id, @JsonProperty(value = "url", required = true) String url) { + Objects.requireNonNull(id); + Objects.requireNonNull(url); + + this.id = id; + this.url = url; + } + + public Repository() { + + } + + public String getId() { + return id; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Repository that = (Repository) o; + return Objects.equals(id, that.id) && Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(id, url); + } +} diff --git a/core/src/main/java/org/wildfly/channel/UnresolvedMavenArtifactException.java b/core/src/main/java/org/wildfly/channel/UnresolvedMavenArtifactException.java index 1d88e45a..2b81054e 100644 --- a/core/src/main/java/org/wildfly/channel/UnresolvedMavenArtifactException.java +++ b/core/src/main/java/org/wildfly/channel/UnresolvedMavenArtifactException.java @@ -38,6 +38,11 @@ public UnresolvedMavenArtifactException(String localizedMessage, this.unresolvedArtifacts = unresolvedArtifacts; } + public UnresolvedMavenArtifactException(String message, Set unresolvedArtifacts) { + super(message); + this.unresolvedArtifacts = unresolvedArtifacts; + } + public Set getUnresolvedArtifacts() { return unresolvedArtifacts; } diff --git a/core/src/main/java/org/wildfly/channel/UnresolvedRequiredManifestException.java b/core/src/main/java/org/wildfly/channel/UnresolvedRequiredManifestException.java new file mode 100644 index 00000000..daf28cee --- /dev/null +++ b/core/src/main/java/org/wildfly/channel/UnresolvedRequiredManifestException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +public class UnresolvedRequiredManifestException extends RuntimeException { + + private final String missingID; + + public UnresolvedRequiredManifestException(String msg, String missingID) { + super(msg); + this.missingID = missingID; + } + + public UnresolvedRequiredManifestException(String msg, String missingID, UnresolvedMavenArtifactException cause) { + super(msg, cause); + this.missingID = missingID; + } + + public String getMissingID() { + return missingID; + } +} diff --git a/core/src/main/java/org/wildfly/channel/Vendor.java b/core/src/main/java/org/wildfly/channel/Vendor.java index ad054d3c..e1384b8b 100644 --- a/core/src/main/java/org/wildfly/channel/Vendor.java +++ b/core/src/main/java/org/wildfly/channel/Vendor.java @@ -27,12 +27,12 @@ public class Vendor { /** * Name of the vendor. */ - private final String name; + private String name; /** * Support level provided by the vendor. */ - private final Support support; + private Support support; /** * Vendor resource @@ -46,6 +46,9 @@ public Vendor(@JsonProperty(value = "name", required = true) String name, @JsonP this.support = support; } + public Vendor() { + } + public String getName() { return name; } diff --git a/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java b/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java index f8426aec..ab3377c7 100644 --- a/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java +++ b/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java @@ -18,10 +18,14 @@ import java.io.Closeable; import java.io.File; +import java.net.URL; +import java.util.Collection; import java.util.List; import java.util.Set; import org.wildfly.channel.ArtifactCoordinate; +import org.wildfly.channel.ChannelMetadataCoordinate; +import org.wildfly.channel.Repository; import org.wildfly.channel.UnresolvedMavenArtifactException; /** @@ -70,6 +74,48 @@ public interface MavenVersionsResolver extends Closeable { */ List resolveArtifacts(List coordinates) throws UnresolvedMavenArtifactException; + /** + * Resolve a list of channel metadata artifacts based on the coordinates. + * If the {@code ChannelMetadataCoordinate} contains non-null URL, that URL is returned. + * If the {@code ChannelMetadataCoordinate} contains non-null Maven coordinates, the Maven artifact will be resolved + * and a URL to it will be returned. + * If the Maven coordinates specify only groupId and artifactId, latest available version of matching Maven artifact + * will be resolved. + * + * The order of returned URLs is the same as order of coordinates. + * + * @param manifestCoords - list of ChannelMetadataCoordinate. + * + * @return a list of URLs to the metadata files + * + * @throws UnresolvedMavenArtifactException if any artifacts can not be resolved. + */ + List resolveChannelMetadata(List manifestCoords) throws UnresolvedMavenArtifactException; + + /** + * Returns the {@code } version according to the repositories' Maven metadata. If multiple repositories + * contain the same artifact, {@link org.wildfly.channel.version.VersionMatcher#COMPARATOR} is used to choose version. + * + * @param groupId Maven GroupId - required + * @param artifactId Maven ArtifactId - required + * + * @return the {@code release} version. + * @throws UnresolvedMavenArtifactException if the metadata can not be resolved or is incomplete. + */ + String getMetadataReleaseVersion(String groupId, String artifactId); + + /** + * Returns the {@code } version according to the repositories' Maven metadata. If multiple repositories + * contain the same artifact, {@link org.wildfly.channel.version.VersionMatcher#COMPARATOR} is used to choose version. + * + * @param groupId Maven GroupId - required + * @param artifactId Maven ArtifactId - required + * + * @return the {@code latest} version. + * @throws UnresolvedMavenArtifactException if the metadata can not be resolved or is incomplete. + */ + String getMetadataLatestVersion(String groupId, String artifactId); + default void close() { } @@ -79,11 +125,11 @@ default void close() { * * A client of this library is responsible to provide an implementation of the {@link Factory} interface. * - * The {@link #create()} method will be called once for every channel. + * The {@link #create(Collection)}} method will be called once for every channel. */ interface Factory extends Closeable { - MavenVersionsResolver create(); + MavenVersionsResolver create(Collection repositories); default void close() { } diff --git a/core/src/main/resources/org/wildfly/blocklist/v1.0.0/schema.json b/core/src/main/resources/org/wildfly/blocklist/v1.0.0/schema.json index cc4cf755..3cc20d7c 100644 --- a/core/src/main/resources/org/wildfly/blocklist/v1.0.0/schema.json +++ b/core/src/main/resources/org/wildfly/blocklist/v1.0.0/schema.json @@ -1,6 +1,6 @@ { "$id": "https://wildfly.org/channels/blocklist/v1.0.0/schema.json", - "$schema": "http://json-schema.org/draft/2019-09/schema#", + "$schema": "https://json-schema.org/draft/2019-09/schema#", "type": "object", "required": ["schemaVersion"], "properties": { @@ -27,6 +27,7 @@ "versions": { "description": "List of blocklisted versions of the artifact.", "type": "array", + "minItems": 1, "items": { "type": "string" } diff --git a/core/src/main/resources/org/wildfly/channel/v1.0.0/schema.json b/core/src/main/resources/org/wildfly/channel/v1.0.0/schema.json index efc4c48d..71b49be1 100644 --- a/core/src/main/resources/org/wildfly/channel/v1.0.0/schema.json +++ b/core/src/main/resources/org/wildfly/channel/v1.0.0/schema.json @@ -96,4 +96,4 @@ } } } -} \ No newline at end of file +} diff --git a/core/src/main/resources/org/wildfly/channel/v2.0.0/schema.json b/core/src/main/resources/org/wildfly/channel/v2.0.0/schema.json index 50b917f3..a4f586c0 100644 --- a/core/src/main/resources/org/wildfly/channel/v2.0.0/schema.json +++ b/core/src/main/resources/org/wildfly/channel/v2.0.0/schema.json @@ -1,6 +1,6 @@ { "$id": "https://wildfly.org/channels/v2.0.0/schema.json", - "$schema": "http://json-schema.org/draft/2019-09/schema#", + "$schema": "https://json-schema.org/draft/2019-09/schema#", "type": "object", "required": ["schemaVersion", "repositories"], "properties": { diff --git a/core/src/main/resources/org/wildfly/manifest/v1.0.0/schema.json b/core/src/main/resources/org/wildfly/manifest/v1.0.0/schema.json index 89680abd..2548c44a 100644 --- a/core/src/main/resources/org/wildfly/manifest/v1.0.0/schema.json +++ b/core/src/main/resources/org/wildfly/manifest/v1.0.0/schema.json @@ -1,6 +1,6 @@ { "$id": "https://wildfly.org/manifests/v1.0.0/schema.json", - "$schema": "http://json-schema.org/draft/2019-09/schema#", + "$schema": "https://json-schema.org/draft/2019-09/schema#", "type": "object", "required": ["schemaVersion"], "properties": { @@ -69,7 +69,7 @@ "type": "string" }, "version" : { - "description": "Version of the stream. This must be either a single version. Only one of version, versionPattern must be set.", + "description": "Version of the stream. This must be a single version. Only one of version, versionPattern must be set.", "type": "string" }, "versionPattern" : { diff --git a/core/src/test/java/org/wildfly/channel/ChannelBuilder.java b/core/src/test/java/org/wildfly/channel/ChannelBuilder.java new file mode 100644 index 00000000..6e2e0614 --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/ChannelBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import java.util.ArrayList; +import java.util.List; + +class ChannelBuilder { + private String name; + private List repositories = new ArrayList<>(); + private ChannelManifestCoordinate manifestCoordinate; + private BlocklistCoordinate blocklistCoordinate; + private Channel.NoStreamStrategy strategy; + + Channel build() { + return new Channel(name, null, null, repositories, manifestCoordinate, blocklistCoordinate, strategy); + } + + ChannelBuilder setName(String name) { + this.name = name; + return this; + } + + ChannelBuilder addRepository(String repoId, String url) { + repositories.add(new Repository(repoId, url)); + return this; + } + + public ChannelBuilder setManifestCoordinate(String groupId, String artifactId, String version) { + this.manifestCoordinate = new ChannelManifestCoordinate(groupId, artifactId, version); + return this; + } + + public ChannelBuilder setBlocklist(String groupId, String artifactId, String version) { + if (version == null) { + this.blocklistCoordinate = new BlocklistCoordinate(groupId, artifactId); + } else { + this.blocklistCoordinate = new BlocklistCoordinate(groupId, artifactId, version); + } + return this; + } + + public ChannelBuilder setResolveStrategy(Channel.NoStreamStrategy strategy) { + this.strategy = strategy; + return this; + } +} diff --git a/core/src/test/java/org/wildfly/channel/ChannelManifestMapperTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelManifestMapperTestCase.java new file mode 100644 index 00000000..2d8ee9c6 --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/ChannelManifestMapperTestCase.java @@ -0,0 +1,87 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import org.junit.jupiter.api.Test; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ChannelManifestMapperTestCase { + + @Test + public void testWriteReadChannel() throws Exception { + final ChannelManifest manifest = new ChannelManifest("test_name", null, "test_desc", Collections.emptyList()); + final String yaml = ChannelManifestMapper.toYaml(manifest); + + final ChannelManifest manifest1 = ChannelManifestMapper.fromString(yaml); + assertEquals("test_desc", manifest1.getDescription()); + } + + @Test + public void testWriteMultipleChannels() throws Exception { + final Stream stream1 = new Stream("org.bar", "example", "1.2.3"); + final Stream stream2 = new Stream("org.bar", "other-example", Pattern.compile("\\.*")); + final ChannelManifest manifest1 = new ChannelManifest("test_name_1", null, "test_desc", Arrays.asList(stream1, stream2)); + final ChannelManifest manifest2 = new ChannelManifest("test_name_2", null, "test_desc", Collections.emptyList()); + final String yaml1 = ChannelManifestMapper.toYaml(manifest1); + final String yaml2 = ChannelManifestMapper.toYaml(manifest2); + + System.out.println(yaml1); + System.out.println(yaml2); + + final ChannelManifest m1 = ChannelManifestMapper.fromString(yaml1); + assertEquals(manifest1.getName(), m1.getName()); + assertEquals(2, m1.getStreams().size()); + assertEquals("example", m1.getStreams().stream().findFirst().get().getArtifactId()); + final ChannelManifest m2 = ChannelManifestMapper.fromString(yaml2); + assertEquals(manifest2.getName(), m2.getName()); + assertEquals(0, m2.getStreams().size()); + } + + @Test + public void testReadChannelWithUnknownProperties() { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + URL file = tccl.getResource("channels/manifest-with-unknown-properties.yaml"); + + Channel channel = ChannelMapper.from(file); + assertNotNull(channel); + } + + @Test + public void testWriteRequires() throws Exception { + final ChannelManifest manifest = new ManifestBuilder() + .setId("test-id") + .addRequires("required-id", "org.test", "required", "1.0.0") + .build(); + + final String yaml1 = ChannelManifestMapper.toYaml(manifest); + System.out.println(yaml1); + final ChannelManifest m1 = ChannelManifestMapper.fromString(yaml1); + + assertEquals("test-id", m1.getId()); + assertEquals("required-id", m1.getManifestRequirements().get(0).getId()); + assertEquals("org.test", m1.getManifestRequirements().get(0).getGroupId()); + assertEquals("required", m1.getManifestRequirements().get(0).getArtifactId()); + assertEquals("1.0.0", m1.getManifestRequirements().get(0).getVersion()); + } +} diff --git a/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java index 19eac0a8..2703ae7a 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java @@ -18,13 +18,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.wildfly.channel.ChannelMapper.CURRENT_SCHEMA_VERSION; import java.net.URL; -import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.regex.Pattern; import org.junit.jupiter.api.Test; @@ -32,7 +28,12 @@ public class ChannelMapperTestCase { @Test public void testWriteReadChannel() throws Exception { - final Channel channel = new Channel("test_name", "test_desc", new Vendor("test_vendor_name", Vendor.Support.COMMUNITY), Collections.emptyList(), Collections.emptyList()); + final Channel channel = new Channel("test_name", "test_desc", + new Vendor("test_vendor_name", Vendor.Support.COMMUNITY), + List.of(new Repository("test", "https://test.org/repository")), + new ChannelManifestCoordinate("test.channels", "channel"), + new BlocklistCoordinate("test.block", "blocklist"), + Channel.NoStreamStrategy.NONE); final String yaml = ChannelMapper.toYaml(channel); final Channel channel1 = ChannelMapper.fromString(yaml).get(0); @@ -41,11 +42,16 @@ public void testWriteReadChannel() throws Exception { @Test public void testWriteMultipleChannels() throws Exception { - final ChannelRequirement req = new ChannelRequirement("org", "foo", "1.2.3"); - final Stream stream1 = new Stream("org.bar", "example", "1.2.3"); - final Stream stream2 = new Stream("org.bar", "other-example", Pattern.compile("\\.*")); - final Channel channel1 = new Channel("test_name_1", "test_desc", new Vendor("test_vendor_name", Vendor.Support.COMMUNITY), Arrays.asList(req), Arrays.asList(stream1, stream2)); - final Channel channel2 = new Channel("test_name_2", "test_desc", new Vendor("test_vendor_name", Vendor.Support.COMMUNITY), Collections.emptyList(), Collections.emptyList()); + final Channel channel1 = new Channel("test_name_1", "test_desc", new Vendor("test_vendor_name", Vendor.Support.COMMUNITY), + List.of(new Repository("test", "https://test.org/repository")), + new ChannelManifestCoordinate("test.channels", "channel"), + new BlocklistCoordinate("test.block", "blocklist"), + Channel.NoStreamStrategy.NONE); + final Channel channel2 = new Channel("test_name_2", "test_desc", new Vendor("test_vendor_name", Vendor.Support.COMMUNITY), + List.of(new Repository("test", "https://test.org/repository")), + new ChannelManifestCoordinate(new URL("http://test.channels/channels")), + new BlocklistCoordinate("test.block", "blocklist"), + Channel.NoStreamStrategy.NONE); final String yaml = ChannelMapper.toYaml(channel1, channel2); System.out.println(yaml); @@ -53,8 +59,6 @@ public void testWriteMultipleChannels() throws Exception { assertEquals(2, channels.size()); final Channel c1 = channels.get(0); assertEquals(channel1.getName(), c1.getName()); - assertEquals(1, c1.getChannelRequirements().size()); - assertEquals("foo", c1.getChannelRequirements().get(0).getArtifactId()); final Channel c2 = channels.get(1); assertEquals(channel2.getName(), c2.getName()); } @@ -67,4 +71,20 @@ public void testReadChannelWithUnknownProperties() { Channel channel = ChannelMapper.from(file); assertNotNull(channel); } + + @Test + public void writeChannelWithUrlBlocklist() throws Exception { + Channel channel = new Channel("test_name_1", "test_desc", new Vendor("test_vendor_name", Vendor.Support.COMMUNITY), + List.of(new Repository("test", "https://test.org/repository")), + new ChannelManifestCoordinate("test.channels", "channel"), + new BlocklistCoordinate(new URL("http://test.te")), + Channel.NoStreamStrategy.NONE); + + final String yaml = ChannelMapper.toYaml(channel); + + System.out.println(yaml); + + Channel readChannel = ChannelMapper.fromString(yaml).get(0); + assertEquals(new URL("http://test.te"), readChannel.getBlocklistCoordinate().getUrl()); + } } diff --git a/core/src/test/java/org/wildfly/channel/ChannelRecorderTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelRecorderTestCase.java index 95403952..0e2c7f1a 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelRecorderTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelRecorderTestCase.java @@ -21,28 +21,33 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.wildfly.channel.ChannelMapper.CURRENT_SCHEMA_VERSION; +import static org.wildfly.channel.ChannelManifestMapper.CURRENT_SCHEMA_VERSION; import java.io.File; -import java.io.IOException; +import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.wildfly.channel.spi.MavenVersionsResolver; public class ChannelRecorderTestCase { + + @TempDir + private Path tempDir; + @Test - public void testChannelRecorder() throws IOException, UnresolvedMavenArtifactException { + public void testChannelRecorder() throws Exception { - List channels = ChannelMapper.fromString("---\n" + + String manifest1 = "---\n" + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.wildfly\n" + @@ -53,20 +58,20 @@ public void testChannelRecorder() throws IOException, UnresolvedMavenArtifactExc " versionPattern: '18\\.\\d+\\.\\d+.Final'\n" + " - groupId: io.undertow\n" + " artifactId: '*'\n" + - " versionPattern: '2\\.\\1\\.\\d+.Final'\n" + - "---\n" + - "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + " versionPattern: '2\\.\\1\\.\\d+.Final'\n"; + + String manifest2 = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: io.undertow\n" + " artifactId: '*'\n" + - " versionPattern: '2\\.\\d+\\.\\d+.Final'"); - Assertions.assertNotNull(channels); - assertEquals(2, channels.size()); + " versionPattern: '2\\.\\d+\\.\\d+.Final'"; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); - when(factory.create()) + final List channels = ChannelSessionTestCase.mockChannel(resolver, tempDir, manifest1, manifest2); + + when(factory.create(any())) .thenReturn(resolver); when(resolver.getAllVersions(eq("org.wildfly"), anyString(), eq(null), eq(null))) .thenReturn(singleton("24.0.0.Final")); @@ -85,10 +90,10 @@ public void testChannelRecorder() throws IOException, UnresolvedMavenArtifactExc // This should not be recorded, size should remain 4. session.resolveMavenArtifact("io.undertow", "undertow-servlet", null, null, "1.0.0.Final"); - Channel recordedChannel = session.getRecordedChannel(); - System.out.println(ChannelMapper.toYaml(recordedChannel)); + ChannelManifest recordedManifest = session.getRecordedChannel(); + System.out.println(ChannelManifestMapper.toYaml(recordedManifest)); - Collection streams = recordedChannel.getStreams(); + Collection streams = recordedManifest.getStreams(); assertStreamExistsFor(streams, "org.wildfly", "wildfly-ee-galleon-pack", "24.0.0.Final"); assertStreamExistsFor(streams, "org.wildfly.core", "wildfly.core.cli", "18.0.0.Final"); diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java new file mode 100644 index 00000000..d72c16f5 --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java @@ -0,0 +1,379 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.channel; + +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.wildfly.channel.spi.MavenVersionsResolver; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ChannelSessionInitTestCase { + @TempDir + private Path tempDir; + + /* + * Verify that a manifest required by ID is resolved from a list of channels + */ + @Test + public void resolveRequiredChannelById() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) + .thenReturn(mock(File.class)); + + final ChannelManifest requiredManifest = new ManifestBuilder() + .setId("required-manifest-one") + .addStream("org.example", "foo-bar", "1.2.0.Final") + .build(); + mockManifest(resolver, requiredManifest, "test.channels:required-manifest:1.0.0"); + + final ChannelManifest baseManifest = new ManifestBuilder() + .addRequires("required-manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest", "1.0.0") + .build() + ); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + MavenArtifact artifact = session.resolveMavenArtifact("org.example", "foo-bar", null, null, "0"); + assertNotNull(artifact); + + assertEquals("org.example", artifact.getGroupId()); + assertEquals("foo-bar", artifact.getArtifactId()); + assertNull(artifact.getExtension()); + assertNull(artifact.getClassifier()); + assertEquals("1.2.0.Final", artifact.getVersion()); + } + } + + @Test + public void throwExceptionRequiredChannelIdNotAvailable() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest baseManifest = new ManifestBuilder() + .addRequires("i-dont-exist") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build() + ); + assertThrows(UnresolvedRequiredManifestException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void throwExceptionRequiredChannelIdNotAvailableAndNotAbleToResolve() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest baseManifest = new ManifestBuilder() + .addRequires("i-dont-exist", "test.channels", "i-dont-exist", "1.0.0") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test.channels", "i-dont-exist", "1.0.0")))) + .thenThrow(UnresolvedMavenArtifactException.class); + + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build() + ); + assertThrows(UnresolvedRequiredManifestException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void versionInRequiredChannelIsOverridenByBase() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + when(resolver.resolveArtifact(eq("org.example"), eq("foo-bar"), eq(null), eq(null), any())) + .thenReturn(mock(File.class)); + + final ChannelManifest requiredManifest = new ManifestBuilder() + .setId("required-manifest-one") + .addStream("org.example", "foo-bar", "1.2.0.Final") + .build(); + mockManifest(resolver, requiredManifest, "test.channels:required-manifest:1.0.0"); + + final ChannelManifest baseManifest = new ManifestBuilder() + .addRequires("required-manifest-one") + .addStream("org.example", "foo-bar", "1.0.0.Final") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest", "1.0.0") + .build() + ); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + MavenArtifact artifact = session.resolveMavenArtifact("org.example", "foo-bar", null, null, "0"); + assertNotNull(artifact); + assertEquals("1.0.0.Final", artifact.getVersion()); + } + } + + @Test + public void cyclicDependencyCausesError() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest requiredManifest = new ManifestBuilder() + .setId("required-manifest-one") + .addRequires("base-manifest") + .build(); + mockManifest(resolver, requiredManifest, "test.channels:required-manifest:1.0.0"); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("base-manifest") + .addRequires("required-manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest", "1.0.0") + .build() + ); + assertThrows(CyclicDependencyException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void indirectCyclicDependency() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest requiredManifest = new ManifestBuilder() + .setId("required-manifest-one") + .addRequires("required-manifest-two") + .build(); + mockManifest(resolver, requiredManifest, "test.channels:required-manifest:1.0.0"); + + final ChannelManifest requiredManifest2 = new ManifestBuilder() + .setId("required-manifest-two") + .addRequires("base-manifest") + .build(); + mockManifest(resolver, requiredManifest2, "test.channels:required-manifest-two:1.0.0"); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("base-manifest") + .addRequires("required-manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel two") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest-two", "1.0.0") + .build() + ); + assertThrows(CyclicDependencyException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void detectCyclicDependencyOnSelf() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("base-manifest") + .addRequires("base-manifest") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build() + ); + assertThrows(CyclicDependencyException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void indirectCyclicDependency2() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest requiredManifest = new ManifestBuilder() + .setId("required-manifest-one") + .addRequires("required-manifest-two") + .build(); + mockManifest(resolver, requiredManifest, "test.channels:required-manifest:1.0.0"); + + final ChannelManifest requiredManifest2 = new ManifestBuilder() + .setId("required-manifest-two") + .addRequires("required-manifest-three") + .build(); + mockManifest(resolver, requiredManifest2, "test.channels:required-manifest-two:1.0.0"); + + final ChannelManifest requiredManifest3 = new ManifestBuilder() + .setId("required-manifest-three") + .addRequires("required-manifest-two") + .build(); + mockManifest(resolver, requiredManifest3, "test.channels:required-manifest-three:1.0.0"); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("base-manifest") + .addRequires("required-manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel two") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest-two", "1.0.0") + .build(), + new ChannelBuilder() + .setName("required channel three") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest-three", "1.0.0") + .build() + ); + assertThrows(CyclicDependencyException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void duplicatedManifestIDsAreDetected() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest requiredManifest = new ManifestBuilder() + .setId("manifest-one") + .build(); + mockManifest(resolver, requiredManifest, "test.channels:required-manifest:1.0.0"); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new ChannelBuilder() + .setName("channel one") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .build(), + new ChannelBuilder() + .setName("channel two") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "required-manifest", "1.0.0") + .build() + ); + assertThrows(RuntimeException.class, () -> new ChannelSession(channels, factory)); + } + + private void mockManifest(MavenVersionsResolver resolver, ChannelManifest manifest, String gav) throws IOException { + mockManifest(resolver, ChannelManifestMapper.toYaml(manifest), gav); + } + + private void mockManifest(MavenVersionsResolver resolver, String manifest, String gav) throws IOException { + Path manifestFile = tempDir.resolve("manifest_" + RandomUtils.nextInt() + ".yaml"); + Files.writeString(manifestFile, manifest); + + mockManifest(resolver, manifestFile.toUri().toURL(), gav); + } + + private void mockManifest(MavenVersionsResolver resolver, URL manifestUrl, String gavString) throws IOException { + final String[] splitGav = gavString.split(":"); + final MavenCoordinate gav = new MavenCoordinate(splitGav[0], splitGav[1], splitGav.length == 3 ? splitGav[2] : null); + when(resolver.resolveChannelMetadata(eq(List.of(ChannelManifestCoordinate.create(null, gav))))) + .thenReturn(List.of(manifestUrl)); + } +} diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java index af90164d..10d24d2a 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; @@ -31,40 +32,47 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.wildfly.channel.ChannelMapper.CURRENT_SCHEMA_VERSION; +import static org.wildfly.channel.ChannelManifestMapper.CURRENT_SCHEMA_VERSION; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.wildfly.channel.spi.MavenVersionsResolver; public class ChannelSessionTestCase { + @TempDir + private Path tempDir; + @Test - public void testFindLatestMavenArtifactVersion() throws UnresolvedMavenArtifactException { - List channels = ChannelMapper.fromString( + public void testFindLatestMavenArtifactVersion() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.wildfly\n" + " artifactId: '*'\n" + - " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - assertNotNull(channels); - assertEquals(1, channels.size()); + " versionPattern: '25\\.\\d+\\.\\d+.Final'"; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(singleton("25.0.0.Final")); + final List channels = mockChannel(resolver, tempDir, manifest); + try (ChannelSession session = new ChannelSession(channels, factory)) { String version = session.findLatestMavenArtifactVersion("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); assertEquals("25.0.0.Final", version); @@ -73,22 +81,42 @@ public void testFindLatestMavenArtifactVersion() throws UnresolvedMavenArtifactE verify(resolver, times(2)).close(); } + public static List mockChannel(MavenVersionsResolver resolver, Path tempDir, String... manifests) throws IOException { + return mockChannel(resolver, tempDir, null, manifests); + } + + public static List mockChannel(MavenVersionsResolver resolver, Path tempDir, Channel.NoStreamStrategy strategy, String... manifests) throws IOException { + List channels = new ArrayList<>(); + for (int i = 0; i < manifests.length; i++) { + channels.add(new ChannelBuilder() + .setManifestCoordinate("org.channels", "channel" + i, "1.0.0") + .setResolveStrategy(strategy) + .build()); + String manifest = manifests[i]; + Path manifestFile = Files.writeString(tempDir.resolve("manifest" + i +".yaml"), manifest); + + when(resolver.resolveChannelMetadata(eq(List.of(new ChannelManifestCoordinate("org.channels", "channel" + i, "1.0.0"))))) + .thenReturn(List.of(manifestFile.toUri().toURL())); + } + return channels; + } + @Test - public void testFindLatestMavenArtifactVersionThrowsUnresolvedMavenArtifactException() throws UnresolvedMavenArtifactException { - List channels = ChannelMapper.fromString( "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testFindLatestMavenArtifactVersionThrowsUnresolvedMavenArtifactException() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.wildfly\n" + " artifactId: '*'\n" + - " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - assertNotNull(channels); - assertEquals(1, channels.size()); + " versionPattern: '25\\.\\d+\\.\\d+.Final'"; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(singleton("26.0.0.Final")); + final List channels = mockChannel(resolver, tempDir, manifest); + try (ChannelSession session = new ChannelSession(channels, factory)) { try { session.findLatestMavenArtifactVersion("org.wildfly", "wildfly-ee-galleon-pack", null, null, "26.0.0.Final"); @@ -102,23 +130,23 @@ public void testFindLatestMavenArtifactVersionThrowsUnresolvedMavenArtifactExcep } @Test - public void testResolveLatestMavenArtifact() throws UnresolvedMavenArtifactException { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testResolveLatestMavenArtifact() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.wildfly\n" + " artifactId: '*'\n" + - " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - assertNotNull(channels); - assertEquals(1, channels.size()); + " versionPattern: '25\\.\\d+\\.\\d+.Final'"; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); File resolvedArtifactFile = mock(File.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(Set.of("25.0.0.Final", "25.0.1.Final")); when(resolver.resolveArtifact("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.1.Final")).thenReturn(resolvedArtifactFile); + final List channels = mockChannel(resolver, tempDir, manifest); + try (ChannelSession session = new ChannelSession(channels, factory)) { MavenArtifact artifact = session.resolveMavenArtifact("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); @@ -136,22 +164,22 @@ public void testResolveLatestMavenArtifact() throws UnresolvedMavenArtifactExcep } @Test - public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException() { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "schemaVersion: 1.0.0\n" + "streams:\n" + " - groupId: org.wildfly\n" + " artifactId: '*'\n" + - " versionPattern: '25\\.\\d+\\.\\d+.Final'"); - assertNotNull(channels); - assertEquals(1, channels.size()); + " versionPattern: '25\\.\\d+\\.\\d+.Final'"; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(singleton("26.0.0.Final")); + final List channels = mockChannel(resolver, tempDir, manifest); + try (ChannelSession session = new ChannelSession(channels, factory)) { try { session.resolveMavenArtifact("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); @@ -165,22 +193,22 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( } @Test - public void testResolveDirectMavenArtifact() throws UnresolvedMavenArtifactException { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testResolveDirectMavenArtifact() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.foo\n" + " artifactId: foo\n" + - " version: \"25.0.0.Final\""); - assertNotNull(channels); - assertEquals(1, channels.size()); + " version: \"25.0.0.Final\""; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); File resolvedArtifactFile = mock(File.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); when(resolver.resolveArtifact("org.bar", "bar", null, null, "1.0.0.Final")).thenReturn(resolvedArtifactFile); + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.NONE, manifest); + try (ChannelSession session = new ChannelSession(channels, factory)) { Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { @@ -203,31 +231,30 @@ public void testResolveDirectMavenArtifact() throws UnresolvedMavenArtifactExcep } @Test - public void testResolveMavenArtifactsFromOneChannel() throws UnresolvedMavenArtifactException { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testResolveMavenArtifactsFromOneChannel() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.foo\n" + " artifactId: foo\n" + " version: \"25.0.0.Final\"\n" + " - groupId: org.bar\n" + " artifactId: bar\n" + - " version: \"26.0.0.Final\"" - ); - assertNotNull(channels); - assertEquals(1, channels.size()); + " version: \"26.0.0.Final\""; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); File resolvedArtifactFile1 = mock(File.class); File resolvedArtifactFile2 = mock(File.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); final List coordinates = asList( new ArtifactCoordinate("org.foo", "foo", null, null, "1.0.0"), new ArtifactCoordinate("org.bar", "bar", null, null, "1.0.0")); when(resolver.resolveArtifacts(argThat(mavenCoordinates -> mavenCoordinates.size() == 2))) .thenReturn(asList(resolvedArtifactFile1, resolvedArtifactFile2)); + final List channels = mockChannel(resolver, tempDir, manifest); + try (ChannelSession session = new ChannelSession(channels, factory)) { List resolved = session.resolveMavenArtifacts(coordinates); @@ -251,28 +278,24 @@ public void testResolveMavenArtifactsFromOneChannel() throws UnresolvedMavenArti } @Test - public void testResolveMavenArtifactsFromTwoChannel() throws UnresolvedMavenArtifactException { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testResolveMavenArtifactsFromTwoChannel() throws Exception { + String manifest1 = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.foo\n" + " artifactId: foo\n" + - " version: \"25.0.0.Final\"\n" + - "---\n" + - "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + " version: \"25.0.0.Final\"\n"; + String manifest2 = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.bar\n" + " artifactId: bar\n" + - " version: \"26.0.0.Final\"" - ); - assertNotNull(channels); - assertEquals(2, channels.size()); + " version: \"26.0.0.Final\""; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); File resolvedArtifactFile1 = mock(File.class); File resolvedArtifactFile2 = mock(File.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); final List coordinates = asList( new ArtifactCoordinate("org.foo", "foo", null, null, "1.0.0"), new ArtifactCoordinate("org.bar", "bar", null, null, "1.0.0")); @@ -292,6 +315,8 @@ public List answer(InvocationOnMock invocationOnMock) throws Throwable { } }); + final List channels = mockChannel(resolver, tempDir, manifest1, manifest2); + try (ChannelSession session = new ChannelSession(channels, factory)) { List resolved = session.resolveMavenArtifacts(coordinates); @@ -315,27 +340,27 @@ public List answer(InvocationOnMock invocationOnMock) throws Throwable { } @Test - public void testResolveDirectMavenArtifacts() throws UnresolvedMavenArtifactException { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testResolveDirectMavenArtifacts() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.not\n" + " artifactId: used\n" + - " version: \"1.0.0.Final\""); - assertNotNull(channels); - assertEquals(1, channels.size()); + " version: \"1.0.0.Final\""; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); File resolvedArtifactFile1 = mock(File.class); File resolvedArtifactFile2 = mock(File.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); final List coordinates = asList( new ArtifactCoordinate("org.foo", "foo", null, null, "25.0.0.Final"), new ArtifactCoordinate("org.bar", "bar", null, null, "26.0.0.Final")); when(resolver.resolveArtifacts(argThat(mavenCoordinates -> mavenCoordinates.size() == 2))) .thenReturn(asList(resolvedArtifactFile1, resolvedArtifactFile2)); + final List channels = mockChannel(resolver, tempDir, manifest); + try (ChannelSession session = new ChannelSession(channels, factory)) { List resolved = session.resolveDirectMavenArtifacts(coordinates); @@ -359,42 +384,264 @@ public void testResolveDirectMavenArtifacts() throws UnresolvedMavenArtifactExce } @Test - public void testResolveMavenArtifactsFromTwoChannelsWithSameStream() throws UnresolvedMavenArtifactException { - Channel channel1 = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + public void testResolveMavenArtifactsFromTwoChannelsWithSameStream() throws Exception { + String manifest1 = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.foo\n" + " artifactId: foo\n" + - " version: \"25.0.0.Final\"" - ).get(0); - assertNotNull(channel1); - Channel channel2 = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + " version: \"25.0.0.Final\""; + String manifest2 = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + "streams:\n" + " - groupId: org.foo\n" + " artifactId: foo\n" + - " version: \"26.0.0.Final\"" - ).get(0); - assertNotNull(channel2); + " version: \"26.0.0.Final\""; MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); File resolvedArtifactFile = mock(File.class); - when(factory.create()).thenReturn(resolver); + when(factory.create(any())).thenReturn(resolver); when(resolver.resolveArtifact(eq("org.foo"), eq("foo"), eq(null), eq(null), anyString())).thenReturn(resolvedArtifactFile); + final List channels = mockChannel(resolver, tempDir, manifest1, manifest2); + // channel order does not matter to determine the latest version - try (ChannelSession session = new ChannelSession(asList(channel1, channel2), factory)) { + try (ChannelSession session = new ChannelSession(channels, factory)) { MavenArtifact resolvedArtifact = session.resolveMavenArtifact("org.foo", "foo", null, null, "1.0.0.Final"); assertNotNull(resolvedArtifact); assertEquals("26.0.0.Final", resolvedArtifact.getVersion()); } - try (ChannelSession session = new ChannelSession(asList(channel2, channel1), factory)) { + + Collections.reverse(channels); + try (ChannelSession session = new ChannelSession(channels, factory)) { MavenArtifact resolvedArtifact = session.resolveMavenArtifact("org.foo", "foo", null, null, "1.0.0.Final"); assertNotNull(resolvedArtifact); assertEquals("26.0.0.Final", resolvedArtifact.getVersion()); } } + @Test + public void testChannelWithOriginalStrategy() throws Exception { + String manifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + File resolvedArtifactFile = mock(File.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.ORIGINAL, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.resolveArtifact(eq("org.foo"), eq("bar"), eq(null), eq(null), eq("1.0.0.Final"))).thenReturn(resolvedArtifactFile); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + MavenArtifact resolvedArtifact = session.resolveMavenArtifact("org.foo", "bar", null, null, "1.0.0.Final"); + assertNotNull(resolvedArtifact); + assertEquals("1.0.0.Final", resolvedArtifact.getVersion()); + } + } + + @Test + public void testChannelWithOriginalStrategyWithoutBaseVersion() throws Exception { + String manifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + File resolvedArtifactFile = mock(File.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.ORIGINAL, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.resolveArtifact(eq("org.foo"), eq("bar"), eq(null), eq(null), eq("1.0.0.Final"))) + .thenReturn(resolvedArtifactFile); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + assertThrows(UnresolvedMavenArtifactException.class, ()-> + session.resolveMavenArtifact("org.foo", "bar", null, null, null) + ); + + } + } + + @Test + public void testChannelWithLatestStrategy() throws Exception { + String manifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + File resolvedArtifactFile = mock(File.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.LATEST, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.resolveArtifact(eq("org.foo"), eq("bar"), eq(null), eq(null), eq("25.0.2.Final"))).thenReturn(resolvedArtifactFile); + when(resolver.getAllVersions("org.foo", "bar", null, null)).thenReturn(Set.of("25.0.2.Final", "25.0.1.Final", "25.0.0.Final")); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + MavenArtifact resolvedArtifact = session.resolveMavenArtifact("org.foo", "bar", null, null, "25.0.1.Final"); + assertNotNull(resolvedArtifact); + assertEquals("25.0.2.Final", resolvedArtifact.getVersion()); + } + } + + @Test + public void testChannelWithLatestStrategyNoArtifact() throws Exception { + String manifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.LATEST, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.foo", "bar", null, null)).thenReturn(Set.of()); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> + session.resolveMavenArtifact("org.foo", "bar", null, null, "25.0.1.Final") + ); + } + } + + @Test + public void testChannelWithLatestStrategyWithVersionPattern() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: bar\n" + + " versionPattern: \".*Final\""; + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.LATEST, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.foo", "bar", null, null)).thenReturn(Set.of("1.0.0")); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> + session.resolveMavenArtifact("org.foo", "bar", null, null, "25.0.1.Final") + ); + } + } + + @Test + public void testChannelWithMavenReleaseStrategy() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + File resolvedArtifactFile = mock(File.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.MAVEN_RELEASE, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getMetadataReleaseVersion(eq("org.foo"), eq("bar"))).thenReturn("25.0.1.Final"); + when(resolver.resolveArtifact(eq("org.foo"), eq("bar"), eq(null), eq(null), eq("25.0.1.Final"))).thenReturn(resolvedArtifactFile); + when(resolver.getAllVersions("org.foo", "bar", null, null)).thenReturn(Set.of("25.0.1.Final", "25.0.0.Final")); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + MavenArtifact resolvedArtifact = session.resolveMavenArtifact("org.foo", "bar", null, null, "25.0.1.Final"); + assertEquals("25.0.1.Final", resolvedArtifact.getVersion()); + } + } + + @Test + public void testChannelWithMavenLatestStrategy() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + File resolvedArtifactFile = mock(File.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.MAVEN_LATEST, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getMetadataLatestVersion(eq("org.foo"), eq("bar"))).thenReturn("25.0.1.Final"); + when(resolver.resolveArtifact(eq("org.foo"), eq("bar"), eq(null), eq(null), eq("25.0.1.Final"))).thenReturn(resolvedArtifactFile); + when(resolver.getAllVersions("org.foo", "bar", null, null)).thenReturn(Set.of("25.0.1.Final", "25.0.0.Final")); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + MavenArtifact resolvedArtifact = session.resolveMavenArtifact("org.foo", "bar", null, null, "25.0.1.Final"); + assertEquals("25.0.1.Final", resolvedArtifact.getVersion()); + } + } + + @Test + public void testChannelWithStrictStrategy() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + File resolvedArtifactFile = mock(File.class); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.NONE, manifest); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.resolveArtifact(eq("org.foo"), eq("bar"), eq(null), eq(null), eq("25.0.1.Final"))).thenReturn(resolvedArtifactFile); + when(resolver.getAllVersions("org.foo", "bar", null, null)).thenReturn(Set.of("25.0.1.Final", "25.0.0.Final")); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> + session.resolveMavenArtifact("org.foo", "bar", null, null, "25.0.1.Final") + ); + } + } + + @Test + public void testChannelWithDefaultStrategy() throws Exception { + String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.foo\n" + + " artifactId: foo\n" + + " version: \"25.0.0.Final\""; + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + File resolvedArtifactFile = mock(File.class); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.resolveArtifact(eq("org.foo"), eq("bar"), eq(null), eq(null), eq("1.0.0.Final"))).thenReturn(resolvedArtifactFile); + + final List channels = mockChannel(resolver, tempDir, Channel.NoStreamStrategy.ORIGINAL, manifest); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + MavenArtifact resolvedArtifact = session.resolveMavenArtifact("org.foo", "bar", null, null, "1.0.0.Final"); + assertNotNull(resolvedArtifact); + assertEquals("1.0.0.Final", resolvedArtifact.getVersion()); + } + } + private static void assertContainsAll(List expected, List actual) { List testList = new ArrayList<>(expected); diff --git a/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java new file mode 100644 index 00000000..9497af9e --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java @@ -0,0 +1,518 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.wildfly.channel.spi.MavenVersionsResolver; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.wildfly.channel.ChannelMapper.CURRENT_SCHEMA_VERSION; + +public class ChannelWithBlocklistTestCase { + + @TempDir + private Path tempDir; + + @Test + public void testFindLatestMavenArtifactVersion() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: wildfly-ee-galleon-pack\n" + + " versionPattern: .*"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) + .thenReturn(new HashSet<>(Arrays.asList("25.0.0.Final", "25.0.1.Final"))); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + String version = session.findLatestMavenArtifactVersion("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); + assertEquals("25.0.0.Final", version); + } + + verify(resolver, times(2)).close(); + } + + @Test + public void testFindLatestMavenArtifactVersionBlocklistDoesntExist() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: '*'\n" + + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(Collections.emptyList()); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-blocklist", + BlocklistCoordinate.EXTENSION, BlocklistCoordinate.CLASSIFIER)).thenReturn(Collections.emptySet()); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) + .thenReturn(new HashSet<>(Arrays.asList("25.0.0.Final", "25.0.1.Final"))); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + String version = session.findLatestMavenArtifactVersion("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); + assertEquals("25.0.1.Final", version); + } + + verify(resolver, times(2)).close(); + } + + @Test + public void testFindLatestMavenArtifactVersionWithWildcardBlocklist() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: '*'\n" + + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist-with-wildcards.yaml"))); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) + .thenReturn(new HashSet<>(Arrays.asList("25.0.0.Final", "25.0.1.Final"))); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + String version = session.findLatestMavenArtifactVersion("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); + assertEquals("25.0.0.Final", version); + } + + verify(resolver, times(2)).close(); + } + + @Test + public void testFindLatestMavenArtifactVersionBlocklistsAllVersionsException() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: '*'\n" + + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(new HashSet<>(singleton("25.0.1.Final"))); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + try { + session.findLatestMavenArtifactVersion("org.wildfly", "wildfly-ee-galleon-pack", null, null, "26.0.0.Final"); + fail("Must throw a UnresolvedMavenArtifactException"); + } catch (UnresolvedMavenArtifactException e) { + // pass + } + } + + verify(resolver, times(2)).close(); + } + + @Test + public void testResolveLatestMavenArtifact() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: '*'\n" + + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); + + File resolvedArtifactFile = mock(File.class); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(new HashSet<>(Set.of("25.0.0.Final", "25.0.1.Final"))); + when(resolver.resolveArtifact("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final")).thenReturn(resolvedArtifactFile); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + + MavenArtifact artifact = session.resolveMavenArtifact("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); + assertNotNull(artifact); + + assertEquals("org.wildfly", artifact.getGroupId()); + assertEquals("wildfly-ee-galleon-pack", artifact.getArtifactId()); + assertNull(artifact.getExtension()); + assertNull(artifact.getClassifier()); + assertEquals("25.0.0.Final", artifact.getVersion()); + assertEquals(resolvedArtifactFile, artifact.getFile()); + } + + verify(resolver, times(2)).close(); + } + + @Test + public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: '*'\n" + + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(new HashSet<>(Set.of("25.0.1.Final","26.0.0.Final"))); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + try { + session.resolveMavenArtifact("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); + fail("Must throw a UnresolvedMavenArtifactException"); + } catch (UnresolvedMavenArtifactException e) { + // pass + } + } + + verify(resolver, times(2)).close(); + } + + @Test + public void testResolveMavenArtifactsFromOneChannel() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: wildfly-ee-galleon-pack\n" + + " versionPattern: \".*\"\n" + + " - groupId: org.wildfly\n" + + " artifactId: wildfly-cli\n" + + " version: \"26.0.0.Final\""); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); + + File resolvedArtifactFile1 = mock(File.class); + File resolvedArtifactFile2 = mock(File.class); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)).thenReturn(new HashSet<>(Set.of("25.0.1.Final","25.0.0.Final"))); + final List coordinates = asList( + new ArtifactCoordinate("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"), + new ArtifactCoordinate("org.wildfly", "wildfly-cli", null, null, "26.0.0.Final")); + when(resolver.resolveArtifacts(argThat(mavenCoordinates -> mavenCoordinates.size() == 2))) + .thenReturn(asList(resolvedArtifactFile1, resolvedArtifactFile2)); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + + List resolved = session.resolveMavenArtifacts(coordinates); + assertNotNull(resolved); + + final List expected = asList( + new MavenArtifact("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final", resolvedArtifactFile1), + new MavenArtifact("org.wildfly", "wildfly-cli", null, null, "26.0.0.Final", resolvedArtifactFile2) + ); + assertContainsAll(expected, resolved); + + Optional stream = session.getRecordedChannel().findStreamFor("org.wildfly", "wildfly-ee-galleon-pack"); + assertTrue(stream.isPresent()); + assertEquals("25.0.0.Final", stream.get().getVersion()); + stream = session.getRecordedChannel().findStreamFor("org.wildfly", "wildfly-cli"); + assertTrue(stream.isPresent()); + assertEquals("26.0.0.Final", stream.get().getVersion()); + } + + verify(resolver, times(2)).close(); + } + + @Test + public void testFindLatestMavenArtifactVersionInRequiredChannel() throws Exception { + List channels =List.of( + new ChannelBuilder() + .setManifestCoordinate("org.test", "base-manifest", "1.0.0") + .addRepository("test", "test") + .build(), + new ChannelBuilder() + .setManifestCoordinate("org.test", "required-manifest", "1.0.0") + .addRepository("test", "test") + .setBlocklist("org.wildfly", "wildfly-blocklist", "1.2.3") + .build()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("required-manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "requires:\n" + + " - id: required-channel\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: wildfly-ee-galleon-pack\n" + + " versionPattern: \".*\""); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("org.test", "required-manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("required-manifest.yaml").toUri().toURL())); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "id: required-channel\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: wildfly-ee-galleon-pack\n" + + " versionPattern: \".*\""); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("org.test", "base-manifest", "1.0.0")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist", "1.2.3")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); + + when(factory.create(any())).thenReturn(resolver); + when(resolver.getAllVersions("org.wildfly", "wildfly-ee-galleon-pack", null, null)) + .thenReturn(new HashSet<>(Arrays.asList("25.0.0.Final", "25.0.1.Final"))); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + String version = session.findLatestMavenArtifactVersion("org.wildfly", "wildfly-ee-galleon-pack", null, null, "25.0.0.Final"); + assertEquals("25.0.0.Final", version); + } + + verify(resolver, times(3)).close(); + } + + @Test + public void testChannelWithInvalidBlacklist() throws Exception { + List channels = ChannelMapper.fromString( + "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "blocklist:\n" + + " maven:\n" + + " groupId: org.wildfly\n" + + " artifactId: wildfly-blocklist\n" + + "manifest:\n" + + " maven:\n" + + " groupId: test\n" + + " artifactId: 'test.manifest'\n" + + "repositories:\n" + + " - id: test\n" + + " url: http://test.te"); + assertNotNull(channels); + assertEquals(1, channels.size()); + + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + Files.writeString(tempDir.resolve("manifest.yaml"), + "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: '*'\n" + + " versionPattern: '25\\.\\d+\\.\\d+.Final'"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test", "test.manifest")))) + .thenReturn(List.of(tempDir.resolve("manifest.yaml").toUri().toURL())); + + when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) + .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/invalid-blocklist.yaml"))); + + when(factory.create(any())).thenReturn(resolver); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + fail("InvalidChannelException should have been thrown."); + } catch (InvalidChannelMetadataException e) { + assertEquals(1, e.getValidationMessages().size()); + assertTrue(e.getValidationMessages().get(0).contains("versions: is missing"), e.getValidationMessages().get(0)); + } + } + + private static void assertContainsAll(List expected, List actual) { + List testList = new ArrayList<>(expected); + for (MavenArtifact a : actual) { + if (!expected.contains(a)) { + fail("Unexpected artifact " + a); + } + testList.remove(a); + } + if (!testList.isEmpty()) { + fail("Expected artifact not found " + expected.get(0)); + } + } +} diff --git a/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java index c95d9ccc..41ff05eb 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java @@ -19,54 +19,75 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.wildfly.channel.ChannelMapper.CURRENT_SCHEMA_VERSION; import java.io.File; -import java.net.URISyntaxException; +import java.io.IOException; import java.net.URL; -import java.nio.file.Paths; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Set; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.wildfly.channel.spi.MavenVersionsResolver; public class ChannelWithRequirementsTestCase { + @TempDir + private Path tempDir; + /** * Test that newest version of required channel is used when required channel version is not specified */ @Test - public void testChannelWhichRequiresAnotherChannel() throws UnresolvedMavenArtifactException, URISyntaxException { + public void testChannelWhichRequiresAnotherChannel() throws Exception { MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); // create a Mock MavenVersionsResolver that will resolve the required channel MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - URL resolvedRequiredChannelURL = tccl.getResource("channels/required-channel.yaml"); - File resolvedRequiredChannelFile = Paths.get(resolvedRequiredChannelURL.toURI()).toFile(); File resolvedArtifactFile = mock(File.class); - when(factory.create()) + URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); + + when(factory.create(any())) .thenReturn(resolver); - when(resolver.getAllVersions("org.foo", "required-channel", "yaml", "channel")) + when(resolver.getAllVersions("test.channels", "required-manifest", "yaml", "manifest")) .thenReturn(Set.of("1", "2", "3")); - when(resolver.resolveArtifact("org.foo", "required-channel", "yaml", "channel", "3")) - .thenReturn(resolvedRequiredChannelFile); - when(resolver.resolveArtifact("org.foo", "required-channel", "yaml", "channel", "1.2.0.Final")) + when(resolver.resolveArtifact("org.example", "required-manifest", "yaml", "manifest", "3")) .thenReturn(resolvedArtifactFile); when(resolver.getAllVersions("org.example", "foo-bar", null, null)) .thenReturn(Set.of("1.0.0.Final, 1.1.0.Final", "1.2.0.Final")); when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) .thenReturn(resolvedArtifactFile); + when(resolver.resolveChannelMetadata(any())).thenReturn(List.of(resolvedRequiredManifestURL)); - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "name: My Channel\n" + + String baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: My manifest\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel"); + " - id: required-manifest-one\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest\n"; + mockManifest(resolver, baseManifest, "org.channels:base-manifest:1.0.0"); + + List channels = ChannelMapper.fromString("schemaVersion: " + ChannelMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: My Channel\n" + + "manifest:\n" + + " maven:\n" + + " groupId: org.channels\n" + + " artifactId: base-manifest\n" + + " version: 1.0.0\n" + + "repositories:\n" + + " - id: test\n" + + " url: test"); assertEquals(1, channels.size()); try (ChannelSession session = new ChannelSession(channels, factory)) { @@ -86,29 +107,42 @@ public void testChannelWhichRequiresAnotherChannel() throws UnresolvedMavenArtif * Test that specific version of required channel is used when required */ @Test - public void testChannelWhichRequiresAnotherVersionedChannel() throws UnresolvedMavenArtifactException, URISyntaxException { + public void testChannelWhichRequiresAnotherVersionedChannel() throws Exception { MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - URL resolvedRequiredChannelURL = tccl.getResource("channels/required-channel.yaml"); - File resolvedRequiredChannelFile = Paths.get(resolvedRequiredChannelURL.toURI()).toFile(); File resolvedArtifactFile = mock(File.class); - when(factory.create()) + URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); + + when(factory.create(any())) .thenReturn(resolver); - when(resolver.resolveArtifact("org.foo", "required-channel", "yaml", "channel", "2.0.0.Final")) - .thenReturn(resolvedRequiredChannelFile); when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) .thenReturn(resolvedArtifactFile); + when(resolver.resolveChannelMetadata(any())).thenReturn(List.of(resolvedRequiredManifestURL)); - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "name: My Channel\n" + + String baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: My manifest\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel\n" + - " version: 2.0.0.Final"); + " - id: required-manifest-one\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest\n" + + " version: 1.0.0"; + mockManifest(resolver, baseManifest, "org.channels:base-manifest:1.0.0"); + + List channels = ChannelMapper.fromString("schemaVersion: " + ChannelMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: My Channel\n" + + "manifest:\n" + + " maven:\n" + + " groupId: org.channels\n" + + " artifactId: base-manifest\n" + + " version: 1.0.0\n" + + "repositories:\n" + + " - id: test\n" + + " url: test"); assertEquals(1, channels.size()); try (ChannelSession session = new ChannelSession(channels, factory)) { @@ -133,22 +167,19 @@ public void testChannelWhichRequiresAnotherVersionedChannel() throws UnresolvedM * Then requiring channel version then MUST be used in resolution. */ @Test - public void testRequiringChannelOverridesStreamFromRequiredChannel() throws UnresolvedMavenArtifactException, URISyntaxException { + public void testRequiringChannelOverridesStreamFromRequiredChannel() throws Exception { MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - URL resolvedRequiredChannelURL = tccl.getResource("channels/required-channel.yaml"); - File resolvedRequiredChannelFile = Paths.get(resolvedRequiredChannelURL.toURI()).toFile(); + URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); File resolvedArtifactFile120Final = mock(File.class); File resolvedArtifactFile200Final = mock(File.class); File resolvedArtifactFile100Final = mock(File.class); - when(factory.create()) + when(factory.create(any())) .thenReturn(resolver); - when(resolver.resolveArtifact("org.foo", "required-channel", "yaml", "channel", "2.0.0.Final")) - .thenReturn(resolvedRequiredChannelFile); // There are 2 version of foo-bar when(resolver.getAllVersions("org.example", "foo-bar", null, null)) .thenReturn(Set.of("1.0.0.Final", "1.2.0.Final", "2.0.0.Final")); @@ -158,19 +189,36 @@ public void testRequiringChannelOverridesStreamFromRequiredChannel() throws Unre .thenReturn(resolvedArtifactFile120Final); when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "2.0.0.Final")) .thenReturn(resolvedArtifactFile200Final); + when(resolver.resolveChannelMetadata(eq(List.of(new ChannelManifestCoordinate("test.channels", "required-manifest", "1.0.0"))))) + .thenReturn(List.of(resolvedRequiredManifestURL)); // The requiring channel requires newer version of foo-bar artifact List channels = ChannelMapper.fromString( - "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "schemaVersion: " + ChannelMapper.CURRENT_SCHEMA_VERSION + "\n" + "name: My Channel\n" + + "manifest:\n" + + " maven:\n" + + " groupId: org.channels\n" + + " artifactId: base-manifest\n" + + " version: 1.0.0\n" + + "repositories:\n" + + "- id: test\n" + + " url: test-repository"); + + String baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: My manifest\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel\n" + - " version: 2.0.0.Final\n" + + " - id: required-manifest-one\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest\n" + + " version: 1.0.0\n" + "streams:\n" + " - groupId: org.example\n" + " artifactId: foo-bar\n" + - " version: 2.0.0.Final"); + " version: 2.0.0.Final"; + + mockManifest(resolver, baseManifest, "org.channels:base-manifest:1.0.0"); assertEquals(1, channels.size()); @@ -188,19 +236,20 @@ public void testRequiringChannelOverridesStreamFromRequiredChannel() throws Unre // The requiring channel requires older version of foo-bar artifact. - channels = ChannelMapper.fromString( - "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + "name: My Channel\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel\n" + - " version: 2.0.0.Final\n" + + " - id: required-manifest-one\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest\n" + + " version: 1.0.0\n" + "streams:\n" + " - groupId: org.example\n" + " artifactId: foo-bar\n" + - " version: 1.0.0.Final"); + " version: 1.0.0.Final"; - assertEquals(1, channels.size()); + mockManifest(resolver, baseManifest, "org.channels:base-manifest:1.0.0"); try (ChannelSession session = new ChannelSession(channels, factory)) { MavenArtifact artifact = session.resolveMavenArtifact("org.example", "foo-bar", null, null, "0"); @@ -217,19 +266,19 @@ public void testRequiringChannelOverridesStreamFromRequiredChannel() throws Unre // The requiring channel specifies wildcard for version, newest version should be used // the newest version is 2.0.0.Final - channels = ChannelMapper.fromString( - "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + "name: My Channel\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel\n" + - " version: 2.0.0.Final\n" + + " - id: required-manifest-one\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest\n" + + " version: 1.0.0\n" + "streams:\n" + " - groupId: org.example\n" + " artifactId: foo-bar\n" + - " versionPattern: '.*'"); - - assertEquals(1, channels.size()); + " versionPattern: '.*'"; + mockManifest(resolver, baseManifest, "org.channels:base-manifest:1.0.0"); try (ChannelSession session = new ChannelSession(channels, factory)) { MavenArtifact artifact = session.resolveMavenArtifact("org.example", "foo-bar", null, null, "0"); @@ -248,22 +297,19 @@ public void testRequiringChannelOverridesStreamFromRequiredChannel() throws Unre * Test that nested requiring channels stream inheritance */ @Test - public void testChannelRequirementNesting() throws UnresolvedMavenArtifactException, URISyntaxException { + public void testChannelRequirementNesting() throws Exception { MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - URL resolvedRequiredChannelURL = tccl.getResource("channels/required-channel.yaml"); - File resolvedRequiredChannelFile = Paths.get(resolvedRequiredChannelURL.toURI()).toFile(); - URL resolved2ndLevelRequiringChannelURL = tccl.getResource("channels/2nd-level-requiring-channel.yaml"); - File resolved2ndLevelRequiringChannelFile = Paths.get(resolved2ndLevelRequiringChannelURL.toURI()).toFile(); + URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); + + URL resolvedRequiredManifestURL2nd = tccl.getResource("channels/2nd-level-requiring-manifest.yaml"); - when(factory.create()) + when(factory.create(any())) .thenReturn(resolver); - when(resolver.resolveArtifact("org.foo", "required-channel", "yaml", "channel", "2.0.0.Final")) - .thenReturn(resolvedRequiredChannelFile); - when(resolver.resolveArtifact("org.foo", "2nd-level-requiring-channel", "yaml", "channel", "2.0.0.Final")) - .thenReturn(resolved2ndLevelRequiringChannelFile); + mockManifest(resolver, resolvedRequiredManifestURL, "test.channels:required-manifest:1.0.0"); + mockManifest(resolver, resolvedRequiredManifestURL2nd, "test.channels:required-2nd-level-manifest:1.0.0"); // There are: // 3 version of foo-bar // 2 versions of im-only-in-required-channel @@ -290,12 +336,21 @@ public void testChannelRequirementNesting() throws UnresolvedMavenArtifactExcept when(resolver.resolveArtifact("org.example", "im-only-in-second-level", null, null, "2.0.0.Final")) .thenReturn(mock(File.class)); - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "name: root level requiring channel\n"+ + String manifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: root level requiring manifest\n"+ "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: 2nd-level-requiring-channel\n" + - " version: 2.0.0.Final"); + " - id: 2nd-level\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-2nd-level-manifest\n" + + " version: 1.0.0"; + mockManifest(resolver, manifest, "org.channels:base-manifest:1.0.0"); + + List channels = List.of(new ChannelBuilder() + .setName("root level requiring channel") + .addRepository("test", "test") + .setManifestCoordinate("org.channels", "base-manifest", "1.0.0") + .build()); // check that streams from required channel propagate to root channel try (ChannelSession session = new ChannelSession(channels, factory)) { @@ -332,12 +387,14 @@ public void testChannelRequirementNesting() throws UnresolvedMavenArtifactExcept } // check that root level can override all streams from required channels - channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "name: root level requiring channel\n" + + manifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: root level requiring manifest\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: 2nd-level-requiring-channel\n" + - " version: 2.0.0.Final\n" + + " - id: 2nd-level\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-2nd-level-manifest\n" + + " version: 1.0.0\n" + "streams:\n" + " - groupId: org.example\n" + " artifactId: im-only-in-required-channel\n" + @@ -347,7 +404,8 @@ public void testChannelRequirementNesting() throws UnresolvedMavenArtifactExcept " version: 2.0.0.Final\n" + " - groupId: org.example\n" + " artifactId: im-only-in-second-level\n" + - " version: 2.0.0.Final"); + " version: 2.0.0.Final"; + mockManifest(resolver, manifest, "org.channels:base-manifest:1.0.0"); try (ChannelSession session = new ChannelSession(channels, factory)) { // foo-bar should get version from layer 2 @@ -388,22 +446,18 @@ public void testChannelRequirementNesting() throws UnresolvedMavenArtifactExcept * If multiple required channels define the same stream, newest defined version of the stream will be used */ @Test - public void testChannelMultipleRequirements() throws UnresolvedMavenArtifactException, URISyntaxException { + public void testChannelMultipleRequirements() throws Exception { MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - URL resolvedRequiredChannelURL = tccl.getResource("channels/required-channel.yaml"); - File resolvedRequiredChannelFile = Paths.get(resolvedRequiredChannelURL.toURI()).toFile(); - URL resolvedRequiredChannel2URL = tccl.getResource("channels/required-channel-2.yaml"); - File resolvedRequiredChannel2File = Paths.get(resolvedRequiredChannel2URL.toURI()).toFile(); - when(factory.create()) + URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); + + URL resolvedRequiredManifestURL2 = tccl.getResource("channels/required-manifest-2.yaml"); + + when(factory.create(any())) .thenReturn(resolver); - when(resolver.resolveArtifact("org.foo", "required-channel", "yaml", "channel", "2.0.0.Final")) - .thenReturn(resolvedRequiredChannelFile); - when(resolver.resolveArtifact("org.foo", "required-channel-2", "yaml", "channel", "2.0.0.Final")) - .thenReturn(resolvedRequiredChannel2File); when(resolver.getAllVersions("org.example", "foo-bar", null, null)) .thenReturn(Set.of("1.0.0.Final", "1.2.0.Final", "2.0.0.Final")); @@ -421,15 +475,34 @@ public void testChannelMultipleRequirements() throws UnresolvedMavenArtifactExce when(resolver.resolveArtifact("org.example", "im-only-in-required-channel", null, null, "2.0.0.Final")) .thenReturn(mock(File.class)); - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "name: root level requiring channel\n" + + mockManifest(resolver, resolvedRequiredManifestURL, "test.channels:required-manifest:1.0.0"); + mockManifest(resolver, resolvedRequiredManifestURL2, "test.channels:required-manifest-2:1.0.0"); + + String baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: My Channel\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel\n" + - " version: 2.0.0.Final\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel-2\n" + - " version: 2.0.0.Final"); + " - id: required-manifest-one\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest\n" + + " version: 1.0.0\n" + + " - id: required-manifest-two\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest-2\n" + + " version: 1.0.0\n"; + mockManifest(resolver, baseManifest, "org.channels:base-manifest:1.0.0"); + + List channels = ChannelMapper.fromString("schemaVersion: " + ChannelMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: root level requiring channel\n" + + "manifest:\n" + + " maven:\n" + + " groupId: org.channels\n" + + " artifactId: base-manifest\n" + + " version: 1.0.0\n" + + "repositories:\n" + + " - id: test\n" + + " url: test"); try (ChannelSession session = new ChannelSession(channels, factory)) { MavenArtifact artifact = session.resolveMavenArtifact("org.example", "foo-bar", null, null, "0"); @@ -452,16 +525,20 @@ public void testChannelMultipleRequirements() throws UnresolvedMavenArtifactExce assertEquals("1.0.0.Final", artifact.getVersion()); } - channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "name: root level requiring channel\n" + + baseManifest = "schemaVersion: " + ChannelManifestMapper.CURRENT_SCHEMA_VERSION + "\n" + + "name: My Channel\n" + "requires:\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel-2\n" + - " version: 2.0.0.Final\n" + - " - groupId: org.foo\n" + - " artifactId: required-channel\n" + - " version: 2.0.0.Final" - ); + " - id: required-manifest-two\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest-2\n" + + " version: 1.0.0\n" + + " - id: required-manifest-one\n" + + " maven:\n" + + " groupId: test.channels\n" + + " artifactId: required-manifest\n" + + " version: 1.0.0\n"; + mockManifest(resolver, baseManifest, "org.channels:base-manifest:1.0.0"); try (ChannelSession session = new ChannelSession(channels, factory)) { MavenArtifact artifact = session.resolveMavenArtifact("org.example", "foo-bar", null, null, "0"); @@ -484,4 +561,65 @@ public void testChannelMultipleRequirements() throws UnresolvedMavenArtifactExce assertEquals("1.0.0.Final", artifact.getVersion()); } } -} \ No newline at end of file + + private void mockManifest(MavenVersionsResolver resolver, String manifest, String gav) throws IOException { + Path manifestFile = tempDir.resolve("manifest_" + RandomUtils.nextInt() + ".yaml"); + Files.writeString(manifestFile, manifest); + + mockManifest(resolver, manifestFile.toUri().toURL(), gav); + } + + @Test + public void testRequiredChannelIgnoresNoStreamStrategy() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + // create a Mock MavenVersionsResolver that will resolve the required channel + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + + // the default strategy is ORIGINAL + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + File resolvedArtifactFile = mock(File.class); + + URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("org.test", "required-manifest", "1.0.0")))) + .thenReturn(List.of(resolvedRequiredManifestURL)); + + when(factory.create(any())) + .thenReturn(resolver); + when(resolver.getAllVersions("org.foo", "required-channel", "yaml", "channel")) + .thenReturn(Set.of("1", "2", "3")); + when(resolver.resolveArtifact("org.foo", "required-channel", "yaml", "channel", "1.2.0.Final")) + .thenReturn(resolvedArtifactFile); + when(resolver.getAllVersions("org.example", "foo-bar", null, null)) + .thenReturn(Set.of("1.0.0.Final, 1.1.0.Final", "1.2.0.Final")); + when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) + .thenReturn(resolvedArtifactFile); + + // strict NoStreamStrategy should result in no matches + List channels = List.of( + new ChannelBuilder() + .setResolveStrategy(Channel.NoStreamStrategy.NONE) + .setManifestCoordinate("org.test", "base-manifest", "1.0.0") + .addRepository("test", "test") + .build() + ); + + final ChannelManifest manifest = new ManifestBuilder() + .addRequires("required-manifest", "org.test", "required-manifest", "1.0.0") + .build(); + + mockManifest(resolver, ChannelManifestMapper.toYaml(manifest), "org.test:base-manifest:1.0.0"); + + try (ChannelSession session = new ChannelSession(channels, factory)) { + Assertions.assertThrows(UnresolvedMavenArtifactException.class, ()-> + session.resolveMavenArtifact("org.example", "idontexist", null, null, "1.2.3") + ); + } + } + + private void mockManifest(MavenVersionsResolver resolver, URL manifestUrl, String gavString) throws IOException { + final String[] splitGav = gavString.split(":"); + final MavenCoordinate gav = new MavenCoordinate(splitGav[0], splitGav[1], splitGav.length == 3 ? splitGav[2] : null); + when(resolver.resolveChannelMetadata(eq(List.of(ChannelManifestCoordinate.create(null, gav))))) + .thenReturn(List.of(manifestUrl)); + } +} diff --git a/core/src/test/java/org/wildfly/channel/ManifestBuilder.java b/core/src/test/java/org/wildfly/channel/ManifestBuilder.java new file mode 100644 index 00000000..01d2c44a --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/ManifestBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel; + +import java.util.ArrayList; +import java.util.List; + +class ManifestBuilder { + + private String id; + private List requirements = new ArrayList<>(); + private List streams = new ArrayList<>(); + + ChannelManifest build() { + return new ChannelManifest(null, id, null, requirements, streams); + } + + ManifestBuilder setId(String id) { + this.id = id; + return this; + } + + ManifestBuilder addRequires(String requiredId) { + requirements.add(new ManifestRequirement(requiredId, null)); + return this; + } + + ManifestBuilder addStream(String groupId, String artifactId, String version) { + streams.add(new Stream(groupId, artifactId, version)); + return this; + } + + public ManifestBuilder addRequires(String requiredId, String groupId, String artifactId, String version) { + requirements.add(new ManifestRequirement(requiredId, new MavenCoordinate(groupId, artifactId, version))); + return this; + } +} diff --git a/core/src/test/java/org/wildfly/channel/StreamResolverTestCase.java b/core/src/test/java/org/wildfly/channel/StreamResolverTestCase.java index 6a847f8e..50234ab9 100644 --- a/core/src/test/java/org/wildfly/channel/StreamResolverTestCase.java +++ b/core/src/test/java/org/wildfly/channel/StreamResolverTestCase.java @@ -19,7 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.wildfly.channel.ChannelMapper.CURRENT_SCHEMA_VERSION; +import static org.wildfly.channel.ChannelManifestMapper.CURRENT_SCHEMA_VERSION; import java.util.Optional; @@ -41,24 +41,24 @@ public void testFindingStreamMatchingArtifactIdAndGroupId() { " - groupId: io.undertow\n" + " artifactId: undertow-servlet\n" + " version: 3.0.2.Final"; - Channel channel = ChannelMapper.fromString(yamlContent).get(0); + ChannelManifest manifest = ChannelManifestMapper.fromString(yamlContent); - Optional stream = channel.findStreamFor("io.undertow", "undertow-core"); + Optional stream = manifest.findStreamFor("io.undertow", "undertow-core"); assertTrue(stream.isPresent()); assertEquals("io.undertow", stream.get().getGroupId()); assertEquals("undertow-core", stream.get().getArtifactId()); - stream = channel.findStreamFor("io.undertow", "undertow-servlet"); + stream = manifest.findStreamFor("io.undertow", "undertow-servlet"); assertTrue(stream.isPresent()); assertEquals("io.undertow", stream.get().getGroupId()); assertEquals("undertow-servlet", stream.get().getArtifactId()); - stream = channel.findStreamFor("io.undertow", "undertow-websockets-jsr"); + stream = manifest.findStreamFor("io.undertow", "undertow-websockets-jsr"); assertTrue(stream.isPresent()); assertEquals("io.undertow", stream.get().getGroupId()); assertEquals("*", stream.get().getArtifactId()); - stream = channel.findStreamFor("org.example", "foo"); + stream = manifest.findStreamFor("org.example", "foo"); assertFalse(stream.isPresent()); } diff --git a/core/src/test/java/org/wildfly/channel/mapping/ChannelManifestTestCase.java b/core/src/test/java/org/wildfly/channel/mapping/ChannelManifestTestCase.java new file mode 100644 index 00000000..f9173865 --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/mapping/ChannelManifestTestCase.java @@ -0,0 +1,118 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.channel.mapping; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestMapper; +import org.wildfly.channel.ManifestRequirement; +import org.wildfly.channel.Stream; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.wildfly.channel.ChannelManifestMapper.CURRENT_SCHEMA_VERSION; + +public class ChannelManifestTestCase { + + @Test + public void nonExistingManifestTest() { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + URL file = tccl.getResource("this-manifest-does-not-exist.yaml"); + Assertions.assertThrows(RuntimeException.class, () -> { + ChannelManifestMapper.from(file); + }); + } + + @Test() + public void emptyManifestTest() { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + URL file = tccl.getResource("channels/empty-channel.yaml"); + Assertions.assertThrows(RuntimeException.class, () -> { + ChannelManifestMapper.from(file); + }); + } + + @Test() + public void multipleManifestsTest() throws IOException { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + URL file = tccl.getResource("channels/multiple-manifests.yaml"); + + try (InputStream in = file.openStream()) + { + byte[] bytes = in.readAllBytes(); + String content = new String(bytes, Charset.defaultCharset()); + ChannelManifest manifest = ChannelManifestMapper.fromString(content); + assertEquals("Channel for WildFly 27", manifest.getName()); + } + } + + @Test + public void simpleManifestTest() throws MalformedURLException { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + URL file = tccl.getResource("channels/simple-manifest.yaml"); + + ChannelManifest manifest = ChannelManifestMapper.from(file); + + assertEquals("My Channel", manifest.getName()); + assertEquals("This is my manifest\n" + + "with my stuff", manifest.getDescription()); + + Collection streams = manifest.getStreams(); + assertEquals(1, streams.size()); + Stream stream = streams.iterator().next(); + assertEquals("org.wildfly", stream.getGroupId()); + assertEquals("wildfly-ee-galleon-pack", stream.getArtifactId()); + assertEquals("26.0.0.Final", stream.getVersion()); + } + + @Test + public void manifestWithoutStreams() { + ChannelManifest manifest = ChannelManifestMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "name: My Channel\n" + + "description: |-\n" + + " This is my manifest\n" + + " with no stream"); + + assertTrue(manifest.getStreams().isEmpty()); + } + + @Test + public void manifestWithRequires() { + ChannelManifest channelManifest = ChannelManifestMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + +"name: My Channel\n" + + "description: |-\n" + + " This is my manifest\n" + + "requires:\n" + + " - id: test\n" + + " maven:\n" + + " groupId: org.foo.channels\n" + + " artifactId: my-required-channel\n"); + + assertEquals(1, channelManifest.getManifestRequirements().size()); + ManifestRequirement requirement = channelManifest.getManifestRequirements().get(0); + assertEquals("org.foo.channels", requirement.getGroupId()); + assertEquals("my-required-channel", requirement.getArtifactId()); + } +} diff --git a/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java b/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java index fa86e4f6..938b03ee 100644 --- a/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java +++ b/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java @@ -18,23 +18,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.wildfly.channel.ChannelMapper.CURRENT_SCHEMA_VERSION; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; -import java.util.Collection; import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.wildfly.channel.BlocklistCoordinate; import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelMapper; -import org.wildfly.channel.ChannelRequirement; -import org.wildfly.channel.Stream; import org.wildfly.channel.Vendor; public class ChannelTestCase { @@ -88,47 +84,19 @@ public void simpleChannelTest() throws MalformedURLException { assertNotNull(vendor); assertEquals("My Vendor", vendor.getName()); assertEquals(Vendor.Support.COMMUNITY, vendor.getSupport()); - - Collection requires = channel.getChannelRequirements(); - assertEquals(0, requires.size()); - - Collection streams = channel.getStreams(); - assertEquals(1, streams.size()); - Stream stream = streams.iterator().next(); - assertEquals("org.wildfly", stream.getGroupId()); - assertEquals("wildfly-ee-galleon-pack", stream.getArtifactId()); - assertEquals("26.0.0.Final", stream.getVersion()); } @Test - public void channelWithoutStreams() { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "name: My Channel\n" + - "description: |-\n" + - " This is my channel\n" + - " with no stream"); - assertEquals(1, channels.size()); - Channel channel = channels.get(0); - - assertTrue(channel.getStreams().isEmpty()); - } + public void channelWithBlocklist() throws MalformedURLException { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + URL file = tccl.getResource("channels/channel-with-blocklist.yaml"); - @Test - public void channelWithRequires() { - List channels = ChannelMapper.fromString("schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" - +"name: My Channel\n" + - "description: |-\n" + - " This is my channel\n" + - " with no stream\n" + - "requires:\n" + - " - groupId: org.foo.channels\n" + - " artifactId: my-required-channel"); - assertEquals(1, channels.size()); - Channel channel = channels.get(0); - - assertEquals(1, channel.getChannelRequirements().size()); - ChannelRequirement requirement = channel.getChannelRequirements().get(0); - assertEquals("org.foo.channels", requirement.getGroupId()); - assertEquals("my-required-channel", requirement.getArtifactId()); + Channel channel = ChannelMapper.from(file); + + BlocklistCoordinate blocklist = channel.getBlocklistCoordinate(); + + assertEquals("blocklist", blocklist.getArtifactId()); + assertEquals("org.wildfly", blocklist.getGroupId()); + assertEquals("1.2.3", blocklist.getVersion()); } } diff --git a/core/src/test/java/org/wildfly/channel/mapping/ChannelRequirementTestCase.java b/core/src/test/java/org/wildfly/channel/mapping/ManifestRequirementTestCase.java similarity index 67% rename from core/src/test/java/org/wildfly/channel/mapping/ChannelRequirementTestCase.java rename to core/src/test/java/org/wildfly/channel/mapping/ManifestRequirementTestCase.java index 57543d8a..0e07e035 100644 --- a/core/src/test/java/org/wildfly/channel/mapping/ChannelRequirementTestCase.java +++ b/core/src/test/java/org/wildfly/channel/mapping/ManifestRequirementTestCase.java @@ -25,19 +25,22 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.wildfly.channel.ChannelRequirement; +import org.wildfly.channel.ManifestRequirement; -public class ChannelRequirementTestCase { +public class ManifestRequirementTestCase { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()); - static ChannelRequirement from(String str) throws IOException { - return OBJECT_MAPPER.readValue(str, ChannelRequirement.class); + static ManifestRequirement from(String str) throws IOException { + return OBJECT_MAPPER.readValue(str, ManifestRequirement.class); } @Test public void testValidRequires() throws IOException { - ChannelRequirement requirement = from("groupId: org.foo.channels\n" + - "artifactId: my-other-channel"); + ManifestRequirement requirement = from( + "id: test\n" + + "maven:\n" + + " groupId: org.foo.channels\n" + + " artifactId: my-other-channel"); assertEquals("org.foo.channels", requirement.getGroupId()); assertEquals("my-other-channel", requirement.getArtifactId()); @@ -48,9 +51,12 @@ public void testValidRequires() throws IOException { @Test public void testValidRequiresWithVersion() throws IOException { - ChannelRequirement requirement = from("groupId: org.foo.channels\n" + - "artifactId: my-other-channel\n" + - "version: 1.2.3.Final"); + ManifestRequirement requirement = from( + "id: test\n" + + "maven:\n" + + " groupId: org.foo.channels\n" + + " artifactId: my-other-channel\n" + + " version: 1.2.3.Final"); assertEquals("org.foo.channels", requirement.getGroupId()); assertEquals("my-other-channel", requirement.getArtifactId()); @@ -63,12 +69,18 @@ public void testValidRequiresWithVersion() throws IOException { public void testInvalidRequires() { Assertions.assertThrows(Exception.class, () -> { // missing artifactID - from("groupId: org.foo.channels"); + from( + "id:test\n" + + "maven:\n" + + " groupId: org.foo.channels"); }); Assertions.assertThrows(Exception.class, () -> { // missing groupId - from("artifactId: my-other-channel"); + from( + "id:test\n" + + "maven:\n" + + " artifactId: my-other-channel"); }); } } diff --git a/core/src/test/resources/channels/2nd-level-requiring-channel.yaml b/core/src/test/resources/channels/2nd-level-requiring-manifest.yaml similarity index 57% rename from core/src/test/resources/channels/2nd-level-requiring-channel.yaml rename to core/src/test/resources/channels/2nd-level-requiring-manifest.yaml index d3563ced..ffb74554 100644 --- a/core/src/test/resources/channels/2nd-level-requiring-channel.yaml +++ b/core/src/test/resources/channels/2nd-level-requiring-manifest.yaml @@ -1,9 +1,12 @@ schemaVersion: "1.0.0" -name: 2nd level requiring channel +name: 2nd level requiring manifest requires: - - groupId: org.foo - artifactId: required-channel - version: 2.0.0.Final + - id: 1st-level + maven: + groupId: test.channels + artifactId: required-manifest + version: 1.0.0 + streams: - groupId: org.example artifactId: foo-bar diff --git a/core/src/test/resources/channels/channel-with-blocklist.yaml b/core/src/test/resources/channels/channel-with-blocklist.yaml new file mode 100644 index 00000000..37792dca --- /dev/null +++ b/core/src/test/resources/channels/channel-with-blocklist.yaml @@ -0,0 +1,22 @@ +schemaVersion: "2.0.0" +name: My Channel +description: |- + This is my channel + with my stuff +vendor: + name: My Vendor + support: community +blocklist: + maven: + groupId: org.wildfly + artifactId: blocklist + version: "1.2.3" +manifest: + maven: + groupId: org.wildfly + artifactId: test-manifest + versionPattern: ".*" +repositories: + - id: test + url: http://test.te + diff --git a/core/src/test/resources/channels/channel-with-unknown-properties.yaml b/core/src/test/resources/channels/channel-with-unknown-properties.yaml index bc358511..c953cd76 100644 --- a/core/src/test/resources/channels/channel-with-unknown-properties.yaml +++ b/core/src/test/resources/channels/channel-with-unknown-properties.yaml @@ -1,4 +1,4 @@ -schemaVersion: "1.0.0" +schemaVersion: "2.0.0" name: My Channel description: |- This is my channel @@ -7,8 +7,11 @@ vendor: name: My Vendor support: community foo: not an known property -streams: - - groupId: org.wildfly - artifactId: wildfly-ee-galleon-pack - version: 26.0.0.Final - bar: not a known property +manifest: + maven: + groupId: test.channels + artifactId: channel + bar: not a known property +repositories: + - id: test + url: https://test.org/repository diff --git a/core/src/test/resources/channels/invalid-blocklist.yaml b/core/src/test/resources/channels/invalid-blocklist.yaml new file mode 100644 index 00000000..0a78fbb2 --- /dev/null +++ b/core/src/test/resources/channels/invalid-blocklist.yaml @@ -0,0 +1,5 @@ +--- +schemaVersion: 1.0.0 +blocks: + - groupId: foo + artifactId: bar \ No newline at end of file diff --git a/core/src/test/resources/channels/manifest-with-unknown-properties.yaml b/core/src/test/resources/channels/manifest-with-unknown-properties.yaml new file mode 100644 index 00000000..1e90969d --- /dev/null +++ b/core/src/test/resources/channels/manifest-with-unknown-properties.yaml @@ -0,0 +1,11 @@ +schemaVersion: "1.0.0" +name: My Manifest +description: |- + This is my manifest + with properties that are not defined in the schema +foo: not an known property +streams: + - groupId: org.wildfly + artifactId: wildfly-ee-galleon-pack + version: 26.0.0.Final + bar: not a known property diff --git a/core/src/test/resources/channels/multiple-manifests.yaml b/core/src/test/resources/channels/multiple-manifests.yaml new file mode 100644 index 00000000..d29667f9 --- /dev/null +++ b/core/src/test/resources/channels/multiple-manifests.yaml @@ -0,0 +1,6 @@ +--- +schemaVersion: "1.0.0" +name: Channel for WildFly 27 +--- +schemaVersion: "1.0.0" +name: Channel for WildFly 28 \ No newline at end of file diff --git a/core/src/test/resources/channels/required-channel-2.yaml b/core/src/test/resources/channels/required-manifest-2.yaml similarity index 78% rename from core/src/test/resources/channels/required-channel-2.yaml rename to core/src/test/resources/channels/required-manifest-2.yaml index af21c168..e230380b 100644 --- a/core/src/test/resources/channels/required-channel-2.yaml +++ b/core/src/test/resources/channels/required-manifest-2.yaml @@ -1,5 +1,5 @@ schemaVersion: "1.0.0" -name: My Required Channel +name: My Required Manifest 2 streams: - groupId: org.example artifactId: foo-bar diff --git a/core/src/test/resources/channels/required-channel.yaml b/core/src/test/resources/channels/required-manifest.yaml similarity index 88% rename from core/src/test/resources/channels/required-channel.yaml rename to core/src/test/resources/channels/required-manifest.yaml index 08300fee..acde667b 100644 --- a/core/src/test/resources/channels/required-channel.yaml +++ b/core/src/test/resources/channels/required-manifest.yaml @@ -1,5 +1,5 @@ schemaVersion: "1.0.0" -name: My Required Channel +name: My Required Manifest streams: - groupId: org.example artifactId: foo-bar diff --git a/core/src/test/resources/channels/simple-channel.yaml b/core/src/test/resources/channels/simple-channel.yaml index cc73efa2..2a388a81 100644 --- a/core/src/test/resources/channels/simple-channel.yaml +++ b/core/src/test/resources/channels/simple-channel.yaml @@ -1,4 +1,4 @@ -schemaVersion: "1.0.0" +schemaVersion: "2.0.0" name: My Channel description: |- This is my channel @@ -6,7 +6,10 @@ description: |- vendor: name: My Vendor support: community -streams: - - groupId: org.wildfly - artifactId: wildfly-ee-galleon-pack - version: 26.0.0.Final +repositories: +- id: test + url: test-repository +manifest: + maven: + groupId: org.test + artifactId: test-manifest diff --git a/core/src/test/resources/channels/simple-manifest.yaml b/core/src/test/resources/channels/simple-manifest.yaml new file mode 100644 index 00000000..795b559b --- /dev/null +++ b/core/src/test/resources/channels/simple-manifest.yaml @@ -0,0 +1,9 @@ +schemaVersion: "1.0.0" +name: My Channel +description: |- + This is my manifest + with my stuff +streams: + - groupId: org.wildfly + artifactId: wildfly-ee-galleon-pack + version: 26.0.0.Final diff --git a/core/src/test/resources/channels/test-blocklist-with-wildcards.yaml b/core/src/test/resources/channels/test-blocklist-with-wildcards.yaml new file mode 100644 index 00000000..61acc491 --- /dev/null +++ b/core/src/test/resources/channels/test-blocklist-with-wildcards.yaml @@ -0,0 +1,6 @@ +--- +schemaVersion: 1.0.0 +blocks: + - groupId: org.wildfly + artifactId: "*" + versions: [ 25.0.1.Final ] diff --git a/core/src/test/resources/channels/test-blocklist.yaml b/core/src/test/resources/channels/test-blocklist.yaml new file mode 100644 index 00000000..0184d0ec --- /dev/null +++ b/core/src/test/resources/channels/test-blocklist.yaml @@ -0,0 +1,6 @@ +--- +schemaVersion: 1.0.0 +blocks: + - groupId: org.wildfly + artifactId: wildfly-ee-galleon-pack + versions: [ 25.0.1.Final ] diff --git a/maven-resolver/src/main/java/org/wildfly/channel/maven/ChannelCoordinate.java b/maven-resolver/src/main/java/org/wildfly/channel/maven/ChannelCoordinate.java index 75c6f122..6c250c52 100644 --- a/maven-resolver/src/main/java/org/wildfly/channel/maven/ChannelCoordinate.java +++ b/maven-resolver/src/main/java/org/wildfly/channel/maven/ChannelCoordinate.java @@ -16,73 +16,32 @@ */ package org.wildfly.channel.maven; -import static java.util.Objects.requireNonNull; +import org.wildfly.channel.Channel; +import org.wildfly.channel.ChannelMetadataCoordinate; import java.net.URL; -import java.util.Objects; /** * A channel coordinate either use Maven coordinates (groupId, artifactId, version) * or it uses a URL from which the channel definition file can be fetched. */ -public class ChannelCoordinate { - private String groupId; - private String artifactId; - private String version; - // raw Channel file from an URL - private URL url; +public class ChannelCoordinate extends ChannelMetadataCoordinate { // empty constructor used by the wildlfy-maven-plugin // through reflection public ChannelCoordinate() { + super(Channel.CLASSIFIER, Channel.EXTENSION); } public ChannelCoordinate(String groupId, String artifactId, String version) { - this(groupId, artifactId, version, null); - requireNonNull(groupId); - requireNonNull(artifactId); - requireNonNull(version); + super(groupId, artifactId, version, Channel.CLASSIFIER, Channel.EXTENSION); } public ChannelCoordinate(String groupId, String artifactId) { - this(groupId, artifactId, null, null); - requireNonNull(groupId); - requireNonNull(artifactId); + super(groupId, artifactId, Channel.CLASSIFIER, Channel.EXTENSION); } public ChannelCoordinate(URL url) { - this(null, null, null, url); - requireNonNull(url); - } - - private ChannelCoordinate(String groupId, String artifactId, String version, URL url) { - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.url = url; - } - - public URL getUrl() { - return url; - } - - public String getGroupId() { - return groupId; - } - - public String getArtifactId() { - return artifactId; - } - - public String getVersion() { - return version; - } - - public String getExtension() { - return "yaml"; - } - - public String getClassifier() { - return "channel"; + super(url); } } \ No newline at end of file diff --git a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java index b77ea05e..6eb6206b 100644 --- a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java +++ b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java @@ -19,23 +19,37 @@ import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Objects.requireNonNull; +import static org.wildfly.channel.version.VersionMatcher.COMPARATOR; import java.io.File; +import java.io.FileReader; +import java.io.IOException; import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.metadata.DefaultMetadata; +import org.eclipse.aether.metadata.Metadata; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.resolution.ArtifactRequest; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.MetadataRequest; +import org.eclipse.aether.resolution.MetadataResult; import org.eclipse.aether.resolution.VersionRangeRequest; import org.eclipse.aether.resolution.VersionRangeResolutionException; import org.eclipse.aether.resolution.VersionRangeResult; @@ -43,6 +57,8 @@ import org.wildfly.channel.ArtifactCoordinate; import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelMapper; +import org.wildfly.channel.ChannelMetadataCoordinate; +import org.wildfly.channel.Repository; import org.wildfly.channel.UnresolvedMavenArtifactException; import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.version.VersionMatcher; @@ -52,22 +68,39 @@ public class VersionResolverFactory implements MavenVersionsResolver.Factory { private static final Logger LOG = Logger.getLogger(VersionResolverFactory.class); + public static final RepositoryPolicy DEFAULT_REPOSITORY_POLICY = new RepositoryPolicy(true, RepositoryPolicy.UPDATE_POLICY_ALWAYS, RepositoryPolicy.CHECKSUM_POLICY_FAIL); + public static final Function DEFAULT_REPOSITORY_MAPPER = r -> new RemoteRepository.Builder(r.getId(), "default", r.getUrl()) + .setPolicy(DEFAULT_REPOSITORY_POLICY) + .build(); + private final RepositorySystem system; private final RepositorySystemSession session; - private final List repositories; + private final Function repositoryFactory; + public VersionResolverFactory(RepositorySystem system, + RepositorySystemSession session) { + this(system, session, DEFAULT_REPOSITORY_MAPPER); + } public VersionResolverFactory(RepositorySystem system, RepositorySystemSession session, - List repositories) { + Function repositoryFactory) { this.system = system; this.session = session; - this.repositories = repositories; + this.repositoryFactory = repositoryFactory; } @Override - public MavenVersionsResolver create() { - MavenVersionsResolver res = new MavenResolverImpl(system, session, repositories); - return res; + public MavenVersionsResolver create(Collection repositories) { + Objects.requireNonNull(repositories); + + final List mvnRepositories = repositories.stream() + .map(repositoryFactory::apply) + .collect(Collectors.toList()); + return create(mvnRepositories); + } + + private MavenResolverImpl create(List mvnRepositories) { + return new MavenResolverImpl(system, session, mvnRepositories); } private class MavenResolverImpl implements MavenVersionsResolver { @@ -160,6 +193,96 @@ public List resolveArtifacts(List coordinates) throws throw new UnresolvedMavenArtifactException(ex.getLocalizedMessage(), ex, failed); } } + + @Override + public List resolveChannelMetadata(List coords) throws UnresolvedMavenArtifactException { + requireNonNull(coords); + + List channels = new ArrayList<>(); + + for (ChannelMetadataCoordinate coord : coords) { + if (coord.getUrl() != null) { + LOG.infof("Resolving channel metadata at %s", coord.getUrl()); + channels.add(coord.getUrl()); + continue; + } + + String version = coord.getVersion(); + if (version == null) { + Set versions = getAllVersions(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier()); + Optional latestVersion = VersionMatcher.getLatestVersion(versions); + version = latestVersion.orElseThrow(() -> { + throw new UnresolvedMavenArtifactException(String.format("Unable to resolve the latest version of channel metadata %s:%s", coord.getGroupId(), coord.getArtifactId())); + }); + } + LOG.infof("Resolving channel metadata from Maven artifact %s:%s:%s", coord.getGroupId(), coord.getArtifactId(), version); + File channelArtifact = resolveArtifact(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), version); + try { + channels.add(channelArtifact.toURI().toURL()); + } catch (MalformedURLException e) { + throw new UnresolvedMavenArtifactException("Unable to resolve channel metadata.", e, + Set.of(new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), + coord.getExtension(), coord.getClassifier(), coord.getVersion()))); + } + } + return channels; + } + + @Override + public String getMetadataReleaseVersion(String groupId, String artifactId) { + requireNonNull(groupId); + requireNonNull(artifactId); + + final List metadataResults = getMavenMetadata(groupId, artifactId); + + final Function getVersion = m -> m.getVersioning().getRelease(); + return findLatestMetadataVersion(metadataResults, getVersion, groupId, artifactId); + } + + @Override + public String getMetadataLatestVersion(String groupId, String artifactId) { + requireNonNull(groupId); + requireNonNull(artifactId); + + final List metadataResults = getMavenMetadata(groupId, artifactId); + + return findLatestMetadataVersion(metadataResults, m -> m.getVersioning().getLatest(), groupId, artifactId); + } + + private String findLatestMetadataVersion(List metadataResults, + Function getVersion, + String groupId, String artifactId) { + final MetadataXpp3Reader reader = new MetadataXpp3Reader(); + return metadataResults.stream() + .filter(r -> r.getMetadata() != null) + .map(m -> m.getMetadata().getFile()) + .map(f -> { + try { + return reader.read(new FileReader(f)); + } catch (IOException | XmlPullParserException e) { + final ArtifactCoordinate requestedArtifact = new ArtifactCoordinate(groupId, artifactId, null, null, "*"); + throw new UnresolvedMavenArtifactException(e.getLocalizedMessage(), e, Set.of(requestedArtifact)); + } + }) + .filter(m->m.getVersioning() != null) + .map(getVersion) + .filter(s->s!=null&&!s.isEmpty()) + .max(COMPARATOR) + .orElseThrow(()->new UnresolvedMavenArtifactException("No versioning information found in metadata.", + Set.of(new ArtifactCoordinate(groupId, artifactId, null, null, "*")))); + } + + private List getMavenMetadata(String groupId, String artifactId) { + final DefaultMetadata metadata = new DefaultMetadata(groupId, artifactId, "maven-metadata.xml", Metadata.Nature.RELEASE); + final List requests = repositories.stream().map(r -> { + final MetadataRequest metadataRequest = new MetadataRequest(); + metadataRequest.setMetadata(metadata); + metadataRequest.setRepository(r); + return metadataRequest; + }).collect(Collectors.toList()); + final List metadataResults = system.resolveMetadata(session, requests); + return metadataResults; + } } /** @@ -174,35 +297,14 @@ public List resolveArtifacts(List coordinates) throws * @throws UnresolvedMavenArtifactException if the channels can not be resolved * @throws MalformedURLException if the channel's rul is not properly formed */ - public List resolveChannels(List channelCoords) throws UnresolvedMavenArtifactException, MalformedURLException { + public List resolveChannels(List channelCoords, List repositories) throws UnresolvedMavenArtifactException, MalformedURLException { requireNonNull(channelCoords); - List channels = new ArrayList<>(); - try (MavenVersionsResolver resolver = create()) { - - for (ChannelCoordinate channelCoord : channelCoords) { - if (channelCoord.getUrl() != null) { - Channel channel = ChannelMapper.from(channelCoord.getUrl()); - LOG.infof("Resolving channel at %s", channelCoord.getUrl()); - channels.add(channel); - continue; - } - - String version = channelCoord.getVersion(); - if (version == null) { - Set versions = resolver.getAllVersions(channelCoord.getGroupId(), channelCoord.getArtifactId(), channelCoord.getExtension(), channelCoord.getClassifier()); - Optional latestVersion = VersionMatcher.getLatestVersion(versions); - version = latestVersion.orElseThrow(() -> { - throw new UnresolvedMavenArtifactException(String.format("Unable to resolve the latest version of channel %s:%s", channelCoord.getGroupId(), channelCoord.getArtifactId())); - }); - } - LOG.infof("Resolving channel from Maven artifact %s:%s:%s", channelCoord.getGroupId(), channelCoord.getArtifactId(), version); - File channelArtifact = resolver.resolveArtifact(channelCoord.getGroupId(), channelCoord.getArtifactId(), channelCoord.getExtension(), channelCoord.getClassifier(), version); - Channel channel = ChannelMapper.from(channelArtifact.toURI().toURL()); - channels.add(channel); - } + try (MavenVersionsResolver resolver = create(repositories)) { + return resolver.resolveChannelMetadata(channelCoords).stream() + .map(ChannelMapper::from) + .collect(Collectors.toList()); } - return channels; } } diff --git a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java index 45628a68..fe202ac7 100644 --- a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java +++ b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java @@ -25,29 +25,42 @@ import static org.mockito.Mockito.when; import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; +import org.apache.maven.artifact.repository.metadata.Versioning; +import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.metadata.DefaultMetadata; +import org.eclipse.aether.metadata.Metadata; import org.eclipse.aether.resolution.ArtifactRequest; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.MetadataRequest; +import org.eclipse.aether.resolution.MetadataResult; import org.eclipse.aether.resolution.VersionRangeRequest; import org.eclipse.aether.resolution.VersionRangeResolutionException; import org.eclipse.aether.resolution.VersionRangeResult; import org.eclipse.aether.version.Version; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.wildfly.channel.ArtifactCoordinate; import org.wildfly.channel.Channel; +import org.wildfly.channel.Repository; import org.wildfly.channel.UnresolvedMavenArtifactException; import org.wildfly.channel.spi.MavenVersionsResolver; @@ -69,8 +82,8 @@ public void testResolverGetAllVersions() throws VersionRangeResolutionException versionRangeResult.setVersions(asList(v100, v110, v111)); when(system.resolveVersionRange(eq(session), any(VersionRangeRequest.class))).thenReturn(versionRangeResult); - VersionResolverFactory factory = new VersionResolverFactory(system, session, Collections.emptyList()); - MavenVersionsResolver resolver = factory.create(); + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); Set allVersions = resolver.getAllVersions("org.foo", "bar", null, null); assertEquals(3, allVersions.size()); @@ -92,8 +105,8 @@ public void testResolverResolveArtifact() throws ArtifactResolutionException { when (artifact.getFile()).thenReturn(artifactFile); when(system.resolveArtifact(eq(session), any(ArtifactRequest.class))).thenReturn(artifactResult); - VersionResolverFactory factory = new VersionResolverFactory(system, session, Collections.emptyList()); - MavenVersionsResolver resolver = factory.create(); + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); File resolvedArtifact = resolver.resolveArtifact("org.foo", "bar", null, null, "1.0.0"); assertEquals(artifactFile, resolvedArtifact); @@ -106,8 +119,8 @@ public void testResolverCanNotResolveArtifact() throws ArtifactResolutionExcepti RepositorySystemSession session = mock(RepositorySystemSession.class); when(system.resolveArtifact(eq(session), any(ArtifactRequest.class))).thenThrow(ArtifactResolutionException.class); - VersionResolverFactory factory = new VersionResolverFactory(system, session, Collections.emptyList()); - MavenVersionsResolver resolver = factory.create(); + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { resolver.resolveArtifact("org.foo", "does-not-exist", null, null, "1.0.0"); @@ -130,11 +143,11 @@ public void testFactoryCanNotResolveChannel() throws MalformedURLException, Arti artifactResult.setArtifact(channelArtifact); when(system.resolveArtifact(eq(session), any(ArtifactRequest.class))).thenReturn(artifactResult); - VersionResolverFactory factory = new VersionResolverFactory(system, session, Collections.emptyList()); + VersionResolverFactory factory = new VersionResolverFactory(system, session); ChannelCoordinate channelCoord1 = new ChannelCoordinate("org.wildfly", "wildfly-galleon-pack", "27.0.0.Final"); List channelCoords = Arrays.asList(channelCoord1); - List channels = factory.resolveChannels(channelCoords); + List channels = factory.resolveChannels(channelCoords, Collections.emptyList()); assertEquals(1, channels.size()); Channel channel = channels.get(0); @@ -160,8 +173,8 @@ public void testResolverResolveAllArtifacts() throws ArtifactResolutionException when(system.resolveArtifacts(eq(session), any(List.class))).thenReturn(Arrays.asList(artifactResult1, artifactResult2)); - VersionResolverFactory factory = new VersionResolverFactory(system, session, Collections.emptyList()); - MavenVersionsResolver resolver = factory.create(); + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); final List coordinates = asList( new ArtifactCoordinate("org.foo", "bar", null, null, "1.0.0"), @@ -171,5 +184,190 @@ public void testResolverResolveAllArtifacts() throws ArtifactResolutionException assertEquals(artifactFile1, res.get(0)); assertEquals(artifactFile2, res.get(1)); } + + @Test + public void testResolverResolveMetadataUsingUrl() throws ArtifactResolutionException, MalformedURLException { + + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + + List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate(new URL("http://test.channel")))); + assertEquals(new URL("http://test.channel"), resolvedURL.get(0)); + } + + @Test + public void testResolverResolveMetadataUsingGa() throws ArtifactResolutionException, MalformedURLException, VersionRangeResolutionException { + + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + File artifactFile = new File("test"); + ArtifactResult artifactResult = new ArtifactResult(new ArtifactRequest()); + Artifact artifact = mock(Artifact.class); + artifactResult.setArtifact(artifact); + when (artifact.getFile()).thenReturn(artifactFile); + VersionRangeResult versionRangeResult = new VersionRangeResult(new VersionRangeRequest()); + Version v100 = mock(Version.class); + when(v100.toString()).thenReturn("1.0.0"); + Version v110 = mock(Version.class); + when(v110.toString()).thenReturn("1.1.0"); + Version v111 = mock(Version.class); + when(v111.toString()).thenReturn("1.1.1"); + versionRangeResult.setVersions(asList(v100, v110, v111)); + when(system.resolveVersionRange(eq(session), any())).thenReturn(versionRangeResult); + final ArgumentCaptor artifactRequestArgumentCaptor = ArgumentCaptor.forClass(ArtifactRequest.class); + when(system.resolveArtifact(eq(session), artifactRequestArgumentCaptor.capture())).thenReturn(artifactResult); + + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + + List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate("org.test", "channel"))); + assertEquals(artifactFile.toURI().toURL(), resolvedURL.get(0)); + assertEquals("1.1.1", artifactRequestArgumentCaptor.getAllValues().get(0).getArtifact().getVersion()); + } + + @Test + public void testResolverResolveMetadataUsingGav() throws ArtifactResolutionException, MalformedURLException, VersionRangeResolutionException { + + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + File artifactFile = new File("test"); + ArtifactResult artifactResult = new ArtifactResult(new ArtifactRequest()); + Artifact artifact = mock(Artifact.class); + artifactResult.setArtifact(artifact); + when (artifact.getFile()).thenReturn(artifactFile); + final ArgumentCaptor artifactRequestArgumentCaptor = ArgumentCaptor.forClass(ArtifactRequest.class); + when(system.resolveArtifact(eq(session), artifactRequestArgumentCaptor.capture())).thenReturn(artifactResult); + + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + + List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate("org.test", "channel", "1.0.0"))); + assertEquals(artifactFile.toURI().toURL(), resolvedURL.get(0)); + assertEquals("1.0.0", artifactRequestArgumentCaptor.getAllValues().get(0).getArtifact().getVersion()); + } + + @Test + public void testRepositoryFactory() throws Exception { + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + VersionResolverFactory factory = new VersionResolverFactory(system, session, + r -> new RemoteRepository.Builder(r.getId(), "default", r.getUrl() + ".new").build()); + MavenVersionsResolver resolver = factory.create(List.of(new Repository("test_1", "http://test_1"))); + + File artifactFile = new File("test"); + ArtifactResult artifactResult = new ArtifactResult(new ArtifactRequest()); + Artifact artifact = mock(Artifact.class); + artifactResult.setArtifact(artifact); + when(artifact.getFile()).thenReturn(artifactFile); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ArtifactRequest.class); + + when(system.resolveArtifact(eq(session), captor.capture())).thenReturn(artifactResult); + + resolver.resolveArtifact("group", "artifact", "ext", null, "1.0.0"); + + final List actualRepos = captor.getAllValues().get(0).getRepositories(); + assertEquals(1, actualRepos.size()); + assertEquals("http://test_1.new", actualRepos.get(0).getUrl()); + } + + public void testResolveReleaseFromMetadata() throws Exception { + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + final MetadataResult result1 = getMetadataResult("1.0.0.Final", "1.0.1.Final-SNAPSHOT"); + final MetadataResult result2 = getMetadataResult("1.0.1.Final", "1.0.1.Final-SNAPSHOT"); + when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result1, result2)); + + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(any()); + + final String res = resolver.getMetadataReleaseVersion("org.foo", "bar"); + + assertEquals("1.0.1.Final", res); + } + + @Test + public void testResolveLatestFromMetadata() throws Exception { + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + final MetadataResult result1 = getMetadataResult("1.0.0.Final", "1.0.0.Final"); + final MetadataResult result2 = getMetadataResult("1.0.0.Final", "1.0.1.Final"); + when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result1, result2)); + + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + + final String res = resolver.getMetadataLatestVersion("org.foo", "bar"); + + assertEquals("1.0.1.Final", res); + } + + @Test + public void testResolveLatestFromMetadataNoVersioning() throws Exception { + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + final org.apache.maven.artifact.repository.metadata.Metadata resMetadata = new org.apache.maven.artifact.repository.metadata.Metadata(); + resMetadata.setGroupId("org.foo"); + resMetadata.setArtifactId("bar"); + + final MetadataResult result = wrapMetadata(resMetadata); + when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result)); + + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + + Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { + resolver.getMetadataLatestVersion("org.foo", "bar"); + }); + } + + @Test + public void testResolveLatestFromMetadataNoLatestVersion() throws Exception { + RepositorySystem system = mock(RepositorySystem.class); + RepositorySystemSession session = mock(RepositorySystemSession.class); + + final MetadataResult result = getMetadataResult("1.0.0.Final", null); + when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result)); + + VersionResolverFactory factory = new VersionResolverFactory(system, session); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); + + Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { + resolver.getMetadataLatestVersion("org.foo", "bar"); + }); + } + + private MetadataResult getMetadataResult(String releaseVersion, String latestVersion) throws IOException { + final org.apache.maven.artifact.repository.metadata.Metadata resMetadata = new org.apache.maven.artifact.repository.metadata.Metadata(); + resMetadata.setGroupId("org.foo"); + resMetadata.setArtifactId("bar"); + final Versioning versioning = new Versioning(); + versioning.setRelease(releaseVersion); + versioning.setLatest(latestVersion); + versioning.setVersions(List.of("1.0.1.Final-SNAPSHOT", releaseVersion)); + resMetadata.setVersioning(versioning); + + return wrapMetadata(resMetadata); + } + + private MetadataResult wrapMetadata(org.apache.maven.artifact.repository.metadata.Metadata resMetadata) throws IOException { + final Path tempFile = Files.createTempFile("test", "xml"); + tempFile.toFile().deleteOnExit(); + new MetadataXpp3Writer().write(new FileWriter(tempFile.toFile()), resMetadata); + final MetadataResult result = new MetadataResult(new MetadataRequest()); + final Metadata metadata = new DefaultMetadata("org.foo", "bar", "maven-metadata.xml", Metadata.Nature.RELEASE) + .setFile(tempFile.toFile()); + result.setMetadata(metadata); + return result; + } }