com.fasterxml.jackson.core
diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java
new file mode 100644
index 00000000000000..db107bd63a9e22
--- /dev/null
+++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java
@@ -0,0 +1,437 @@
+package io.quarkus.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.maven.model.Model;
+import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
+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;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.cache.FileTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateExceptionHandler;
+
+/**
+ * Creates a triple of stub Maven modules (Parent, Runtime and Deployment) to implement a new
+ * Quarkus Extension.
+ *
+ * If this Mojo is executed in a directory that contains a {@code pom.xml} file with packaging {@code pom} the newly
+ * created Parent module is added as a child module to the existing {@code pom.xml} file.
+ *
+ * Executing this Mojo in an empty directory is not supported yet.
+ */
+@Mojo(name = "create-extension")
+public class CreateExtensionMojo extends AbstractMojo {
+
+ private static final Pattern EOL_PATTERN = Pattern.compile("[\n\r]+");
+ private static final Pattern BRACKETS_PATTERN = Pattern.compile("[()]+");
+ private static final Pattern MODULES_START_PATTERN = Pattern.compile("([\\s]*)<");
+ private static final Pattern BUILD_START_PATTERN = Pattern.compile("(\r?\n[ \t]+)?");
+ private static final Pattern MODULES_END_PATTERN = Pattern.compile("[\\s]*");
+ private static final Pattern PACKAGING_START_PATTERN = Pattern.compile("(\\r?\\n[ \\t]+)?");
+ private static final Pattern PROJECT_END_PATTERN = Pattern.compile("([\\s]*)");
+ private static final String CLASSPATH_PREFIX = "classpath:";
+ private static final String FILE_PREFIX = "file:";
+
+ static final String DEFAULT_ENCODING = "utf-8";
+ static final String DEFAULT_QUARKUS_VERSION = "@{quarkus.version}";
+ static final String DEFAULT_TEMPLATES_URI_BASE = "classpath:/create-extension-templates";
+
+ @Parameter(defaultValue = "${project.basedir}", readonly = true, required = true)
+ File basedir;
+ /**
+ * The {@code groupId} for the newly created Maven modules. If {@code groupId} is left unset, the {@code groupId}
+ * from the {@code pom.xml} in the current directory will be used. Otherwise, an exception is thrown.
+ */
+ @Parameter(property = "quarkus.groupId")
+ String groupId;
+ /**
+ * {@code artifactId} of the runtime module. The {@code artifactId}s of the extension parent
+ * (${artifactId}-parent
) and deployment (${artifactId}-deployment
) modules will be based
+ * on this {@code artifactId} too.
+ *
+ * Optionally, this value can contain the proper name of the extension enclosed in round brackets, e.g.
+ * {@code my-project-(extension-proper-name)}. If present, the {@code extension-proper-name} will be used as a name
+ * of the extension directory; otherwise the whole {@code artifactId} will be used as a name of the extension
+ * directory
+ */
+ @Parameter(required = true, property = "quarkus.artifactId")
+ String artifactId;
+
+ /**
+ * The {@code version} for the newly created Maven modules. If {@code version} is left unset, the {@code version}
+ * from the {@code pom.xml} in the current directory will be used. Otherwise, an exception is thrown.
+ */
+ @Parameter(property = "quarkus.version")
+ String version;
+
+ /**
+ * The {@code name} of the runtime module. The {@code name}s of the extension parent and deployment modules will be
+ * based on this {@code name} too.
+ */
+ @Parameter(property = "quarkus.name")
+ String name;
+
+ /** Base Java package under which Java classes should be created in Runtime and Deployment modules */
+ @Parameter(property = "quarkus.javaPackageBase")
+ String javaPackageBase;
+
+ /**
+ * This mojo creates a triple of Maven modules (Parent, Runtime and Deployment). "Grand parent" is the parent of the
+ * Parent module. If {@code grandParentArtifactId} is left unset, the {@code artifactId} from the {@code pom.xml} in
+ * the current directory will be used. Otherwise, an exception is thrown.
+ */
+ @Parameter(property = "quarkus.grandParentArtifactId")
+ String grandParentArtifactId;
+
+ /**
+ * This mojo creates a triple of Maven modules (Parent, Runtime and Deployment). "Grand parent" is the parent of the
+ * Parent module. If {@code grandParentGroupId} is left unset, the {@code groupId} from the {@code pom.xml} in the
+ * current directory will be used. Otherwise, an exception is thrown.
+ */
+ @Parameter(property = "quarkus.grandParentGroupId")
+ String grandParentGroupId;
+
+ /**
+ * This mojo creates a triple of Maven modules (Parent, Runtime and Deployment). "Grand parent" is the parent of the
+ * Parent module. If {@code grandParentRelativePath} is left unset, the default {@code relativePath}
+ * {@code "../pom.xml"} is used.
+ */
+ @Parameter(property = "quarkus.grandParentRelativePath")
+ String grandParentRelativePath;
+
+ /**
+ * This mojo creates a triple of Maven modules (Parent, Runtime and Deployment). "Grand parent" is the parent of the
+ * Parent module. If {@code grandParentVersion} is left unset, the {@code version} from the {@code pom.xml} in the
+ * current directory will be used. Otherwise, an exception is thrown.
+ */
+ @Parameter(property = "quarkus.grandParentVersion")
+ String grandParentVersion;
+
+ /**
+ * Quarkus version the newly created extension should depend on. If you want to pass a property placeholder, use
+ * {@code @} instead if {@code $} so that the property is not evaluated by the current mojo - e.g.
+ * @{quarkus.version}
+ */
+ @Parameter(defaultValue = DEFAULT_QUARKUS_VERSION, required = true, property = "quarkus.quarkusVersion")
+ String quarkusVersion;
+
+ /**
+ * If {@code true} the Maven dependencies in Runtime and Deployment modules will not have their versions set;
+ * otherwise the version set in {@link #quarkusVersion} will be used.
+ */
+ @Parameter(defaultValue = "true", required = true, property = "quarkus.assumeManaged")
+ boolean assumeManaged;
+
+ /**
+ * URI prefix to use when looking up FreeMarker templates when generating various source files. The following
+ * schemes are supported:
+ *
+ * - {@code classpath:}
+ * - {@code file:} (relative to the current directory)
+ *
+ * If you choose to override the default you need to provide the following files under the specified path:
+ *
+ * - {@code deployment-pom.xml}
+ * - {@code parent-pom.xml}
+ * - {@code runtime-pom.xml}
+ *
+ */
+ @Parameter(defaultValue = DEFAULT_TEMPLATES_URI_BASE, required = true, property = "quarkus.templatesUriBase")
+ String templatesUriBase;
+
+ /** Encoding to read and write files in the current source tree */
+ @Parameter(defaultValue = DEFAULT_ENCODING, required = true, property = "quarkus.encoding")
+ String encoding;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+
+ final Charset charset = Charset.forName(encoding);
+
+ final Path basePomXml = basedir.toPath().resolve("pom.xml");
+ if (Files.exists(basePomXml)) {
+ MavenXpp3Reader reader = new MavenXpp3Reader();
+ try (Reader r = Files.newBufferedReader(basePomXml, charset)) {
+ Model basePom = reader.read(r);
+ if (!"pom".equals(basePom.getPackaging())) {
+ throw new MojoFailureException(
+ "Can add extensiopn modules only under a project with packagin 'pom'; found: "
+ + basePom.getPackaging() + "");
+ }
+ addModules(basePomXml, basePom, charset);
+ } catch (IOException e) {
+ throw new MojoExecutionException(String.format("Could not read %s", basePomXml), e);
+ } catch (XmlPullParserException e) {
+ throw new MojoExecutionException(String.format("Could not parse %s", basePomXml), e);
+ } catch (TemplateException e) {
+ throw new MojoExecutionException(String.format("Could not process a FreeMarker template"), e);
+ }
+ } else {
+ newParent(basedir.toPath());
+ }
+ }
+
+ void addModules(Path basePomXml, Model basePom, Charset charset) throws IOException, TemplateException {
+
+ final String cleanArtifactId = cleanArtifactId(artifactId);
+
+ final Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
+ cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+ cfg.setTemplateLoader(createTemplateLoader(templatesUriBase));
+ cfg.setDefaultEncoding(charset.name());
+ cfg.setInterpolationSyntax(Configuration.SQUARE_BRACKET_INTERPOLATION_SYNTAX);
+ cfg.setTagSyntax(Configuration.SQUARE_BRACKET_TAG_SYNTAX);
+
+ TemplateParams model = new TemplateParams();
+
+ model.properExtensionName = getProperExtensionName(artifactId);
+ model.properExtensionNameCamelCase = toCapCamelCase(model.properExtensionName);
+ model.assumeManaged = assumeManaged;
+ model.project.groupId = this.groupId == null ? basePom.getGroupId() : this.groupId;
+ model.project.artifactId = cleanArtifactId + "-parent";
+ model.project.version = this.version == null ? basePom.getVersion() : this.version;
+ model.project.name = this.name != null ? this.name + " Parent" : null;
+ model.quarkusVersion = quarkusVersion.replace('@', '$');
+
+ model.parent.groupId = grandParentGroupId != null ? grandParentGroupId : basePom.getGroupId();
+ model.parent.artifactId = grandParentArtifactId != null ? grandParentArtifactId : basePom.getArtifactId();
+ model.parent.version = grandParentVersion != null ? grandParentVersion : basePom.getVersion();
+ model.parent.relativePath = grandParentRelativePath != null ? grandParentRelativePath : "../pom.xml";
+ evalTemplate(cfg, "parent-pom.xml", basedir.toPath().resolve(model.properExtensionName + "/pom.xml"), charset,
+ model);
+
+ model.project.artifactId = cleanArtifactId;
+ model.parent.groupId = model.project.groupId;
+ model.parent.artifactId = cleanArtifactId + "-parent";
+ model.parent.version = model.project.version;
+ model.project.name = this.name != null ? this.name + " Runtime" : null;
+ model.javaPackage = javaPackageBase != null ? javaPackageBase + ".runtime"
+ : getJavaPackage(model.project.groupId, model.project.artifactId);
+ model.parent.relativePath = "../pom.xml";
+ Files.createDirectories(basedir.toPath()
+ .resolve(model.properExtensionName + "/runtime/src/main/java/" + model.javaPackage.replace('.', '/')));
+ evalTemplate(cfg, "runtime-pom.xml", basedir.toPath().resolve(model.properExtensionName + "/runtime/pom.xml"),
+ charset, model);
+
+ model.project.artifactId = cleanArtifactId + "-deployment";
+ model.project.name = this.name != null ? this.name + " Deployment" : null;
+ model.javaPackage = javaPackageBase != null ? javaPackageBase + ".deployment"
+ : getJavaPackage(model.project.groupId, model.project.artifactId);
+ evalTemplate(cfg, "deployment-pom.xml",
+ basedir.toPath().resolve(model.properExtensionName + "/deployment/pom.xml"), charset, model);
+ final Path processorPath = basedir.toPath().resolve(model.properExtensionName + "/deployment/src/main/java/"
+ + model.javaPackage.replace('.', '/') + "/" + model.properExtensionNameCamelCase + "Processor.java");
+ evalTemplate(cfg, "Processor.java", processorPath, charset, model);
+
+ if (!basePom.getModules().contains(model.properExtensionName)) {
+ final String basePomSource = new String(Files.readAllBytes(basePomXml), charset);
+ final String newSource = addModule(basePomXml, basePomSource, model.properExtensionName);
+ Files.write(basePomXml, newSource.getBytes(charset));
+ }
+
+ }
+
+ static String toCapCamelCase(String properExtensionName) {
+ final StringBuilder sb = new StringBuilder(properExtensionName.length());
+ for (String segment : properExtensionName.split("[.\\-]+")) {
+ sb.append(Character.toUpperCase(segment.charAt(0)));
+ if (segment.length() > 1) {
+ sb.append(segment.substring(1));
+ }
+ }
+ return sb.toString();
+ }
+
+ static String getJavaPackage(String groupId, String artifactId) {
+ final Stack segments = new Stack<>();
+ for (String segment : groupId.split("[.\\-]+")) {
+ if (segments.isEmpty() || !segments.peek().equals(segment)) {
+ segments.add(segment);
+ }
+ }
+ for (String segment : artifactId.split("[.\\-]+")) {
+ if (!segments.contains(segment)) {
+ segments.add(segment);
+ }
+ }
+ return segments.stream().collect(Collectors.joining("."));
+ }
+
+ /**
+ * Adds the given new {@code module} to the given {@code pomSource}. Done using regular expressions because
+ * MavenXpp3Writer, XSL and DOM based solutions tend to add/remove whitespace at places we do not want to touch.
+ *
+ * @param pomXmlPath path of the {@code pom.xml} file we are editing - used only in error messages
+ * @param pomSource the source of the {@code pom.xml} we are editing
+ * @param module module name to add
+ * @return the changed source
+ * @throws IOException
+ */
+ static String addModule(Path pomXmlPath, String pomSource, String module) throws IOException {
+ final Matcher modulesStartMatcher = MODULES_START_PATTERN.matcher(pomSource);
+ if (modulesStartMatcher.find()) {
+ final String ws = modulesStartMatcher.group(1);
+ final Matcher endMatcher = MODULES_END_PATTERN.matcher(pomSource);
+ if (endMatcher.find()) {
+ final String match = endMatcher.group();
+ return endMatcher.replaceFirst(ws + "" + module + "" + match);
+ } else {
+ throw new IllegalStateException(
+ String.format("Could not find '%s' in '%s'", MODULES_END_PATTERN.pattern(), pomXmlPath));
+ }
+ } else {
+ final Matcher buildStartMatcher = BUILD_START_PATTERN.matcher(pomSource);
+ if (buildStartMatcher.find()) {
+ final String ws = buildStartMatcher.group(1);
+ final String match = buildStartMatcher.group();
+ return buildStartMatcher.replaceFirst(ws + "" //
+ + ws + EOL_PATTERN.matcher(ws).replaceAll("") + "" + module + "" //
+ + ws + "" //
+ + match);
+ } else {
+ final Matcher projectEndMatcher = PROJECT_END_PATTERN.matcher(pomSource);
+ if (projectEndMatcher.find()) {
+ final Matcher packagingStartMatcher = PACKAGING_START_PATTERN.matcher(pomSource);
+ final String ws = packagingStartMatcher.find() ? packagingStartMatcher.group(1)
+ : projectEndMatcher.group(1);
+ final String match = projectEndMatcher.group();
+ return projectEndMatcher.replaceFirst(ws + "" //
+ + ws + EOL_PATTERN.matcher(ws).replaceAll("") + "" + module + "" //
+ + ws + "" //
+ + match);
+ } else {
+ throw new IllegalStateException(String.format("Unable to add a new module to '%s'", pomXmlPath));
+ }
+ }
+ }
+ }
+
+ void newParent(Path path) {
+ throw new UnsupportedOperationException(
+ "Creating standalone extension projects is not supported yet. Only adding modules under and existing pom.xml file is supported.");
+ }
+
+ static String cleanArtifactId(String artifactId) {
+ return BRACKETS_PATTERN.matcher(artifactId).replaceAll("");
+ }
+
+ static TemplateLoader createTemplateLoader(String templatesUriBase) throws IOException {
+ if (templatesUriBase.startsWith(CLASSPATH_PREFIX)) {
+ return new ClassTemplateLoader(CreateExtensionMojo.class,
+ templatesUriBase.substring(CLASSPATH_PREFIX.length()));
+ } else if (templatesUriBase.startsWith(FILE_PREFIX)) {
+ return new FileTemplateLoader(new File(templatesUriBase.substring(FILE_PREFIX.length())));
+ } else {
+ throw new IllegalStateException(String.format(
+ "Cannot handle templatesUriBase '%s'; only value starting with '%s' or '%s' are supported",
+ templatesUriBase, CLASSPATH_PREFIX, FILE_PREFIX));
+ }
+ }
+
+ static void evalTemplate(Configuration cfg, String templateUri, Path dest, Charset charset, TemplateParams model)
+ throws IOException, TemplateException {
+ final Template template = cfg.getTemplate(templateUri);
+ Files.createDirectories(dest.getParent());
+ try (Writer out = Files.newBufferedWriter(dest)) {
+ template.process(model, out);
+ }
+ }
+
+ static String getProperExtensionName(String artifactId) {
+ final int lBPos = artifactId.indexOf('(');
+ final int rBPos = artifactId.indexOf(')');
+ if (lBPos >= 0 && rBPos >= 0) {
+ return artifactId.substring(lBPos + 1, rBPos);
+ } else {
+ return artifactId;
+ }
+ }
+
+ public static class TemplateParams {
+ String properExtensionName;
+ String properExtensionNameCamelCase;
+ String javaPackage;
+ boolean assumeManaged;
+ Parent parent = new Parent();
+ Project project = new Project();
+ String quarkusVersion;
+
+ public String getJavaPackage() {
+ return javaPackage;
+ }
+
+ public boolean isAssumeManaged() {
+ return assumeManaged;
+ }
+
+ public String getProperExtensionName() {
+ return properExtensionName;
+ }
+
+ public String getProperExtensionNameCamelCase() {
+ return properExtensionNameCamelCase;
+ }
+
+ public Parent getParent() {
+ return parent;
+ }
+
+ public Project getProject() {
+ return project;
+ }
+
+ public String getQuarkusVersion() {
+ return quarkusVersion;
+ }
+
+ public static class Parent extends Project {
+ public String relativePath;
+
+ public String getRelativePath() {
+ return relativePath;
+ }
+ }
+
+ public static class Project {
+ String artifactId;
+ String groupId;
+ String name;
+ String version;
+
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+ }
+ }
+}
diff --git a/devtools/maven/src/main/resources/create-extension-templates/Processor.java b/devtools/maven/src/main/resources/create-extension-templates/Processor.java
new file mode 100644
index 00000000000000..1c692d1981e487
--- /dev/null
+++ b/devtools/maven/src/main/resources/create-extension-templates/Processor.java
@@ -0,0 +1,31 @@
+/*
+ * 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 [=javaPackage];
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+
+class [=properExtensionNameCamelCase]Processor {
+
+ private static final String FEATURE = "[=properExtensionName]";
+
+ @BuildStep
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+
+}
diff --git a/devtools/maven/src/main/resources/create-extension-templates/deployment-pom.xml b/devtools/maven/src/main/resources/create-extension-templates/deployment-pom.xml
new file mode 100644
index 00000000000000..3ee8a1ebbb0c18
--- /dev/null
+++ b/devtools/maven/src/main/resources/create-extension-templates/deployment-pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+[#if parent.groupId?? ]
+ [=parent.groupId]
+ [=parent.artifactId]
+ [=parent.version]
+ [=parent.relativePath]
+
+[/#if]
+
+[#if project.groupId?? && project.groupId != parent.groupId ] [=project.groupId]
+[/#if]
+[#if project.artifactId?? ] [=project.artifactId]
+[/#if]
+[#if project.version?? && project.version != parent.version ] [=project.version]
+[/#if]
+[#if project.name?? ] [=project.name]
+[/#if]
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+[#if !assumeManaged ] [=quarkusVersion]
+[/#if]
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ [=quarkusVersion]
+
+
+
+
+
+
+
+
diff --git a/devtools/maven/src/main/resources/create-extension-templates/parent-pom.xml b/devtools/maven/src/main/resources/create-extension-templates/parent-pom.xml
new file mode 100644
index 00000000000000..e0e58095201ebd
--- /dev/null
+++ b/devtools/maven/src/main/resources/create-extension-templates/parent-pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+[#if parent.groupId?? ]
+ [=parent.groupId]
+ [=parent.artifactId]
+ [=parent.version]
+ [=parent.relativePath]
+
+[/#if]
+
+[#if project.groupId?? && project.groupId != parent.groupId ] [=project.groupId]
+[/#if]
+[#if project.artifactId?? ] [=project.artifactId]
+[/#if]
+[#if project.version?? && project.version != parent.version ] [=project.version]
+[/#if]
+[#if project.name?? ] [=project.name]
+[/#if]
+
+ pom
+
+ deployment
+ runtime
+
+
diff --git a/devtools/maven/src/main/resources/create-extension-templates/runtime-pom.xml b/devtools/maven/src/main/resources/create-extension-templates/runtime-pom.xml
new file mode 100644
index 00000000000000..d042004d39d226
--- /dev/null
+++ b/devtools/maven/src/main/resources/create-extension-templates/runtime-pom.xml
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+[#if parent.groupId?? ]
+ [=parent.groupId]
+ [=parent.artifactId]
+ [=parent.version]
+ [=parent.relativePath]
+
+[/#if]
+
+[#if project.groupId?? && project.groupId != parent.groupId ] [=project.groupId]
+[/#if]
+[#if project.artifactId?? ] [=project.artifactId]
+[/#if]
+[#if project.version?? && project.version != parent.version ] [=project.version]
+[/#if]
+[#if project.name?? ] [=project.name]
+[/#if]
+
+
+
+
+ io.quarkus
+ quarkus-bootstrap-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ [=quarkusVersion]
+
+
+
+
+
+
+
diff --git a/devtools/maven/src/test/java/io/quarkus/maven/CreateExtensionMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/CreateExtensionMojoTest.java
new file mode 100644
index 00000000000000..a50cf41b53608f
--- /dev/null
+++ b/devtools/maven/src/test/java/io/quarkus/maven/CreateExtensionMojoTest.java
@@ -0,0 +1,195 @@
+package io.quarkus.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.codehaus.plexus.util.FileUtils;
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+
+public class CreateExtensionMojoTest {
+
+ static CreateExtensionMojo createMojo(String testProjectName) throws IllegalArgumentException,
+ IllegalAccessException, IOException, NoSuchFieldException, SecurityException {
+ final File srcDir = new File("target/test-classes/projects/" + testProjectName);
+ /* We want to run on the same project multiple times with different args so let's create a copy with a random
+ * suffix */
+ final File copyDir = new File(
+ "target/test-classes/projects/" + testProjectName + "-" + ((int) (Math.random() * 1000)));
+ FileUtils.copyDirectory(srcDir, copyDir);
+
+ final CreateExtensionMojo mojo = new CreateExtensionMojo();
+ mojo.basedir = copyDir;
+ mojo.encoding = CreateExtensionMojo.DEFAULT_ENCODING;
+ mojo.templatesUriBase = CreateExtensionMojo.DEFAULT_TEMPLATES_URI_BASE;
+ mojo.quarkusVersion = CreateExtensionMojo.DEFAULT_QUARKUS_VERSION;
+ mojo.assumeManaged = true;
+ return mojo;
+ }
+
+ @Test
+ void createExtensionUnderExistingPomMinimal() throws MojoExecutionException, MojoFailureException,
+ IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException, IOException {
+ final CreateExtensionMojo mojo = createMojo("create-extension-under-existing-pom");
+ mojo.artifactId = "my-project-(minimal-extension)";
+ mojo.assumeManaged = false;
+ mojo.execute();
+
+ assertTreesMatch(Paths.get("target/test-classes/expected/create-extension-under-existing-pom-minimal"),
+ mojo.basedir.toPath());
+ }
+
+ @Test
+ void createExtensionUnderExistingPomCustomGrandParent() throws MojoExecutionException, MojoFailureException,
+ IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException, IOException {
+ final CreateExtensionMojo mojo = createMojo("create-extension-under-existing-pom");
+ mojo.artifactId = "my-project-(custom-grand-parent-extension)";
+ mojo.grandParentArtifactId = "build-bom";
+ mojo.grandParentRelativePath = "../../build-bom/pom.xml";
+ mojo.execute();
+
+ assertTreesMatch(
+ Paths.get("target/test-classes/expected/create-extension-under-existing-pom-custom-grand-parent"),
+ mojo.basedir.toPath());
+ }
+
+ static void assertTreesMatch(Path expected, Path actual) throws IOException {
+ final Set expectedFiles = new LinkedHashSet<>();
+ Files.walk(expected).filter(Files::isRegularFile).forEach(p -> {
+ final Path relative = expected.relativize(p);
+ expectedFiles.add(relative);
+ final Path actualPath = actual.resolve(relative);
+ try {
+ Assert.assertEquals(new String(Files.readAllBytes(p), StandardCharsets.UTF_8),
+ new String(Files.readAllBytes(actualPath), StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ final Set unexpectedFiles = new LinkedHashSet<>();
+ Files.walk(actual).filter(Files::isRegularFile).forEach(p -> {
+ final Path relative = actual.relativize(p);
+ if (!expectedFiles.contains(relative)) {
+ unexpectedFiles.add(relative);
+ }
+ });
+ if (!unexpectedFiles.isEmpty()) {
+ Assert.fail(String.format("Files found under [%s] but not defined as expected under [%s]:%s", actual,
+ expected, unexpectedFiles.stream().map(Path::toString).collect(Collectors.joining("\n "))));
+ }
+ }
+
+ @Test
+ void getPackage() throws IOException {
+ Assert.assertEquals("org.apache.camel.quarkus.aws.sns.deployment",
+ CreateExtensionMojo.getJavaPackage("org.apache.camel.quarkus", "camel-quarkus-aws-sns-deployment"));
+ }
+
+ @Test
+ void toCapCamelCase() throws IOException {
+ Assert.assertEquals("FooBarBaz", CreateExtensionMojo.toCapCamelCase("foo-bar-baz"));
+ }
+
+ @Test
+ void addModule() throws IOException {
+ final Path path = Paths.get("pom.xml");
+ {
+ final String source = "\n" //
+ + "\n" //
+ + " 4.0.0\n" //
+ + " org.acme\n" //
+ + " grand-parent\n" //
+ + " 0.1-SNAPSHOT\n" //
+ + " pom\n" //
+ + "\n";
+ final String actual = CreateExtensionMojo.addModule(path, source, "new-module");
+ final String expected = "\n" //
+ + "\n" //
+ + " 4.0.0\n" //
+ + " org.acme\n" //
+ + " grand-parent\n" //
+ + " 0.1-SNAPSHOT\n" //
+ + " pom\n" //
+ + " \n" //
+ + " new-module\n" //
+ + " \n" + "\n";
+ Assert.assertEquals(expected, actual);
+ }
+ {
+ final String source = "\n" //
+ + "\n" //
+ + " 4.0.0\n" //
+ + " org.acme\n" //
+ + " grand-parent\n" //
+ + " 0.1-SNAPSHOT\n" //
+ + "\n" //
+ + " pom\n" //
+ + "\n" //
+ + " \n" //
+ + " old-module\n" //
+ + " \n" + "\n";
+ final String actual = CreateExtensionMojo.addModule(path, source, "new-module");
+ final String expected = "\n" //
+ + "\n" //
+ + " 4.0.0\n" //
+ + " org.acme\n" //
+ + " grand-parent\n" //
+ + " 0.1-SNAPSHOT\n" //
+ + "\n" //
+ + " pom\n" //
+ + "\n" //
+ + " \n" //
+ + " old-module\n" //
+ + " new-module\n" //
+ + " \n" //
+ + "\n";
+ Assert.assertEquals(expected, actual);
+ }
+ {
+ final String source = "\n" //
+ + "\n" //
+ + " 4.0.0\n" //
+ + " org.acme\n" //
+ + " grand-parent\n" //
+ + " 0.1-SNAPSHOT\n" //
+ + " pom\n" //
+ + "\n" //
+ + " \n" //
+ + " \n" //
+ + "\n";
+ final String actual = CreateExtensionMojo.addModule(path, source, "new-module");
+ final String expected = "\n" //
+ + "\n" //
+ + " 4.0.0\n" //
+ + " org.acme\n" //
+ + " grand-parent\n" //
+ + " 0.1-SNAPSHOT\n" //
+ + " pom\n" //
+ + "\n" //
+ + " \n" //
+ + " new-module\n" //
+ + " \n" //
+ + " \n" //
+ + " \n" //
+ + "\n";
+ Assert.assertEquals(expected, actual);
+ }
+ }
+
+}
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/deployment/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/deployment/pom.xml
new file mode 100644
index 00000000000000..6c11a1ede12228
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/deployment/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+ org.acme
+ my-project-custom-grand-parent-extension-parent
+ 0.1-SNAPSHOT
+ ../pom.xml
+
+
+ my-project-custom-grand-parent-extension-deployment
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
+
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/deployment/src/main/java/org/acme/my/project/custom/grand/parent/extension/deployment/CustomGrandParentExtensionProcessor.java b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/deployment/src/main/java/org/acme/my/project/custom/grand/parent/extension/deployment/CustomGrandParentExtensionProcessor.java
new file mode 100644
index 00000000000000..73e181283eb7e9
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/deployment/src/main/java/org/acme/my/project/custom/grand/parent/extension/deployment/CustomGrandParentExtensionProcessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.acme.my.project.custom.grand.parent.extension.deployment;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+
+class CustomGrandParentExtensionProcessor {
+
+ private static final String FEATURE = "custom-grand-parent-extension";
+
+ @BuildStep
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+
+}
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/pom.xml
new file mode 100644
index 00000000000000..6af5222b92b371
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/pom.xml
@@ -0,0 +1,20 @@
+
+
+ 4.0.0
+
+ org.acme
+ build-bom
+ 0.1-SNAPSHOT
+ ../../build-bom/pom.xml
+
+
+ my-project-custom-grand-parent-extension-parent
+
+ pom
+
+ deployment
+ runtime
+
+
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/runtime/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/runtime/pom.xml
new file mode 100644
index 00000000000000..e01b94707ccab2
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/custom-grand-parent-extension/runtime/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ org.acme
+ my-project-custom-grand-parent-extension-parent
+ 0.1-SNAPSHOT
+ ../pom.xml
+
+
+ my-project-custom-grand-parent-extension
+
+
+
+
+ io.quarkus
+ quarkus-bootstrap-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/pom.xml
new file mode 100644
index 00000000000000..f191fba3fd0e2a
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/pom.xml
@@ -0,0 +1,15 @@
+
+
+ 4.0.0
+ org.acme
+ grand-parent
+ 0.1-SNAPSHOT
+ pom
+
+ 0.19.0
+
+
+ custom-grand-parent-extension
+
+
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/deployment/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/deployment/pom.xml
new file mode 100644
index 00000000000000..db67b883ba750e
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/deployment/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ org.acme
+ my-project-minimal-extension-parent
+ 0.1-SNAPSHOT
+ ../pom.xml
+
+
+ my-project-minimal-extension-deployment
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+ ${quarkus.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
+
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/deployment/src/main/java/org/acme/my/project/minimal/extension/deployment/MinimalExtensionProcessor.java b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/deployment/src/main/java/org/acme/my/project/minimal/extension/deployment/MinimalExtensionProcessor.java
new file mode 100644
index 00000000000000..1bf2c4d33b8929
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/deployment/src/main/java/org/acme/my/project/minimal/extension/deployment/MinimalExtensionProcessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.acme.my.project.minimal.extension.deployment;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+
+class MinimalExtensionProcessor {
+
+ private static final String FEATURE = "minimal-extension";
+
+ @BuildStep
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+
+}
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/pom.xml
new file mode 100644
index 00000000000000..852820aef61950
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/pom.xml
@@ -0,0 +1,20 @@
+
+
+ 4.0.0
+
+ org.acme
+ grand-parent
+ 0.1-SNAPSHOT
+ ../pom.xml
+
+
+ my-project-minimal-extension-parent
+
+ pom
+
+ deployment
+ runtime
+
+
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/runtime/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/runtime/pom.xml
new file mode 100644
index 00000000000000..43b216a68282f9
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/runtime/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ org.acme
+ my-project-minimal-extension-parent
+ 0.1-SNAPSHOT
+ ../pom.xml
+
+
+ my-project-minimal-extension
+
+
+
+
+ io.quarkus
+ quarkus-bootstrap-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
diff --git a/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/pom.xml b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/pom.xml
new file mode 100644
index 00000000000000..262938d4c1d3d0
--- /dev/null
+++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/pom.xml
@@ -0,0 +1,15 @@
+
+
+ 4.0.0
+ org.acme
+ grand-parent
+ 0.1-SNAPSHOT
+ pom
+
+ 0.19.0
+
+
+ minimal-extension
+
+
diff --git a/devtools/maven/src/test/resources/projects/create-extension-under-existing-pom/pom.xml b/devtools/maven/src/test/resources/projects/create-extension-under-existing-pom/pom.xml
new file mode 100644
index 00000000000000..b015f10828e2c2
--- /dev/null
+++ b/devtools/maven/src/test/resources/projects/create-extension-under-existing-pom/pom.xml
@@ -0,0 +1,12 @@
+
+
+ 4.0.0
+ org.acme
+ grand-parent
+ 0.1-SNAPSHOT
+ pom
+
+ 0.19.0
+
+