diff --git a/devtools/maven/pom.xml b/devtools/maven/pom.xml index e715fc92ecac39..cdffe48c7e53d9 100644 --- a/devtools/maven/pom.xml +++ b/devtools/maven/pom.xml @@ -102,6 +102,12 @@ 2.14.6 + + org.freemarker + freemarker + 2.3.28 + + 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: + *

+ * If you choose to override the default you need to provide the following files under the specified path: + * + */ + @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 + +