Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add props-file option to save user inputs during project generation #836

Merged
merged 11 commits into from
Feb 24, 2023
8 changes: 7 additions & 1 deletion archetype/engine-v2/etc/spotbugs/exclude.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

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.
Expand Down Expand Up @@ -82,4 +82,10 @@
<Bug pattern="BC_UNCONFIRMED_CAST"/>
</Match>

<Match>
<!-- This API reads a file whose location might be specified by user input -->
<Class name="io.helidon.build.archetype.engine.v2.ArchetypeEngineV2"/>
<Bug pattern="PATH_TRAVERSAL_IN"/>
</Match>

</FindBugsFilter>
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -16,13 +16,16 @@

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;
import java.util.function.Function;

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;

Expand All @@ -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<String, String> externalValues,
Map<String, String> externalDefaults,
Function<String, Path> directorySupplier) {

return generate(inputResolver, externalValues, externalDefaults, () -> {}, directorySupplier);
private final InputResolver inputResolver;
private final Map<String, String> externalValues;
private final Map<String, String> externalDefaults;
private final Runnable onResolved;
private final Function<String, Path> 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<String, String> externalValues,
Map<String, String> externalDefaults,
Runnable onResolved,
Function<String, Path> directorySupplier) {

public Path generate() {
Context context = Context.builder()
.cwd(cwd)
.externalValues(externalValues)
Expand All @@ -104,6 +87,124 @@ public Path generate(InputResolver inputResolver,
Controller.walk(outputGenerator, script, context);
context.requireRootScope();

if (outputPropsFile != null) {
Map<String, String> 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<String, String> externalValues = Map.of();
private Map<String, String> externalDefaults = Map.of();
private Runnable onResolved = () -> {};
private Function<String, Path> 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<String, Path> 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<String, String> externalDefaults) {
this.externalDefaults = requireNonNull(externalDefaults, "externalDefaults is null");
return this;
}

/**
* Set external values.
*
* @param externalValues external values
* @return this builder
*/
public Builder externalValues(Map<String, String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ContextValue.ValueKind> DEFAULT_VALUE_KIND_FILTER =
Set.of(ContextValue.ValueKind.EXTERNAL, ContextValue.ValueKind.USER);

private static final String DEFAULT_VALUE_DELIMITER = ",";

private final Map<String, String> result;
private final Predicate<ContextEdge> filter;
private final CharSequence valueDelimiter;
private final Function<String, String> valueMapper;

private ContextSerializer(Map<String, String> result,
Predicate<ContextEdge> filter,
Function<String, String> 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<String> 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<String, String> serialize(Context context,
Predicate<ContextEdge> filter,
Function<String, String> valueMapper,
CharSequence valueDelimiter) {
Map<String, String> 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<String, String> serialize(Context context) {
Map<String, String> 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<Character> 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());
}
}
Loading