From 2727c6dce4d1d6dcd1e76adadce6755cf86af753 Mon Sep 17 00:00:00 2001
From: Jean-Francois Denise <jdenise@redhat.com>
Date: Tue, 21 Nov 2023 17:28:24 +0100
Subject: [PATCH] Add support for WildFly Channel in arquillian plugin

---
 arquillian-plugin/pom.xml                     |   8 ++
 .../arquillian/ChannelConfiguration.java      | 112 ++++++++++++++++++
 .../plugin/arquillian/ConfiguredChannels.java |  78 ++++++++++++
 .../glow/plugin/arquillian/FeaturePack.java   |   4 +-
 .../glow/plugin/arquillian/ScanMojo.java      |  64 +++++++++-
 .../java/org/wildfly/glow/GlowSession.java    |  33 ++++--
 .../src/main/java/org/wildfly/glow/Utils.java |   7 +-
 pom.xml                                       |  16 ++-
 tests/run-cli-tests.sh                        |   9 ++
 9 files changed, 312 insertions(+), 19 deletions(-)
 create mode 100644 arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ChannelConfiguration.java
 create mode 100644 arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ConfiguredChannels.java

diff --git a/arquillian-plugin/pom.xml b/arquillian-plugin/pom.xml
index 90561409..77cb40db 100644
--- a/arquillian-plugin/pom.xml
+++ b/arquillian-plugin/pom.xml
@@ -61,6 +61,14 @@
             <groupId>org.jboss.shrinkwrap</groupId>
             <artifactId>shrinkwrap-impl-base</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.wildfly.channel</groupId>
