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 c87a5123dc8c42c0e35757873fc66bb28d117b51
Author: Sylwester Lachiewicz <[email protected]>
AuthorDate: Wed Oct 28 01:12:34 2026 +0000

    Expose SPDX license IDs to resource templates (#316)
    
    The alias table is our own, not a copy of the SPDX License List, which is
    published without a licence that clears it for an ASF source release.
    GNU URLs stay unmapped: they do not say "only" or "or later".
---
 src/it/resources/spdx-license-id/a-name/pom.xml    |  41 ++++
 src/it/resources/spdx-license-id/b-url/pom.xml     |  42 ++++
 .../resources/spdx-license-id/c-expression/pom.xml |  41 ++++
 src/it/resources/spdx-license-id/d-unknown/pom.xml |  41 ++++
 src/it/resources/spdx-license-id/e-dual/pom.xml    |  46 ++++
 src/it/resources/spdx-license-id/pom.xml           |  44 ++++
 src/it/resources/spdx-license-id/project/pom.xml   |  87 ++++++++
 .../project/src/main/resources/DEPENDENCIES.vm     |  24 +++
 .../remote/AbstractProcessRemoteResourcesMojo.java |   4 +
 .../plugin/resources/remote/SpdxLicenses.java      | 239 +++++++++++++++++++++
 .../plugin/resources/remote/license-aliases.txt    | 167 ++++++++++++++
 src/site/markdown/usage.md.vm                      |  18 ++
 .../plugin/resources/remote/SpdxLicensesTest.java  | 134 ++++++++++++
 .../resources/remote/it/ITSpdxLicenseId.java       |  63 ++++++
 14 files changed, 991 insertions(+)

diff --git a/src/it/resources/spdx-license-id/a-name/pom.xml 
b/src/it/resources/spdx-license-id/a-name/pom.xml
new file mode 100644
index 0000000..61bfedf
--- /dev/null
+++ b/src/it/resources/spdx-license-id/a-name/pom.xml
@@ -0,0 +1,41 @@
+<?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>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+  <artifactId>a-name</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+    </license>
+  </licenses>
+
+  <distributionManagement>
+    <repository>
+      <id>it-remote</id>
+      <url>file:${remoteRepository}</url>
+    </repository>
+  </distributionManagement>
+</project>
diff --git a/src/it/resources/spdx-license-id/b-url/pom.xml 
b/src/it/resources/spdx-license-id/b-url/pom.xml
new file mode 100644
index 0000000..c460412
--- /dev/null
+++ b/src/it/resources/spdx-license-id/b-url/pom.xml
@@ -0,0 +1,42 @@
+<?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>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+  <artifactId>b-url</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <licenses>
+    <license>
+      <name>Apache License</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </license>
+  </licenses>
+
+  <distributionManagement>
+    <repository>
+      <id>it-remote</id>
+      <url>file:${remoteRepository}</url>
+    </repository>
+  </distributionManagement>
+</project>
diff --git a/src/it/resources/spdx-license-id/c-expression/pom.xml 
b/src/it/resources/spdx-license-id/c-expression/pom.xml
new file mode 100644
index 0000000..6b8ec0b
--- /dev/null
+++ b/src/it/resources/spdx-license-id/c-expression/pom.xml
@@ -0,0 +1,41 @@
+<?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>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+  <artifactId>c-expression</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <licenses>
+    <license>
+      <name>(Apache-2.0 OR EPL-2.0)</name>
+    </license>
+  </licenses>
+
+  <distributionManagement>
+    <repository>
+      <id>it-remote</id>
+      <url>file:${remoteRepository}</url>
+    </repository>
+  </distributionManagement>
+</project>
diff --git a/src/it/resources/spdx-license-id/d-unknown/pom.xml 
b/src/it/resources/spdx-license-id/d-unknown/pom.xml
new file mode 100644
index 0000000..ca07cad
--- /dev/null
+++ b/src/it/resources/spdx-license-id/d-unknown/pom.xml
@@ -0,0 +1,41 @@
+<?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>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+  <artifactId>d-unknown</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <licenses>
+    <license>
+      <name>Public Domain</name>
+    </license>
+  </licenses>
+
+  <distributionManagement>
+    <repository>
+      <id>it-remote</id>
+      <url>file:${remoteRepository}</url>
+    </repository>
+  </distributionManagement>
+</project>
diff --git a/src/it/resources/spdx-license-id/e-dual/pom.xml 
b/src/it/resources/spdx-license-id/e-dual/pom.xml
new file mode 100644
index 0000000..38f522c
--- /dev/null
+++ b/src/it/resources/spdx-license-id/e-dual/pom.xml
@@ -0,0 +1,46 @@
+<?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>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+  <artifactId>e-dual</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <licenses>
+    <license>
+      <name>CDDL 1.1</name>
+      <url>https://glassfish.java.net/public/CDDL+GPL_1_1.html</url>
+    </license>
+    <license>
+      <name>GPL2 w/ CPE</name>
+      <url>https://glassfish.java.net/public/CDDL+GPL_1_1.html</url>
+    </license>
+  </licenses>
+
+  <distributionManagement>
+    <repository>
+      <id>it-remote</id>
+      <url>file:${remoteRepository}</url>
+    </repository>
+  </distributionManagement>
+</project>
diff --git a/src/it/resources/spdx-license-id/pom.xml 
b/src/it/resources/spdx-license-id/pom.xml
new file mode 100644
index 0000000..cbf7256
--- /dev/null
+++ b/src/it/resources/spdx-license-id/pom.xml
@@ -0,0 +1,44 @@
+<?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/maven-v4_0_0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+  <artifactId>aggregator</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <modules>
+    <module>a-name</module>
+    <module>b-url</module>
+    <module>c-expression</module>
+    <module>d-unknown</module>
+    <module>e-dual</module>
+  </modules>
+
+  <distributionManagement>
+    <repository>
+      <id>it-remote</id>
+      <url>file:${remoteRepository}</url>
+    </repository>
+  </distributionManagement>
+</project>
diff --git a/src/it/resources/spdx-license-id/project/pom.xml 
b/src/it/resources/spdx-license-id/project/pom.xml
new file mode 100644
index 0000000..78227d9
--- /dev/null
+++ b/src/it/resources/spdx-license-id/project/pom.xml
@@ -0,0 +1,87 @@
+<?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/maven-v4_0_0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.plugin.rresource.it</groupId>
+    <artifactId>it-parent</artifactId>
+    <version>${project.version}</version>
+    <relativePath>../../it-parent/pom.xml</relativePath>
+  </parent>
+
+  <artifactId>spdx-license-id</artifactId>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+      <artifactId>a-name</artifactId>
+      <version>1.0</version>
+      <type>pom</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+      <artifactId>b-url</artifactId>
+      <version>1.0</version>
+      <type>pom</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+      <artifactId>c-expression</artifactId>
+      <version>1.0</version>
+      <type>pom</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+      <artifactId>d-unknown</artifactId>
+      <version>1.0</version>
+      <type>pom</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.plugin.rresource.it.spdx</groupId>
+      <artifactId>e-dual</artifactId>
+      <version>1.0</version>
+      <type>pom</type>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-remote-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>remote-resources</id>
+            <goals>
+              <goal>process</goal>
+            </goals>
+            <configuration>
+              <resourceBundles>
+                
<resourceBundle>org.apache.maven.plugin.rresource.it:resource-bundle-with-template:${project.version}</resourceBundle>
+              </resourceBundles>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
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
new file mode 100644
index 0000000..3ab3930
--- /dev/null
+++ 
b/src/it/resources/spdx-license-id/project/src/main/resources/DEPENDENCIES.vm
@@ -0,0 +1,24 @@
+## 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.
+Override: #if ($projectName)$projectName#else${project.name}#end
+#foreach( $dependency in $projects )
+$dependency.artifactId expression: $!spdx.expression($dependency)
+#foreach( $license in $dependency.licenses )
+#set( $id = $spdx.id($license) )
+$dependency.artifactId: #if( $id )$id#else$license.name#end
+#end
+#end
diff --git 
a/src/main/java/org/apache/maven/plugin/resources/remote/AbstractProcessRemoteResourcesMojo.java
 
b/src/main/java/org/apache/maven/plugin/resources/remote/AbstractProcessRemoteResourcesMojo.java
index 755189a..1e4caf3 100644
--- 
a/src/main/java/org/apache/maven/plugin/resources/remote/AbstractProcessRemoteResourcesMojo.java
+++ 
b/src/main/java/org/apache/maven/plugin/resources/remote/AbstractProcessRemoteResourcesMojo.java
@@ -230,6 +230,9 @@ public abstract class AbstractProcessRemoteResourcesMojo 
extends AbstractMojo {
      * <li><code>projectsSortedByOrganization</code> - the list of dependency 
projects sorted by organization</li>
      * <li><code>projectTimespan</code> - the timespan of the current project 
(requires inceptionYear in pom)</li>
      * <li><code>locator</code> - the ResourceManager that can be used to 
retrieve additional resources</li>
+     * <li><code>spdx</code> - resolves a dependency license to an SPDX 
license ID, for example
+     * <code>$!spdx.id($license)</code>, or a dependency to an SPDX 
expression, for example
+     * <code>$!spdx.expression($dependency)</code> (since 3.4.0)</li>
      * </ul>
      * See <a
      * 
href="https://maven.apache.org/ref/current/maven-project/apidocs/org/apache/maven/project/MavenProject.html";>
 the
@@ -806,6 +809,7 @@ public abstract class AbstractProcessRemoteResourcesMojo 
extends AbstractMojo {
         context.put("project", project);
         context.put("presentYear", year);
         context.put("locator", locator);
+        context.put("spdx", new SpdxLicenses());
 
         if (inceptionYear.equals(year)) {
             context.put("projectTimespan", year);
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
new file mode 100644
index 0000000..c3d9a96
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java
@@ -0,0 +1,239 @@
+/*
+ * 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.plugin.resources.remote;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.maven.model.License;
+import org.apache.maven.project.MavenProject;
+
+/**
+ * Resolves a license declared in a POM to an <a 
href="https://spdx.org/licenses/";>SPDX license ID</a>.
+ * Available in templates as <code>$spdx</code>, for example 
<code>$!spdx.id($license)</code>.
+ * <p>
+ * The declared name is tried first: as a known SPDX ID, as an SPDX license 
expression made of known IDs,
+ * and as a known alias. The declared URL is tried next: as a known alias, and 
as an <code>opensource.org</code>
+ * or <code>spdx.org</code> URL that ends with a known ID. When the name and 
the URL resolve to different IDs,
+ * nothing matches. The known IDs and aliases cover the licenses that POMs 
declare most often; a deprecated
+ * SPDX ID such as <code>GPL-2.0</code> is returned as declared.
+ * <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>.
+ *
+ * @since 3.4.0
+ */
+public class SpdxLicenses {
+
+    private static final String RESOURCE = "license-aliases.txt";
+
+    private static final Pattern EXPRESSION_TOKEN = 
Pattern.compile("[^\\s()]+");
+
+    private static final Pattern ID_URL =
+            
Pattern.compile("(?:opensource\\.org/licenses?|spdx\\.org/licenses)/([^/?]+)");
+
+    /** Lower-case ID to ID. */
+    private final Map<String, String> ids = new HashMap<>();
+
+    /** Normalised name to ID or expression. */
+    private final Map<String, String> names = new HashMap<>();
+
+    /** Normalised URL to ID or expression. */
+    private final Map<String, String> urls = new HashMap<>();
+
+    SpdxLicenses() {
+        InputStream in = SpdxLicenses.class.getResourceAsStream(RESOURCE);
+        if (in == null) {
+            throw new IllegalStateException("Missing resource " + RESOURCE);
+        }
+        try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(in, StandardCharsets.UTF_8))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (line.trim().isEmpty() || line.startsWith("#")) {
+                    continue;
+                }
+                String[] fields = line.split("\t", 2);
+                String id = fields[0].trim();
+                if (id.indexOf(' ') < 0) {
+                    ids.put(id.toLowerCase(Locale.ROOT), id);
+                }
+                if (fields.length > 1) {
+                    String alias = fields[1].trim();
+                    if (alias.startsWith("http://";) || 
alias.startsWith("https://";)) {
+                        put(urls, normaliseUrl(alias), id);
+                    } else {
+                        put(names, normaliseName(alias), id);
+                    }
+                }
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private static void put(Map<String, String> map, String key, String id) {
+        String previous = map.put(key, id);
+        if (previous != null && !previous.equals(id)) {
+            throw new IllegalStateException(RESOURCE + " maps '" + key + "' to 
both " + previous + " and " + id);
+        }
+    }
+
+    /**
+     * Returns the SPDX license ID, or SPDX license expression, of a declared 
license.
+     *
+     * @param license a license from a project model
+     * @return the SPDX ID or expression, or {@code null} when nothing matches
+     */
+    public String id(License license) {
+        return license == null ? null : id(license.getName(), 
license.getUrl());
+    }
+
+    /**
+     * Returns the SPDX license expression of a project: the ID of its 
license, or the IDs of its licenses
+     * joined with <code>OR</code>, since a project that declares several 
licenses may be used under any one
+     * of them.
+     *
+     * @param project a project, typically a dependency
+     * @return the SPDX expression, or {@code null} when the project declares 
no license or one of its
+     *         licenses matches nothing
+     */
+    public String expression(MavenProject project) {
+        List<License> licenses = project == null ? null : 
project.getLicenses();
+        if (licenses == null || licenses.isEmpty()) {
+            return null;
+        }
+        Set<String> choices = new LinkedHashSet<>();
+        for (License license : licenses) {
+            String id = id(license);
+            if (id == null) {
+                return null;
+            }
+            choices.add(id);
+        }
+        if (choices.size() == 1) {
+            return choices.iterator().next();
+        }
+        StringBuilder result = new StringBuilder();
+        for (String id : choices) {
+            if (result.length() > 0) {
+                result.append(" OR ");
+            }
+            boolean compound = id.indexOf(' ') >= 0 && !id.startsWith("(");
+            result.append(compound ? "(" + id + ")" : id);
+        }
+        return result.toString();
+    }
+
+    /**
+     * Returns the SPDX license ID, or SPDX license expression, for a declared 
license name and URL.
+     *
+     * @param name the declared license name, may be {@code null}
+     * @param url the declared license URL, may be {@code null}
+     * @return the SPDX ID or expression, or {@code null} when nothing matches
+     */
+    public String id(String name, String url) {
+        String byName = isBlank(name) ? null : idOfName(name.trim());
+        String byUrl = isBlank(url) ? null : idOfUrl(normaliseUrl(url));
+        if (byName == null) {
+            return byUrl;
+        }
+        return byUrl == null || byUrl.equals(byName) ? byName : null;
+    }
+
+    private String idOfName(String name) {
+        String id = ids.get(name.toLowerCase(Locale.ROOT));
+        if (id == null) {
+            id = parseExpression(name);
+        }
+        if (id == null) {
+            id = names.get(normaliseName(name));
+        }
+        return id;
+    }
+
+    private String idOfUrl(String url) {
+        String id = urls.get(url);
+        if (id == null) {
+            Matcher m = ID_URL.matcher(url);
+            if (m.matches()) {
+                id = ids.get(m.group(1));
+            }
+        }
+        return id;
+    }
+
+    /**
+     * Returns the expression with each ID in its SPDX spelling, or {@code 
null} unless it combines at least
+     * two known IDs with <code>AND</code>, <code>OR</code> or 
<code>WITH</code>.
+     */
+    private String parseExpression(String declared) {
+        Matcher m = EXPRESSION_TOKEN.matcher(declared);
+        StringBuffer result = new StringBuffer();
+        int operators = 0;
+        while (m.find()) {
+            String token = m.group();
+            String upper = token.toUpperCase(Locale.ROOT);
+            if (upper.equals("AND") || upper.equals("OR") || 
upper.equals("WITH")) {
+                operators++;
+                m.appendReplacement(result, upper);
+                continue;
+            }
+            String id = ids.get(token.toLowerCase(Locale.ROOT));
+            if (id == null) {
+                return null;
+            }
+            m.appendReplacement(result, Matcher.quoteReplacement(id));
+        }
+        m.appendTail(result);
+        return operators > 0 ? result.toString() : null;
+    }
+
+    private static boolean isBlank(String s) {
+        return s == null || s.trim().isEmpty();
+    }
+
+    /** Lower case, with every run of other characters than letters and digits 
turned into one space. */
+    static String normaliseName(String name) {
+        String n = name.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", " 
").trim();
+        return n.startsWith("the ") ? n.substring(4) : n;
+    }
+
+    /** Lower case, without scheme, <code>www.</code>, fragment, trailing 
slashes and document extension. */
+    static String normaliseUrl(String url) {
+        String u = url.trim().toLowerCase(Locale.ROOT);
+        int fragment = u.indexOf('#');
+        if (fragment >= 0) {
+            u = u.substring(0, fragment);
+        }
+        u = u.replaceFirst("^https?://", "").replaceFirst("^www\\.", 
"").replaceFirst("/+$", "");
+        return u.replaceFirst("\\.(txt|html?|php|md)$", "");
+    }
+}
diff --git 
a/src/main/resources/org/apache/maven/plugin/resources/remote/license-aliases.txt
 
b/src/main/resources/org/apache/maven/plugin/resources/remote/license-aliases.txt
new file mode 100644
index 0000000..7f4a4e1
--- /dev/null
+++ 
b/src/main/resources/org/apache/maven/plugin/resources/remote/license-aliases.txt
@@ -0,0 +1,167 @@
+# 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.
+
+# License names and URLs, as POMs declare them, mapped to SPDX license IDs.
+#
+# Each line holds an SPDX ID or expression, a tab, and one declared name or 
URL.
+# An alias that starts with http:// or https:// is a URL. A line with an ID 
alone makes the ID known
+# without adding an alias. Names and URLs are normalised before matching, so 
case,
+# punctuation, the URL scheme, "www." and a trailing ".txt" or ".html" do not 
matter.
+#
+# GNU license URLs and unversioned GNU names are left out on purpose: they do 
not say
+# whether "only" or "or later" applies, and a wrong ID is worse than none. So 
are URLs
+# of pages that cover two licenses at once, such as GlassFish's 
CDDL+GPL_1_1.html.
+
+0BSD
+AGPL-3.0-only
+AGPL-3.0-or-later
+BSL-1.0        Boost Software License 1.0
+BSL-1.0        Boost Software License, Version 1.0
+BSL-1.0        https://www.boost.org/LICENSE_1_0.txt
+CC0-1.0        CC0
+CC0-1.0        CC0 1.0 Universal
+CC0-1.0        Creative Commons Zero v1.0 Universal
+CC0-1.0        https://creativecommons.org/publicdomain/zero/1.0/
+Classpath-exception-2.0
+GPL-2.0
+GPL-2.0+
+GPL-2.0-only
+GPL-2.0-or-later
+GPL-3.0
+GPL-3.0+
+GPL-3.0-only
+GPL-3.0-or-later
+ISC    ISC License
+LGPL-2.1
+LGPL-2.1+
+LGPL-2.1-only
+LGPL-2.1-or-later
+LGPL-3.0
+LGPL-3.0+
+LGPL-3.0-only
+LGPL-3.0-or-later
+MIT-0
+Unlicense      The Unlicense
+Unlicense      https://unlicense.org/
+
+Apache-1.1     Apache License 1.1
+Apache-1.1     Apache License, Version 1.1
+Apache-1.1     Apache Software License, Version 1.1
+Apache-1.1     The Apache Software License, Version 1.1
+Apache-1.1     https://www.apache.org/licenses/LICENSE-1.1
+
+Apache-2.0     ASL 2.0
+Apache-2.0     ASL2
+Apache-2.0     Apache 2
+Apache-2.0     Apache 2.0
+Apache-2.0     Apache 2.0 License
+Apache-2.0     Apache License 2.0
+Apache-2.0     Apache License Version 2.0
+Apache-2.0     Apache License, Version 2
+Apache-2.0     Apache License, Version 2.0
+Apache-2.0     Apache Software License - Version 2.0
+Apache-2.0     Apache Software License 2.0
+Apache-2.0     Apache Software License, Version 2.0
+Apache-2.0     The Apache License, Version 2.0
+Apache-2.0     The Apache Software License, Version 2.0
+Apache-2.0     https://www.apache.org/licenses/LICENSE-2.0
+Apache-2.0     https://opensource.org/licenses/Apache-2.0
+Apache-2.0     https://opensource.org/licenses/apache2.0.php
+
+BSD-2-Clause   BSD 2-Clause
+BSD-2-Clause   BSD 2-Clause License
+BSD-2-Clause   BSD 2-Clause "Simplified" License
+BSD-2-Clause   Simplified BSD License
+BSD-2-Clause   https://opensource.org/licenses/BSD-2-Clause
+
+# The Eclipse Distribution License 1.0 is the 3-clause BSD license.
+BSD-3-Clause   3-Clause BSD License
+BSD-3-Clause   BSD 3-Clause
+BSD-3-Clause   BSD 3-Clause License
+BSD-3-Clause   BSD-3-Clause License
+BSD-3-Clause   New BSD License
+BSD-3-Clause   Revised BSD
+BSD-3-Clause   Revised BSD License
+BSD-3-Clause   The BSD 3-Clause License
+BSD-3-Clause   EDL 1.0
+BSD-3-Clause   Eclipse Distribution License - v 1.0
+BSD-3-Clause   Eclipse Distribution License v. 1.0
+BSD-3-Clause   https://www.eclipse.org/org/documents/edl-v10.php
+BSD-3-Clause   https://opensource.org/licenses/BSD-3-Clause
+
+CDDL-1.0       CDDL 1.0
+CDDL-1.0       Common Development and Distribution License 1.0
+CDDL-1.0       Common Development and Distribution License (CDDL) v1.0
+CDDL-1.0       https://opensource.org/licenses/CDDL-1.0
+CDDL-1.1       CDDL 1.1
+CDDL-1.1       Common Development and Distribution License 1.1
+
+CPL-1.0        Common Public License 1.0
+CPL-1.0        Common Public License Version 1.0
+CPL-1.0        https://www.opensource.org/licenses/cpl1.0.txt
+
+EPL-1.0        EPL 1.0
+EPL-1.0        Eclipse Public License - v 1.0
+EPL-1.0        Eclipse Public License - Version 1.0
+EPL-1.0        Eclipse Public License 1.0
+EPL-1.0        Eclipse Public License v1.0
+EPL-1.0        Eclipse Public License version 1.0
+EPL-1.0        Eclipse Public License, Version 1.0
+EPL-1.0        The Eclipse Public License, Version 1.0
+EPL-1.0        https://www.eclipse.org/legal/epl-v10.html
+EPL-1.0        https://www.eclipse.org/org/documents/epl-v10.php
+
+EPL-2.0        EPL 2.0
+EPL-2.0        EPLv2
+EPL-2.0        Eclipse Public License - v 2.0
+EPL-2.0        Eclipse Public License - Version 2.0
+EPL-2.0        Eclipse Public License 2.0
+EPL-2.0        Eclipse Public License v. 2.0
+EPL-2.0        Eclipse Public License v2.0
+EPL-2.0        Eclipse Public License, Version 2.0
+EPL-2.0        https://www.eclipse.org/legal/epl-2.0/
+EPL-2.0        https://www.eclipse.org/legal/epl-v20.html
+EPL-2.0        https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
+EPL-2.0        https://projects.eclipse.org/license/epl-2.0
+
+GPL-2.0-only WITH Classpath-exception-2.0      GPL2 w/ CPE
+GPL-2.0-only WITH Classpath-exception-2.0      GNU General Public License, 
version 2, with the Classpath Exception
+GPL-2.0-only WITH Classpath-exception-2.0      GNU General Public License, 
version 2 with the GNU Classpath Exception
+GPL-2.0-only WITH Classpath-exception-2.0      
https://openjdk.java.net/legal/gplv2+ce.html
+GPL-2.0-only WITH Classpath-exception-2.0      
https://openjdk.org/legal/gplv2+ce.html
+
+MIT    MIT License
+MIT    The MIT License
+MIT    The MIT License (MIT)
+MIT    https://opensource.org/licenses/MIT
+MIT    https://opensource.org/licenses/mit-license.php
+
+MPL-1.0        Mozilla Public License Version 1.0
+MPL-1.0        https://www.mozilla.org/MPL/MPL-1.0.html
+MPL-1.1        MPL 1.1
+MPL-1.1        Mozilla Public License, Version 1.1
+MPL-1.1        Mozilla Public License Version 1.1
+MPL-1.1        https://www.mozilla.org/MPL/MPL-1.1.html
+MPL-2.0        MPL 2.0
+MPL-2.0        Mozilla Public License, Version 2.0
+MPL-2.0        Mozilla Public License Version 2.0
+MPL-2.0        https://www.mozilla.org/MPL/2.0/
+
+UPL-1.0        Universal Permissive License, Version 1.0
+UPL-1.0        Universal Permissive License v1.0
+UPL-1.0        https://opensource.org/licenses/UPL
+UPL-1.0        https://oss.oracle.com/licenses/upl
diff --git a/src/site/markdown/usage.md.vm b/src/site/markdown/usage.md.vm
index fdbcbc2..ca0a731 100644
--- a/src/site/markdown/usage.md.vm
+++ b/src/site/markdown/usage.md.vm
@@ -97,6 +97,24 @@ To use remote resource bundles you need to configure the 
plugin as follows:
 
 This will retrieve the `apache-jar-resource-bundle-1.0.jar` from the remote 
repositories specified in your POM, process each resource in the bundle and 
deposit them in your projects `${esc.d}{basedir}/target/classes` directory. 
 
+Using SPDX License IDs in Templates
+----------------------------------
+
+Dependencies declare the same license under many names, such as "Apache 
License, Version 2.0" and "The Apache Software License, Version 2.0". Since 
version 3.4.0, templates can use `${esc.d}spdx.id(${esc.d}license)` to get the 
[SPDX license ID](https://spdx.org/licenses/) of a declared license, for 
example `Apache-2.0`. The plugin tries the declared name first, then the URL, 
against a table of the licenses that POMs declare most often. A declaration 
that is already an SPDX ID or SPDX lic [...]
+
+When nothing matches, or the name and URL resolve to different IDs, 
`${esc.d}spdx.id` returns null. Write `${esc.d}!spdx.id(${esc.d}license)` to 
print nothing in that case, or fall back to the declared name:
+
+```
+#[[#foreach( $dependency in $projects )
+#foreach( $license in $dependency.licenses )
+#set( $id = $spdx.id($license) )
+$dependency.groupId:$dependency.artifactId #if( $id )$id#else$license.name#end
+#end
+#end]]#
+```
+
+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.
+
 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
new file mode 100644
index 0000000..81493ec
--- /dev/null
+++ 
b/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.plugin.resources.remote;
+
+import org.apache.maven.model.License;
+import org.apache.maven.model.Model;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class SpdxLicensesTest {
+
+    private final SpdxLicenses spdx = new SpdxLicenses();
+
+    @Test
+    void declaredId() {
+        assertEquals("Apache-2.0", spdx.id("Apache-2.0", null));
+        assertEquals("Apache-2.0", spdx.id("apache-2.0", null));
+        assertEquals("GPL-2.0", spdx.id("GPL-2.0", null));
+    }
+
+    @Test
+    void expression() {
+        assertEquals("(Apache-2.0 OR EPL-2.0)", spdx.id("(Apache-2.0 or 
epl-2.0)", null));
+        assertEquals(
+                "GPL-2.0-only WITH Classpath-exception-2.0",
+                spdx.id("GPL-2.0-only with Classpath-exception-2.0", null));
+        assertNull(spdx.id("Apache-2.0 OR Unknown-1.0", null));
+        assertNull(spdx.id("Apache-2.0 MIT", null));
+    }
+
+    @Test
+    void name() {
+        assertEquals("Apache-2.0", spdx.id("The Apache Software License, 
Version 2.0", null));
+        assertEquals("Apache-2.0", spdx.id("  apache license version 2.0 ", 
null));
+        assertEquals("MIT", spdx.id("The MIT License (MIT)", null));
+        assertEquals("BSD-3-Clause", spdx.id("Eclipse Distribution License - v 
1.0", null));
+        assertEquals("GPL-2.0-only WITH Classpath-exception-2.0", 
spdx.id("GPL2 w/ CPE", null));
+    }
+
+    @Test
+    void url() {
+        assertEquals("Apache-2.0", spdx.id(null, 
"http://www.apache.org/licenses/LICENSE-2.0.txt";));
+        assertEquals("Apache-2.0", spdx.id("", 
"https://apache.org/licenses/LICENSE-2.0/";));
+        assertEquals("EPL-2.0", spdx.id("Some name", 
"https://www.eclipse.org/legal/epl-v20.html";));
+    }
+
+    @Test
+    void idAtEndOfUrl() {
+        assertEquals("MIT", spdx.id(null, 
"https://spdx.org/licenses/MIT.txt";));
+        assertEquals("BSD-2-Clause", spdx.id(null, 
"https://opensource.org/license/bsd-2-clause";));
+        assertEquals("MIT-0", spdx.id(null, 
"https://spdx.org/licenses/MIT-0.html";));
+        assertNull(spdx.id(null, 
"https://opensource.org/licenses/bsd-license.php";));
+    }
+
+    @Test
+    void nameAndUrlDisagree() {
+        assertNull(spdx.id("MIT License", 
"https://www.apache.org/licenses/LICENSE-2.0";));
+    }
+
+    @Test
+    void ambiguousGnuDeclarationsMatchNothing() {
+        assertNull(spdx.id("GNU Lesser General Public License", 
"https://www.gnu.org/licenses/lgpl.html";));
+        assertNull(spdx.id(null, 
"https://www.gnu.org/licenses/old-licenses/gpl-2.0.html";));
+    }
+
+    @Test
+    void unknown() {
+        assertNull(spdx.id("Public Domain", null));
+        assertNull(spdx.id(null, null));
+        assertNull(spdx.id((License) null));
+    }
+
+    @Test
+    void severalLicensesAreAChoice() {
+        assertEquals(
+                "CDDL-1.1 OR (GPL-2.0-only WITH Classpath-exception-2.0)",
+                spdx.expression(project(
+                        license("CDDL 1.1", 
"https://glassfish.java.net/public/CDDL+GPL_1_1.html";),
+                        license("GPL2 w/ CPE", 
"https://glassfish.java.net/public/CDDL+GPL_1_1.html";))));
+        assertEquals(
+                "Apache-2.0 OR (MIT OR EPL-2.0)",
+                spdx.expression(project(license("Apache 2.0", null), 
license("MIT OR EPL-2.0", null))));
+    }
+
+    @Test
+    void projectExpression() {
+        assertEquals("MIT", spdx.expression(project(license("MIT", null), 
license("The MIT License", null))));
+        assertNull(spdx.expression(project(license("MIT", null), 
license("Public Domain", null))));
+        assertNull(spdx.expression(project()));
+        assertNull(spdx.expression(null));
+    }
+
+    private static License license(String name, String url) {
+        License license = new License();
+        license.setName(name);
+        license.setUrl(url);
+        return license;
+    }
+
+    private static MavenProject project(License... licenses) {
+        Model model = new Model();
+        for (License license : licenses) {
+            model.addLicense(license);
+        }
+        return new MavenProject(model);
+    }
+
+    @Test
+    void license() {
+        License license = new License();
+        license.setName("Apache License, Version 2.0");
+        license.setUrl("https://www.apache.org/licenses/LICENSE-2.0";);
+        assertEquals("Apache-2.0", spdx.id(license));
+    }
+}
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
new file mode 100644
index 0000000..cec9cdf
--- /dev/null
+++ 
b/src/test/java/org/apache/maven/plugin/resources/remote/it/ITSpdxLicenseId.java
@@ -0,0 +1,63 @@
+/*
+ * 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.plugin.resources.remote.it;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import org.apache.maven.plugin.resources.remote.it.support.MavenRunner;
+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.assertTrue;
+
+public class ITSpdxLicenseId extends AbstractIT {
+    @Test
+    public void test() throws IOException, URISyntaxException {
+        File dir = TestUtils.getTestDir("spdx-license-id");
+
+        MavenRunner verifier = TestUtils.newVerifier(dir);
+        verifier.addCliArgument("deploy");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        File projectDir = new File(dir, "project");
+        verifier = TestUtils.newVerifier(projectDir);
+        verifier.deleteArtifacts("org.apache.maven.plugin.rresource.it.spdx");
+        verifier.addCliArgument("generate-resources");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        File output = new File(projectDir, 
"target/maven-shared-archive-resources/DEPENDENCIES");
+        String content = FileUtils.fileRead(output);
+
+        assertTrue(content.contains("a-name: Apache-2.0\n"), content);
+        assertTrue(content.contains("b-url: Apache-2.0\n"), content);
+        assertTrue(content.contains("c-expression: (Apache-2.0 OR 
EPL-2.0)\n"), content);
+        assertTrue(content.contains("d-unknown: Public Domain\n"), content);
+        assertTrue(content.contains("d-unknown expression: \n"), content);
+        assertTrue(content.contains("e-dual: CDDL-1.1\n"), content);
+        assertTrue(content.contains("e-dual: GPL-2.0-only WITH 
Classpath-exception-2.0\n"), content);
+        assertTrue(
+                content.contains("e-dual expression: CDDL-1.1 OR (GPL-2.0-only 
WITH Classpath-exception-2.0)\n"),
+                content);
+    }
+}

Reply via email to