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..9bec523c1587f8 --- /dev/null +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java @@ -0,0 +1,608 @@ +package io.quarkus.maven; + +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.nio.file.Paths; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import freemarker.cache.ClassTemplateLoader; +import freemarker.cache.FileTemplateLoader; +import freemarker.cache.MultiTemplateLoader; +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 Logger log = LoggerFactory.getLogger(CreateExtensionMojo.class); + + 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"; + static final String DEFAULT_NAME_SEGMENT_DELIMITER = " - "; + + /** + * Directory where the changes should be performed. Default is the current directory of the current Java process. + */ + @Parameter(property = "quarkus.basedir") + Path 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 {@link #artifactIdBase} enclosed in round brackets, e.g. + * {@code my-project-(cool-extension)}, where {@code cool-extension} is an {@link #artifactIdBase} and + * {@code my-project-} is {@link #artifactIdPrefix}. This is a way to avoid defining of {@link #artifactIdPrefix} + * and {@link #artifactIdBase} separately. If no round brackets are present in {@link #artifactId}, + * {@link #artifactIdBase} will be equal to {@link #artifactId} and {@link #artifactIdPrefix} will be an empty + * string. + */ + @Parameter(property = "quarkus.artifactId") + String artifactId; + + /** + * A prefix common to all extension artifactIds in the current source tree. If you set {@link #artifactIdPrefix}, + * set also {@link #artifactIdBase}, but do not set {@link #artifactId}. + */ + @Parameter(property = "quarkus.artifactIdPrefix") + String artifactIdPrefix; + + /** + * The unique part of the {@link #artifactId}. If you set {@link #artifactIdBase}, set also + * {@link #artifactIdPrefix}, but do not set {@link #artifactId}. + */ + @Parameter(property = "quarkus.artifactIdBase") + String artifactIdBase; + + /** + * 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.artifactVersion") + 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. + *

