[MENFORCER-500] New rule to enforce that Maven coordinates match given patterns(#309)

Optionally allows to enforce the module directory name being equal to
the module's artifactId.
diff --git a/enforcer-rules/src/main/java/org/apache/maven/enforcer/rules/RequireMatchingCoordinates.java b/enforcer-rules/src/main/java/org/apache/maven/enforcer/rules/RequireMatchingCoordinates.java
new file mode 100644
index 0000000..f5aff82
--- /dev/null
+++ b/enforcer-rules/src/main/java/org/apache/maven/enforcer/rules/RequireMatchingCoordinates.java
@@ -0,0 +1,110 @@
+/*
+ * 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.enforcer.rules;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
+import org.apache.maven.project.MavenProject;
+
+/**
+ * This rule checks that the Maven coordinates (i.e. the project's {@code groupId} and {@code artifactId}) each match a given pattern.
+ * @since 3.5.0
+ */
+@Named("requireMatchingCoordinates")
+public final class RequireMatchingCoordinates extends AbstractStandardEnforcerRule {
+
+    private Pattern groupIdPattern;
+
+    private Pattern artifactIdPattern;
+
+    private boolean moduleNameMustMatchArtifactId;
+
+    private final MavenProject project;
+
+    @Inject
+    public RequireMatchingCoordinates(MavenProject project) {
+        this.project = Objects.requireNonNull(project);
+    }
+
+    @Override
+    public void execute() throws EnforcerRuleException {
+        StringBuilder msgBuilder = new StringBuilder();
+        if (groupIdPattern != null
+                && !groupIdPattern.matcher(project.getGroupId()).matches()) {
+            msgBuilder
+                    .append("Group ID must match pattern \"")
+                    .append(groupIdPattern)
+                    .append("\" but is \"")
+                    .append(project.getGroupId())
+                    .append("\"");
+        }
+        if (artifactIdPattern != null
+                && !artifactIdPattern.matcher(project.getArtifactId()).matches()) {
+            if (msgBuilder.length() > 0) {
+                msgBuilder.append(System.lineSeparator());
+            }
+            msgBuilder
+                    .append("Artifact ID must match pattern \"")
+                    .append(artifactIdPattern)
+                    .append("\" but is \"")
+                    .append(project.getArtifactId())
+                    .append("\"");
+        }
+        if (moduleNameMustMatchArtifactId
+                && !project.isExecutionRoot()
+                && !project.getBasedir().getName().equals(project.getArtifactId())) {
+            if (msgBuilder.length() > 0) {
+                msgBuilder.append(System.lineSeparator());
+            }
+            msgBuilder
+                    .append("Module directory name must be equal to its artifact ID \"")
+                    .append(project.getArtifactId())
+                    .append("\" but is \"")
+                    .append(project.getBasedir().getName())
+                    .append("\"");
+        }
+        if (msgBuilder.length() > 0) {
+            throw new EnforcerRuleException(msgBuilder.toString());
+        }
+    }
+
+    public void setGroupIdPattern(String groupIdPattern) {
+        this.groupIdPattern = Pattern.compile(groupIdPattern);
+    }
+
+    public void setArtifactIdPattern(String artifactIdPattern) {
+        this.artifactIdPattern = Pattern.compile(artifactIdPattern);
+    }
+
+    public void setModuleNameMustMatchArtifactId(boolean moduleNameMustMatchArtifactId) {
+        this.moduleNameMustMatchArtifactId = moduleNameMustMatchArtifactId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "requireMatchingCoordinates[groupIdPattern=%s, artifactIdPattern=%s, moduleNameMustMatchArtifactId=%b]",
+                groupIdPattern, artifactIdPattern, moduleNameMustMatchArtifactId);
+    }
+}
diff --git a/enforcer-rules/src/site/apt/index.apt b/enforcer-rules/src/site/apt/index.apt
index ac384ba..bdf21ae 100644
--- a/enforcer-rules/src/site/apt/index.apt
+++ b/enforcer-rules/src/site/apt/index.apt
@@ -73,6 +73,8 @@
 
   * {{{./requireJavaVersion.html}requireJavaVersion}} - enforces the JDK version.
 
+  * {{{./requireMatchingCoordinates.html}requireMatchingCoordinates}} - enforces specific group ID and/or artifact ID patterns.
+
   * {{{./requireMavenVersion.html}requireMavenVersion}} - enforces the Maven version.
 
   * {{{./requireNoRepositories.html}requireNoRepositories}} - enforces to not include repositories.
