diff --git a/archetype/engine-v2/etc/spotbugs/exclude.xml b/archetype/engine-v2/etc/spotbugs/exclude.xml index 6a0778692..80fd51ca2 100644 --- a/archetype/engine-v2/etc/spotbugs/exclude.xml +++ b/archetype/engine-v2/etc/spotbugs/exclude.xml @@ -1,7 +1,7 @@ + + + + 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 77de2021b..afdb0c8d9 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 @@ -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. @@ -16,6 +16,7 @@ package io.helidon.build.archetype.engine.v2; +import java.io.File; import java.nio.file.FileSystem; import java.nio.file.Path; import java.util.Map; @@ -23,6 +24,8 @@ 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 static java.util.Objects.requireNonNull; @@ -35,49 +38,29 @@ public class ArchetypeEngineV2 { private static final String ARTIFACT_ID = "artifactId"; private final Path cwd; - - /** - * Create a new archetype engine. - * - * @param fs archetype file system - */ - public ArchetypeEngineV2(FileSystem fs) { - this.cwd = fs.getPath("/"); - } - - /** - * Generate a project. - * - * @param inputResolver input resolver - * @param externalValues external values - * @param externalDefaults external defaults - * @param directorySupplier output directory supplier - * @return output directory - */ - public Path generate(InputResolver inputResolver, - Map externalValues, - Map externalDefaults, - Function directorySupplier) { - - return generate(inputResolver, externalValues, externalDefaults, () -> {}, directorySupplier); + private final InputResolver inputResolver; + private final Map externalValues; + private final Map externalDefaults; + private final Runnable onResolved; + private final Function directorySupplier; + private final File outputPropsFile; + + private ArchetypeEngineV2(Builder builder) { + this.cwd = builder.cwd; + this.inputResolver = builder.inputResolver; + this.externalValues = builder.externalValues; + this.externalDefaults = builder.externalDefaults; + this.onResolved = builder.onResolved; + this.directorySupplier = builder.directorySupplier; + this.outputPropsFile = builder.outputPropsFile; } /** * Generate a project. * - * @param inputResolver input resolver - * @param externalValues external values - * @param externalDefaults external defaults - * @param onResolved callback executed when inputs are fully resolved - * @param directorySupplier output directory supplier * @return output directory */ - public Path generate(InputResolver inputResolver, - Map externalValues, - Map externalDefaults, - Runnable onResolved, - Function directorySupplier) { - + public Path generate() { Context context = Context.builder() .cwd(cwd) .externalValues(externalValues) @@ -104,6 +87,124 @@ public Path generate(InputResolver inputResolver, Controller.walk(outputGenerator, script, context); context.requireRootScope(); + if (outputPropsFile != null) { + Map userInputsMap = ContextSerializer.serialize(context); + Path path = outputPropsFile.isAbsolute() ? outputPropsFile.toPath() : directory.resolve(outputPropsFile.toPath()); + FileUtils.saveToPropertiesFile(userInputsMap, path); + } + return directory; } + + /** + * Create a new builder. + * + * @return builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * ArchetypeEngineV2 builder. + */ + public static final class Builder { + + private Path cwd; + private InputResolver inputResolver; + private Map externalValues = Map.of(); + private Map externalDefaults = Map.of(); + private Runnable onResolved = () -> {}; + private Function directorySupplier; + private File outputPropsFile; + + private Builder() { + } + + /** + * Set the output properties file to save user inputs. + * + * @param outputPropsFile output properties file + * @return this builder + */ + public Builder outputPropsFile(File outputPropsFile) { + this.outputPropsFile = outputPropsFile; + return this; + } + + /** + * Set the output directory supplier. + * + * @param directorySupplier output directory supplier + * @return this builder + */ + public Builder directorySupplier(Function directorySupplier) { + this.directorySupplier = requireNonNull(directorySupplier, "directorySupplier is null"); + return this; + } + + /** + * Set the callback executed when inputs are fully resolved. + * + * @param onResolved callback executed when inputs are fully resolved + * @return this builder + */ + public Builder onResolved(Runnable onResolved) { + this.onResolved = requireNonNull(onResolved, "onResolved is null"); + return this; + } + + /** + * Set external defaults. + * + * @param externalDefaults external defaults + * @return this builder + */ + public Builder externalDefaults(Map externalDefaults) { + this.externalDefaults = requireNonNull(externalDefaults, "externalDefaults is null"); + return this; + } + + /** + * Set external values. + * + * @param externalValues external values + * @return this builder + */ + public Builder externalValues(Map externalValues) { + this.externalValues = requireNonNull(externalValues, "externalValues is null"); + return this; + } + + /** + * Set the input resolver. + * + * @param inputResolver input resolver + * @return this builder + */ + public Builder inputResolver(InputResolver inputResolver) { + this.inputResolver = requireNonNull(inputResolver, "inputResolver is null"); + return this; + } + + /** + * Set the archetype file system. + * + * @param fileSystem archetype file system + * @return this builder + */ + public Builder fileSystem(FileSystem fileSystem) { + this.cwd = fileSystem.getPath("/"); + return this; + } + + /** + * Build the ArchetypeEngineV2 instance. + * + * @return new ArchetypeEngineV2 + */ + public ArchetypeEngineV2 build() { + return new ArchetypeEngineV2(this); + } + } } diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/context/ContextPrinter.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/context/ContextPrinter.java index 764ebb5cc..455ea46e8 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/context/ContextPrinter.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/context/ContextPrinter.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. @@ -40,7 +40,7 @@ private static boolean isLastChild(ContextNode node) { } private static String printValue(ContextValue value) { - return value.value().unwrap() + " (" + value.kind() + ')'; + return value == null ? "null" : value.value().unwrap() + " (" + value.kind() + ')'; } @Override diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/context/ContextSerializer.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/context/ContextSerializer.java new file mode 100644 index 000000000..ca3ffac83 --- /dev/null +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/context/ContextSerializer.java @@ -0,0 +1,120 @@ +/* + * 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 io.helidon.build.archetype.engine.v2.context; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Context serializer. + */ +public class ContextSerializer implements ContextEdge.Visitor { + + private static final Set DEFAULT_VALUE_KIND_FILTER = + Set.of(ContextValue.ValueKind.EXTERNAL, ContextValue.ValueKind.USER); + + private static final String DEFAULT_VALUE_DELIMITER = ","; + + private final Map result; + private final Predicate filter; + private final CharSequence valueDelimiter; + private final Function valueMapper; + + private ContextSerializer(Map result, + Predicate filter, + Function valueMapper, + CharSequence valueDelimiter) { + this.result = result; + this.filter = filter; + this.valueMapper = valueMapper; + this.valueDelimiter = valueDelimiter; + } + + @Override + public void visit(ContextEdge edge) { + ContextNode node = edge.node(); + ContextNode parent = node.parent0(); + if (parent != null) { + String key = node.path(); + Set valueSet = edge.variations().stream() + .filter(filter) + .map(variation -> variation.value().unwrap().toString()) + .map(valueMapper) + .collect(Collectors.toSet()); + if (valueSet.size() > 0) { + result.put(key, String.join(valueDelimiter, valueSet)); + } + } + } + + /** + * Visit the given archetype context and return values from the context in form of a map where keys are paths of nodes and + * values are related values for these nodes. + * + * @param context context for processing + * @param filter filter for context values + * @param valueMapper mapper for the values of the nodes + * @param valueDelimiter delimiter for the values + * @return map where keys are paths of nodes and values are related values for these nodes + */ + public static Map serialize(Context context, + Predicate filter, + Function valueMapper, + CharSequence valueDelimiter) { + Map result = new HashMap<>(); + context.scope().visitEdges(new ContextSerializer(result, filter, valueMapper, valueDelimiter), false); + return result; + } + + /** + * Visit the given archetype context and return values from the context that were used by an user during the project + * generation in form of a map where keys are paths of nodes and values are related values for these nodes. + * + * @param context context for processing + * @return map where keys are paths of nodes and values are related values for these nodes + */ + public static Map serialize(Context context) { + Map result = new HashMap<>(); + context.scope().visitEdges(new ContextSerializer(result, ContextSerializer::defaultContextFilter, + ContextSerializer::defaultValueMapper, DEFAULT_VALUE_DELIMITER), false); + return result; + } + + private static String defaultValueMapper(String value) { + Set forRemoval = Set.of('[', ']'); + if (value == null || value.length() == 0) { + return value; + } + StringBuilder builder = new StringBuilder(value); + if (forRemoval.contains(builder.charAt(0))) { + builder.deleteCharAt(0); + } + if (builder.length() > 0 && forRemoval.contains(builder.charAt(builder.length() - 1))) { + builder.deleteCharAt(builder.length() - 1); + } + return builder.toString(); + } + + private static boolean defaultContextFilter(ContextEdge edge) { + return edge.value() != null + && !edge.node().visibility().equals(ContextScope.Visibility.UNSET) + && DEFAULT_VALUE_KIND_FILTER.contains(edge.value().kind()); + } +} diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2Test.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2Test.java index 7c1675753..cbfa8a758 100644 --- a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2Test.java +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ArchetypeEngineV2Test.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. @@ -15,6 +15,7 @@ */ package io.helidon.build.archetype.engine.v2; +import java.io.File; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; @@ -29,6 +30,7 @@ import static io.helidon.build.common.FileUtils.unique; import static io.helidon.build.common.FileUtils.zip; import static io.helidon.build.common.test.utils.TestFiles.targetDir; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; @@ -250,6 +252,16 @@ void testCustomDefaultShapesZip() throws IOException { asserCustomShapesModernStyle(outputDir); } + @Test + void testOutputPropsFile() throws IOException { + Path outputDir = e2eZip("testOutputPropsFile", Map.of( + "theme", "shapes", + "theme.base", "custom"), + "output.properties"); + + assertOutputPropsFile(outputDir); + } + @Test void testCustomShapesZip() throws IOException { Path outputDir = e2eZip("testCustomShapesZip", Map.of( @@ -331,6 +343,14 @@ private void asserCustomShapesModernStyle(Path outputDir) throws IOException { + "- Shapes can have many styles\n")); } + private void assertOutputPropsFile(Path outputDir) throws IOException { + Path outputPropsFile = outputDir.resolve("output.properties"); + assertThat(Files.exists(outputPropsFile), is(true)); + String props = readFile(outputPropsFile); + assertThat(props, containsString("theme=shapes\n")); + assertThat(props, containsString("theme.base=custom\n")); + } + private void assertCustomReadme(Path outputDir) throws IOException { Path readmeFile = outputDir.resolve("README.md"); assertThat(Files.exists(readmeFile), is(true)); @@ -425,6 +445,17 @@ private Path e2eZip(String name, Map externalValues) throws IOEx return e2eZip(name, externalValues, Map.of()); } + private Path e2eZip(String name, Map externalValues, String outputPropsFile) throws IOException { + Path targetDir = targetDir(this.getClass()); + Path sourceDir = targetDir.resolve("test-classes/e2e"); + Path testOutputDir = targetDir.resolve("engine-ut"); + Path zipFile = unique(testOutputDir, "archetype", ".zip"); + zip(zipFile, sourceDir); + FileSystem fs = FileSystems.newFileSystem(zipFile, this.getClass().getClassLoader()); + Path outputDir = unique(testOutputDir, name); + return e2e(fs, outputDir, externalValues, Map.of(), Path.of(outputPropsFile).toFile()); + } + private Path e2eZip(String name, Map externalValues, Map externalDefaults) throws IOException { @@ -436,7 +467,7 @@ private Path e2eZip(String name, zip(zipFile, sourceDir); FileSystem fs = FileSystems.newFileSystem(zipFile, this.getClass().getClassLoader()); Path outputDir = unique(testOutputDir, name); - return e2e(fs, outputDir, externalValues, externalDefaults); + return e2e(fs, outputDir, externalValues, externalDefaults, null); } private Path e2eDir(String name, Map externalValues) { @@ -449,15 +480,23 @@ private Path e2eDir(String name, Map externalValues, Map externalValues, - Map externalDefaults) { - ArchetypeEngineV2 engine = new ArchetypeEngineV2(archetype); - Path outputDir = engine.generate(new BatchInputResolver(), externalValues, externalDefaults, n -> unique(directory, n)); + Map externalDefaults, + File outputPropsFile) { + ArchetypeEngineV2 engine = ArchetypeEngineV2.builder() + .fileSystem(archetype) + .inputResolver(new BatchInputResolver()) + .directorySupplier(n -> unique(directory, n)) + .externalDefaults(externalDefaults) + .externalValues(externalValues) + .outputPropsFile(outputPropsFile) + .build(); + Path outputDir = engine.generate(); assertThat(Files.exists(outputDir), is(true)); return outputDir; } diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/context/ContextSerializerTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/context/ContextSerializerTest.java new file mode 100644 index 000000000..59e3e6d1e --- /dev/null +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/context/ContextSerializerTest.java @@ -0,0 +1,59 @@ +/* + * 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 io.helidon.build.archetype.engine.v2.context; + +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.isIn; + +/** + * Tests {@link ContextSerializer}. + */ +class ContextSerializerTest { + + @Test + public void testSerialize() { + Map expectedResult = Map.of( + "foo", "foo", + "foo1", "", + "foo2", "some_var_default", + "foo3", "bar1_value"); + Context context = Context.builder() + .externalDefaults(Map.of( + "some_var", "some_var_default", + "bar1", "bar1_default_value")) + .externalValues(Map.of( + "foo", "foo", + "bar", "${foo}", + "foo1", "${non_exist_var}", + "foo2", "${some_var}", + "bar1", "bar1_value", + "foo3", "${bar1}")) + .build(); + + Map result = ContextSerializer.serialize(context, + edge -> edge.node().path().startsWith("foo"), + Function.identity(), + ","); + + assertThat(result.entrySet(), everyItem(isIn(expectedResult.entrySet()))); + } +} diff --git a/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandModel.java b/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandModel.java index 55eaa1b6e..7f730d837 100644 --- a/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandModel.java +++ b/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandModel.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. @@ -35,7 +35,7 @@ public abstract class CommandModel extends CommandParameters { * @param params command parameters */ protected CommandModel(CommandInfo commandInfo, ParameterInfo... params) { - super(GlobalOptions.GLOBAL_FLAGS, params); + super(GlobalOptions.GLOBAL_OPTIONS_INFO, params); this.commandInfo = Objects.requireNonNull(commandInfo, "commandInfo is null"); } diff --git a/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParameters.java b/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParameters.java index 8934d3fb3..5e9b0b85c 100644 --- a/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParameters.java +++ b/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParameters.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. @@ -46,13 +46,13 @@ protected CommandParameters(ParameterInfo... params) { /** * Create a new command parameters. * - * @param globalFlags global flags info + * @param globalOptions global options info * @param params parameters info */ - protected CommandParameters(CommandModel.FlagInfo[] globalFlags, ParameterInfo... params) { + protected CommandParameters(CommandParameters.ParameterInfo[] globalOptions, ParameterInfo... params) { this.params = new LinkedList<>(); - if (globalFlags != null) { - Collections.addAll(this.params, globalFlags); + if (globalOptions != null) { + Collections.addAll(this.params, globalOptions); } if (params != null) { Collections.addAll(this.params, params); diff --git a/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParser.java b/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParser.java index 9fc5f50d4..b3d8fc550 100644 --- a/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParser.java +++ b/cli/harness/src/main/java/io/helidon/build/cli/harness/CommandParser.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. @@ -39,6 +39,7 @@ import io.helidon.build.cli.harness.CommandModel.KeyValueInfo; import io.helidon.build.cli.harness.CommandModel.KeyValuesInfo; import io.helidon.build.cli.harness.CommandParameters.ParameterInfo; +import io.helidon.build.common.FileUtils; /** * Command parser. @@ -209,7 +210,7 @@ Resolver parseCommand(CommandParameters command) { * @return resolver that can be used to resolve the values for the parsed parameters */ Resolver parseCommand() { - return parseCommand(new CommandParameters(GlobalOptions.GLOBAL_FLAGS)); + return parseCommand(new CommandParameters(GlobalOptions.GLOBAL_OPTIONS_INFO)); } private Resolver parseCommand(Map> parametersMap) { @@ -223,14 +224,12 @@ private Resolver parseCommand(Map> parametersMap) { } rawArg = rawArg.trim(); String arg = rawArg.toLowerCase(); - if (GlobalOptions.isGlobalFlag(arg)) { - parsedParams.put(arg, new FlagParam(arg)); - } else if (isParam(arg)) { + if (isParam(arg)) { String optionName = arg.substring(2); if (!Option.VALID_NAME.test(optionName)) { throw new CommandParserException(INVALID_OPTION_NAME + ": " + optionName); } - ParameterInfo paramInfo = parametersMap.get(optionName); + ParameterInfo paramInfo = parameterInfo(optionName, parametersMap); if (paramInfo instanceof FlagInfo) { parsedParams.put(optionName, new FlagParam(optionName)); } else if (paramInfo instanceof KeyValueInfo) { @@ -242,7 +241,12 @@ private Resolver parseCommand(Map> parametersMap) { continue; } } - parsedParams.put(optionName, new KeyValueParam(optionName, it.next().trim())); + KeyValueParam keyValueParam = new KeyValueParam(optionName, it.next().trim()); + parsedParams.put(optionName, keyValueParam); + if (keyValueParam.name().equals(GlobalOptions.PROPS_FILE_OPTION_NAME)) { + Properties props = FileUtils.loadProperties(Path.of(keyValueParam.value)); + props.forEach((key, value) -> properties.put(String.valueOf(key), String.valueOf(value))); + } } else if (paramInfo instanceof KeyValuesInfo) { boolean required = ((KeyValuesInfo) paramInfo).required(); if (!it.hasNext()) { @@ -282,6 +286,13 @@ private Resolver parseCommand(Map> parametersMap) { return new Resolver(parsedParams, properties); } + private ParameterInfo parameterInfo(String paramName, Map> parametersMap) { + if (GlobalOptions.isGlobal(paramName)) { + return GlobalOptions.GLOBAL_OPTIONS.get(paramName); + } + return parametersMap.get(paramName); + } + /** * Parser error. */ diff --git a/cli/harness/src/main/java/io/helidon/build/cli/harness/GlobalOptions.java b/cli/harness/src/main/java/io/helidon/build/cli/harness/GlobalOptions.java index 580a32d64..de0d3568d 100644 --- a/cli/harness/src/main/java/io/helidon/build/cli/harness/GlobalOptions.java +++ b/cli/harness/src/main/java/io/helidon/build/cli/harness/GlobalOptions.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. @@ -15,11 +15,14 @@ */ package io.helidon.build.cli.harness; +import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import io.helidon.build.cli.harness.CommandModel.FlagInfo; +import io.helidon.build.cli.harness.CommandParameters.ParameterInfo; /** * Global option constants. @@ -131,6 +134,58 @@ public class GlobalOptions { */ public static final String PLAIN_FLAG_ARGUMENT = "--" + PLAIN_FLAG_NAME; + /** + * The --args-file option name. + */ + public static final String ARGS_FILE_OPTION_NAME = "args-file"; + + /** + * The --args-file option description. + */ + public static final String ARGS_FILE_OPTION_DESCRIPTION = "Path to a file with arguments for Helidon CLI tool"; + + /** + * The --args-file option argument. + */ + public static final String ARGS_FILE_OPTION_ARGUMENT = "--" + ARGS_FILE_OPTION_NAME; + + /** + * The --args-file option info. + */ + public static final ParameterInfo ARGS_FILE_OPTION_INFO = new CommandModel.KeyValueInfo<>( + String.class, + ARGS_FILE_OPTION_NAME, + ARGS_FILE_OPTION_DESCRIPTION, + null, + false, + false); + + /** + * The --props-file option name. + */ + public static final String PROPS_FILE_OPTION_NAME = "props-file"; + + /** + * The --props-file option description. + */ + public static final String PROPS_FILE_OPTION_DESCRIPTION = "Path to a properties file with user inputs for Helidon CLI tool"; + + /** + * The --props-file option argument. + */ + public static final String PROPS_FILE_OPTION_ARGUMENT = "--" + PROPS_FILE_OPTION_NAME; + + /** + * The --props-file option info. + */ + public static final ParameterInfo PROPS_FILE_OPTION_INFO = new CommandModel.KeyValueInfo<>( + String.class, + PROPS_FILE_OPTION_NAME, + PROPS_FILE_OPTION_DESCRIPTION, + null, + false, + false); + /** * Tests whether the given argument is a global flag. * @@ -138,23 +193,44 @@ public class GlobalOptions { * @return {@code true} if a global flag. */ public static boolean isGlobalFlag(String argument) { - return GLOBAL_FLAG_ARGUMENTS.contains(argument); + return GLOBAL_OPTION_ARGUMENTS.contains(argument) && GLOBAL_OPTIONS.get(argument.substring(2)) instanceof FlagInfo; + } + + /** + * Tests whether the given argument is a global option. + * + * @param argument The argument. + * @return {@code true} if a global option. + */ + public static boolean isGlobal(String argument) { + return GLOBAL_OPTIONS.containsKey(argument); } /** - * Global flags. + * Global options info. */ - static final FlagInfo[] GLOBAL_FLAGS = new FlagInfo[]{ + static final ParameterInfo[] GLOBAL_OPTIONS_INFO = new ParameterInfo[]{ HELP_FLAG_INFO, VERBOSE_FLAG_INFO, DEBUG_FLAG_INFO, ERROR_FLAG_INFO, - PLAIN_FLAG_INFO + PLAIN_FLAG_INFO, + PROPS_FILE_OPTION_INFO, + ARGS_FILE_OPTION_INFO }; - private static final Set GLOBAL_FLAG_ARGUMENTS = Stream.of(GLOBAL_FLAGS) - .map(f -> "--" + f.name()) - .collect(Collectors.toSet()); + private static final Set GLOBAL_OPTION_ARGUMENTS = Stream.of(GLOBAL_OPTIONS_INFO) + .map(info -> (CommandModel.NamedOptionInfo) info) + .map(info -> "--" + info.name()) + .collect(Collectors.toSet()); + + /** + * Global options. + */ + static final Map> GLOBAL_OPTIONS = + Stream.of(GLOBAL_OPTIONS_INFO) + .map(info -> (CommandModel.NamedOptionInfo) info) + .collect(Collectors.toMap(CommandModel.NamedOptionInfo::name, Function.identity())); private GlobalOptions() { } diff --git a/cli/harness/src/main/java/io/helidon/build/cli/harness/UsageCommand.java b/cli/harness/src/main/java/io/helidon/build/cli/harness/UsageCommand.java index de63d545f..ecdc8db9a 100644 --- a/cli/harness/src/main/java/io/helidon/build/cli/harness/UsageCommand.java +++ b/cli/harness/src/main/java/io/helidon/build/cli/harness/UsageCommand.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. @@ -53,6 +53,8 @@ private static Map createGlobalOptionsMap() { map.put(GlobalOptions.DEBUG_FLAG_ARGUMENT, GlobalOptions.DEBUG_FLAG_DESCRIPTION); map.put(GlobalOptions.ERROR_FLAG_ARGUMENT, GlobalOptions.ERROR_FLAG_DESCRIPTION); map.put(GlobalOptions.PLAIN_FLAG_ARGUMENT, GlobalOptions.PLAIN_FLAG_DESCRIPTION); + map.put(GlobalOptions.ARGS_FILE_OPTION_ARGUMENT, GlobalOptions.ARGS_FILE_OPTION_DESCRIPTION); + map.put(GlobalOptions.PROPS_FILE_OPTION_ARGUMENT, GlobalOptions.PROPS_FILE_OPTION_DESCRIPTION); return map; } diff --git a/cli/harness/src/test/java/io/helidon/build/cli/harness/CommandParserTest.java b/cli/harness/src/test/java/io/helidon/build/cli/harness/CommandParserTest.java index a35c26666..7ca0f01c9 100644 --- a/cli/harness/src/test/java/io/helidon/build/cli/harness/CommandParserTest.java +++ b/cli/harness/src/test/java/io/helidon/build/cli/harness/CommandParserTest.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. @@ -20,6 +20,7 @@ import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.Map; +import java.util.Objects; import java.util.Properties; import io.helidon.build.cli.harness.CommandModel.KeyValueInfo; @@ -305,4 +306,32 @@ public void testArgsFileOptionWithIncorrectFile() { ); assertThat(e.getMessage(), containsString("java.nio.file.NoSuchFileException: not_existing_file.txt")); } + + @Test + public void testPropsFileOptionWithExistingFile() { + String argsFilePath = Objects.requireNonNull(getClass().getResource("test-props-file.properties")).getPath(); + KeyValueInfo propsFileOption = new KeyValueInfo<>(String.class, "props-file", "properties file", null, false); + CommandParameters cmd = new CommandParameters(propsFileOption); + + CommandParser parser = CommandParser.create("command", "--props-file", argsFilePath); + CommandParser.Resolver resolver = parser.parseCommand(cmd); + + Properties properties = resolver.properties(); + assertThat(properties.get("security.atz"), is("abac")); + assertThat(properties.get("flavor"), is("mp")); + assertThat(properties.get("media"), is("json,multipart")); + } + + @Test + public void testPropsFileOptionWithIncorrectFile() { + KeyValueInfo propsFileOption = new KeyValueInfo<>(String.class, "props-file", "properties file", null, false); + CommandParameters cmd = new CommandParameters(propsFileOption); + + CommandParser parser = CommandParser.create("command", "--props-file", "not_existing_props_file.txt"); + + UncheckedIOException e = assertThrows( + UncheckedIOException.class, + () -> parser.parseCommand(cmd)); + assertThat(e.getMessage(), containsString("NoSuchFileException: not_existing_props_file.txt")); + } } diff --git a/cli/harness/src/test/resources/io/helidon/build/cli/harness/cli-usage.txt b/cli/harness/src/test/resources/io/helidon/build/cli/harness/cli-usage.txt index be27d8763..9f02a2608 100644 --- a/cli/harness/src/test/resources/io/helidon/build/cli/harness/cli-usage.txt +++ b/cli/harness/src/test/resources/io/helidon/build/cli/harness/cli-usage.txt @@ -10,6 +10,8 @@ Options --debug Produce debug output --error Print error stack traces --plain Do not use color or styles in output + --args-file Path to a file with arguments for Helidon CLI tool + --props-file Path to a properties file with user inputs for Helidon CLI tool Commands diff --git a/cli/harness/src/test/resources/io/helidon/build/cli/harness/help-cmd-help.txt b/cli/harness/src/test/resources/io/helidon/build/cli/harness/help-cmd-help.txt index 27ff079f9..a84b28333 100644 --- a/cli/harness/src/test/resources/io/helidon/build/cli/harness/help-cmd-help.txt +++ b/cli/harness/src/test/resources/io/helidon/build/cli/harness/help-cmd-help.txt @@ -10,3 +10,5 @@ Options --debug Produce debug output --error Print error stack traces --plain Do not use color or styles in output + --args-file Path to a file with arguments for Helidon CLI tool + --props-file Path to a properties file with user inputs for Helidon CLI tool diff --git a/cli/harness/src/test/resources/io/helidon/build/cli/harness/simple-cmd-help.txt b/cli/harness/src/test/resources/io/helidon/build/cli/harness/simple-cmd-help.txt index 9f61400f7..830ad8ab2 100644 --- a/cli/harness/src/test/resources/io/helidon/build/cli/harness/simple-cmd-help.txt +++ b/cli/harness/src/test/resources/io/helidon/build/cli/harness/simple-cmd-help.txt @@ -10,5 +10,7 @@ Options --debug Produce debug output --error Print error stack traces --plain Do not use color or styles in output + --args-file Path to a file with arguments for Helidon CLI tool + --props-file Path to a properties file with user inputs for Helidon CLI tool --foo Foo option --bar Bar option diff --git a/cli/harness/src/test/resources/io/helidon/build/cli/harness/test-props-file.properties b/cli/harness/src/test/resources/io/helidon/build/cli/harness/test-props-file.properties new file mode 100644 index 000000000..9d874dd47 --- /dev/null +++ b/cli/harness/src/test/resources/io/helidon/build/cli/harness/test-props-file.properties @@ -0,0 +1,20 @@ +# +# 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. +# + +#test properties +flavor=mp +security.atz=abac +media=json,multipart diff --git a/cli/impl/etc/spotbugs/exclude.xml b/cli/impl/etc/spotbugs/exclude.xml index f1b61600c..84dcc9735 100644 --- a/cli/impl/etc/spotbugs/exclude.xml +++ b/cli/impl/etc/spotbugs/exclude.xml @@ -1,7 +1,7 @@ + + + + diff --git a/cli/impl/src/main/java/io/helidon/build/cli/impl/ArchetypeInvoker.java b/cli/impl/src/main/java/io/helidon/build/cli/impl/ArchetypeInvoker.java index 463126451..d7ea354bc 100644 --- a/cli/impl/src/main/java/io/helidon/build/cli/impl/ArchetypeInvoker.java +++ b/cli/impl/src/main/java/io/helidon/build/cli/impl/ArchetypeInvoker.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. @@ -459,9 +459,17 @@ Path invoke() { } //noinspection ConstantConditions - ArchetypeEngineV2 engine = new ArchetypeEngineV2(archetype()); + ArchetypeEngineV2 engine = ArchetypeEngineV2.builder() + .fileSystem(archetype()) + .inputResolver(resolver) + .externalValues(externalValues) + .externalDefaults(externalDefaults) + .onResolved(onResolved()) + .directorySupplier(projectDirSupplier()) + .outputPropsFile(initOptions.outputPropsFileOption()) + .build(); try { - return engine.generate(resolver, externalValues, externalDefaults, onResolved(), projectDirSupplier()); + return engine.generate(); } catch (InvocationException ie) { Throwable cause = ie.getCause(); if (cause instanceof UnresolvedInputException) { diff --git a/cli/impl/src/main/java/io/helidon/build/cli/impl/CommonOptions.java b/cli/impl/src/main/java/io/helidon/build/cli/impl/CommonOptions.java index 3c0167681..f18422384 100644 --- a/cli/impl/src/main/java/io/helidon/build/cli/impl/CommonOptions.java +++ b/cli/impl/src/main/java/io/helidon/build/cli/impl/CommonOptions.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. diff --git a/cli/impl/src/main/java/io/helidon/build/cli/impl/InitCommand.java b/cli/impl/src/main/java/io/helidon/build/cli/impl/InitCommand.java index b9032ae4d..3fad3ebd1 100644 --- a/cli/impl/src/main/java/io/helidon/build/cli/impl/InitCommand.java +++ b/cli/impl/src/main/java/io/helidon/build/cli/impl/InitCommand.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. diff --git a/cli/impl/src/main/java/io/helidon/build/cli/impl/InitOptions.java b/cli/impl/src/main/java/io/helidon/build/cli/impl/InitOptions.java index a8e0ccc36..2f2eff083 100644 --- a/cli/impl/src/main/java/io/helidon/build/cli/impl/InitOptions.java +++ b/cli/impl/src/main/java/io/helidon/build/cli/impl/InitOptions.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. @@ -15,6 +15,7 @@ */ package io.helidon.build.cli.impl; +import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,6 +66,7 @@ public final class InitOptions { private final String groupIdOption; private final String artifactIdOption; private final String packageNameOption; + private final File outputPropsFileOption; private final boolean batch; private String projectName; private String groupId; @@ -107,6 +109,8 @@ public String toString() { @KeyValue(name = "artifactid", description = "Project's artifact ID") String artifactId, @KeyValue(name = "package", description = "Project's package name") String packageName, @KeyValue(name = "name", description = "Project's name") String projectName, + @KeyValue(name = "output-props-file", description = "Path to file where user inputs will be saved") + File outputPropsFile, @Option.Flag(name = "batch", description = "Enable non-interactive mode") boolean batch) { this.buildOption = build; @@ -120,6 +124,7 @@ public String toString() { this.groupIdOption = groupId; this.artifactIdOption = artifactId; this.packageNameOption = packageName; + this.outputPropsFileOption = outputPropsFile; this.batch = batch; // The following will be updated by applyConfig: @@ -130,6 +135,15 @@ public String toString() { this.packageName = packageName; } + /** + * Get the output-props-file option. + * + * @return output-props-file + */ + public File outputPropsFileOption() { + return outputPropsFileOption; + } + /** * Get the flavor option. * diff --git a/cli/impl/src/test/java/io/helidon/build/cli/impl/ArchetypeInvokerTest.java b/cli/impl/src/test/java/io/helidon/build/cli/impl/ArchetypeInvokerTest.java index c13624cb6..f810faf6c 100644 --- a/cli/impl/src/test/java/io/helidon/build/cli/impl/ArchetypeInvokerTest.java +++ b/cli/impl/src/test/java/io/helidon/build/cli/impl/ArchetypeInvokerTest.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. @@ -46,6 +46,6 @@ private static ArchetypeInvoker invoker(String helidonVersion) { } private static InitOptions initOptions(String helidonVersion) { - return new InitOptions(null, null, helidonVersion, null, null, null, null, null, false); + return new InitOptions(null, null, helidonVersion, null, null, null, null, null, null, false); } } diff --git a/common/common/etc/spotbugs/exclude.xml b/common/common/etc/spotbugs/exclude.xml index 2936a1657..ca573f6b9 100644 --- a/common/common/etc/spotbugs/exclude.xml +++ b/common/common/etc/spotbugs/exclude.xml @@ -1,7 +1,7 @@ + + + + diff --git a/common/common/src/main/java/io/helidon/build/common/FileUtils.java b/common/common/src/main/java/io/helidon/build/common/FileUtils.java index 0e983d15d..c194c355f 100644 --- a/common/common/src/main/java/io/helidon/build/common/FileUtils.java +++ b/common/common/src/main/java/io/helidon/build/common/FileUtils.java @@ -17,6 +17,8 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; @@ -40,6 +42,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Properties; import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -969,4 +972,36 @@ public static List readAllLines(URI fileUri) throws IOException, URISynt Path path = Paths.get(fileUri.getPath()); return Files.readAllLines(path); } + + /** + * Load content of the properties file. + * + * @param filePath path to file + * @return content of the properties file + */ + public static Properties loadProperties(Path filePath) { + try (InputStream input = Files.newInputStream(filePath)) { + Properties props = new Properties(); + props.load(input); + return props; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Save data that is stored in Map {@code values} to properties file. + * + * @param values data to store + * @param filePath path to file + */ + public static void saveToPropertiesFile(Map values, Path filePath) { + try (OutputStream output = Files.newOutputStream(filePath)) { + Properties props = new Properties(); + props.putAll(values); + props.store(output, null); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/IntegrationTestMojo.java b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/IntegrationTestMojo.java index 396af95aa..8db3bb91b 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/IntegrationTestMojo.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/IntegrationTestMojo.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. @@ -465,8 +465,13 @@ private static int maxKeyWidth(Map... maps) { private void generate(Path archetypeFile, Properties props, Path outputDir) { try { FileSystem fileSystem = newFileSystem(archetypeFile, this.getClass().getClassLoader()); - ArchetypeEngineV2 engine = new ArchetypeEngineV2(fileSystem); - engine.generate(new BatchInputResolver(), Maps.fromProperties(props), Map.of(), n -> outputDir); + ArchetypeEngineV2 engine = ArchetypeEngineV2.builder() + .fileSystem(fileSystem) + .inputResolver(new BatchInputResolver()) + .externalValues(Maps.fromProperties(props)) + .directorySupplier(n -> outputDir) + .build(); + engine.generate(); } catch (IOException ex) { throw new UncheckedIOException(ex); } diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/EngineFacade.java b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/EngineFacade.java index 07919e73a..b540836a3 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/EngineFacade.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/EngineFacade.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. @@ -137,7 +137,7 @@ public static void generate(ArchetypeGenerationRequest request, List dep Path projectDir = Paths.get(request.getOutputDirectory()).resolve(request.getArtifactId()); Files.delete(projectDir.resolve("pom.xml")); boolean interactiveMode = !"false".equals(System.getProperty("interactiveMode")); - new ReflectedEngine(ecl, fileSystem).generate(interactiveMode, props, emptyMap(), n -> projectDir); + new ReflectedEngine(ecl, fileSystem, interactiveMode, props, emptyMap(), n -> projectDir).generate(); } catch (IOException ioe) { throw new IllegalStateException(ioe); } diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngine.java b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngine.java index 9e8f4898c..4c6f8b4f5 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngine.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/main/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngine.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. @@ -23,6 +23,8 @@ import java.util.Map; import java.util.function.Function; +import io.helidon.build.archetype.engine.v2.ArchetypeEngineV2; + /** * Utility class to invoke the archetype engine using reflection. */ @@ -32,6 +34,7 @@ final class ReflectedEngine { private static final String RESOLVER_FCN = "io.helidon.build.archetype.engine.v2.InputResolver"; private static final String BATCH_RESOLVER_FCN = "io.helidon.build.archetype.engine.v2.BatchInputResolver"; private static final String TERMINAL_RESOLVER_FCN = "io.helidon.build.archetype.engine.v2.TerminalInputResolver"; + private static final String ENGINE_FCN_BUILDER = "io.helidon.build.archetype.engine.v2.ArchetypeEngineV2$Builder"; private final ClassLoader classLoader; private final Object engineInstance; @@ -40,22 +43,45 @@ final class ReflectedEngine { /** * Create a new engine. * - * @param cl class loader - * @param fileSystem archetype file system + * @param cl class loader + * @param fileSystem archetype file system + * @param isInteractive {@code true} if interactive + * @param externalValues external values + * @param externalDefaults external defaults + * @param directorySupplier directory supplier */ - ReflectedEngine(ClassLoader cl, FileSystem fileSystem) { + ReflectedEngine(ClassLoader cl, + FileSystem fileSystem, + boolean isInteractive, + Map externalValues, + Map externalDefaults, + Function directorySupplier) { try { classLoader = cl; Class engineClass = cl.loadClass(ENGINE_FCN); - Constructor constructor = engineClass.getConstructor(FileSystem.class); - engineInstance = constructor.newInstance(fileSystem); + Class engineBuilderClass = cl.loadClass(ENGINE_FCN_BUILDER); + Constructor builderClassConstructor = engineBuilderClass.getDeclaredConstructor(); + builderClassConstructor.setAccessible(true); + Object builder = builderClassConstructor.newInstance(); + engineBuilderClass.getDeclaredMethod("directorySupplier", Function.class) + .invoke(builder, directorySupplier); + engineBuilderClass.getDeclaredMethod("externalDefaults", Map.class) + .invoke(builder, externalDefaults); + engineBuilderClass.getDeclaredMethod("externalValues", Map.class) + .invoke(builder, externalValues); Class inputResolverClass = cl.loadClass(RESOLVER_FCN); - generateMethod = engineClass.getDeclaredMethod("generate", inputResolverClass, Map.class, Map.class, - Function.class); + engineBuilderClass.getDeclaredMethod("inputResolver", inputResolverClass) + .invoke(builder, inputResolver(isInteractive)); + engineBuilderClass.getDeclaredMethod("fileSystem", FileSystem.class) + .invoke(builder, fileSystem); + Constructor constructor = engineClass.getDeclaredConstructor(ArchetypeEngineV2.Builder.class); + constructor.setAccessible(true); + engineInstance = constructor.newInstance(builder); + generateMethod = engineClass.getDeclaredMethod("generate"); } catch (InstantiationException - | IllegalAccessException - | NoSuchMethodException - | ClassNotFoundException ex) { + | IllegalAccessException + | NoSuchMethodException + | ClassNotFoundException ex) { throw new IllegalStateException(ex); } catch (InvocationTargetException ex) { Throwable cause = ex.getCause(); @@ -68,24 +94,10 @@ final class ReflectedEngine { /** * Generate the project. - * - * @param isInteractive {@code true} if interactive - * @param externalValues external values - * @param externalDefaults external defaults - * @param directorySupplier directory supplier */ - Path generate(boolean isInteractive, - Map externalValues, - Map externalDefaults, - Function directorySupplier) { - + Path generate() { try { - return (Path) generateMethod.invoke( - engineInstance, - inputResolver(isInteractive), - externalValues, - externalDefaults, - directorySupplier); + return (Path) generateMethod.invoke(engineInstance); } catch (IllegalAccessException ex) { throw new IllegalStateException(ex); } catch (InvocationTargetException e) { diff --git a/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngineTest.java b/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngineTest.java index b3f1f5edd..d5ab7a368 100644 --- a/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngineTest.java +++ b/maven-plugins/helidon-archetype-maven-plugin/src/test/java/io/helidon/build/maven/archetype/postgenerate/ReflectedEngineTest.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,9 +43,11 @@ void testGenerate() throws IOException { Path sourceDir = targetDir.resolve("test-classes/simple"); Path outputDir = targetDir.resolve("reflected-engine-ut"); FileSystem fs = VirtualFileSystem.create(sourceDir); - ReflectedEngine engine = new ReflectedEngine(this.getClass().getClassLoader(), fs); Map externalValues = Map.of("color", "red", "artifactId", "testGenerate"); - Path projectDir = engine.generate(false, externalValues, Map.of(), n -> unique(outputDir, n)); + ReflectedEngine engine = new ReflectedEngine( + this.getClass().getClassLoader(), fs, false, + externalValues, Map.of(), n -> unique(outputDir, n)); + Path projectDir = engine.generate(); assertThat(Files.exists(projectDir.resolve("color.txt")), is(true)); assertThat(normalizeNewLines(readString(projectDir.resolve("color.txt"))), is("red\n")); }