+ * Optionally, this value can contain the {@link #nameBase} enclosed in round brackets, e.g. + * {@code My Project - (Cool Extension)}, where {@code Cool Extension} is a {@link #nameBase} and + * {@code My Project - } is {@link #namePrefix}. This is a way to avoid defining of {@link #namePrefix} and + * {@link #nameBase} separately. If no round brackets are present in {@link #name}, the {@link #nameBase} will be + * equal to {@link #name} and {@link #namePrefix} will be an empty string. + */ + @Parameter(property = "quarkus.name") + String name; + + /** + * A prefix common to all extension names in the current source tree. If you set {@link #namePrefix}, set also + * {@link #nameBase}, but do not set {@link #name}. + */ + @Parameter(property = "quarkus.namePrefix") + String namePrefix; + + /** + * The unique part of the {@link #name}. If you set {@link #nameBase}, set also {@link #namePrefix}, but do not set + * {@link #name}. + *

+ * If neither {@link #name} nor @{link #nameBase} is set, @{link #nameBase} will be derived from + * {@link #artifactIdBase}. + */ + @Parameter(property = "quarkus.nameBase") + String nameBase; + + /** + * A string that will delimit {@link #name} from {@code Parent}, {@code Runtime} and {@code Deployment} tokens in + * the respective modules. + */ + @Parameter(property = "quarkus.nameSegmentDelimiter", defaultValue = DEFAULT_NAME_SEGMENT_DELIMITER) + String nameSegmentDelimiter; + + /** + * Base Java package under which Java classes should be created in Runtime and Deployment modules. If not set, the + * Java package will be auto-generated out of {@link #groupId}, {@link #javaPackageInfix} and {@link #artifactId} + */ + @Parameter(property = "quarkus.javaPackageBase") + String javaPackageBase; + + /** + * If {@link #javaPackageBase} is not set explicitly, this infix will be put between package segments taken from + * {@link #groupId} and {@link #artifactId}. + *

+ * Example: Given + *

+ * Then the auto-generated {@link #javaPackageBase} will be + * {@code org.example.quarkus.extensions.foo.bar.cool.extension} + */ + @Parameter(property = "quarkus.javaPackageInfix") + String javaPackageInfix; + + /** + * 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. You need to touch + * this only if you want to provide your own custom templates. + *

+ * The following URI schemes are supported: + *

+ * These are the template files you may want to provide under your custom {@link #templatesUriBase}: + * + * Note that you do not need to provide all of them. Files not available in your custom {@link #templatesUriBase} + * will be looked up in the default URI base {@value #DEFAULT_TEMPLATES_URI_BASE}. The default templates are + * maintained here. + */ + @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 { + + if (this.basedir == null) { + this.basedir = Paths.get(".").toAbsolutePath().normalize(); + } + if (artifactId != null) { + artifactIdBase = artifactIdBase(artifactId); + artifactIdPrefix = artifactId.substring(0, artifactId.length() - artifactIdBase.length()); + artifactId = BRACKETS_PATTERN.matcher(artifactId).replaceAll(""); + } else if (artifactIdPrefix != null && artifactIdBase != null) { + artifactId = artifactIdPrefix + artifactIdBase; + } else { + throw new MojoFailureException(String.format( + "Either artifactId or both artifactIdPrefix and artifactIdBase must be specified; found: artifactId=[%s], artifactIdPrefix=[%s], artifactIdBase[%s]", + artifactId, artifactIdPrefix, artifactIdBase)); + } + + if (name != null) { + final int pos = name.lastIndexOf(nameSegmentDelimiter); + if (pos >= 0) { + nameBase = name.substring(pos + nameSegmentDelimiter.length()); + } else { + nameBase = name; + } + namePrefix = name.substring(0, name.length() - nameBase.length()); + } else { + if (nameBase == null) { + nameBase = toCapWords(artifactIdBase); + } + if (namePrefix == null) { + namePrefix = ""; + } + if (nameBase != null && namePrefix != null) { + name = namePrefix + nameBase; + } else { + throw new MojoFailureException("Either name or both namePrefix and nameBase must be specified"); + } + } + + final Charset charset = Charset.forName(encoding); + + final Path basePomXml = basedir.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); + } + } + + void addModules(Path basePomXml, Model basePom, Charset charset) throws IOException, TemplateException { + + final Configuration cfg = new Configuration(Configuration.VERSION_2_3_28); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + cfg.setTemplateLoader(createTemplateLoader(basedir, templatesUriBase)); + cfg.setDefaultEncoding(charset.name()); + cfg.setInterpolationSyntax(Configuration.SQUARE_BRACKET_INTERPOLATION_SYNTAX); + cfg.setTagSyntax(Configuration.SQUARE_BRACKET_TAG_SYNTAX); + + TemplateParams model = new TemplateParams(); + + model.artifactId = artifactId; + model.artifactIdPrefix = artifactIdPrefix; + model.artifactIdBase = artifactIdBase; + model.artifactIdBaseCamelCase = toCapCamelCase(model.artifactIdBase); + + model.groupId = this.groupId != null ? this.groupId : getGroupId(basePom); + model.version = this.version != null ? this.version : getVersion(basePom); + + model.namePrefix = namePrefix; + model.nameBase = nameBase; + model.nameSegmentDelimiter = nameSegmentDelimiter; + model.assumeManaged = assumeManaged; + model.quarkusVersion = quarkusVersion.replace('@', '$'); + + model.grandParentGroupId = grandParentGroupId != null ? grandParentGroupId : getGroupId(basePom); + model.grandParentArtifactId = grandParentArtifactId != null ? grandParentArtifactId : basePom.getArtifactId(); + model.grandParentVersion = grandParentVersion != null ? grandParentVersion : getVersion(basePom); + model.grandParentRelativePath = grandParentRelativePath != null ? grandParentRelativePath : "../pom.xml"; + model.javaPackageBase = javaPackageBase != null ? javaPackageBase + : getJavaPackage(model.groupId, javaPackageInfix, artifactId); + + evalTemplate(cfg, "parent-pom.xml", basedir.resolve(model.artifactIdBase + "/pom.xml"), charset, model); + + Files.createDirectories(basedir + .resolve(model.artifactIdBase + "/runtime/src/main/java/" + model.javaPackageBase.replace('.', '/'))); + evalTemplate(cfg, "runtime-pom.xml", basedir.resolve(model.artifactIdBase + "/runtime/pom.xml"), charset, + model); + + evalTemplate(cfg, "deployment-pom.xml", basedir.resolve(model.artifactIdBase + "/deployment/pom.xml"), charset, + model); + final Path processorPath = basedir + .resolve(model.artifactIdBase + "/deployment/src/main/java/" + model.javaPackageBase.replace('.', '/') + + "/deployment/" + model.artifactIdBaseCamelCase + "Processor.java"); + evalTemplate(cfg, "Processor.java", processorPath, charset, model); + + if (!basePom.getModules().contains(model.artifactIdBase)) { + final String basePomSource = new String(Files.readAllBytes(basePomXml), charset); + final String newSource = addModule(basePomXml, basePomSource, model.artifactIdBase); + Files.write(basePomXml, newSource.getBytes(charset)); + } + + } + + static String getGroupId(Model basePom) { + return basePom.getGroupId() != null ? basePom.getGroupId() + : basePom.getParent() != null && basePom.getParent().getGroupId() != null + ? basePom.getParent().getGroupId() + : null; + } + + static String getVersion(Model basePom) { + return basePom.getVersion() != null ? basePom.getVersion() + : basePom.getParent() != null && basePom.getParent().getVersion() != null + ? basePom.getParent().getVersion() + : null; + } + + static String toCapCamelCase(String artifactIdBase) { + final StringBuilder sb = new StringBuilder(artifactIdBase.length()); + for (String segment : artifactIdBase.split("[.\\-]+")) { + sb.append(Character.toUpperCase(segment.charAt(0))); + if (segment.length() > 1) { + sb.append(segment.substring(1)); + } + } + return sb.toString(); + } + + static String toCapWords(String artifactIdBase) { + final StringBuilder sb = new StringBuilder(artifactIdBase.length()); + for (String segment : artifactIdBase.split("[.\\-]+")) { + if (sb.length() > 0) { + sb.append(' '); + } + 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 javaPackageInfix, String artifactId) { + final Stack segments = new Stack<>(); + for (String segment : groupId.split("[.\\-]+")) { + if (segments.isEmpty() || !segments.peek().equals(segment)) { + segments.add(segment); + } + } + if (javaPackageInfix != null) { + for (String segment : javaPackageInfix.split("[.\\-]+")) { + 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 TemplateLoader createTemplateLoader(Path basedir, String templatesUriBase) throws IOException { + final TemplateLoader defaultLoader = new ClassTemplateLoader(CreateExtensionMojo.class, + DEFAULT_TEMPLATES_URI_BASE.substring(CLASSPATH_PREFIX.length())); + if (DEFAULT_TEMPLATES_URI_BASE.equals(templatesUriBase)) { + return defaultLoader; + } else if (templatesUriBase.startsWith(CLASSPATH_PREFIX)) { + return new MultiTemplateLoader( // + new TemplateLoader[] { // + new ClassTemplateLoader(CreateExtensionMojo.class, + templatesUriBase.substring(CLASSPATH_PREFIX.length())), // + defaultLoader // + }); + } else if (templatesUriBase.startsWith(FILE_PREFIX)) { + return new MultiTemplateLoader( // + new TemplateLoader[] { // + new FileTemplateLoader( + basedir.resolve(templatesUriBase.substring(FILE_PREFIX.length())).toFile()), // + defaultLoader // + }); + } 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 { + log.info("Adding '{}'", dest); + final Template template = cfg.getTemplate(templateUri); + Files.createDirectories(dest.getParent()); + try (Writer out = Files.newBufferedWriter(dest)) { + template.process(model, out); + } + } + + static String artifactIdBase(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 grandParentRelativePath; + String grandParentVersion; + String grandParentArtifactId; + String grandParentGroupId; + String groupId; + String artifactId; + String artifactIdPrefix; + String artifactIdBase; + String artifactIdBaseCamelCase; + String version; + String namePrefix; + String nameBase; + String nameSegmentDelimiter; + String javaPackageBase; + boolean assumeManaged; + String quarkusVersion; + + public String getJavaPackageBase() { + return javaPackageBase; + } + + public boolean isAssumeManaged() { + return assumeManaged; + } + + public String getArtifactIdPrefix() { + return artifactIdPrefix; + } + + public String getArtifactIdBase() { + return artifactIdBase; + } + + public String getNamePrefix() { + return namePrefix; + } + + public String getNameBase() { + return nameBase; + } + + public String getNameSegmentDelimiter() { + return nameSegmentDelimiter; + } + + public String getArtifactIdBaseCamelCase() { + return artifactIdBaseCamelCase; + } + + public String getQuarkusVersion() { + return quarkusVersion; + } + + public String getGrandParentRelativePath() { + return grandParentRelativePath; + } + + public String getGrandParentVersion() { + return grandParentVersion; + } + + public String getGrandParentArtifactId() { + return grandParentArtifactId; + } + + public String getGrandParentGroupId() { + return grandParentGroupId; + } + + public String getGroupId() { + return groupId; + } + + public String getVersion() { + return version; + } + + public String getArtifactId() { + return artifactId; + } + } +} 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..b5b672945b3663 --- /dev/null +++ b/devtools/maven/src/main/resources/create-extension-templates/Processor.java @@ -0,0 +1,15 @@ +package [=javaPackageBase].deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class [=artifactIdBaseCamelCase]Processor { + + private static final String FEATURE = "[=artifactIdBase]"; + + @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..422ba4ef43febd --- /dev/null +++ b/devtools/maven/src/main/resources/create-extension-templates/deployment-pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + [=groupId] + [=artifactId]-parent + [=version] + ../pom.xml + + + [=artifactId]-deployment +[#if nameBase?? ] [=namePrefix][=nameBase][=nameSegmentDelimiter]Deployment +[/#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..cdd46a5a0e1657 --- /dev/null +++ b/devtools/maven/src/main/resources/create-extension-templates/parent-pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 +[#if grandParentGroupId?? ] + [=grandParentGroupId] + [=grandParentArtifactId] + [=grandParentVersion] + [=grandParentRelativePath] + +[/#if] + +[#if groupId?? && groupId != grandParentGroupId ] [=groupId] +[/#if] + [=artifactId]-parent +[#if groupId?? && groupId != grandParentGroupId && version?? && version != grandParentVersion ] [=version] +[/#if] +[#if nameBase?? ] [=namePrefix][=nameBase][=nameSegmentDelimiter]Parent +[/#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..70fc5cbb9cfb6b --- /dev/null +++ b/devtools/maven/src/main/resources/create-extension-templates/runtime-pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + [=groupId] + [=artifactId]-parent + [=version] + ../pom.xml + + + [=artifactId] +[#if nameBase?? ] [=namePrefix][=nameBase][=nameSegmentDelimiter]Runtime +[/#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..c34f4d65bf6c0d --- /dev/null +++ b/devtools/maven/src/test/java/io/quarkus/maven/CreateExtensionMojoTest.java @@ -0,0 +1,205 @@ +package io.quarkus.maven; + +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.junit.Assert; +import org.junit.jupiter.api.Test; + +public class CreateExtensionMojoTest { + + static CreateExtensionMojo createMojo(String testProjectName) throws IllegalArgumentException, + IllegalAccessException, IOException, NoSuchFieldException, SecurityException { + final Path srcDir = Paths.get("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 Path copyDir = Paths + .get("target/test-classes/projects/" + testProjectName + "-" + ((int) (Math.random() * 1000))); + Files.walk(srcDir).forEach(source -> { + try { + Files.copy(source, copyDir.resolve(srcDir.relativize(source))); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + 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; + mojo.nameSegmentDelimiter = CreateExtensionMojo.DEFAULT_NAME_SEGMENT_DELIMITER; + 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); + } + + @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.templatesUriBase = "file:templates"; + mojo.execute(); + + assertTreesMatch( + Paths.get("target/test-classes/expected/create-extension-under-existing-pom-custom-grand-parent"), + mojo.basedir); + } + + 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", null, "camel-quarkus-aws-sns-deployment")); + Assert.assertEquals("org.apache.camel.quarkus.component.aws.sns.deployment", CreateExtensionMojo + .getJavaPackage("org.apache.camel.quarkus", "component", "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..b2f6fd02ced66e --- /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,41 @@ + + + 4.0.0 + + org.acme + my-project-custom-grand-parent-extension-parent + 0.1-SNAPSHOT + ../pom.xml + + + my-project-custom-grand-parent-extension-deployment + 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..cc8f81669286d3 --- /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,17 @@ +package org.acme.my.project.custom.grand.parent.extension.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class CustomGrandParentExtensionProcessor { + + // Custom + + 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..b01ac8dec704e7 --- /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,21 @@ + + + 4.0.0 + + org.acme + build-bom + 0.1-SNAPSHOT + ../../build-bom/pom.xml + + + my-project-custom-grand-parent-extension-parent + 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..7e4ca4cf4a84b4 --- /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,37 @@ + + + 4.0.0 + + org.acme + my-project-custom-grand-parent-extension-parent + 0.1-SNAPSHOT + ../pom.xml + + + my-project-custom-grand-parent-extension + Custom Grand Parent Extension - Runtime + + + + + 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-custom-grand-parent/templates/Processor.java b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/templates/Processor.java new file mode 100644 index 00000000000000..585388c7a7e32c --- /dev/null +++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-custom-grand-parent/templates/Processor.java @@ -0,0 +1,17 @@ +package [=javaPackageBase].deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class [=artifactIdBaseCamelCase]Processor { + + // Custom + + private static final String FEATURE = "[=artifactIdBase]"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + +} 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..0acc771afd010a --- /dev/null +++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/deployment/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.acme + my-project-minimal-extension-parent + 0.1-SNAPSHOT + ../pom.xml + + + my-project-minimal-extension-deployment + 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..e06f670d072c13 --- /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,15 @@ +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..c3be7b50962c93 --- /dev/null +++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + org.acme + grand-parent + 0.1-SNAPSHOT + ../pom.xml + + + my-project-minimal-extension-parent + 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..a76a22e95a2f00 --- /dev/null +++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/minimal-extension/runtime/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + org.acme + my-project-minimal-extension-parent + 0.1-SNAPSHOT + ../pom.xml + + + my-project-minimal-extension + Minimal Extension - Runtime + + + + + 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/expected/create-extension-under-existing-pom-minimal/templates/Processor.java b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/templates/Processor.java new file mode 100644 index 00000000000000..585388c7a7e32c --- /dev/null +++ b/devtools/maven/src/test/resources/expected/create-extension-under-existing-pom-minimal/templates/Processor.java @@ -0,0 +1,17 @@ +package [=javaPackageBase].deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class [=artifactIdBaseCamelCase]Processor { + + // Custom + + private static final String FEATURE = "[=artifactIdBase]"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + +} 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 + + diff --git a/devtools/maven/src/test/resources/projects/create-extension-under-existing-pom/templates/Processor.java b/devtools/maven/src/test/resources/projects/create-extension-under-existing-pom/templates/Processor.java new file mode 100644 index 00000000000000..585388c7a7e32c --- /dev/null +++ b/devtools/maven/src/test/resources/projects/create-extension-under-existing-pom/templates/Processor.java @@ -0,0 +1,17 @@ +package [=javaPackageBase].deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class [=artifactIdBaseCamelCase]Processor { + + // Custom + + private static final String FEATURE = "[=artifactIdBase]"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + +}