Skip to content

Commit a5159dc

Browse files
Add project generator to core (#1545)
1 parent 87d54b1 commit a5159dc

12 files changed

+605
-14
lines changed

buildSrc/src/main/groovy/io/micronaut/guides/GuideProjectGenerator.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.micronaut.context.ApplicationContext
88
import io.micronaut.core.annotation.NonNull
99
import io.micronaut.core.annotation.Nullable
1010
import io.micronaut.guides.core.App
11+
import io.micronaut.guides.core.CopyFileVisitor
1112
import io.micronaut.guides.core.Guide
1213
import io.micronaut.guides.core.GuideUtils
1314
import io.micronaut.guides.core.GuidesOption

buildSrc/src/main/java/io/micronaut/guides/CopyFileVisitor.java renamed to buildSrc/src/main/java/io/micronaut/guides/core/CopyFileVisitor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.micronaut.guides;
1+
package io.micronaut.guides.core;
22

33
import java.io.IOException;
44
import java.nio.file.FileVisitResult;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package io.micronaut.guides.core;
2+
3+
import io.micronaut.context.exceptions.ConfigurationException;
4+
import io.micronaut.core.annotation.NonNull;
5+
import io.micronaut.core.annotation.Nullable;
6+
import io.micronaut.http.exceptions.HttpStatusException;
7+
import io.micronaut.starter.api.TestFramework;
8+
import io.micronaut.starter.application.ApplicationType;
9+
import io.micronaut.starter.application.Project;
10+
import io.micronaut.starter.application.generator.GeneratorContext;
11+
import io.micronaut.starter.application.generator.ProjectGenerator;
12+
import io.micronaut.starter.io.ConsoleOutput;
13+
import io.micronaut.starter.io.FileSystemOutputHandler;
14+
import io.micronaut.starter.options.BuildTool;
15+
import io.micronaut.starter.options.JdkVersion;
16+
import io.micronaut.starter.options.Language;
17+
import io.micronaut.starter.options.Options;
18+
import io.micronaut.starter.util.NameUtils;
19+
import jakarta.inject.Singleton;
20+
import jakarta.validation.constraints.NotNull;
21+
import jakarta.validation.constraints.Pattern;
22+
import org.gradle.api.JavaVersion;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
import java.io.File;
27+
import java.io.IOException;
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
import java.util.ArrayList;
31+
import java.util.Collections;
32+
import java.util.List;
33+
34+
import static io.micronaut.core.util.StringUtils.EMPTY_STRING;
35+
import static io.micronaut.http.HttpStatus.BAD_REQUEST;
36+
import static io.micronaut.starter.options.BuildTool.GRADLE;
37+
import static io.micronaut.starter.options.JdkVersion.JDK_8;
38+
39+
@Singleton
40+
public class DefaultGuideProjectGenerator implements GuideProjectGenerator {
41+
private static final Logger LOG = LoggerFactory.getLogger(DefaultGuideProjectGenerator.class);
42+
private final GuidesConfiguration guidesConfiguration;
43+
private final ProjectGenerator projectGenerator;
44+
45+
DefaultGuideProjectGenerator(GuidesConfiguration guidesConfiguration,
46+
ProjectGenerator projectGenerator) {
47+
this.guidesConfiguration = guidesConfiguration;
48+
this.projectGenerator = projectGenerator;
49+
}
50+
51+
@Override
52+
public void generate(@NotNull @NonNull File outputDirectory, @NotNull @NonNull Guide guide) throws IOException {
53+
if (!outputDirectory.exists()) {
54+
throw new ConfigurationException("Output directory does not exist");
55+
}
56+
if (!outputDirectory.isDirectory()) {
57+
throw new ConfigurationException("Output directory must be a directory");
58+
}
59+
60+
JdkVersion javaVersion = GuideGenerationUtils.resolveJdkVersion(guidesConfiguration, guide);
61+
if (guide.maximumJavaVersion() != null && javaVersion.majorVersion() > guide.maximumJavaVersion()) {
62+
if (LOG.isTraceEnabled()) {
63+
LOG.trace("not generating project for {}, JDK {} > {}", guide.slug(), javaVersion.majorVersion(), guide.maximumJavaVersion());
64+
}
65+
return;
66+
}
67+
List<GuidesOption> guidesOptionList = GuideGenerationUtils.guidesOptions(guide,LOG);
68+
for (GuidesOption guidesOption : guidesOptionList) {
69+
generate(outputDirectory, guide, guidesOption, javaVersion);
70+
}
71+
}
72+
73+
public void generate(@NonNull File outputDirectory,
74+
@NonNull Guide guide,
75+
@NonNull GuidesOption guidesOption,
76+
@NonNull JdkVersion javaVersion) throws IOException {
77+
for (App app : guide.apps()) {
78+
generate(outputDirectory, guide, guidesOption, javaVersion, app);
79+
}
80+
}
81+
82+
public void generate(@NonNull File outputDirectory,
83+
@NonNull Guide guide,
84+
@NonNull GuidesOption guidesOption,
85+
@NonNull JdkVersion javaVersion,
86+
@NonNull App app) throws IOException {
87+
List<String> appFeatures = new ArrayList<>(GuideUtils.getAppFeatures(app, guidesOption.getLanguage()));
88+
if (!guidesConfiguration.getJdkVersionsSupportedByGraalvm().contains(javaVersion)) {
89+
appFeatures.remove("graalvm");
90+
}
91+
92+
if (guidesOption.getTestFramework() == TestFramework.SPOCK) {
93+
appFeatures.remove("mockito");
94+
}
95+
96+
// typical guides use 'default' as name, multi-project guides have different modules
97+
String folder = MacroUtils.getSourceDir(guide.slug(), guidesOption);
98+
99+
String appName = app.name().equals(guidesConfiguration.getDefaultAppName()) ? EMPTY_STRING : app.name();
100+
101+
Path destinationPath = Paths.get(outputDirectory.getAbsolutePath(), folder, appName);
102+
File destination = destinationPath.toFile();
103+
destination.mkdir();
104+
105+
String packageAndName = guidesConfiguration.getPackageName() + '.' + app.name();
106+
GeneratorContext generatorContext = createProjectGeneratorContext(app.applicationType(),
107+
packageAndName,
108+
app.framework(),
109+
appFeatures,
110+
guidesOption.getBuildTool(),
111+
app.testFramework() != null ? app.testFramework() : guidesOption.getTestFramework(),
112+
guidesOption.getLanguage(),
113+
javaVersion);
114+
try {
115+
projectGenerator.generate(app.applicationType(),
116+
generatorContext.getProject(),
117+
new FileSystemOutputHandler(destination, ConsoleOutput.NOOP),
118+
generatorContext);
119+
} catch (Exception e) {
120+
LOG.error("Error generating application: " + e.getMessage(), e);
121+
throw new IOException(e.getMessage(), e);
122+
}
123+
}
124+
125+
private GeneratorContext createProjectGeneratorContext(
126+
ApplicationType type,
127+
@Pattern(regexp = "[\\w\\d-_\\.]+") String packageAndName,
128+
@Nullable String framework,
129+
@Nullable List<String> features,
130+
@Nullable BuildTool buildTool,
131+
@Nullable TestFramework testFramework,
132+
@Nullable Language lang,
133+
@Nullable JdkVersion javaVersion) throws IllegalArgumentException {
134+
Project project;
135+
try {
136+
project = NameUtils.parse(packageAndName);
137+
} catch (IllegalArgumentException e) {
138+
throw new HttpStatusException(BAD_REQUEST, "Invalid project name: " + e.getMessage());
139+
}
140+
141+
return projectGenerator.createGeneratorContext(
142+
type,
143+
project,
144+
new Options(
145+
lang,
146+
testFramework != null ? testFramework.toTestFramework() : null,
147+
buildTool == null ? GRADLE : buildTool,
148+
javaVersion != null ? javaVersion : JDK_8).withFramework(framework),
149+
null,
150+
features != null ? features : Collections.emptyList(),
151+
ConsoleOutput.NOOP
152+
);
153+
}
154+
}

buildSrc/src/main/java/io/micronaut/guides/core/DefaultLicenseLoader.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,42 @@
99
import java.io.IOException;
1010
import java.io.InputStream;
1111
import java.io.InputStreamReader;
12+
import java.time.LocalDate;
13+
import java.util.Map;
1214
import java.util.Optional;
15+
import java.util.concurrent.ConcurrentHashMap;
1316

1417
@Singleton
1518
public class DefaultLicenseLoader implements LicenseLoader {
1619
private static final Logger LOG = LoggerFactory.getLogger(DefaultLicenseLoader.class);
1720
private final int numberOfLines;
21+
private final String licenseHeaderText;
22+
private final Map<Integer, String> headerByYear = new ConcurrentHashMap<>();
1823

1924
public DefaultLicenseLoader(GuidesConfiguration guidesConfiguration,
2025
ResourceLoader resourceLoader) {
2126
Optional<InputStream> resourceAsStreamOptional = resourceLoader.getResourceAsStream(guidesConfiguration.getLicensePath());
22-
this.numberOfLines = resourceAsStreamOptional.map(DefaultLicenseLoader::countLines).orElse(0);
27+
this.licenseHeaderText = resourceAsStreamOptional.map(this::readLicenseHeader).orElse("");
28+
this.numberOfLines = (int) licenseHeaderText.lines().count() + 1;
2329
}
2430

25-
private static int countLines(InputStream inputStream) {
26-
int numberOfLines = 0;
27-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
28-
numberOfLines = (int) reader.lines().count() + 1;
29-
} catch (IOException e) {
30-
LOG.error("", e);
31-
}
32-
return numberOfLines;
31+
private String readLicenseHeader(InputStream inputStream) {
32+
return headerByYear.computeIfAbsent(LocalDate.now().getYear(), year -> {
33+
StringBuilder sb = new StringBuilder();
34+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
35+
reader.lines().forEach(line -> sb.append(line).append("\n"));
36+
} catch (IOException e) {
37+
LOG.error("", e);
38+
}
39+
return sb.toString().replace("$YEAR", String.valueOf(year));
40+
});
3341
}
3442

43+
@Override
3544
public int getNumberOfLines() {
3645
return this.numberOfLines;
3746
}
47+
48+
@Override
49+
public String getLicenseHeaderText() { return this.licenseHeaderText; }
3850
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package io.micronaut.guides.core;
2+
3+
import io.micronaut.core.annotation.NonNull;
4+
import io.micronaut.core.annotation.Nullable;
5+
import io.micronaut.core.util.StringUtils;
6+
import io.micronaut.starter.api.TestFramework;
7+
import io.micronaut.starter.options.BuildTool;
8+
import io.micronaut.starter.options.JdkVersion;
9+
import io.micronaut.starter.options.Language;
10+
import jakarta.validation.constraints.NotNull;
11+
import org.gradle.api.GradleException;
12+
import org.gradle.api.JavaVersion;
13+
import org.slf4j.Logger;
14+
15+
import java.nio.file.Path;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
import static io.micronaut.starter.api.TestFramework.JUNIT;
20+
import static io.micronaut.starter.api.TestFramework.SPOCK;
21+
import static io.micronaut.starter.options.Language.GROOVY;
22+
23+
public class GuideGenerationUtils {
24+
25+
private GuideGenerationUtils() {
26+
}
27+
28+
@NonNull
29+
static String mainPath(@NonNull String appName,
30+
@NonNull String fileName,
31+
@NonNull GuidesOption option,
32+
@NonNull GuidesConfiguration configuration) {
33+
return pathByFolder(appName, fileName, "main", option, configuration);
34+
}
35+
36+
@NonNull
37+
static String testPath(@NonNull String appName,
38+
@NonNull String name,
39+
@NonNull GuidesOption option,
40+
@NonNull GuidesConfiguration configuration) {
41+
String fileName = name;
42+
if (option.getTestFramework() != null) {
43+
if (name.endsWith("Test")) {
44+
fileName = name.substring(0, name.indexOf("Test"));
45+
fileName += option.getTestFramework() == SPOCK ? "Spec" : "Test";
46+
}
47+
}
48+
49+
return pathByFolder(appName, fileName, "test", option, configuration);
50+
}
51+
52+
@NonNull
53+
static String pathByFolder(@NonNull String appName,
54+
@NonNull String fileName,
55+
@NonNull String folder,
56+
@NonNull GuidesOption option,
57+
@NonNull GuidesConfiguration configuration) {
58+
String module = StringUtils.isNotEmpty(appName) ? appName + "/" : "";
59+
Path path = Path.of(module,
60+
"src",
61+
folder,
62+
option.getLanguage().getName(),
63+
configuration.getPackageName().replace(".", "/"),
64+
fileName + "." + option.getLanguage().getExtension());
65+
return path.toString();
66+
}
67+
68+
@NonNull
69+
static List<GuidesOption> guidesOptions(@NonNull Guide guideMetadata,
70+
@NonNull Logger logger) {
71+
List<BuildTool> buildTools = guideMetadata.buildTools();
72+
List<Language> languages = guideMetadata.languages();
73+
TestFramework testFramework = guideMetadata.testFramework();
74+
List<GuidesOption> guidesOptionList = new ArrayList<>();
75+
76+
for (BuildTool buildTool : buildTools) {
77+
for (Language language : Language.values()) {
78+
if (GuideUtils.shouldSkip(guideMetadata, buildTool)) {
79+
logger.info("Skipping index guide for {} and {}", buildTool, language);
80+
continue;
81+
}
82+
if (languages.contains(language)) {
83+
guidesOptionList.add(new GuidesOption(buildTool, language, testFrameworkOption(language, testFramework)));
84+
}
85+
}
86+
}
87+
88+
return guidesOptionList;
89+
}
90+
91+
@NonNull
92+
static TestFramework testFrameworkOption(@NonNull Language language,
93+
@Nullable TestFramework testFramework) {
94+
if (testFramework != null) {
95+
return testFramework;
96+
}
97+
if (language == GROOVY) {
98+
return SPOCK;
99+
}
100+
return JUNIT;
101+
}
102+
103+
@NonNull
104+
static JdkVersion resolveJdkVersion(@NonNull GuidesConfiguration guidesConfiguration) {
105+
JdkVersion javaVersion;
106+
if (System.getenv(guidesConfiguration.getEnvJdkVersion()) != null) {
107+
try {
108+
int majorVersion = Integer.parseInt(System.getenv(guidesConfiguration.getEnvJdkVersion()));
109+
javaVersion = JdkVersion.valueOf(majorVersion);
110+
} catch (NumberFormatException ignored) {
111+
throw new GradleException("Could not parse env " + guidesConfiguration.getEnvJdkVersion() + " to JdkVersion");
112+
}
113+
} else {
114+
try {
115+
javaVersion = JdkVersion.valueOf(Integer.parseInt(JavaVersion.current().getMajorVersion()));
116+
} catch (IllegalArgumentException ex) {
117+
System.out.println("WARNING: " + ex.getMessage() + ": Defaulting to " + guidesConfiguration.getDefaultJdkVersion());
118+
javaVersion = guidesConfiguration.getDefaultJdkVersion();
119+
}
120+
}
121+
return javaVersion;
122+
}
123+
124+
public static JdkVersion resolveJdkVersion(GuidesConfiguration guidesConfiguration, @NotNull @NonNull Guide guide) {
125+
JdkVersion javaVersion = GuideGenerationUtils.resolveJdkVersion(guidesConfiguration);
126+
if (guide.minimumJavaVersion() != null) {
127+
JdkVersion minimumJavaVersion = JdkVersion.valueOf(guide.minimumJavaVersion());
128+
if (minimumJavaVersion.majorVersion() > javaVersion.majorVersion()) {
129+
return minimumJavaVersion;
130+
}
131+
}
132+
return javaVersion;
133+
}
134+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.micronaut.guides.core;
2+
3+
import io.micronaut.core.annotation.NonNull;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
import java.io.File;
7+
import java.io.IOException;
8+
9+
public interface GuideProjectGenerator {
10+
void generate(@NotNull @NonNull File outputDirectory,
11+
@NotNull @NonNull Guide guide) throws IOException;
12+
}

buildSrc/src/main/java/io/micronaut/guides/core/GuidesConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.micronaut.guides.core;
22

3+
import io.micronaut.starter.options.JdkVersion;
4+
35
import java.util.List;
46

57
public interface GuidesConfiguration {
@@ -12,5 +14,8 @@ public interface GuidesConfiguration {
1214
int getDefaultMinJdk();
1315
String getApiUrl();
1416
String getVersionPath();
17+
String getEnvJdkVersion();
1518
List<String> getFilesWithHeader();
19+
JdkVersion getDefaultJdkVersion();
20+
List<JdkVersion> getJdkVersionsSupportedByGraalvm();
1621
}

0 commit comments

Comments
 (0)