+            <artifactId>channel-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.wildfly.channel</groupId>
+            <artifactId>maven-resolver</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ChannelConfiguration.java b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ChannelConfiguration.java
new file mode 100644
index 00000000..a60d606c
--- /dev/null
+++ b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ChannelConfiguration.java
@@ -0,0 +1,112 @@
+/*
+ * 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.glow.plugin.arquillian;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.wildfly.channel.Channel;
+import org.wildfly.channel.ChannelManifestCoordinate;
+import org.wildfly.channel.Repository;
+
+/**
+ * A channel configuration. Contains a {@code manifest} composed of a {@code groupId}, an {@code artifactId}
+ * an optional {@code version} or a {@code url}.
+ *
+ * @author jdenise
+ */
+public class ChannelConfiguration {
+    private static final Pattern FILE_MATCHER = Pattern.compile("^(file|http|https)://.*");
+
+    private ChannelManifestCoordinate manifest;
+
+    /**
+     * @return the manifest
+     */
+    public ChannelManifestCoordinate getManifest() {
+        return manifest;
+    }
+
+    public void set(final String channel) {
+        // Is this a URL?
+        if (FILE_MATCHER.matcher(channel).matches()) {
+            try {
+                this.manifest = new ChannelManifestCoordinate(new URL(channel));
+            } catch (MalformedURLException e) {
+                throw new IllegalArgumentException("Failed to parse URL for " + channel, e);
+            }
+        } else {
+            // Treat as a Maven GAV
+            final String[] coords = channel.split(":");
+            if (coords.length > 2) {
+                this.manifest = new ChannelManifestCoordinate(coords[0], coords[1], coords[2]);
+            } else if (coords.length == 2) {
+                this.manifest = new ChannelManifestCoordinate(coords[0], coords[1]);
+            } else {
+                throw new IllegalArgumentException(
+                        "A channel must be a Maven GAV in the format groupId:artifactId:version. The groupId and artifactId are both required.");
+            }
+        }
+    }
+
+    void setManifest(ChannelManifestCoordinate manifest) {
+        this.manifest = manifest;
+    }
+
+    private void validate() throws MojoExecutionException {
+        if (getManifest() == null) {
+            throw new MojoExecutionException("Invalid Channel. No manifest specified.");
+        }
+        ChannelManifestCoordinate coordinates = getManifest();
+        if (coordinates.getUrl() == null && coordinates.getGroupId() == null && coordinates.getArtifactId() == null) {
+            throw new MojoExecutionException(
+                    "Invalid Channel. Manifest must contain a groupId, artifactId and (optional) version or an url.");
+        }
+        if (coordinates.getUrl() == null) {
+            if (coordinates.getGroupId() == null) {
+                throw new MojoExecutionException("Invalid Channel. Manifest groupId is null.");
+            }
+            if (coordinates.getArtifactId() == null) {
+                throw new MojoExecutionException("Invalid Channel. Manifest artifactId is null.");
+            }
+        } else {
+            if (coordinates.getGroupId() != null) {
+                throw new MojoExecutionException("Invalid Channel. Manifest groupId is set although an URL is provided.");
+            }
+            if (coordinates.getArtifactId() != null) {
+                throw new MojoExecutionException("Invalid Channel. Manifest artifactId is set although an URL is provided.");
+            }
+            if (coordinates.getVersion() != null) {
+                throw new MojoExecutionException("Invalid Channel. Manifest version is set although an URL is provided.");
+            }
+        }
+    }
+
+    public Channel toChannel(List<RemoteRepository> repositories) throws MojoExecutionException {
+        validate();
+        List<Repository> repos = new ArrayList<>();
+        for (RemoteRepository r : repositories) {
+            repos.add(new Repository(r.getId(), r.getUrl()));
+        }
+        return new Channel(null, null, null, repos, getManifest(), null, null);
+    }
+}
diff --git a/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ConfiguredChannels.java b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ConfiguredChannels.java
new file mode 100644
index 00000000..96a0d274
--- /dev/null
+++ b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ConfiguredChannels.java
@@ -0,0 +1,78 @@
+/*
+ * 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.glow.plugin.arquillian;
+
+import static org.wildfly.channel.maven.VersionResolverFactory.DEFAULT_REPOSITORY_MAPPER;
+
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.wildfly.channel.Channel;
+import org.wildfly.channel.ChannelSession;
+import org.wildfly.channel.Repository;
+import org.wildfly.channel.UnresolvedMavenArtifactException;
+import org.wildfly.channel.maven.VersionResolverFactory;
+
+public class ConfiguredChannels {
+
+    private final ChannelSession channelSession;
+    public ConfiguredChannels(List<ChannelConfiguration> channels,
+            RepositorySystem system,
+            RepositorySystemSession contextSession,
+            List<RemoteRepository> repositories, Log log, boolean offline)
+            throws MalformedURLException, UnresolvedMavenArtifactException, MojoExecutionException {
+        if (channels.isEmpty()) {
+            throw new MojoExecutionException("No channel specified.");
+        }
+        DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
+        session.setLocalRepositoryManager(contextSession.getLocalRepositoryManager());
+        session.setOffline(offline);
+        Map<String, RemoteRepository> mapping = new HashMap<>();
+        for (RemoteRepository r : repositories) {
+            mapping.put(r.getId(), r);
+        }
+        List<Channel> channelDefinitions = new ArrayList<>();
+        for (ChannelConfiguration channelConfiguration : channels) {
+            channelDefinitions.add(channelConfiguration.toChannel(repositories));
+        }
+        Function<Repository, RemoteRepository> mapper = r -> {
+            RemoteRepository rep = mapping.get(r.getId());
+            if (rep == null) {
+                rep = DEFAULT_REPOSITORY_MAPPER.apply(r);
+            }
+            return rep;
+        };
+        VersionResolverFactory factory = new VersionResolverFactory(system, session, mapper);
+        channelSession = new ChannelSession(channelDefinitions, factory);
+    }
+
+    ChannelSession getChannelSession() {
+        return channelSession;
+    }
+
+}
diff --git a/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/FeaturePack.java b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/FeaturePack.java
index ec5b1b40..ac4b14c2 100644
--- a/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/FeaturePack.java
+++ b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/FeaturePack.java
@@ -85,8 +85,10 @@ public String getMavenCoords() {
             builder.append(":").append(getClassifier() == null ? "" : getClassifier()).append(":")
                     .append(type == null ? "" : type);
         }
+        // For WildFly Channel we need the Maven GA to be ended by a ':' if no version.
+        builder.append(":");
         if (getVersion() != null) {
-            builder.append(":").append(getVersion());
+            builder.append(getVersion());
         }
         return builder.toString();
     }
diff --git a/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ScanMojo.java b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ScanMojo.java
index 00cdd672..d39271b7 100644
--- a/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ScanMojo.java
+++ b/arquillian-plugin/src/main/java/org/wildfly/glow/plugin/arquillian/ScanMojo.java
@@ -52,6 +52,7 @@
 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.net.URLClassLoader;
@@ -66,6 +67,9 @@
 import java.util.Map;
 import java.util.Set;
 import org.apache.maven.plugin.logging.Log;
+import org.jboss.galleon.universe.maven.repo.MavenRepoManager;
+import org.wildfly.channel.UnresolvedMavenArtifactException;
+import org.wildfly.channel.VersionResult;
 import org.wildfly.glow.error.IdentifiedError;
 import static org.wildfly.glow.plugin.arquillian.GlowArquillianDeploymentExporter.TEST_CLASSPATH;
 import static org.wildfly.glow.plugin.arquillian.GlowArquillianDeploymentExporter.TEST_PATHS;
@@ -203,6 +207,43 @@ public void trace(Object s) {
     @Parameter(property = "org.wildfly.glow.verbose")
     private boolean verbose = false;
 
+        /**
+     * A list of channels used for resolving artifacts while provisioning.
+     * <p>
+     * Defining a channel:
+     *
+     * <pre>
+     * <channels>
+     *     <channel>
+     *         <manifest>
+     *             <groupId>org.wildfly.channels</groupId>
+     *             <artifactId>wildfly-30.0</artifactId>
+     *         </manifest>
+     *     </channel>
+     *     <channel>
+     *         <manifest>
+     *             <url>https://example.example.org/channel/30</url>
+     *         </manifest>
+     *     </channel>
+     * </channels>
+     * </pre>
+     * </p>
+     * <p>
+     * The {@code wildfly.channels} property can be used pass a comma delimited string for the channels. The channel
+     * can be a URL or a Maven GAV. If a Maven GAV is used, the groupId and artifactId are required.
+     * <br>
+     * Examples:
+     *
+     * <pre>
+     *     -Dorg.wildfly.glow.channels=&quot;https://channels.example.org/30&quot;
+     *     -Dorg.wildfly.glow.channels=&quot;https://channels.example.org/30,org.example.channel:updates-30&quot;
+     *     -Dorg.wildfly.glow.channels=&quot;https://channels.example.org/30,org.example.channel:updates-30:1.0.2&quot;
+     * </pre>
+     * </p>
+     */
+    @Parameter(alias = "channels", property = "org.wildfly.glow.channels")
+    List<ChannelConfiguration> channels;
+
     @Override
     public void execute() throws MojoExecutionException, MojoFailureException {
         // Make sure that the 'hidden' properties used by the Arguments class come from the Maven configuration
@@ -232,6 +273,27 @@ public void execute() throws MojoExecutionException, MojoFailureException {
             for (String s : project.getTestClasspathElements()) {
                 paths.add(new File(s).getAbsolutePath());
             }
+            MavenRepoManager artifactResolver = new MavenArtifactRepositoryManager(repoSystem, repoSession, repositories);
+            if (channels != null && !channels.isEmpty()) {
+                getLog().debug("WildFly channel enabled, feature-pack versions are retrieved from channels (if stream known).");
+                try {
+                    ConfiguredChannels cr = new ConfiguredChannels(channels,
+                            repoSystem, repoSession, repositories,
+                            getLog(), true);
+                    for (FeaturePack fp : featurePacks) {
+                        try {
+                            VersionResult res = cr.getChannelSession().findLatestMavenArtifactVersion(fp.getGroupId(), fp.getArtifactId(),
+                                    fp.getExtension(), fp.getClassifier(), null);
+                            getLog().debug(fp.getGroupId() +":"+fp.getArtifactId() + ", Channel resolved version " + res.getVersion());
+                            fp.setVersion(res.getVersion());
+                        } catch (Exception ex) {
+                            getLog().debug("Got exception trying to resolve " + fp.getGroupId() +":"+fp.getArtifactId(), ex);
+                        }
+                    }
+                } catch (MalformedURLException | UnresolvedMavenArtifactException ex) {
+                    throw new MojoExecutionException(ex.getLocalizedMessage(), ex);
+                }
+            }
             Arguments arguments = Arguments.scanBuilder().
                     setExecutionProfiles(profiles).
                     setBinaries(retrieveDeployments(paths, classesRootFolder, outputFolder)).
@@ -242,8 +304,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
                     setJndiLayers(layersForJndi).
                     setVerbose(verbose || getLog().isDebugEnabled()).
                     setOutput(OutputFormat.PROVISIONING_XML).build();
-            MavenArtifactRepositoryManager artifactResolver
-                    = new MavenArtifactRepositoryManager(repoSystem, repoSession, repositories);
             ScanResults results = GlowSession.scan(artifactResolver,
                     arguments, writer);
             if (expectedDiscovery != null) {
diff --git a/core/src/main/java/org/wildfly/glow/GlowSession.java b/core/src/main/java/org/wildfly/glow/GlowSession.java
index d54495b7..8f8c7249 100644
--- a/core/src/main/java/org/wildfly/glow/GlowSession.java
+++ b/core/src/main/java/org/wildfly/glow/GlowSession.java
@@ -57,6 +57,7 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import org.jboss.galleon.universe.FeaturePackLocation.ProducerSpec;
 import static org.wildfly.glow.OutputFormat.BOOTABLE_JAR;
 import static org.wildfly.glow.OutputFormat.DOCKER_IMAGE;
 
@@ -809,16 +810,34 @@ private static ProvisioningConfig buildProvisioningConfig(ProvisioningConfig inp
         Map<FPID, FeaturePackConfig> map = new HashMap<>();
         Map<FPID, FPID> universeToGav = new HashMap<>();
         for (FeaturePackConfig cfg : input.getFeaturePackDeps()) {
-            FeaturePackLocation.FPID fpid = Utils.toMavenCoordinates(cfg.getLocation().getFPID(), universeResolver);
-            map.put(fpid, cfg);
-            universeToGav.put(cfg.getLocation().getFPID(), fpid);
+            FeaturePackLocation.FPID loc = null;
+            for (FeaturePackLocation.FPID f : fpDependencies.keySet()) {
+                if (cfg.getLocation().getProducer().equals(f.getProducer())) {
+                    loc = f;
+                    break;
+                }
+            }
+            if(loc == null) {
+                throw new ProvisioningException("Input fp "+ cfg.getLocation() + " not found in resolved feature-packs " + fpDependencies.keySet());
+            }
+            map.put(loc, cfg);
+            universeToGav.put(cfg.getLocation().getFPID(), loc);
         }
-        Set<FeaturePackLocation.FPID> activeFeaturePacks = new LinkedHashSet<>();
-        // Add WildFly first.
+        Map<ProducerSpec, FeaturePackLocation.FPID> tmpFps = new HashMap<>();
         FeaturePackLocation.FPID baseFPID = universeToGav.get(input.getFeaturePackDeps().iterator().next().getLocation().getFPID());
-        activeFeaturePacks.add(baseFPID);
+        tmpFps.put(baseFPID.getProducer(), baseFPID);
         for (Layer l : allBaseLayers) {
-            activeFeaturePacks.addAll(l.getFeaturePacks());
+            for(FPID fpid : l.getFeaturePacks()) {
+                tmpFps.put(fpid.getProducer(), fpid);
+            }
+        }
+        Set<FeaturePackLocation.FPID> activeFeaturePacks = new LinkedHashSet<>();
+        // Order follow the one from the input
+        for(FeaturePackConfig cfg : input.getFeaturePackDeps()) {
+            FeaturePackLocation.FPID fpid = tmpFps.get(cfg.getLocation().getProducer());
+            if (fpid != null) {
+                activeFeaturePacks.add(fpid);
+            }
         }
         // Remove dependencies that are not Main FP...
         //System.out.println("Active FP " + activeFeaturePacks);
diff --git a/core/src/main/java/org/wildfly/glow/Utils.java b/core/src/main/java/org/wildfly/glow/Utils.java
index e418e63f..4b7eb9a0 100644
--- a/core/src/main/java/org/wildfly/glow/Utils.java
+++ b/core/src/main/java/org/wildfly/glow/Utils.java
@@ -222,13 +222,8 @@ public static Map<String, Layer> getAllLayers(UniverseResolver universeResolver,
                 }
                 l.getFeaturePacks().add(fpid);
             }
-
+            Set<ProducerSpec> producers = fpDependencies.computeIfAbsent(fpid, (value) -> new HashSet<>());
             for (FeaturePackConfig cfg : fp.getSpec().getFeaturePackDeps()) {
-                Set<ProducerSpec> producers = fpDependencies.get(fpid);
-                if (producers == null) {
-                    producers = new HashSet<>();
-                    fpDependencies.put(fpid, producers);
-                }
                 FPID fpidDep = toMavenCoordinates(cfg.getLocation().getFPID(), universeResolver);
                 producers.add(fpidDep.getProducer());
             }
diff --git a/pom.xml b/pom.xml
index d5c3471d..b0ca9cf7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,9 +28,10 @@
         <version.org.apache.maven>3.8.6</version.org.apache.maven>
         <version.org.apache.maven.checkstyle>3.0.0</version.org.apache.maven.checkstyle>
         <version.org.apache.maven.resolver>1.6.3</version.org.apache.maven.resolver>
-        <version.org.jboss.galleon>5.2.0.Final</version.org.jboss.galleon>
+        <version.org.jboss.galleon>5.2.2.Final</version.org.jboss.galleon>
+         <version.org.wildfly.channel>1.0.5.Final</version.org.wildfly.channel>
         <version.org.jboss.logging.slf4j-jboss-logging>1.2.1.Final</version.org.jboss.logging.slf4j-jboss-logging>
-        <version.org.wildfly.galleon-plugins>6.4.2.Final</version.org.wildfly.galleon-plugins>
+        <version.org.wildfly.galleon-plugins>6.5.3.Final</version.org.wildfly.galleon-plugins>
         <version.org.yaml.snakeyaml>2.0</version.org.yaml.snakeyaml>
         <version.org.apache.commons>3.12.0</version.org.apache.commons>
         <version.org.apache.maven.maven-core>3.6.2</version.org.apache.maven.maven-core>
@@ -389,7 +390,16 @@
                 <artifactId>slf4j-jboss-logging</artifactId>
                 <version>${version.org.jboss.logging.slf4j-jboss-logging}</version>
             </dependency>
-
+            <dependency>
+                <groupId>org.wildfly.channel</groupId>
+                <artifactId>channel-core</artifactId>
+                <version>${version.org.wildfly.channel}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.wildfly.channel</groupId>
+                <artifactId>maven-resolver</artifactId>
+                <version>${version.org.wildfly.channel}</version>
+            </dependency>
             <!-- Test dependencies -->
             <dependency>
                 <groupId>jakarta.annotation</groupId>
diff --git a/tests/run-cli-tests.sh b/tests/run-cli-tests.sh
index 13e72164..2674c2fe 100644
--- a/tests/run-cli-tests.sh
+++ b/tests/run-cli-tests.sh
@@ -80,6 +80,15 @@ if [ $? -ne 0 ]; then
     exit 1
 fi
 
+echo "* Show configuration cloud"
+
+java -jar $jar show-configuration --cloud
+
+if [ $? -ne 0 ]; then
+    echo "Error, check log"
+    exit 1
+fi
+
 echo kitchensink
 test \
 "[bean-validation, cdi, ee-integration, ejb-lite, h2-driver, jaxrs, jpa, jsf]==>ee-core-profile-server,ejb-lite,h2-driver,jaxrs,jpa,jsf" \