diff --git a/enforcer-rules/src/site/apt/requireMatchingCoordinates.apt.vm b/enforcer-rules/src/site/apt/requireMatchingCoordinates.apt.vm
new file mode 100644
index 0000000..906d61f
--- /dev/null
+++ b/enforcer-rules/src/site/apt/requireMatchingCoordinates.apt.vm
@@ -0,0 +1,75 @@
+~~ 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.
+
+  ------
+  Require Matching Coordinates
+  ------
+  Konrad Windszus
+  ------
+  2024-03-22
+  ------
+
+Require Matching Coordinates
+
+  This rule checks that the Maven coordinates (i.e. the project's <<<groupId>>> and <<<artifactId>>>) each match a given pattern. 
+  Optionally one can also enforce that in a multi-module build the module directory name is always equal to the module's <<<artifactId>>>.
+
+   The following parameters are supported by this rule:
+
+   * <<message>> - an optional message to the user if the rule fails. If not set a default message will be used.
+
+   * <<groupIdPattern>> - an optional {{{https://docs.oracle.com/javase/tutorial/essential/regex/}regular expression}}, which must match the project's <<<groupId>>>. If not set there is no check on the <<<groupId>>>.
+
+   * <<artifactIdPattern>> - an optional {{{https://docs.oracle.com/javase/tutorial/essential/regex/}regular expression}}, which must match the project's <<<artifactId>>>. If not set there is no check on the <<<artifactId>>>.
+
+   * <<moduleNameMustMatchArtifactId>> - boolean flag to enforce that the the module's directory name is always equal to the module's <<<artifactId>>>. By default <<<false>>>.
+
+   []
+
+
+  Sample Plugin Configuration:
+
++---+
+<project>
+  [...]
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <version>${project.version}</version>
+        <executions>
+          <execution>
+            <id>enforce-group-id</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <rules>
+                <requireMatchingCoordinates>
+                  <groupIdPattern>com\.example.\namespace\..*</groupIdPattern>
+                </requireTextFileChecksum>
+              </rules>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  [...]
+</project>
++---+
diff --git a/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/mod2/pom.xml b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/mod2/pom.xml
new file mode 100644
index 0000000..baa5856
--- /dev/null
+++ b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/mod2/pom.xml
@@ -0,0 +1,43 @@
+<?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>
+
+  <parent>
+  	<groupId>org.apache.maven.its.enforcer</groupId>
+    <artifactId>test-multimodule-project</artifactId>
+    <version>1.0</version>
+  </parent>
+
+  <artifactId>invalid-test-multimodule-mod2</artifactId>
+
+  <description>
+  </description>
+  
+  <dependencies>
+  	<dependency>
+ 	  <groupId>${project.groupId}</groupId>
+	  <artifactId>test-multimodule-mod1</artifactId>
+  	</dependency>
+  </dependencies>
+
+</project>
diff --git a/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/pom.xml b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/pom.xml
new file mode 100644
index 0000000..0797df8
--- /dev/null
+++ b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/pom.xml
@@ -0,0 +1,75 @@
+<?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.its.enforcer</groupId>
+  <artifactId>test-multimodule-project</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <description>
+  </description>
+
+  <modules>
+  	<module>test-multimodule-mod1</module>
+  	<module>mod2</module>
+  </modules>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+ 	    <groupId>${project.groupId}</groupId>
+	    <artifactId>test-multimodule-mod1</artifactId>
+	    <version>${project.version}</version>
+  	  </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <version>@project.version@</version>
+        <executions>
+          <execution>
+            <id>test</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <rules>
+                <requireMatchingCoordinates>
+                  <groupIdPattern>org\.apache\.maven\.its\.enforcer\.somepackage</groupIdPattern>
+                  <artifactIdPattern>test-.*</artifactIdPattern>
+                  <moduleNameMustMatchArtifactId>true</moduleNameMustMatchArtifactId>
+                </requireMatchingCoordinates>
+              </rules>
+              <fail>false</fail>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/test-multimodule-mod1/pom.xml b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/test-multimodule-mod1/pom.xml
new file mode 100644
index 0000000..4165af7
--- /dev/null
+++ b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/test-multimodule-mod1/pom.xml
@@ -0,0 +1,37 @@
+<?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>
+
+  <parent>
+  	<groupId>org.apache.maven.its.enforcer</groupId>
+    <artifactId>test-multimodule-project</artifactId>
+    <version>1.0</version>
+  </parent>
+
+  <groupId>org.apache.maven.its.enforcer.somepackage</groupId>
+  <artifactId>test-multimodule-mod1</artifactId>
+
+  <description>
+  </description>
+
+</project>
diff --git a/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/verify.groovy b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/verify.groovy
new file mode 100644
index 0000000..d9ce9f0
--- /dev/null
+++ b/maven-enforcer-plugin/src/it/projects/require-matching-coordinates/verify.groovy
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+File buildLog = new File(basedir, 'build.log')
+def log = buildLog.text.normalize()
+assert log.contains('[WARNING] Rule 0: org.apache.maven.enforcer.rules.RequireMatchingCoordinates failed with message:\nGroup ID must match pattern "org\\.apache\\.maven\\.its\\.enforcer\\.somepackage" but is "org.apache.maven.its.enforcer"\n')
+assert log.contains('[WARNING] Rule 0: org.apache.maven.enforcer.rules.RequireMatchingCoordinates failed with message:\nGroup ID must match pattern "org\\.apache\\.maven\\.its\\.enforcer\\.somepackage" but is "org.apache.maven.its.enforcer"\nArtifact ID must match pattern "test-.*" but is "invalid-test-multimodule-mod2"\nModule directory name must be equal to its artifact ID "invalid-test-multimodule-mod2" but is "mod2"\n')