From c4a0633569e6000fe79a6a4be2d2a640c9a4737f Mon Sep 17 00:00:00 2001 From: aserkes Date: Thu, 13 Apr 2023 19:04:25 +0200 Subject: [PATCH 1/2] check of compatibility archetype version and cli (draft) helidon init compatibility support fix check compatibility in ArchetypeEngineV2.java add logs to test add logs to test, fix copyright fix check compatibility in ArchetypeEngineV2 use maven-remote-resources-plugin to share resources between modules refactoring refactoring code change after review fix IT test Signed-off-by: aserkes --- archetype/engine-v2/pom.xml | 38 +- .../engine/v2/ArchetypeEngineV2.java | 35 + .../engine-v2/src/main/java/module-info.java | 3 +- .../src/main/schema/archetype-2.0.xsd | 1375 +++++++++++++++++ .../helidon-archetype-maven-plugin/pom.xml | 26 +- .../src/it/projects/test7/expected.log | 1 + .../src/it/projects/test7/pom.xml | 51 + .../src/it/projects/test7/postbuild.groovy | 29 + .../test7/src/main/archetype/circle.xml | 28 + .../src/main/archetype/files/pom.xml.mustache | 35 + .../src/main/java/__pkg__/Shape.java.mustache | 26 + .../test7/src/main/archetype/main.xml | 59 + .../build/maven/archetype/Converter.java | 30 +- .../build/maven/archetype/JarMojo.java | 94 +- .../helidon/build/maven/archetype/Schema.java | 62 +- .../build/maven/archetype/Validator.java | 53 +- .../build/maven/archetype/SchemaTest.java | 4 +- .../resources/simple/META-INF/MANIFEST.MF | 1 + pom.xml | 6 + 19 files changed, 1898 insertions(+), 58 deletions(-) create mode 100644 archetype/engine-v2/src/main/schema/archetype-2.0.xsd create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/expected.log create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/pom.xml create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/postbuild.groovy create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/circle.xml create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/pom.xml.mustache create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/src/main/java/__pkg__/Shape.java.mustache create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/main.xml create mode 100644 maven-plugins/helidon-archetype-maven-plugin/src/test/resources/simple/META-INF/MANIFEST.MF diff --git a/archetype/engine-v2/pom.xml b/archetype/engine-v2/pom.xml index 00ac9e273..fe64cbcbb 100644 --- a/archetype/engine-v2/pom.xml +++ b/archetype/engine-v2/pom.xml @@ -49,6 +49,11 @@ helidon-build-common-xml ${project.version} + + io.helidon.build-tools.common + helidon-build-common-maven + ${project.version} + com.github.spullara.mustache.java compiler @@ -77,25 +82,34 @@ + + + ${project.basedir}/src/main/resources + + + ${project.basedir}/src/main/schema + ${project.build.outputDirectory}/schemas + + *.xsd + + + - org.codehaus.mojo - build-helper-maven-plugin + org.apache.maven.plugins + maven-remote-resources-plugin - attach-artifacts - package + bundle-resources + process-resources - attach-artifact + bundle - - - src/main/schema/archetype.xsd - xsd - schema-2.0 - - + ${project.build.outputDirectory} + + **/schemas/**/*.xsd + diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2.java index c7f2dfcaf..c2a105cfe 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2.java @@ -17,15 +17,21 @@ package io.helidon.build.archetype.engine.v2; import java.io.File; +import java.io.IOException; import java.nio.file.FileSystem; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.function.Function; +import java.util.jar.Manifest; import io.helidon.build.archetype.engine.v2.ast.Script; import io.helidon.build.archetype.engine.v2.context.Context; import io.helidon.build.archetype.engine.v2.context.ContextSerializer; import io.helidon.build.common.FileUtils; +import io.helidon.build.common.RequirementFailure; +import io.helidon.build.common.maven.MavenVersion; +import io.helidon.build.common.maven.VersionRange; import static java.util.Objects.requireNonNull; @@ -37,6 +43,8 @@ public class ArchetypeEngineV2 { private static final String ENTRYPOINT = "main.xml"; private static final String ARTIFACT_ID = "artifactId"; + private static final VersionRange COMPATIBLE_SCHEMA_RANGE = VersionRange.createFromVersionSpec("[2.0.0,3.0.0)"); + private final Path cwd; private final InputResolver inputResolver; private final Map externalValues; @@ -46,6 +54,7 @@ public class ArchetypeEngineV2 { private final File outputPropsFile; private ArchetypeEngineV2(Builder builder) { + checkCompatability(builder.cwd); this.cwd = builder.cwd; this.inputResolver = builder.inputResolver; this.externalValues = builder.externalValues; @@ -55,6 +64,32 @@ private ArchetypeEngineV2(Builder builder) { this.outputPropsFile = builder.outputPropsFile; } + private void checkCompatability(Path cwd) { + Path manifestPath = cwd.resolve("META-INF/MANIFEST.MF"); + if (!Files.exists(manifestPath)) { + //it is not an archetype archive + return; + } + try { + Manifest manifest = new Manifest(Files.newInputStream(manifestPath)); + String maxVersion = manifest.getMainAttributes().getValue("archetype-schema-version"); + if (maxVersion == null) { + //it is a some previous version of archetype archive + return; + } + MavenVersion mavenVersion = MavenVersion.toMavenVersion(maxVersion); + if (!COMPATIBLE_SCHEMA_RANGE.containsVersion(mavenVersion)) { + throw new RequirementFailure( + String.format("Version of schema %s is not compatible with the current version of " + + "the Archetype engine (v2). The version of schema must be in range %s", + maxVersion, + COMPATIBLE_SCHEMA_RANGE)); + } + } catch (IOException e) { + throw new RequirementFailure(e.getMessage()); + } + } + /** * Generate a project. * diff --git a/archetype/engine-v2/src/main/java/module-info.java b/archetype/engine-v2/src/main/java/module-info.java index c09f37632..40786ae5a 100644 --- a/archetype/engine-v2/src/main/java/module-info.java +++ b/archetype/engine-v2/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ requires io.helidon.build.common.ansi; requires io.helidon.build.common.xml; requires com.github.mustachejava; + requires io.helidon.build.common.maven; exports io.helidon.build.archetype.engine.v2; exports io.helidon.build.archetype.engine.v2.ast; diff --git a/archetype/engine-v2/src/main/schema/archetype-2.0.xsd b/archetype/engine-v2/src/main/schema/archetype-2.0.xsd new file mode 100644 index 000000000..0a891607f --- /dev/null +++ b/archetype/engine-v2/src/main/schema/archetype-2.0.xsd @@ -0,0 +1,1375 @@ + + + + + + + + 2.0 + + File path of a script relative to the current script directory ; a leading / can be + used to + refer to the archetype root directory. + + + + + + 2.0 + URL of a script. + + + + + + + + 2.0 + + Directive that executes a script in its respective directory. + + + + + + + + 2.0 + + Directive that executes a script in the current script directory. +
+
+ NOTE: + Relative file path will be resolved in the current script directory, thus files that + are co-located with the script to be executed won't be resolved. +
+
+ +
+ + + + 2.0 + + Directive that executes a method. + + + + + 2.0 + + Name of the method to invoke. + + + + + + + + 2.0 + + Ant glob pattern. +
+
    +
  • + Use * to wildcard file and directory names and + ** + to wildcard any depth of directory structure. +
  • +
  • + E.g. + <include>**/*.java</include> +
  • +
+
+
+ + + + + +
+ + + + + + 2.0 + + File path of a base directory that contains the files to be included. Relative file path + are resolved relative to the current script directory. A leading / can be used to + refer to the archetype root directory. +
+
+ By default everything is included, unless includes or excludes patterns are set. +
+
+
+ + + 2.0 + + Set of include patterns. + + + + + + + + + + + 2.0 + + Set of exclude patterns. + + + + + + + + +
+
+ + + + + 2.0 + + Define the set of path transformations to apply. +
+
+ Multiple transformations can be specified, separated by a comma (,) character. +
+
+
+
+ + + + + 2.0 + + Expression to guard this element. +
+
+

+ Expressions are boolean expressions that can be used to query the context. +
+
+ Values can be either context values (${path}) or literal, and are only of the + following types: +

+
    +
  • Boolean: true or + false +
  • +
  • Text: + 'foo' +
  • +
  • Array: + ['foo', 'bar'] +
  • +
+ The following operators are supported: +
    +
  • + + && + + : logical AND +
  • +
  • + + || + + : logical OR +
  • +
  • + + ! + + : logical negation +
  • +
  • + + contains + + : (array|string) contains +
  • +
  • + + == + + : equality +
  • +
+ Example: +
+ +
+ if="${security}" +
+
+ if="${media-support.json.provider} == 'jackson'" +
+
+ if="${security} && ${media-support}" +
+
+ if="${security} || ${media-support}" +
+
+ if="${security.authentication.provider} contains 'basic-auth'" +
+
+ if="!(${security} && ${media-support}) || ${health}" +
+
+
+
+
+ + + + + 2.0 + + Data key. +
+
+

+ The template data model is shared. The key identifies the data to set. + + The key is the identifier used when resolving data from a template. The syntax for + resolving data from a template is template specific. E.g. in mustache: {{my-key}}. +

+
+
+
+
+ + + + + 2.0 + + Data model order. +
+
+

+ The order is used to affect how the data for the same keys are merged. The value is an integer + with a default value of 100. +

+

+ Data is sorted by comparing order, data with higher order is inserted before data with lower + order. When the order is equal, the distance to the root input node and the declaration order + is used. +

+
+
+
+
+ + + + 2.0 + + Value data type. +
+
+

+ The value for the data can be declared as a literal, or it can also use a property like syntax + to resolve the value of an input. +
+
+ E.g. +

    +
  • + <value>${media-support.json.provider}</value> +
  • +
  • + <value>foo</value> +
  • +
+ Inline search and replace regular expressions are also supported: +
    +
  • + <value>${package/\./\/}</value> +
  • +
+

+
+
+ + + + + 2.0 + + File path to a file containing the content for this value. + + + + + + 2.0 + + URL point at a file containing the content for this value. + + + + + + 2.0 + + Pre-process the value with the given template engine. The value must match + a template engine registered with the archetype engine. + + + + + + + +
+ + + + 2.0 + + Value data type. +
+
+

+ The value for the data can be declared as a literal, or it can also use a property like syntax + to resolve the value of an input. +
+
+ E.g. +

    +
  • + <value>${media-support.json.provider}</value> +
  • +
  • + <value>foo</value> +
  • +
+ Inline search and replace regular expressions are also supported: +
    +
  • + <value>${package/\./\/}</value> +
  • +
+

+
+
+ + + + + +
+ + + + 2.0 + + Map data type. + + + + + + + + + + + + + + + + + + + + + + 2.0 + + List data type. Nested elements do not require a key attribute. + + + + + + + + + + + + + + + + + + + + + + + 2.0 + + Indicate if a variable should be serialized to a client. + + + + + + + + + 2.0 + + The path of an input whose value is to be set in the context. +
+
    +
  • A path contains segments separated by "." characters
  • +
  • Segments are ids of the parents for a given input
  • +
  • Segments can contain only letters, digits and separator "-"
  • +
  • The segment separator "-" must be used between valid characters ; + ("--" is prohibited +
  • +
  • Two reference operators are available, root scope: "~" ; parent scope: ".." +
  • +
  • A path that starts with "~" is absolute. I.e. relative to the root scope
  • +
  • A path that starts with a segment is relative, or a parent reference is relative
  • +
+ +
+ Inputs can be declared as global in order to shorten the external path of an input. +
+ The external path is the control visible to end-users. +
+
+ An input can be declared global if: +
    +
  • the input has no parent input
  • +
  • the input is nested under a global input
  • +
+
+ There are effectively two kinds of path: +
+
    +
  • internal: all inputs are included in the path
  • +
  • external: global parents are excluded
  • +
+
+ Example: +
+ "foo.bar.bob" is equivalent to "bob" if: +
    +
  • all inputs are global
  • +
  • "foo" and "bar" are global
  • +
+
+ "foo.bar.bob" is equivalent to "bar.bob" if: +
    +
  • all inputs are global
  • +
  • "foo" is global
  • +
+
+
+
+ + + + + + + + + + + + 2.0 + + Define a variable. + + + + + + + 2.0 + + Define a boolean value. + + + + + + + + + + + + + + 2.0 + + Define a text value. + + + + + + + + + + + + + + 2.0 + + Define an enum value. + + + + + + + + + + + + + + 2.0 + + Define a list value. + + + + + + + + + + + + + + + + + + + 2.0 + + Define a read-only value for an input. + + + + + + + 2.0 + + Define a boolean value. + + + + + + + + + + + + + 2.0 + + Define a text value. + + + + + + + + + + + + + 2.0 + + Define an enum value. + + + + + + + + + + + + + 2.0 + + Define a list value. + + + + + + + + + + + + + + + + + + 2.0 + + Define rich help text for the enclosing element. +
+
+
    +
  • + When enclosed by <archetype-script> in the entry-point script, it defines + the help text for the entire archetype +
  • +
  • + When enclosed by <archetype-script> in a script that is invoked, it defines + the help text for the enclosing element of the directive used to invoke the script +
  • +
+
+ The rich text format is a limited Markdown format: +
+
+ + **bold text** +
_italic_ +
paragraphs +
`code` +
[Links](https://example.com) +
{::color-primary}text in primary color{:/} +
{::color-secondary}text in secondary color{:/} +
{::color-accent}text in accent color{:/} +
{::color-error}text in error color{:/} +
{::color-info}text in info color{:/} +
{::color-success}text in success color{:/} +
{::color-warning}text in warning color{:/} +
+
+
+ Example: +
+
+ + <help><![CDATA[ +
This is a rich help text. **THIS IS BOLD** ; **THIS IS ITALIC**. +
+
This is a paragraph +
+
This is a Java code snippet: +
```java +
request.content() +
       .as(JsonObject.class) +
       .thenAccept(json -> { +
+
    System.output.println(json); +
    response.send("OK"); +
}); +
``` +
]]></help> +
+
+
+ + + +
+ + + + + 2.0 + + Human readable name for step or input components. + + + + + + + + + 2.0 + + Input description (single line). + + + + + + + + + 2.0 + + Indicate if the step or input is optional. + + + + + + + + + 2.0 + + Indicate if the step or input is global. + + + + + + + + + + + + + + + + + + + + + + + + 2.0 + + The input id. Must be unique among siblings. + + + + + + 2.0 + + The prompt for this input. Defaults to the name if not defined. + + + + + + + + + + + + + + + + + + 2.0 + + Default text input value. + + + + + + 2.0 + + List of validation id. + + + + + + + + + + + + + + + + + + + 2.0 + + Default boolean input value. + + + + + + + + + + + + + + + + + + + 2.0 + + Enum constant value. + + + + + + + + + + + + + + + + + + 2.0 + + Default enum input value. + + + + + + + + + + + + + + + + + + + 2.0 + + List constant value. + + + + + + + + + + + + + + + + + + 2.0 + + Default list input value. + + + + + + + + + + + + + + 2.0 + + Configuration of validation. + + + + + + + + 2.0 + + The id of the validation. The id must be unique across all the descriptors + of an archetype. + + + + + + 2.0 + + The validation description. + + + + + + + + 2.0 + + Regular expression to apply during pattern validation. + + + + + + + + + + 2.0 + + A user input. + + + + + + + + + + + + + + + + + 2.0 + + Step. A logical group of inputs to be presented together like a pane or a window. + + + + + + + + + + + + + + + + + + + + + + 2.0 + + Define data for the template model. The data-model is global unless it is scoped under <template> + or <templates>. + + + + + + + + + + + + + 2.0 + + Configuration of files, template and data model for the current scope. + + + + + + + 2.0 + + Define a set of replacements to be applied on the path of created files. +
+
+

+ Replacements are search and replace regular expressions declared using + <replace>. They form a chain where the first input is the included file + path, + the result is passed to the next replacement and the last result is used as the final + file + path. +

+
+ Example: +
+
+ <transformation id="packaged"> +
  <replace regex="__pkg__" replacement="${package/\./\/}"/> +
</transformation> +
<files transformations="packaged"> +
  <directory>files/src/main/java</directory> +
  <includes> +
    <include>**/*.java</include> +
  </includes> +
</files> +
+
+
+ + + + + 2.0 + + Pair of search and replace regular expressions. + + + + + + 2.0 + + Search regular expression. +
+
+

+ A property like syntax can be used to resolve the value of an input. +
+
+ E.g. +

    +
  • + ${media-support.json.provider} +
  • +
  • + ${security.authentication.provider} +
  • +
  • + ${security} +
  • +
+ Inline search and replace regular expressions are also supported: +
    +
  • + ${package/\./\/} +
  • +
+

+
+
+
+ + + 2.0 + + Replace regular expression. +
+
+

+ A property like syntax can be used to resolve the value of an input. +
+
+ E.g. +

    +
  • + ${media-support.json.provider} +
  • +
  • + ${security.authentication.provider} +
  • +
  • + ${security} +
  • +
+ Inline search and replace regular expressions are also supported: +
    +
  • + ${package/\./\/} +
  • +
+

+
+
+
+
+
+
+ + + 2.0 + + The id of the path transformation. The id must be unique across all the descriptors + of an archetype. + + + +
+
+ + + 2.0 + + Defines a static file. +
+
    +
  • + The path of the file is relative to the configured directory. +
  • +
+
+
+ + + + 2.0 + + The file source. + + + + + + 2.0 + + The file target. + + + + + +
+ + + 2.0 + + Defines a set of static files. +
+
    +
  • + The path of the files included are relative to the configured directory. +
  • +
  • Transformations can be used to modify the final path of the files to be created
  • +
+
+
+ + + + + +
+ + + 2.0 + + Define a template file. +
+
    +
  • + The path of the template file is relative to the configured + directory +
  • +
+
+
+ + + + + + + 2.0 + + Associates the included template files with a template engine. +
+
+ The value must match a template engine registered with the archetype engine. +
+
+
+ + + 2.0 + + The template file source. + + + + + + 2.0 + + The processed file target. + + + + +
+
+ + + 2.0 + + Defines a set of template files. +
+
    +
  • + The path of the template files included are relative to the configured + directory +
  • +
  • Transformations can be used to modify the final path of the files to be created
  • +
+
+
+ + + + + + + + 2.0 + + Associates the included template files with a template engine. +
+
+ The value must match a template engine registered with the archetype engine. +
+
+
+ + +
+
+
+ + + +
+ +
+ + + + 2.0 + + Method declarations. + + + + + + + + + + 2.0 + + Method declaration. + + + + + + + + + + 2.0 + + Name of the method. + + + + + + + + 2.0 + + The <archetype-script> element is the root of the descriptor. + + + + + + + + + + + +
diff --git a/maven-plugins/helidon-archetype-maven-plugin/pom.xml b/maven-plugins/helidon-archetype-maven-plugin/pom.xml index 8c828e0f8..81094aa4e 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/pom.xml +++ b/maven-plugins/helidon-archetype-maven-plugin/pom.xml @@ -157,32 +157,22 @@
org.apache.maven.plugins - maven-dependency-plugin + maven-remote-resources-plugin + + + io.helidon.build-tools.archetype:helidon-archetype-engine-v2:${project.version} + + - schema generate-resources - copy + process - - - - io.helidon.build-tools.archetype - helidon-archetype-engine-v2 - schema-2.0 - xsd - ${project.version} - ${project.build.outputDirectory} - schema-2.0.xsd - - - false - true - + org.apache.maven.plugins maven-surefire-plugin diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/expected.log b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/expected.log new file mode 100644 index 000000000..ead52cf5f --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/expected.log @@ -0,0 +1 @@ +Max archetype schema version is 2.0 \ No newline at end of file diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/pom.xml b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/pom.xml new file mode 100644 index 000000000..2e1b00805 --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + io.helidon.build-tools.archetype.tests + test-archetype7 + @project.version@ + Test Archetype 7 + helidon-archetype + + + UTF-8 + + + + + io.helidon.build-tools.archetype + helidon-archetype-engine-v2 + ${project.version} + + + + + + + io.helidon.build-tools + helidon-archetype-maven-plugin + ${project.version} + true + + + + diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/postbuild.groovy b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/postbuild.groovy new file mode 100644 index 000000000..f6976449b --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/postbuild.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +import io.helidon.build.common.test.utils.JUnitLauncher +import io.helidon.build.maven.archetype.ProjectsTestIT + +//noinspection GroovyAssignabilityCheck,GrUnresolvedAccess +JUnitLauncher.builder() + .select(ProjectsTestIT.class, "test7", String.class) + .parameter("basedir", basedir.getAbsolutePath()) + .reportsDir(basedir) + .outputFile(new File(basedir, "test.log")) + .suiteId("archetype-it-test7") + .suiteDisplayName("Archetype Maven Plugin Integration Test 7") + .build() + .launch() diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/circle.xml b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/circle.xml new file mode 100644 index 000000000..5c81d8691 --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/circle.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/pom.xml.mustache b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/pom.xml.mustache new file mode 100644 index 000000000..0c3183046 --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/pom.xml.mustache @@ -0,0 +1,35 @@ + + + + + + 4.0.0 + {{groupId}} + {{artifactId}} + {{version}} + {{artifactId}} + + + UTF-8 + 1.8 + 1.8 + + diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/src/main/java/__pkg__/Shape.java.mustache b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/src/main/java/__pkg__/Shape.java.mustache new file mode 100644 index 000000000..c0538f154 --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/files/src/main/java/__pkg__/Shape.java.mustache @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 {{package}}; + +public class Shape { + + private Shape(){ + } + + public static void main(String[] args) { + System.out.println("{{shape}}"); + } +} diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/main.xml b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/main.xml new file mode 100644 index 000000000..7931ae898 --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/it/projects/test7/src/main/archetype/main.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + files + + **/*.mustache + + + + ${shape} + ${package} + ${groupId} + ${artifactId} + ${version} + + + + + diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Converter.java b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Converter.java index a0c7d7d71..1a2edafa7 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Converter.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Converter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,22 +40,42 @@ private Converter() { * * @param config plexus config node * @param output output file + * @param schemaNamespace namespace for the archetype schema + * @param schemaLocation location of the archetype schema + * @return the output file + * @throws IOException if an IO error occurs + */ + static Path convert(PlexusConfiguration config, Path output, String schemaNamespace, + String schemaLocation) throws IOException { + FileWriter writer = new FileWriter(output.toFile()); + Xpp3DomWriter.write(writer, convert(config, schemaNamespace, schemaLocation)); + writer.flush(); + writer.close(); + return output; + } + + /** + * Convert the given plexus config node to a valid archetype v2 description with the default namespace and location for the + * archetype schema. + * + * @param config plexus config node + * @param output output file * @return the output file * @throws IOException if an IO error occurs */ static Path convert(PlexusConfiguration config, Path output) throws IOException { FileWriter writer = new FileWriter(output.toFile()); - Xpp3DomWriter.write(writer, convert(config)); + Xpp3DomWriter.write(writer, convert(config, Schema.DEFAULT_NAMESPACE, Schema.DEFAULT_LOCATION)); writer.flush(); writer.close(); return output; } - private static Xpp3Dom convert(PlexusConfiguration config) { + private static Xpp3Dom convert(PlexusConfiguration config, String schemaNamespace, String schemaLocation) { Xpp3Dom rootElt = new Xpp3Dom(Schema.ROOT_ELEMENT); - rootElt.setAttribute("xmlns", Schema.NAMESPACE); + rootElt.setAttribute("xmlns", schemaNamespace); rootElt.setAttribute("xmlns:xsi", W3C_XML_SCHEMA_INSTANCE_NS_URI); - rootElt.setAttribute("xsi:schemaLocation", Schema.LOCATION); + rootElt.setAttribute("xsi:schemaLocation", schemaLocation); Deque stack = new ArrayDeque<>(); for (PlexusConfiguration child : config.getChildren()) { stack.add(new Node(rootElt, child)); diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/JarMojo.java b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/JarMojo.java index bd8d5f53a..3e9616f34 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/JarMojo.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/JarMojo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,16 @@ import java.io.InputStream; import java.io.StringWriter; import java.io.UncheckedIOException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -37,6 +43,8 @@ import io.helidon.build.common.SourcePath; import io.helidon.build.common.VirtualFileSystem; +import io.helidon.build.common.maven.MavenVersion; +import io.helidon.build.common.maven.VersionRange; import io.helidon.build.maven.archetype.MustacheHelper.RawString; import org.apache.commons.io.FilenameUtils; @@ -64,7 +72,6 @@ import static io.helidon.build.common.FileUtils.toBase64; import static io.helidon.build.maven.archetype.MustacheHelper.MUSTACHE_EXT; import static io.helidon.build.maven.archetype.MustacheHelper.renderMustacheTemplate; -import static io.helidon.build.maven.archetype.Schema.RESOURCE_NAME; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.util.regex.Pattern.DOTALL; @@ -149,15 +156,29 @@ public class JarMojo extends AbstractMojo { @Parameter private PlexusConfiguration entrypoint; - private final Schema schema = new Schema(resolveResource(RESOURCE_NAME)); + private final Map schemaMap = initiateSchemas(); @Override public void execute() throws MojoExecutionException { try { Path archetypeDir = outputDirectory.toPath().resolve("archetype"); Files.createDirectories(archetypeDir); - processSources(archetypeDir); - processEntryPoint(archetypeDir); + List schemaInfoList = processSources(archetypeDir); + MavenVersion maxSchemaVersion = VersionRange.createFromVersionSpec("[1.0,)") + .matchVersion(schemaInfoList.stream() + .map(Schema.Info::version) + .map(MavenVersion::toMavenVersion) + .collect(Collectors.toList())); + Schema.Info mainSchemaInfo; + if (maxSchemaVersion == null) { + maxSchemaVersion = MavenVersion.toMavenVersion(Schema.DEFAULT_VERSION); + mainSchemaInfo = new Schema.Info(Schema.DEFAULT_VERSION, Schema.DEFAULT_LOCATION, Schema.DEFAULT_NAMESPACE); + } else { + mainSchemaInfo = schemaInfo(maxSchemaVersion.toString(), schemaInfoList); + } + processEntryPoint(archetypeDir, mainSchemaInfo); + archive.addManifestEntries(Map.of("archetype-schema-version", maxSchemaVersion.toString())); + getLog().info("Max archetype schema version is " + maxSchemaVersion); validateEntryPoint(archetypeDir); if (mavenArchetypeCompatible) { processMavenCompat(archetypeDir); @@ -169,6 +190,13 @@ public void execute() throws MojoExecutionException { } } + private Schema.Info schemaInfo(String schemaVersion, List schemaInfoList) { + return schemaInfoList.stream() + .filter(i -> i.version().equals(schemaVersion)) + .findFirst() + .orElse(null); + } + private void renderPostScript(Path archetypeDir) throws IOException { // mustache scope Map scope = new HashMap<>(); @@ -235,14 +263,18 @@ private void processMavenCompat(Path archetypeDir) throws IOException { renderPostScript(archetypeDir); } - private void processEntryPoint(Path outputDir) throws MojoExecutionException, IOException { + private void processEntryPoint(Path outputDir, Schema.Info mainSchemaInfo) throws MojoExecutionException, IOException { if (entrypoint != null) { Path main = outputDir.resolve("main.xml"); if (Files.exists(main)) { throw new MojoExecutionException("Cannot generate custom entry-point, main.xml already exists"); } - Converter.convert(entrypoint, main); - validateSchema(outputDir, "main.xml").ifPresent(getLog()::error); + if (mainSchemaInfo == null) { + Converter.convert(entrypoint, main); + } else { + Converter.convert(entrypoint, main, mainSchemaInfo.namespace(), mainSchemaInfo.schemaLocation()); + } + validateSchema(outputDir, "main.xml", new ArrayList<>()).ifPresent(getLog()::error); } } @@ -286,10 +318,11 @@ private Map.Entry encodeClass(Path classFile) { return Map.entry(className, new RawString(toBase64(classFile).replaceAll("(.{100})", "$1\n"))); } - private void processSources(Path outputDir) throws MojoExecutionException { + private List processSources(Path outputDir) throws MojoExecutionException { getLog().debug("Scanning source files"); Path sourceDir = sourceDirectory.toPath(); List errors = new LinkedList<>(); + List schemaInfoList = new ArrayList<>(); SourcePath.scan(sourceDir).stream() .map(p -> p.asString(false)) .forEach(file -> { @@ -297,7 +330,7 @@ private void processSources(Path outputDir) throws MojoExecutionException { getLog().debug("Found source file: " + file); } if (FilenameUtils.getExtension(file).equalsIgnoreCase("xml")) { - validateSchema(sourceDir, file).ifPresent(errors::add); + validateSchema(sourceDir, file, schemaInfoList).ifPresent(errors::add); } try { Path target = outputDir.resolve(file); @@ -311,11 +344,15 @@ private void processSources(Path outputDir) throws MojoExecutionException { errors.forEach(getLog()::error); throw new MojoExecutionException("Schema validation failed"); } + return schemaInfoList; } - private Optional validateSchema(Path sourceDir, String filename) { + private Optional validateSchema(Path sourceDir, String filename, List schemaInfoList) { try { - schema.validate(sourceDir.resolve(filename)); + Schema.Info schemaInfo = Validator.validateSchema(schemaMap, sourceDir.resolve(filename)); + if (schemaInfo != null) { + schemaInfoList.add(schemaInfo); + } return Optional.empty(); } catch (Schema.ValidationException ex) { return Optional.of( @@ -327,6 +364,39 @@ private Optional validateSchema(Path sourceDir, String filename) { } } + private Map initiateSchemas() { + Map schemaMap = new HashMap<>(); + try { + Enumeration dirs = this.getClass().getClassLoader().getResources("schemas"); + while (dirs.hasMoreElements()) { + URI uri = dirs.nextElement().toURI(); + try (FileSystem fileSystem = fileSystem(uri)) { + Path schemas = fileSystem.getPath("schemas"); + List schemaList = Files.walk(schemas, 1) + .skip(1) + .collect(Collectors.toList()); + for (Path schema : schemaList) { + schemaMap.put(schema.getFileName().toString(), new Schema(Files.newInputStream(schema))); + } + } + } + } catch (IOException e) { + throw new IllegalStateException("Unable to resolve resource during the creation of a Schema object", e); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + return schemaMap; + } + + private FileSystem fileSystem(URI uri) throws IOException { + try { + return FileSystems.newFileSystem(uri, Map.of()); + } catch (FileSystemAlreadyExistsException ex) { + return FileSystems.getFileSystem(uri); + } + + } + private InputStream resolveResource(String path) { InputStream is = this.getClass().getClassLoader().getResourceAsStream(path); if (is == null) { diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Schema.java b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Schema.java index f1f93d5a3..81d673161 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Schema.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Schema.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,14 +43,19 @@ class Schema { /** - * Schema location. + * Default schema location. */ - static final String LOCATION = "https://helidon.io/archetype/2.0 https://helidon.io/xsd/archetype-2.0.xsd"; + static final String DEFAULT_LOCATION = "https://helidon.io/archetype/2.0 https://helidon.io/xsd/archetype-2.0.xsd"; /** - * Schema namespace. + * Default schema namespace. */ - static final String NAMESPACE = "https://helidon.io/archetype/2.0"; + static final String DEFAULT_NAMESPACE = "https://helidon.io/archetype/2.0"; + + /** + * Default schema version. + */ + static final String DEFAULT_VERSION = "2.0"; /** * XML root element. @@ -58,12 +63,13 @@ class Schema { static final String ROOT_ELEMENT = "archetype-script"; /** - * Archetype 2.0 schema. + * Default archetype schema. */ - static final String RESOURCE_NAME = "schema-2.0.xsd"; + static final String DEFAULT_RESOURCE_NAME = "schemas/archetype-2.0.xsd"; private final Validator validator; + /** * Create a new validator. * @@ -111,6 +117,18 @@ void validate(Supplier supplier) throws ValidationException { } } + void validate(Path file, Document doc) throws ValidationException { + try { + if (doc.getDocumentElement().getNodeName().equals(ROOT_ELEMENT)) { + validator.validate(new StreamSource(Files.newInputStream(file))); + } + } catch (SAXParseException ex) { + throw new ValidationException(ex); + } catch (SAXException | IOException e) { + throw new RuntimeException(e); + } + } + /** * Validation exception. */ @@ -125,6 +143,12 @@ private ValidationException(SAXParseException cause) { colNo = cause.getColumnNumber(); } + ValidationException(String message) { + super(message); + lineNo = -1; + colNo = -1; + } + /** * Get the column number. * @@ -143,4 +167,28 @@ public int lineNo() { return lineNo; } } + + static class Info { + private final String version; + private final String location; + private final String namespace; + + Info(String version, String location, String namespace) { + this.version = version; + this.location = location; + this.namespace = namespace; + } + + public String namespace() { + return namespace; + } + + String version() { + return version; + } + + String schemaLocation() { + return location; + } + } } diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Validator.java b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Validator.java index bcac0de8d..73220ced1 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Validator.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/Validator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,73 @@ package io.helidon.build.maven.archetype; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import io.helidon.build.archetype.engine.v2.util.ArchetypeValidator; import org.apache.maven.plugin.MojoExecutionException; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import static io.helidon.build.maven.archetype.Schema.ROOT_ELEMENT; /** * Validation operations. */ class Validator { + private static final Pattern SCHEMA_PATTERN = + Pattern.compile("(/archetype/)(?[\\w-.]+)(\\s+\\S+/)(?[\\w-.]+)"); + private static final String SCHEMA_LOCATION_ATTR_NAME = "xsi:schemaLocation"; + private static final String SCHEMA_NAMESPACE_ATTR_NAME = "xmlns"; + private Validator() { } + static Schema.Info validateSchema(Map schemaMap, Path file) { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(Files.newInputStream(file)); + if (doc.getDocumentElement().getNodeName().equals(ROOT_ELEMENT)) { + String schemaLocation = doc.getDocumentElement().getAttribute(SCHEMA_LOCATION_ATTR_NAME); + String schemaNamespace = doc.getDocumentElement().getAttribute(SCHEMA_NAMESPACE_ATTR_NAME); + Matcher matcher = SCHEMA_PATTERN.matcher(schemaLocation); + String schemaVersion = null; + String schemaFile = null; + if (matcher.find()) { + schemaVersion = matcher.group("version"); + schemaFile = matcher.group("location"); + } + if (schemaVersion == null || schemaFile == null) { + var message = String.format("File %s does not contain required data in xsi:schemaLocation : version - %s, " + + "location - %s", file, schemaVersion, schemaFile); + throw new Schema.ValidationException(message); + } + schemaMap.get(schemaFile).validate(file, doc); + return new Schema.Info(schemaVersion, schemaLocation, schemaNamespace); + } + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new RuntimeException(e); + } + return null; + } + + + /** * Validate archetype regular expression. * diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/SchemaTest.java b/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/SchemaTest.java index fc7890e01..0c2e4a8e4 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/SchemaTest.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/SchemaTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ class SchemaTest { private static final Schema VALIDATOR = new Schema( - SchemaTest.class.getClassLoader().getResourceAsStream(Schema.RESOURCE_NAME)); + SchemaTest.class.getClassLoader().getResourceAsStream(Schema.DEFAULT_RESOURCE_NAME)); @Test void testValidate() { diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/test/resources/simple/META-INF/MANIFEST.MF b/maven-plugins/helidon-archetype-maven-plugin/src/test/resources/simple/META-INF/MANIFEST.MF new file mode 100644 index 000000000..1e1ccb964 --- /dev/null +++ b/maven-plugins/helidon-archetype-maven-plugin/src/test/resources/simple/META-INF/MANIFEST.MF @@ -0,0 +1 @@ +archetype-schema-version: 2.0 diff --git a/pom.xml b/pom.xml index 5412a3f00..52d0c9c32 100644 --- a/pom.xml +++ b/pom.xml @@ -200,6 +200,7 @@ 1.6.13 2.5.3 2.7 + 3.0.0 3.0.1 4.7.1.1 3.1.2 @@ -221,6 +222,11 @@ + + org.apache.maven.plugins + maven-remote-resources-plugin + ${version.plugin.remote-resources} + org.apache.maven.plugins maven-assembly-plugin From a69b625d67dd56b025d97b1e5ecbdad53335c304 Mon Sep 17 00:00:00 2001 From: Romain Grecourt Date: Fri, 4 Aug 2023 14:33:09 -0700 Subject: [PATCH 2/2] Add ProjectsTestIT.test7 --- .../io/helidon/build/maven/archetype/ProjectsTestIT.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/ProjectsTestIT.java b/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/ProjectsTestIT.java index 0a839f5f9..68c840eb6 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/ProjectsTestIT.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/ProjectsTestIT.java @@ -106,6 +106,15 @@ void test6(String basedir) throws IOException { assertDiffs(diffs); } + @ParameterizedTest + @ConfigurationParameterSource("basedir") + void test7(String basedir) throws IOException { + BuildLog log = new BuildLog(new File(basedir, "build.log")); + List diffs = log.containsLines(new File(basedir, "expected.log")); + assertDiffs(diffs); + } + + private static Path projectsDir(String baseDir) { return projectsDir(baseDir, null); }