You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2024/04/03 14:49:18 UTC

(maven-gpg-plugin) branch master updated: [MGPG-120] New mojo sign-deployed (#88)

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-gpg-plugin.git


The following commit(s) were added to refs/heads/master by this push:
     new 50222d3  [MGPG-120] New mojo sign-deployed (#88)
50222d3 is described below

commit 50222d351ac12e746ce9921a957654e5e24a55de
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Wed Apr 3 16:49:12 2024 +0200

    [MGPG-120] New mojo sign-deployed (#88)
    
    New mojo, "sign-deployed" that is able to sign already deployed artifacts.
    Assuming there is no Maven project, hence mojo should not require project, just a list of artifacts.
    
    ---
    
    https://issues.apache.org/jira/browse/MGPG-120
---
 pom.xml                                            |   7 +
 src/it/sign-deployed/invoker.properties            |  19 ++
 .../org/foo/bar/1.0/bar-1.0-javadoc.jar            | Bin 0 -> 424 bytes
 .../org/foo/bar/1.0/bar-1.0-sources.jar            | Bin 0 -> 424 bytes
 .../remote-repo/org/foo/bar/1.0/bar-1.0-src.tar.gz | Bin 0 -> 357 bytes
 .../remote-repo/org/foo/bar/1.0/bar-1.0.jar        | Bin 0 -> 345 bytes
 .../remote-repo/org/foo/bar/1.0/bar-1.0.pom        |  34 +++
 .../remote-repo/org/foo/bar/1.0/bar-1.0.tar.gz     | Bin 0 -> 357 bytes
 .../remote-repo/org/foo/bar/1.0/bar-1.0.zip        | Bin 0 -> 345 bytes
 src/it/sign-deployed/test.properties               |  20 ++
 src/it/sign-deployed/verify.groovy                 |  39 ++++
 .../maven/plugins/gpg/ArtifactCollectorSPI.java    |  41 ++++
 .../maven/plugins/gpg/SignAndDeployFileMojo.java   |   2 +-
 .../apache/maven/plugins/gpg/SignDeployedMojo.java | 252 +++++++++++++++++++++
 14 files changed, 413 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index a60987f..068e024 100644
--- a/pom.xml
+++ b/pom.xml
@@ -123,6 +123,13 @@ under the License.
       <version>${resolverVersion}</version>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-util</artifactId>
+      <version>${resolverVersion}</version>
+      <!-- This is needed to be in compile to work with Maven pre 3.9 -->
+      <scope>compile</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.maven.plugin-tools</groupId>
       <artifactId>maven-plugin-annotations</artifactId>
diff --git a/src/it/sign-deployed/invoker.properties b/src/it/sign-deployed/invoker.properties
new file mode 100644
index 0000000..ef0c219
--- /dev/null
+++ b/src/it/sign-deployed/invoker.properties
@@ -0,0 +1,19 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:sign-deployed
+invoker.environmentVariables.MAVEN_GPG_PASSPHRASE = TEST
diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-javadoc.jar b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-javadoc.jar
new file mode 100644
index 0000000..d53f442
Binary files /dev/null and b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-javadoc.jar differ
diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-sources.jar b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-sources.jar
new file mode 100644
index 0000000..9ede928
Binary files /dev/null and b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-sources.jar differ
diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-src.tar.gz b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-src.tar.gz
new file mode 100644
index 0000000..2169f44
Binary files /dev/null and b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-src.tar.gz differ
diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.jar b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.jar
new file mode 100644
index 0000000..8e8f7f4
Binary files /dev/null and b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.jar differ
diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.pom b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.pom
new file mode 100644
index 0000000..a7bb7d6
--- /dev/null
+++ b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.pom
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.gpg.sadfs</groupId>
+  <artifactId>test</artifactId>
+  <version>1.0</version>
+  <packaging>jar</packaging>
+
+  <name>MGPG-12</name>
+  <description>
+    Tests the signing and deployment of a simple release JAR along with its POM.
+  </description>
+</project>
diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.tar.gz b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.tar.gz
new file mode 100644
index 0000000..ad736b0
Binary files /dev/null and b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.tar.gz differ
diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.zip b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.zip
new file mode 100644
index 0000000..226277c
Binary files /dev/null and b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.zip differ
diff --git a/src/it/sign-deployed/test.properties b/src/it/sign-deployed/test.properties
new file mode 100644
index 0000000..d30aa15
--- /dev/null
+++ b/src/it/sign-deployed/test.properties
@@ -0,0 +1,20 @@
+# 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.
+
+url = file://remote-repo
+repositoryId = staging-1
+artifacts = org.foo:bar:pom:1.0,org.foo:bar:jar:1.0,org.foo:bar:zip:1.0,org.foo:bar:tar.gz:1.0,org.foo:bar:tar.gz:src:1.0
diff --git a/src/it/sign-deployed/verify.groovy b/src/it/sign-deployed/verify.groovy
new file mode 100644
index 0000000..383fb46
--- /dev/null
+++ b/src/it/sign-deployed/verify.groovy
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+var artifactDir = new File(basedir, "remote-repo/org/foo/bar/1.0")
+
+var expectedFiles = [
+    "bar-1.0.jar.asc",
+    "bar-1.0-sources.jar.asc",
+    "bar-1.0-javadoc.jar.asc",
+    "bar-1.0.pom.asc",
+    "bar-1.0.zip.asc",
+    "bar-1.0-src.tar.gz.asc",
+    "bar-1.0.tar.gz.asc"
+]
+
+for (String expectedFile : expectedFiles) {
+    var file = new File(artifactDir, expectedFile)
+
+    println "Checking for existence of $file"
+
+    if (!file.isFile()) {
+        throw new Exception("Missing file $file")
+    }
+}
diff --git a/src/main/java/org/apache/maven/plugins/gpg/ArtifactCollectorSPI.java b/src/main/java/org/apache/maven/plugins/gpg/ArtifactCollectorSPI.java
new file mode 100644
index 0000000..0c0419e
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/gpg/ArtifactCollectorSPI.java
@@ -0,0 +1,41 @@
+/*
+ * 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.apache.maven.plugins.gpg;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Artifact collector SPI, that collects artifacts in some way from given {@link RemoteRepository}.
+ *
+ * @since 3.2.3
+ */
+public interface ArtifactCollectorSPI {
+    /**
+     * Returns collected artifacts or {@code null} if collection was not possible for any reason.
+     * <p>
+     * Collector should collect only <em>relevant artifacts</em>, those that are subject to signing.
+     */
+    Collection<Artifact> collectArtifacts(RepositorySystemSession session, RemoteRepository remoteRepository)
+            throws IOException;
+}
diff --git a/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java b/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
index 12b191f..e4853fc 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
@@ -58,7 +58,7 @@ import org.eclipse.aether.deployment.DeploymentException;
 import org.eclipse.aether.repository.RemoteRepository;
 
 /**
- * Signs artifacts and installs the artifact in the remote repository.
+ * Signs artifacts and deploys the artifacts and signatures in the remote repository.
  *
  * @author Daniel Kulp
  * @since 1.0-beta-4
diff --git a/src/main/java/org/apache/maven/plugins/gpg/SignDeployedMojo.java b/src/main/java/org/apache/maven/plugins/gpg/SignDeployedMojo.java
new file mode 100644
index 0000000..a05c230
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/gpg/SignDeployedMojo.java
@@ -0,0 +1,252 @@
+/*
+ * 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.apache.maven.plugins.gpg;
+
+import javax.inject.Inject;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.codehaus.plexus.util.FileUtils;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeploymentException;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.util.artifact.SubArtifact;
+
+/**
+ * Resolves given artifacts from a given remote repository, signs them, and deploys the signatures next to signed
+ * artifacts, and cleans up afterward. This mojo will use "own" local repository for all the operations to not
+ * "pollute" user local repository, and also to be able to fully clean up (delete) after job done.
+ *
+ * @since 3.2.3
+ */
+@Mojo(name = "sign-deployed", requiresProject = false, threadSafe = true)
+public class SignDeployedMojo extends AbstractGpgMojo {
+
+    /**
+     * URL where the artifacts are deployed.
+     */
+    @Parameter(property = "url", required = true)
+    private String url;
+
+    /**
+     * Server ID to map on the &lt;id&gt; under &lt;server&gt; section of <code>settings.xml</code>. In most cases, this
+     * parameter will be required for authentication.
+     */
+    @Parameter(property = "repositoryId", required = true)
+    private String repositoryId;
+
+    /**
+     * Should generate coordinates "javadoc" sub-artifacts?
+     */
+    @Parameter(property = "javadoc", defaultValue = "true", required = true)
+    private boolean javadoc;
+
+    /**
+     * Should generate coordinates "sources" sub-artifacts?
+     */
+    @Parameter(property = "sources", defaultValue = "true", required = true)
+    private boolean sources;
+
+    /**
+     * If no {@link ArtifactCollectorSPI} is added, this Mojo will fall back to this parameter to collect GAVs that are
+     * deployed and needs signatures deployed next to them. This parameter can contain multiple things:
+     * <ul>
+     *     <li>A path to an existing file, that contains one GAV spec at a line. File may also contain empty lines or
+     *     lines starting with {@code #} that will be ignored.</li>
+     *     <li>A comma separated list of GAV specs.</li>
+     * </ul>
+     * <p>
+     * Note: format of GAV entries must be {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>}.
+     */
+    @Parameter(property = "artifacts")
+    private String artifacts;
+
+    @Component
+    private RepositorySystem repositorySystem;
+
+    @Inject
+    private Map<String, ArtifactCollectorSPI> artifactCollectors;
+
+    @Override
+    protected void doExecute() throws MojoExecutionException, MojoFailureException {
+        if (settings.isOffline()) {
+            throw new MojoFailureException("Cannot deploy artifacts when Maven is in offline mode");
+        }
+
+        Path tempDirectory = null;
+        Set<Artifact> artifacts = new HashSet<>();
+        try {
+            tempDirectory = Files.createTempDirectory("gpg-sign-deployed");
+            getLog().debug("Using temp directory " + tempDirectory);
+
+            DefaultRepositorySystemSession signingSession =
+                    new DefaultRepositorySystemSession(session.getRepositorySession());
+            signingSession.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(
+                    signingSession, new LocalRepository(tempDirectory.toFile())));
+
+            // remote repo where deployed artifacts are, and where signatures need to be deployed
+            RemoteRepository deploymentRepository = repositorySystem.newDeploymentRepository(
+                    signingSession, new RemoteRepository.Builder(repositoryId, "default", url).build());
+
+            // get artifacts list
+            getLog().debug("Collecting artifacts for signing...");
+            artifacts.addAll(collectArtifacts(signingSession, deploymentRepository));
+            getLog().info("Collected " + artifacts.size() + " artifact" + ((artifacts.size() > 1) ? "s" : "")
+                    + " for signing");
+
+            // create additional ones if needed
+            if (sources || javadoc) {
+                getLog().debug("Adding additional artifacts...");
+                List<Artifact> additions = new ArrayList<>();
+                for (Artifact artifact : artifacts) {
+                    if (artifact.getClassifier().isEmpty()) {
+                        if (sources) {
+                            additions.add(new SubArtifact(artifact, "sources", "jar"));
+                        }
+                        if (javadoc) {
+                            additions.add(new SubArtifact(artifact, "javadoc", "jar"));
+                        }
+                    }
+                }
+                artifacts.addAll(additions);
+            }
+
+            // resolve them all
+            getLog().info("Resolving " + artifacts.size() + " artifact" + ((artifacts.size() > 1) ? "s" : "")
+                    + " artifacts for signing...");
+            List<ArtifactResult> results = repositorySystem.resolveArtifacts(
+                    signingSession,
+                    artifacts.stream()
+                            .map(a -> new ArtifactRequest(a, Collections.singletonList(deploymentRepository), "gpg"))
+                            .collect(Collectors.toList()));
+            artifacts = results.stream().map(ArtifactResult::getArtifact).collect(Collectors.toSet());
+
+            // sign all
+            AbstractGpgSigner signer = newSigner(null);
+            signer.setOutputDirectory(tempDirectory.toFile());
+            getLog().info("Signer '" + signer.signerName() + "' is signing " + artifacts.size() + " file"
+                    + ((artifacts.size() > 1) ? "s" : "") + " with key " + signer.getKeyInfo());
+
+            HashSet<Artifact> signatures = new HashSet<>();
+            for (Artifact a : artifacts) {
+                signatures.add(new DefaultArtifact(
+                                a.getGroupId(),
+                                a.getArtifactId(),
+                                a.getClassifier(),
+                                a.getExtension() + AbstractGpgSigner.SIGNATURE_EXTENSION,
+                                a.getVersion())
+                        .setFile(signer.generateSignatureForArtifact(a.getFile())));
+            }
+
+            // deploy all signature
+            getLog().info("Deploying artifact signatures...");
+            repositorySystem.deploy(
+                    signingSession,
+                    new DeployRequest()
+                            .setRepository(deploymentRepository)
+                            .setArtifacts(signatures)
+                            .setTrace(RequestTrace.newChild(null, this)));
+        } catch (IOException e) {
+            throw new MojoExecutionException("IO error: " + e.getMessage(), e);
+        } catch (ArtifactResolutionException e) {
+            throw new MojoExecutionException(
+                    "Error resolving deployed artifacts " + artifacts + ": " + e.getMessage(), e);
+        } catch (DeploymentException e) {
+            throw new MojoExecutionException("Error deploying signatures: " + e.getMessage(), e);
+        } finally {
+            if (tempDirectory != null) {
+                getLog().info("Cleaning up...");
+                try {
+                    FileUtils.deleteDirectory(tempDirectory.toFile());
+                } catch (IOException e) {
+                    getLog().warn("Could not clean up temp directory " + tempDirectory);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns a collection of remotely deployed artifacts that needs to be signed and have signatures deployed
+     * next to them.
+     */
+    protected Collection<Artifact> collectArtifacts(RepositorySystemSession session, RemoteRepository remoteRepository)
+            throws IOException {
+        Collection<Artifact> result = null;
+        for (ArtifactCollectorSPI artifactCollector : artifactCollectors.values()) {
+            result = artifactCollector.collectArtifacts(session, remoteRepository);
+            if (result != null) {
+                break;
+            }
+        }
+        if (result == null) {
+            if (artifacts != null) {
+                try {
+                    Path path = Paths.get(artifacts);
+                    if (Files.isRegularFile(path)) {
+                        try (Stream<String> lines = Files.lines(path)) {
+                            result = lines.filter(l -> !l.isEmpty() && !l.startsWith("#"))
+                                    .map(DefaultArtifact::new)
+                                    .collect(Collectors.toSet());
+                        }
+                    }
+                } catch (InvalidPathException e) {
+                    // ignore
+                }
+                if (result == null) {
+                    result = Arrays.stream(artifacts.split(","))
+                            .map(DefaultArtifact::new)
+                            .collect(Collectors.toSet());
+                }
+            }
+        }
+        if (result == null) {
+            throw new IllegalStateException("No source to collect from (set -Dartifacts=g:a:v... or add collector)");
+        }
+        return result;
+    }
+}