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

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

commit b6b5a0da8a2372b82f2861b35e354234c02de2d6
Author: Sylwester Lachiewicz <[email protected]>
AuthorDate: Wed Oct 28 01:27:15 2026 +0000

    Group dependencies by SPDX license so templates list each license once 
(#316)
---
 .../project/src/main/resources/DEPENDENCIES.vm     | 10 ++++
 .../plugin/resources/remote/SpdxLicenses.java      | 66 ++++++++++++++++++++++
 src/site/markdown/usage.md.vm                      | 15 +++++
 .../plugin/resources/remote/SpdxLicensesTest.java  | 23 ++++++++
 .../resources/remote/it/ITSpdxLicenseId.java       |  7 +++
 5 files changed, 121 insertions(+)

diff --git 
a/src/it/resources/spdx-license-id/project/src/main/resources/DEPENDENCIES.vm 
b/src/it/resources/spdx-license-id/project/src/main/resources/DEPENDENCIES.vm
index 3ab3930..3d3323b 100644
--- 
a/src/it/resources/spdx-license-id/project/src/main/resources/DEPENDENCIES.vm
+++ 
b/src/it/resources/spdx-license-id/project/src/main/resources/DEPENDENCIES.vm
@@ -22,3 +22,13 @@ $dependency.artifactId expression: 
$!spdx.expression($dependency)
 $dependency.artifactId: #if( $id )$id#else$license.name#end
 #end
 #end
+#foreach( $entry in $spdx.group($projects).entrySet() )
+Group $entry.key:#foreach( $dependency in $entry.value ) 
$dependency.artifactId#end
+
+#end
+#foreach( $id in $spdx.ids($projects) )
+License text: $id
+#end
+#foreach( $dependency in $spdx.unresolved($projects) )
+Unresolved: $dependency.artifactId
+#end
diff --git 
a/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java 
b/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java
index a6e980a..9dfbc2b 100644
--- a/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java
+++ b/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java
@@ -32,6 +32,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -50,6 +52,10 @@ import org.apache.maven.project.MavenProject;
  * <p>
  * A project that declares several licenses may be used under any one of them, 
so
  * <code>$!spdx.expression($project)</code> joins their IDs with 
<code>OR</code>.
+ * <p>
+ * To list each license once, <code>$spdx.group($projects)</code> groups 
dependencies by SPDX expression,
+ * <code>$spdx.ids($projects)</code> returns every ID they use, and 
<code>$spdx.unresolved($projects)</code>
+ * returns the dependencies that match nothing.
  *
  * @since 3.4.0
  */
@@ -165,6 +171,66 @@ public class SpdxLicenses {
         return result.toString();
     }
 
+    /**
+     * Groups projects by their SPDX license expression, leaving out the 
projects whose expression is unknown.
+     *
+     * @param projects projects, typically <code>$projects</code>
+     * @return the projects for each expression, sorted by expression
+     */
+    public Map<String, List<MavenProject>> group(List<MavenProject> projects) {
+        Map<String, List<MavenProject>> groups = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        if (projects != null) {
+            for (MavenProject project : projects) {
+                String expression = expression(project);
+                if (expression != null) {
+                    groups.computeIfAbsent(expression, k -> new 
ArrayList<>()).add(project);
+                }
+            }
+        }
+        return groups;
+    }
+
+    /**
+     * Returns every SPDX license and exception ID that the projects' 
expressions use, each one once, so that a
+     * template can add each license text once.
+     *
+     * @param projects projects, typically <code>$projects</code>
+     * @return the IDs, sorted
+     */
+    public Set<String> ids(List<MavenProject> projects) {
+        Set<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+        for (String expression : group(projects).keySet()) {
+            Matcher m = EXPRESSION_TOKEN.matcher(expression);
+            while (m.find()) {
+                String token = m.group();
+                if (!token.equalsIgnoreCase("AND")
+                        && !token.equalsIgnoreCase("OR")
+                        && !token.equalsIgnoreCase("WITH")) {
+                    result.add(token);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns the projects whose SPDX license expression is unknown.
+     *
+     * @param projects projects, typically <code>$projects</code>
+     * @return the projects that declare no license or a license that matches 
nothing
+     */
+    public List<MavenProject> unresolved(List<MavenProject> projects) {
+        List<MavenProject> result = new ArrayList<>();
+        if (projects != null) {
+            for (MavenProject project : projects) {
+                if (expression(project) == null) {
+                    result.add(project);
+                }
+            }
+        }
+        return result;
+    }
+
     /**
      * Returns the SPDX license ID, or SPDX license expression, for a declared 
license name and URL.
      *
diff --git a/src/site/markdown/usage.md.vm b/src/site/markdown/usage.md.vm
index ded24d3..20315aa 100644
--- a/src/site/markdown/usage.md.vm
+++ b/src/site/markdown/usage.md.vm
@@ -131,6 +131,21 @@ A mapping matches a spelling wherever it appears, like 
`licenseMerges` in the [L
 
 A dependency that declares several licenses may be used under any one of them. 
`${esc.d}spdx.expression(${esc.d}dependency)` returns one SPDX license 
expression for the whole dependency, joining the IDs with `OR`, for example 
`CDDL-1.1 OR (GPL-2.0-only WITH Classpath-exception-2.0)`. It returns null when 
the dependency declares no license or when one of its licenses matches nothing, 
because a partial expression would leave out a choice.
 
+To list each license once rather than once per dependency, group the 
dependencies by SPDX expression. `${esc.d}spdx.ids(${esc.d}projects)` returns 
every license and exception ID in use, each one once, so a template can add 
each license text once. `${esc.d}spdx.unresolved(${esc.d}projects)` returns the 
dependencies that `${esc.d}spdx` could not resolve, which still need their 
declared licenses listed:
+
+```
+#[[#foreach( $group in $spdx.group($projects).entrySet() )
+$group.key:
+#foreach( $dependency in $group.value )
+  - $dependency.name ($dependency.groupId:$dependency.artifactId)
+#end
+#end
+#foreach( $dependency in $spdx.unresolved($projects) )
+$dependency.name: #foreach( $license in $dependency.licenses )$license.name 
#end
+
+#end]]#
+```
+
 Running Once in a Multi-Module Build
 ------------------------------------
 
diff --git 
a/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java 
b/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java
index f8296d6..47fcb51 100644
--- 
a/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java
+++ 
b/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java
@@ -19,6 +19,8 @@
 package org.apache.maven.plugin.resources.remote;
 
 import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
 
 import org.apache.maven.model.License;
 import org.apache.maven.model.Model;
@@ -126,6 +128,27 @@ class SpdxLicensesTest {
         assertNull(spdx.expression(null));
     }
 
+    @Test
+    void groupsListEachLicenseOnce() {
+        MavenProject a = project(license("Apache License, Version 2.0", null));
+        MavenProject b = project(license("The Apache Software License, Version 
2.0", null));
+        MavenProject c = project(license("CDDL 1.1", null), license("GPL2 w/ 
CPE", null));
+        MavenProject d = project(license("Public Domain", null));
+        MavenProject e = project(license("Apache-2.0 OR MIT", null));
+        List<MavenProject> projects = Arrays.asList(a, b, c, d, e);
+
+        Map<String, List<MavenProject>> groups = spdx.group(projects);
+        assertEquals(
+                Arrays.asList(
+                        "Apache-2.0", "Apache-2.0 OR MIT", "CDDL-1.1 OR 
(GPL-2.0-only WITH Classpath-exception-2.0)"),
+                Arrays.asList(groups.keySet().toArray()));
+        assertEquals(Arrays.asList(a, b), groups.get("Apache-2.0"));
+        assertEquals(
+                Arrays.asList("Apache-2.0", "CDDL-1.1", 
"Classpath-exception-2.0", "GPL-2.0-only", "MIT"),
+                Arrays.asList(spdx.ids(projects).toArray()));
+        assertEquals(Arrays.asList(d), spdx.unresolved(projects));
+    }
+
     private static License license(String name, String url) {
         License license = new License();
         license.setName(name);
diff --git 
a/src/test/java/org/apache/maven/plugin/resources/remote/it/ITSpdxLicenseId.java
 
b/src/test/java/org/apache/maven/plugin/resources/remote/it/ITSpdxLicenseId.java
index c99e11f..7b03e2c 100644
--- 
a/src/test/java/org/apache/maven/plugin/resources/remote/it/ITSpdxLicenseId.java
+++ 
b/src/test/java/org/apache/maven/plugin/resources/remote/it/ITSpdxLicenseId.java
@@ -27,6 +27,7 @@ import 
org.apache.maven.plugin.resources.remote.it.support.TestUtils;
 import org.codehaus.plexus.util.FileUtils;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class ITSpdxLicenseId extends AbstractIT {
@@ -60,5 +61,11 @@ public class ITSpdxLicenseId extends AbstractIT {
                 content.contains("e-dual expression: CDDL-1.1 OR (GPL-2.0-only 
WITH Classpath-exception-2.0)\n"),
                 content);
         assertTrue(content.contains("f-mapped: BSD-3-Clause\n"), content);
+
+        assertTrue(content.contains("Group Apache-2.0: a-name b-url\n"), 
content);
+        assertTrue(content.contains("Group (Apache-2.0 OR EPL-2.0): 
c-expression\n"), content);
+        assertTrue(content.contains("Unresolved: d-unknown\n"), content);
+        assertEquals(1, content.split("License text: Apache-2.0\n", -1).length 
- 1, content);
+        assertTrue(content.contains("License text: 
Classpath-exception-2.0\n"), content);
     }
 }

Reply via email to