From 3fcb2eab7141ed3b50cf72450495e551d78bbf2b Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Sun, 1 Mar 2026 21:23:54 +0100 Subject: [PATCH 1/3] Split output dir preparation into separate mojo The reason being that the record analyzing for code generation is based on reflection, i.e. actually loading the classes to figure out how to generate corresponding Matchers, and for IDEs able to integrate plugin executions as part of their integral build processes, will typically execute within the runtime JVM of the IDE, which may be incompatible (i.e. earlier version) than the compilation target of the records to load. But creating the output directory and adding it as a test source folder is still desireable to happen within the build of the IDE, so doing this as a separate goal makes the IDE "discover" the output directory automatically. The generate goal will inform about this change if the output directory has not been created, and suggest adding the goal to the plugin execution configuration. --- README.adoc | 2 +- .../record/matcher/CodeGeneratorBaseMojo.java | 41 +++++++++ .../matcher/GenerateRecordMatcherMojo.java | 89 ++++++++----------- .../matcher/PrepareOutputDirectoryMojo.java | 58 ++++++++++++ .../m2e/lifecycle-mapping-metadata.xml | 13 ++- 5 files changed, 151 insertions(+), 52 deletions(-) create mode 100644 maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java create mode 100644 maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java diff --git a/README.adoc b/README.adoc index f57b997..111df6a 100644 --- a/README.adoc +++ b/README.adoc @@ -269,7 +269,7 @@ assertThat(effectiveJava).is(matching(aBook().withTitle("Effective Java").withAu - true + true ---- diff --git a/maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java b/maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java new file mode 100644 index 0000000..8f80f8e --- /dev/null +++ b/maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java @@ -0,0 +1,41 @@ +package no.rune.record.matcher; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import java.io.File; +import java.nio.file.Path; +import java.util.Set; + +abstract class CodeGeneratorBaseMojo extends AbstractMojo { + + protected static final String PLUGIN_CONF_PROP_PREFIX = "recordmatcher."; + + /** + * The directory where the generated code will be written to. + */ + @Parameter(required = true, + defaultValue = "${project.build.directory}/generated-test-sources/record-matchers", + property = PLUGIN_CONF_PROP_PREFIX + "outputDirectory") + private File outputDirectory; + + /** + * Project packaging types where execution is skipped. + */ + @Parameter(required = true, + defaultValue = "pom", + property = PLUGIN_CONF_PROP_PREFIX + "skipForPackaging") + protected Set skipForPackaging; + + + @Parameter(required = true, readonly = true, + defaultValue = "${project}") + protected MavenProject mavenProject; + + + + Path resolveOutputDirectory() { + return outputDirectory.toPath(); + } +} diff --git a/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java b/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java index 28ab658..53926c6 100644 --- a/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java +++ b/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java @@ -1,6 +1,5 @@ package no.rune.record.matcher; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -17,13 +16,13 @@ import java.io.UncheckedIOException; import java.net.URL; import java.net.URLClassLoader; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.stream.Stream; +import static java.nio.file.Files.isDirectory; import static java.util.Objects.requireNonNullElseGet; import static java.util.function.Predicate.not; import static java.util.stream.Collectors.joining; @@ -33,11 +32,15 @@ import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_TEST_SOURCES; import static org.apache.maven.plugins.annotations.ResolutionScope.COMPILE; -@Mojo(name = "generate", defaultPhase = GENERATE_TEST_SOURCES, requiresDependencyResolution = COMPILE) -public class GenerateRecordMatcherMojo extends AbstractMojo { +@Mojo( + name = GenerateRecordMatcherMojo.GOAL_NAME, + defaultPhase = GENERATE_TEST_SOURCES, + requiresDependencyResolution = COMPILE) +public class GenerateRecordMatcherMojo extends CodeGeneratorBaseMojo { + + static final String GOAL_NAME = "generate"; private static final Logger LOG = LoggerFactory.getLogger(GenerateRecordMatcherMojo.class); - private static final String PLUGIN_CONF_PROP_PREFIX = "recordmatcher."; /** * Specifies the fully qualified class names of the records to @@ -73,37 +76,6 @@ public class GenerateRecordMatcherMojo extends AbstractMojo { private Set excludes; - /** - * The directory where the generated Hamcrest matchers will be written to. - */ - @Parameter(required = true, - defaultValue = "${project.build.directory}/generated-test-sources/record-matchers", - property = PLUGIN_CONF_PROP_PREFIX + "outputDirectory") - private File outputDirectory; - - - /** - * Whether the {@link #outputDirectory} should be included as a test sources root. - */ - @Parameter(required = true, - defaultValue = "true", - property = PLUGIN_CONF_PROP_PREFIX + "includeGeneratedCodeAsTestSources") - private boolean includeGeneratedCodeAsTestSources; - - - /** - * Project packaging types where execution is skipped. - */ - @Parameter(required = true, - defaultValue = "pom", - property = PLUGIN_CONF_PROP_PREFIX + "skipForPackaging") - private Set skipForPackaging; - - - @Parameter(required = true, readonly = true, - defaultValue = "${project}") - private MavenProject mavenProject; - @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -113,18 +85,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } Path outputDirectory = resolveOutputDirectory(); - try { - Files.createDirectories(outputDirectory); - LOG.info("Generating Hamcrest matchers in {}", outputDirectory); - } catch (IOException e) { - throw new UncheckedIOException( - "Unable to create output directory " + outputDirectory + ", " + - "because " + e.getClass().getSimpleName() + ": " + e.getMessage(), e); - } - if (includeGeneratedCodeAsTestSources) { - mavenProject.addTestCompileSourceRoot(outputDirectory.toString()); - LOG.debug("{} has been added as a compiler test sources directory", outputDirectory); - } + LOG.info("Generating matchers in {}", outputDirectory); var generator = new RecordMatcherGenerator(); var writtenFiles = resolveIncludedRecords() @@ -154,8 +115,36 @@ public void execute() throws MojoExecutionException, MojoFailureException { } - private Path resolveOutputDirectory() { - return outputDirectory.toPath(); + @Override + protected Path resolveOutputDirectory() { + Path outputDirectory = super.resolveOutputDirectory(); + if (!isDirectory(outputDirectory)) { + throw new IllegalStateException( + "Directory " + outputDirectory + " does not exist. " + + """ + From record-matcher-maven-plugin version >= 0.4.0, it is required to configure the \ + goal 'prepare-output-directory' to create this directory separately before the \ + 'generate' goal is run. Please ensure the plugin's execution is configured like this: + + + + + prepare-output-directory + generate + + + + + If you are running the goal directly from the command line, you can try prepending \ + the generate goal with the prepare-output-directory goal: + + mvn record-matcher:prepare-output-directory record-matcher:generate + + The README at https://github.com/runeflobakk/record-matcher further explains how to \ + run standalone goals of the record-matcher-maven-plugin. + """); + } + return outputDirectory; } diff --git a/maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java b/maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java new file mode 100644 index 0000000..faa09c4 --- /dev/null +++ b/maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java @@ -0,0 +1,58 @@ +package no.rune.record.matcher; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_TEST_SOURCES; + +@Mojo( + name = PrepareOutputDirectoryMojo.GOAL_NAME, + defaultPhase = GENERATE_TEST_SOURCES) +public class PrepareOutputDirectoryMojo extends CodeGeneratorBaseMojo { + + static final String GOAL_NAME = "prepare-output-directory"; + + private static final Logger LOG = LoggerFactory.getLogger(PrepareOutputDirectoryMojo.class); + + /** + * Whether the {@link #outputDirectory} should be included as a test sources root. + */ + @Parameter(required = true, + defaultValue = "true", + property = PLUGIN_CONF_PROP_PREFIX + "includeAsTestSources") + private boolean includeAsTestSources; + + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + + Path outputDirectory = resolveOutputDirectory(); + try { + Files.createDirectories(outputDirectory); + } catch (IOException e) { + throw new UncheckedIOException( + "Unable to create output directory " + outputDirectory + ", " + + "because " + e.getClass().getSimpleName() + ": " + e.getMessage(), e); + } + + if (includeAsTestSources) { + mavenProject.addTestCompileSourceRoot(outputDirectory.toString()); + LOG.info("Prepared output directory {}, and included as test sources", outputDirectory); + } else { + LOG.info("Prepared output directory {}, but not included as compilation source", outputDirectory); + } + + } + + + +} diff --git a/maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml b/maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml index 3a892f4..406c345 100644 --- a/maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml +++ b/maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml @@ -5,14 +5,25 @@ - generate + prepare-output-directory + true true + + + + generate + + + + + + From 286905d6961a2cc46b997880c177f4b23251cbc5 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Sun, 1 Mar 2026 21:57:22 +0100 Subject: [PATCH 2/3] Add new prepare-output-directory goal to readme --- README.adoc | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.adoc b/README.adoc index 111df6a..e928d32 100644 --- a/README.adoc +++ b/README.adoc @@ -50,19 +50,23 @@ You can try the Maven plugin without configuring anything in an existing project [source,bash] ---- -mvn test-compile com.github.runeflobakk:record-matcher-maven-plugin::generate +mvn test-compile \ + com.github.runeflobakk:record-matcher-maven-plugin::prepare-output-directory \ + com.github.runeflobakk:record-matcher-maven-plugin::generate ---- -The plugin attempts to discover records in a particular base package (more on this below). If the package is not present in your project, you are likely to get see the following error: +The plugin attempts to discover records in a particular base package (more on this below). If the package is not present in your project, you are likely to get see the following error: > There was an error scanning for records in package(s) [the.attempted.pkg]: IllegalArgumentException 'One or more of the input resources are incorrect'. Ensure that the packages are correctly defined and exists. You can specify a package (which includes sub-packages) to scan for records: -.... -mvn test-compile com.github.runeflobakk:record-matcher-maven-plugin::generate\ - -Drecordmatcher.scanPackages=pkg.in.your.project -.... +---- +mvn test-compile \ + com.github.runeflobakk:record-matcher-maven-plugin::prepare-output-directory \ + com.github.runeflobakk:record-matcher-maven-plugin::generate + -Drecordmatcher.scanPackages=pkg.in.your.project +---- Lastly, you can add `target/generated-sources/record-matchers` as a _source folder_ in your IDE to access and use the generated Hamcrest Matchers. Read on for how to configure Record Matcher Generator as an integral part of your Maven project. @@ -83,10 +87,11 @@ Likely, you want to use this via the https://central.sonatype.com/artifact/com.g com.github.runeflobakk record-matcher-maven-plugin - 0.3.2 + 0.4.0 + prepare-output-directory generate From 08ccf6a43106966ca27cb1ecf9c7ad81f6b97bb0 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Sun, 1 Mar 2026 23:21:33 +0100 Subject: [PATCH 3/3] Emit warning instead of exception Allows the use of the generate goal without requiring the prepare-output-directory goal, but if the directory does not exist, emits a warning about the dir will not be included as test sources. This will allow to try the plugin with the single goal to generate matchers and manually include the folder in your IDE, and when configured properly in the POM with both goals, it will still be applicable to only run the single generate goal to update the generated matchers when needed. --- README.adoc | 14 ++-- .../record/matcher/CodeGeneratorBaseMojo.java | 5 +- .../matcher/GenerateRecordMatcherMojo.java | 64 +++++++++++-------- .../rune/record/matcher/OutputDirectory.java | 26 ++++++++ .../matcher/PrepareOutputDirectoryMojo.java | 14 +--- 5 files changed, 70 insertions(+), 53 deletions(-) create mode 100644 maven-plugin/src/main/java/no/rune/record/matcher/OutputDirectory.java diff --git a/README.adoc b/README.adoc index e928d32..405d069 100644 --- a/README.adoc +++ b/README.adoc @@ -50,9 +50,7 @@ You can try the Maven plugin without configuring anything in an existing project [source,bash] ---- -mvn test-compile \ - com.github.runeflobakk:record-matcher-maven-plugin::prepare-output-directory \ - com.github.runeflobakk:record-matcher-maven-plugin::generate +mvn test-compile com.github.runeflobakk:record-matcher-maven-plugin::generate ---- The plugin attempts to discover records in a particular base package (more on this below). If the package is not present in your project, you are likely to get see the following error: @@ -62,9 +60,7 @@ The plugin attempts to discover records in a particular base package (more on th You can specify a package (which includes sub-packages) to scan for records: ---- -mvn test-compile \ - com.github.runeflobakk:record-matcher-maven-plugin::prepare-output-directory \ - com.github.runeflobakk:record-matcher-maven-plugin::generate +mvn test-compile com.github.runeflobakk:record-matcher-maven-plugin::generate\ -Drecordmatcher.scanPackages=pkg.in.your.project ---- @@ -109,7 +105,7 @@ Using the `groupId` of your project as the base package for the plugin to discov com.github.runeflobakk record-matcher-maven-plugin - 0.3.2 + 0.4.0 com.base.service @@ -283,14 +279,14 @@ The following command can also be used anywhere to view the plugin’s own descr [source,bash] ---- -mvn com.github.runeflobakk:record-matcher-maven-plugin::help -Dgoal=generate -Ddetail +mvn com.github.runeflobakk:record-matcher-maven-plugin::help -Ddetail ---- Or the short form if you have already configured the plugin in your Maven project: [source,bash] ---- -mvn record-matcher:help +mvn record-matcher:help -Ddetail ---- diff --git a/maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java b/maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java index 8f80f8e..14ee1b7 100644 --- a/maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java +++ b/maven-plugin/src/main/java/no/rune/record/matcher/CodeGeneratorBaseMojo.java @@ -5,7 +5,6 @@ import org.apache.maven.project.MavenProject; import java.io.File; -import java.nio.file.Path; import java.util.Set; abstract class CodeGeneratorBaseMojo extends AbstractMojo { @@ -35,7 +34,7 @@ abstract class CodeGeneratorBaseMojo extends AbstractMojo { - Path resolveOutputDirectory() { - return outputDirectory.toPath(); + OutputDirectory outputDirectory() { + return new OutputDirectory(outputDirectory.toPath()); } } diff --git a/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java b/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java index 53926c6..e6bc437 100644 --- a/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java +++ b/maven-plugin/src/main/java/no/rune/record/matcher/GenerateRecordMatcherMojo.java @@ -22,7 +22,6 @@ import java.util.Set; import java.util.stream.Stream; -import static java.nio.file.Files.isDirectory; import static java.util.Objects.requireNonNullElseGet; import static java.util.function.Predicate.not; import static java.util.stream.Collectors.joining; @@ -84,7 +83,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } - Path outputDirectory = resolveOutputDirectory(); + Path outputDirectory = outputDirectory().path(); LOG.info("Generating matchers in {}", outputDirectory); var generator = new RecordMatcherGenerator(); @@ -116,33 +115,42 @@ public void execute() throws MojoExecutionException, MojoFailureException { } @Override - protected Path resolveOutputDirectory() { - Path outputDirectory = super.resolveOutputDirectory(); - if (!isDirectory(outputDirectory)) { - throw new IllegalStateException( - "Directory " + outputDirectory + " does not exist. " + - """ - From record-matcher-maven-plugin version >= 0.4.0, it is required to configure the \ - goal 'prepare-output-directory' to create this directory separately before the \ - 'generate' goal is run. Please ensure the plugin's execution is configured like this: - - - - - prepare-output-directory - generate - - + protected OutputDirectory outputDirectory() { + var outputDirectory = super.outputDirectory(); + if (!outputDirectory.exists()) { + outputDirectory.create(); + LOG.warn(""" + Directory {} was created because it did not already exist, but you need to use \ + the {} goal in order to include it as test sources for the project.\ + """, + outputDirectory.path(), PrepareOutputDirectoryMojo.GOAL_NAME); + + LOG.info(""" + From record-matcher-maven-plugin version >= 0.4.0, in order to include the generated \ + matchers as compiled test code, it is required to configure the goal '{}' to create \ + this directory separately before the '{}' goal is run. Please ensure the plugin's \ + execution is configured like this: + + - - If you are running the goal directly from the command line, you can try prepending \ - the generate goal with the prepare-output-directory goal: - - mvn record-matcher:prepare-output-directory record-matcher:generate - - The README at https://github.com/runeflobakk/record-matcher further explains how to \ - run standalone goals of the record-matcher-maven-plugin. - """); + + prepare-output-directory + generate + + + + + If you are running the goal directly from the command line, you can try prepending \ + the {} goal with the {} goal: + + mvn record-matcher:{} record-matcher:{} + + See the README at https://github.com/runeflobakk/record-matcher for further details on \ + configuring and/or running standalone goals of the record-matcher-maven-plugin. + """, + PrepareOutputDirectoryMojo.GOAL_NAME, GenerateRecordMatcherMojo.GOAL_NAME, + GenerateRecordMatcherMojo.GOAL_NAME, PrepareOutputDirectoryMojo.GOAL_NAME, + PrepareOutputDirectoryMojo.GOAL_NAME, GenerateRecordMatcherMojo.GOAL_NAME); } return outputDirectory; } diff --git a/maven-plugin/src/main/java/no/rune/record/matcher/OutputDirectory.java b/maven-plugin/src/main/java/no/rune/record/matcher/OutputDirectory.java new file mode 100644 index 0000000..c1be3f0 --- /dev/null +++ b/maven-plugin/src/main/java/no/rune/record/matcher/OutputDirectory.java @@ -0,0 +1,26 @@ +package no.rune.record.matcher; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static java.nio.file.Files.isDirectory; + +record OutputDirectory(Path path) { + + boolean exists() { + return isDirectory(path); + } + + Path create() { + try { + return Files.createDirectories(path); + } catch (IOException e) { + throw new UncheckedIOException( + "Unable to create output directory " + path + ", " + + "because " + e.getClass().getSimpleName() + ": " + e.getMessage(), e); + } + } + +} diff --git a/maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java b/maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java index faa09c4..f8cc429 100644 --- a/maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java +++ b/maven-plugin/src/main/java/no/rune/record/matcher/PrepareOutputDirectoryMojo.java @@ -7,11 +7,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; - import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_TEST_SOURCES; @Mojo( @@ -35,14 +30,7 @@ public class PrepareOutputDirectoryMojo extends CodeGeneratorBaseMojo { @Override public void execute() throws MojoExecutionException, MojoFailureException { - Path outputDirectory = resolveOutputDirectory(); - try { - Files.createDirectories(outputDirectory); - } catch (IOException e) { - throw new UncheckedIOException( - "Unable to create output directory " + outputDirectory + ", " + - "because " + e.getClass().getSimpleName() + ": " + e.getMessage(), e); - } + var outputDirectory = outputDirectory().create(); if (includeAsTestSources) { mavenProject.addTestCompileSourceRoot(outputDirectory.toString());