Skip to content

Commit

Permalink
Add project generator to core (#1545)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreaLaGrotteria authored Nov 6, 2024
1 parent 87d54b1 commit a5159dc
Show file tree
Hide file tree
Showing 12 changed files with 605 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.micronaut.context.ApplicationContext
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import io.micronaut.guides.core.App
import io.micronaut.guides.core.CopyFileVisitor
import io.micronaut.guides.core.Guide
import io.micronaut.guides.core.GuideUtils
import io.micronaut.guides.core.GuidesOption
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.micronaut.guides;
package io.micronaut.guides.core;

import java.io.IOException;
import java.nio.file.FileVisitResult;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package io.micronaut.guides.core;

import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.starter.api.TestFramework;
import io.micronaut.starter.application.ApplicationType;
import io.micronaut.starter.application.Project;
import io.micronaut.starter.application.generator.GeneratorContext;
import io.micronaut.starter.application.generator.ProjectGenerator;
import io.micronaut.starter.io.ConsoleOutput;
import io.micronaut.starter.io.FileSystemOutputHandler;
import io.micronaut.starter.options.BuildTool;
import io.micronaut.starter.options.JdkVersion;
import io.micronaut.starter.options.Language;
import io.micronaut.starter.options.Options;
import io.micronaut.starter.util.NameUtils;
import jakarta.inject.Singleton;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import org.gradle.api.JavaVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static io.micronaut.core.util.StringUtils.EMPTY_STRING;
import static io.micronaut.http.HttpStatus.BAD_REQUEST;
import static io.micronaut.starter.options.BuildTool.GRADLE;
import static io.micronaut.starter.options.JdkVersion.JDK_8;

@Singleton
public class DefaultGuideProjectGenerator implements GuideProjectGenerator {
private static final Logger LOG = LoggerFactory.getLogger(DefaultGuideProjectGenerator.class);
private final GuidesConfiguration guidesConfiguration;
private final ProjectGenerator projectGenerator;

DefaultGuideProjectGenerator(GuidesConfiguration guidesConfiguration,
ProjectGenerator projectGenerator) {
this.guidesConfiguration = guidesConfiguration;
this.projectGenerator = projectGenerator;
}

@Override
public void generate(@NotNull @NonNull File outputDirectory, @NotNull @NonNull Guide guide) throws IOException {
if (!outputDirectory.exists()) {
throw new ConfigurationException("Output directory does not exist");
}
if (!outputDirectory.isDirectory()) {
throw new ConfigurationException("Output directory must be a directory");
}

JdkVersion javaVersion = GuideGenerationUtils.resolveJdkVersion(guidesConfiguration, guide);
if (guide.maximumJavaVersion() != null && javaVersion.majorVersion() > guide.maximumJavaVersion()) {
if (LOG.isTraceEnabled()) {
LOG.trace("not generating project for {}, JDK {} > {}", guide.slug(), javaVersion.majorVersion(), guide.maximumJavaVersion());
}
return;
}
List<GuidesOption> guidesOptionList = GuideGenerationUtils.guidesOptions(guide,LOG);
for (GuidesOption guidesOption : guidesOptionList) {
generate(outputDirectory, guide, guidesOption, javaVersion);
}
}

public void generate(@NonNull File outputDirectory,
@NonNull Guide guide,
@NonNull GuidesOption guidesOption,
@NonNull JdkVersion javaVersion) throws IOException {
for (App app : guide.apps()) {
generate(outputDirectory, guide, guidesOption, javaVersion, app);
}
}

public void generate(@NonNull File outputDirectory,
@NonNull Guide guide,
@NonNull GuidesOption guidesOption,
@NonNull JdkVersion javaVersion,
@NonNull App app) throws IOException {
List<String> appFeatures = new ArrayList<>(GuideUtils.getAppFeatures(app, guidesOption.getLanguage()));
if (!guidesConfiguration.getJdkVersionsSupportedByGraalvm().contains(javaVersion)) {
appFeatures.remove("graalvm");
}

if (guidesOption.getTestFramework() == TestFramework.SPOCK) {
appFeatures.remove("mockito");
}

// typical guides use 'default' as name, multi-project guides have different modules
String folder = MacroUtils.getSourceDir(guide.slug(), guidesOption);

String appName = app.name().equals(guidesConfiguration.getDefaultAppName()) ? EMPTY_STRING : app.name();

Path destinationPath = Paths.get(outputDirectory.getAbsolutePath(), folder, appName);
File destination = destinationPath.toFile();
destination.mkdir();

String packageAndName = guidesConfiguration.getPackageName() + '.' + app.name();
GeneratorContext generatorContext = createProjectGeneratorContext(app.applicationType(),
packageAndName,
app.framework(),
appFeatures,
guidesOption.getBuildTool(),
app.testFramework() != null ? app.testFramework() : guidesOption.getTestFramework(),
guidesOption.getLanguage(),
javaVersion);
try {
projectGenerator.generate(app.applicationType(),
generatorContext.getProject(),
new FileSystemOutputHandler(destination, ConsoleOutput.NOOP),
generatorContext);
} catch (Exception e) {
LOG.error("Error generating application: " + e.getMessage(), e);
throw new IOException(e.getMessage(), e);
}
}

private GeneratorContext createProjectGeneratorContext(
ApplicationType type,
@Pattern(regexp = "[\\w\\d-_\\.]+") String packageAndName,
@Nullable String framework,
@Nullable List<String> features,
@Nullable BuildTool buildTool,
@Nullable TestFramework testFramework,
@Nullable Language lang,
@Nullable JdkVersion javaVersion) throws IllegalArgumentException {
Project project;
try {
project = NameUtils.parse(packageAndName);
} catch (IllegalArgumentException e) {
throw new HttpStatusException(BAD_REQUEST, "Invalid project name: " + e.getMessage());
}

return projectGenerator.createGeneratorContext(
type,
project,
new Options(
lang,
testFramework != null ? testFramework.toTestFramework() : null,
buildTool == null ? GRADLE : buildTool,
javaVersion != null ? javaVersion : JDK_8).withFramework(framework),
null,
features != null ? features : Collections.emptyList(),
ConsoleOutput.NOOP
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,42 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.LocalDate;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

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

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

private static int countLines(InputStream inputStream) {
int numberOfLines = 0;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
numberOfLines = (int) reader.lines().count() + 1;
} catch (IOException e) {
LOG.error("", e);
}
return numberOfLines;
private String readLicenseHeader(InputStream inputStream) {
return headerByYear.computeIfAbsent(LocalDate.now().getYear(), year -> {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
reader.lines().forEach(line -> sb.append(line).append("\n"));
} catch (IOException e) {
LOG.error("", e);
}
return sb.toString().replace("$YEAR", String.valueOf(year));
});
}

@Override
public int getNumberOfLines() {
return this.numberOfLines;
}

@Override
public String getLicenseHeaderText() { return this.licenseHeaderText; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.micronaut.guides.core;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.StringUtils;
import io.micronaut.starter.api.TestFramework;
import io.micronaut.starter.options.BuildTool;
import io.micronaut.starter.options.JdkVersion;
import io.micronaut.starter.options.Language;
import jakarta.validation.constraints.NotNull;
import org.gradle.api.GradleException;
import org.gradle.api.JavaVersion;
import org.slf4j.Logger;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import static io.micronaut.starter.api.TestFramework.JUNIT;
import static io.micronaut.starter.api.TestFramework.SPOCK;
import static io.micronaut.starter.options.Language.GROOVY;

public class GuideGenerationUtils {

private GuideGenerationUtils() {
}

@NonNull
static String mainPath(@NonNull String appName,
@NonNull String fileName,
@NonNull GuidesOption option,
@NonNull GuidesConfiguration configuration) {
return pathByFolder(appName, fileName, "main", option, configuration);
}

@NonNull
static String testPath(@NonNull String appName,
@NonNull String name,
@NonNull GuidesOption option,
@NonNull GuidesConfiguration configuration) {
String fileName = name;
if (option.getTestFramework() != null) {
if (name.endsWith("Test")) {
fileName = name.substring(0, name.indexOf("Test"));
fileName += option.getTestFramework() == SPOCK ? "Spec" : "Test";
}
}

return pathByFolder(appName, fileName, "test", option, configuration);
}

@NonNull
static String pathByFolder(@NonNull String appName,
@NonNull String fileName,
@NonNull String folder,
@NonNull GuidesOption option,
@NonNull GuidesConfiguration configuration) {
String module = StringUtils.isNotEmpty(appName) ? appName + "/" : "";
Path path = Path.of(module,
"src",
folder,
option.getLanguage().getName(),
configuration.getPackageName().replace(".", "/"),
fileName + "." + option.getLanguage().getExtension());
return path.toString();
}

@NonNull
static List<GuidesOption> guidesOptions(@NonNull Guide guideMetadata,
@NonNull Logger logger) {
List<BuildTool> buildTools = guideMetadata.buildTools();
List<Language> languages = guideMetadata.languages();
TestFramework testFramework = guideMetadata.testFramework();
List<GuidesOption> guidesOptionList = new ArrayList<>();

for (BuildTool buildTool : buildTools) {
for (Language language : Language.values()) {
if (GuideUtils.shouldSkip(guideMetadata, buildTool)) {
logger.info("Skipping index guide for {} and {}", buildTool, language);
continue;
}
if (languages.contains(language)) {
guidesOptionList.add(new GuidesOption(buildTool, language, testFrameworkOption(language, testFramework)));
}
}
}

return guidesOptionList;
}

@NonNull
static TestFramework testFrameworkOption(@NonNull Language language,
@Nullable TestFramework testFramework) {
if (testFramework != null) {
return testFramework;
}
if (language == GROOVY) {
return SPOCK;
}
return JUNIT;
}

@NonNull
static JdkVersion resolveJdkVersion(@NonNull GuidesConfiguration guidesConfiguration) {
JdkVersion javaVersion;
if (System.getenv(guidesConfiguration.getEnvJdkVersion()) != null) {
try {
int majorVersion = Integer.parseInt(System.getenv(guidesConfiguration.getEnvJdkVersion()));
javaVersion = JdkVersion.valueOf(majorVersion);
} catch (NumberFormatException ignored) {
throw new GradleException("Could not parse env " + guidesConfiguration.getEnvJdkVersion() + " to JdkVersion");
}
} else {
try {
javaVersion = JdkVersion.valueOf(Integer.parseInt(JavaVersion.current().getMajorVersion()));
} catch (IllegalArgumentException ex) {
System.out.println("WARNING: " + ex.getMessage() + ": Defaulting to " + guidesConfiguration.getDefaultJdkVersion());
javaVersion = guidesConfiguration.getDefaultJdkVersion();
}
}
return javaVersion;
}

public static JdkVersion resolveJdkVersion(GuidesConfiguration guidesConfiguration, @NotNull @NonNull Guide guide) {
JdkVersion javaVersion = GuideGenerationUtils.resolveJdkVersion(guidesConfiguration);
if (guide.minimumJavaVersion() != null) {
JdkVersion minimumJavaVersion = JdkVersion.valueOf(guide.minimumJavaVersion());
if (minimumJavaVersion.majorVersion() > javaVersion.majorVersion()) {
return minimumJavaVersion;
}
}
return javaVersion;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.micronaut.guides.core;

import io.micronaut.core.annotation.NonNull;
import jakarta.validation.constraints.NotNull;

import java.io.File;
import java.io.IOException;

public interface GuideProjectGenerator {
void generate(@NotNull @NonNull File outputDirectory,
@NotNull @NonNull Guide guide) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.micronaut.guides.core;

import io.micronaut.starter.options.JdkVersion;

import java.util.List;

public interface GuidesConfiguration {
Expand All @@ -12,5 +14,8 @@ public interface GuidesConfiguration {
int getDefaultMinJdk();
String getApiUrl();
String getVersionPath();
String getEnvJdkVersion();
List<String> getFilesWithHeader();
JdkVersion getDefaultJdkVersion();
List<JdkVersion> getJdkVersionsSupportedByGraalvm();
}
Loading

0 comments on commit a5159dc

Please sign in to comment.