diff --git a/typescript-generator-gradle-plugin/pom.xml b/typescript-generator-gradle-plugin/pom.xml index 43742569d..67131d82b 100644 --- a/typescript-generator-gradle-plugin/pom.xml +++ b/typescript-generator-gradle-plugin/pom.xml @@ -17,6 +17,17 @@ + + dev.gradleplugins + gradle-test-kit + 8.2.1 + test + + + com.fasterxml.jackson.core + jackson-databind + 2.14.2 + org.gradle gradle-core @@ -58,11 +69,42 @@ groovy-all 2.4.21 + + org.junit.jupiter + junit-jupiter-api + test + + + + com.fasterxml.jackson.module + jackson-module-scala_2.13 + 2.14.2 + test + cz.habarta.typescript-generator typescript-generator-core 3.2-SNAPSHOT + + com.fasterxml.jackson.module + jackson-module-kotlin + 2.14.2 + test + + + + commons-io + commons-io + 2.11.0 + + + org.sonatype.sisu + sisu-inject-bean + 2.3.0 + compile + + diff --git a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java index 12b282619..6f69d4ef4 100644 --- a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java +++ b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java @@ -25,21 +25,24 @@ import cz.habarta.typescript.generator.TypeScriptOutputKind; import cz.habarta.typescript.generator.util.Utils; import java.io.File; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import javax.inject.Inject; import org.gradle.api.DefaultTask; -import org.gradle.api.Task; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.TaskAction; +import org.jetbrains.annotations.NotNull; -public class GenerateTask extends DefaultTask { - public String outputFile; +public abstract class GenerateTask extends DefaultTask { + public TypeScriptFileType outputFileType; public TypeScriptOutputKind outputKind; public String module; @@ -130,6 +133,20 @@ public class GenerateTask extends DefaultTask { public List jackson2Modules; public Logger.Level loggingLevel; + public String projectName; + + private final ProjectLayout projectLayout; + + @Classpath + abstract ConfigurableFileCollection getClasspath(); + + public String outputFile; + + @Inject + public GenerateTask(ProjectLayout projectLayout) { + this.projectLayout = projectLayout; + } + private Settings createSettings(URLClassLoader classLoader) { final Settings settings = new Settings(); if (outputFileType != null) { @@ -200,7 +217,7 @@ private Settings createSettings(URLClassLoader classLoader) { settings.primitivePropertiesRequired = primitivePropertiesRequired; settings.generateInfoJson = generateInfoJson; settings.generateNpmPackageJson = generateNpmPackageJson; - settings.npmName = npmName == null && generateNpmPackageJson ? getProject().getName() : npmName; + settings.npmName = npmName == null && generateNpmPackageJson ? projectName : npmName; settings.npmVersion = npmVersion == null && generateNpmPackageJson ? settings.getDefaultNpmVersion() : npmVersion; settings.npmTypescriptVersion = npmTypescriptVersion; settings.npmBuildScript = npmBuildScript; @@ -215,6 +232,7 @@ private Settings createSettings(URLClassLoader classLoader) { return settings; } + @TaskAction public void generate() throws Exception { if (outputKind == null) { @@ -226,55 +244,54 @@ public void generate() throws Exception { TypeScriptGenerator.setLogger(new Logger(loggingLevel)); TypeScriptGenerator.printVersion(); + try (URLClassLoader classLoader = createClassloader()) { + final Settings settings = createSettings(classLoader); + final Input.Parameters parameters = parameters(classLoader, settings); + File finalOutputFile = calculateOutputFile(settings); + settings.validateFileName(finalOutputFile); + new TypeScriptGenerator(settings).generateTypeScript(Input.from(parameters), Output.to(finalOutputFile)); + } + } + @NotNull + private URLClassLoader createClassloader() throws MalformedURLException { // class loader final Set urls = new LinkedHashSet<>(); - for (Task task : getProject().getTasks()) { - if (task.getName().startsWith("compile") && !task.getName().startsWith("compileTest")) { - for (File file : task.getOutputs().getFiles()) { - urls.add(file.toURI().toURL()); - } - } + for (File file : getClasspath()) { + urls.add(file.toURI().toURL()); } - urls.addAll(getFilesFromConfiguration("compileClasspath")); - - try (URLClassLoader classLoader = Settings.createClassLoader(getProject().getName(), urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader())) { - - final Settings settings = createSettings(classLoader); - - final Input.Parameters parameters = new Input.Parameters(); - parameters.classNames = classes; - parameters.classNamePatterns = classPatterns; - parameters.classesWithAnnotations = classesWithAnnotations; - parameters.classesImplementingInterfaces = classesImplementingInterfaces; - parameters.classesExtendingClasses = classesExtendingClasses; - parameters.jaxrsApplicationClassName = classesFromJaxrsApplication; - parameters.automaticJaxrsApplication = classesFromAutomaticJaxrsApplication; - parameters.isClassNameExcluded = settings.getExcludeFilter(); - parameters.classLoader = classLoader; - parameters.scanningAcceptedPackages = scanningAcceptedPackages; - parameters.debug = loggingLevel == Logger.Level.Debug; + return Settings.createClassLoader(projectName, urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader()); + } - final File output = outputFile != null - ? getProject().file(outputFile) - : new File(new File(getProject().getBuildDir(), "typescript-generator"), getProject().getName() + settings.getExtension()); - settings.validateFileName(output); + @NotNull + private File calculateOutputFile(Settings settings) { + return new File(outputFile != null ? outputFile : defaultOutputFile(settings)); + } - new TypeScriptGenerator(settings).generateTypeScript(Input.from(parameters), Output.to(output)); - } + @NotNull + private String defaultOutputFile(Settings settings) { + return projectLayout.getBuildDirectory().dir("typescript-generator").get().file(projectName + ext(settings.outputFileType)).getAsFile().getAbsolutePath(); } - private List getFilesFromConfiguration(String configuration) { - try { - final List urls = new ArrayList<>(); - for (File file : getProject().getConfigurations().getAt(configuration).getFiles()) { - urls.add(file.toURI().toURL()); - } - return urls; - } catch (Exception e) { - TypeScriptGenerator.getLogger().warning(String.format("Cannot get file names from configuration '%s': %s", configuration, e.getMessage())); - return Collections.emptyList(); - } + @NotNull + private Input.Parameters parameters(URLClassLoader classLoader, Settings settings) { + final Input.Parameters parameters = new Input.Parameters(); + parameters.classNames = classes; + parameters.classNamePatterns = classPatterns; + parameters.classesWithAnnotations = classesWithAnnotations; + parameters.classesImplementingInterfaces = classesImplementingInterfaces; + parameters.classesExtendingClasses = classesExtendingClasses; + parameters.jaxrsApplicationClassName = classesFromJaxrsApplication; + parameters.automaticJaxrsApplication = classesFromAutomaticJaxrsApplication; + parameters.isClassNameExcluded = settings.getExcludeFilter(); + parameters.classLoader = classLoader; + parameters.scanningAcceptedPackages = scanningAcceptedPackages; + parameters.debug = loggingLevel == Logger.Level.Debug; + return parameters; } + private String ext(TypeScriptFileType outputFileType) { + return outputFileType.equals(TypeScriptFileType.implementationFile) ? ".ts" : ".d.ts"; + } } + diff --git a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/TypeScriptGeneratorPlugin.java b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/TypeScriptGeneratorPlugin.java index d9e0eb8c4..23a4f43b2 100644 --- a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/TypeScriptGeneratorPlugin.java +++ b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/TypeScriptGeneratorPlugin.java @@ -1,7 +1,6 @@ package cz.habarta.typescript.generator.gradle; -import java.util.Collections; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; @@ -11,14 +10,13 @@ public class TypeScriptGeneratorPlugin implements Plugin { @Override public void apply(Project project) { - final Task generateTsTask = project.task(Collections.singletonMap(Task.TASK_TYPE, GenerateTask.class), "generateTypeScript"); - + GenerateTask generateTsTask = project.getTasks().create("generateTypeScript", GenerateTask.class); + generateTsTask.projectName = project.getName(); for (Task task : project.getTasks()) { if (task.getName().startsWith("compile") && !task.getName().startsWith("compileTest")) { generateTsTask.dependsOn(task.getName()); - generateTsTask.getInputs().files(task); } } - } + } } diff --git a/typescript-generator-gradle-plugin/src/test/java/cz/habarta/typescript/generator/gradle/BuildLogicFunctionalTest.java b/typescript-generator-gradle-plugin/src/test/java/cz/habarta/typescript/generator/gradle/BuildLogicFunctionalTest.java new file mode 100644 index 000000000..e5d0b4dbb --- /dev/null +++ b/typescript-generator-gradle-plugin/src/test/java/cz/habarta/typescript/generator/gradle/BuildLogicFunctionalTest.java @@ -0,0 +1,107 @@ +package cz.habarta.typescript.generator.gradle; + +import com.google.common.io.Files; +import static cz.habarta.typescript.generator.gradle.GradlePluginClasspathProvider.getClasspath; +import java.io.BufferedWriter; +import java.io.File; +import static java.io.File.pathSeparator; +import static java.io.File.separator; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.FileUtils; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class BuildLogicFunctionalTest { + + String sampleGradle = "../../typescript-generator/sample-gradle"; + File sourceDir = new File(sampleGradle + "/src"); + private File testKitDir = Files.createTempDir(); + + @TempDir + File testProjectDir; + private File buildFile; + private File classpathFile; + + @BeforeEach + public void setup() { + buildFile = new File(testProjectDir, "build.gradle"); + classpathFile = new File(buildGradleTemplate().getParent(), "plugin-under-test-metadata.properties"); + } + + @Test + public void shouldWorkWithConfigurationCache() throws IOException, NoSuchFieldException, IllegalAccessException { + try { + String classpath = "implementation-classpath=" + String.join(pathSeparator, getClasspath(testProjectDir)); + System.out.println("Classpath: " + classpath); + writeFile(classpathFile, classpath); + FileUtils.copyToFile(buildGradleTemplateUrl().openStream(), buildFile); + FileUtils.copyDirectory(sourceDir, new File(testProjectDir, "src")); + + assertTrue(runGradle("assemble").getOutput().contains("BUILD SUCCESSFUL")); + BuildResult generateTypeScript = runGradle("generateTypeScript"); + assertTrue(generateTypeScript.getOutput().contains("BUILD SUCCESSFUL")); + + String testFileName = testProjectDir.getName() + ".d.ts"; + String testFilePath = testProjectDir + separator + "build" + separator + "typescript-generator" + separator + testFileName; + String schema = FileUtils.readFileToString(new File(testFilePath), StandardCharsets.UTF_8); + assertThat(schema, containsString("export interface Person {")); + assertThat(schema, containsString("export interface PersonGroovy {")); + assertThat(schema, containsString("export interface PersonKt {")); + assertThat(schema, containsString("export interface PersonScala {")); + } finally { + deleteGradleDir(testKitDir); + } + } + + private static void deleteGradleDir(File testKitDir) { + try { + FileUtils.deleteDirectory(testKitDir); + }catch (IOException e) + { + //might happen on Windows but should be ignored + } + } + + private BuildResult runGradle(String task) { + System.setProperty("org.gradle.testkit.dir", testKitDir.getAbsolutePath()); + return GradleRunner.create() + .withProjectDir(testProjectDir) + .withGradleVersion("8.2.1") + .withPluginClasspath() + .withArguments( + "--stacktrace", + "--info", + "--configuration-cache", + task + ) + .build(); + } + + @NotNull + private static File buildGradleTemplate() { + return new File(buildGradleTemplateUrl().getPath()); + } + + @Nullable + private static URL buildGradleTemplateUrl() { + return BuildLogicFunctionalTest.class.getResource("/build.gradle.template"); + } + + private void writeFile(File destination, String content) throws IOException { + try (BufferedWriter output = new BufferedWriter(new FileWriter(destination))) { + output.write(content); + } + } +} + diff --git a/typescript-generator-gradle-plugin/src/test/java/cz/habarta/typescript/generator/gradle/GradlePluginClasspathProvider.java b/typescript-generator-gradle-plugin/src/test/java/cz/habarta/typescript/generator/gradle/GradlePluginClasspathProvider.java new file mode 100644 index 000000000..d7fabf2bc --- /dev/null +++ b/typescript-generator-gradle-plugin/src/test/java/cz/habarta/typescript/generator/gradle/GradlePluginClasspathProvider.java @@ -0,0 +1,102 @@ +package cz.habarta.typescript.generator.gradle; + +import java.io.File; +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.io.FileUtils; +import org.jetbrains.annotations.NotNull; +import sun.misc.Unsafe; + +public class GradlePluginClasspathProvider { + + public static List getClasspath(File projectDir) throws NoSuchFieldException, IllegalAccessException { + List list = GradlePluginClasspathProvider.getUrls(ClassLoader.getSystemClassLoader()) + .stream().filter(file -> !gradleDependency(file)) + .collect(Collectors.toList()); + list.addAll(buildDirs(projectDir)); + return list.stream().map(file -> path(file)).collect(Collectors.toList()); + } + + private static boolean gradleDependency(File file) { + return file.getAbsolutePath().contains(String.format("%sorg%sgradle%s", File.separator, File.separator, File.separator)); + } + + @NotNull + private static String path(File file) { + String path = file.getAbsolutePath(); + return path.replace("\\", "\\\\"); + } + + public static List getUrls(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException { + System.out.println(classLoader.getClass().getName()); + if (classLoader instanceof URLClassLoader) { + return (Arrays.asList(((URLClassLoader) classLoader).getURLs())).stream().map(URL -> toFile(URL)).collect(Collectors.toList()); + } + + // jdk9 + if (classLoader.getClass().getName().startsWith("jdk.internal.loader.ClassLoaders$")) { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + Unsafe unsafe = (Unsafe) field.get(null); + + // jdk.internal.loader.ClassLoaders.AppClassLoader.ucp + Field ucpField = classLoader.getClass().getSuperclass().getDeclaredField("ucp"); + long ucpFieldOffset = unsafe.objectFieldOffset(ucpField); + Object ucpObject = unsafe.getObject(classLoader, ucpFieldOffset); + + // jdk.internal.loader.URLClassPath.path + Field pathField = ucpField.getType().getDeclaredField("path"); + long pathFieldOffset = unsafe.objectFieldOffset(pathField); + List path = (ArrayList) unsafe.getObject(ucpObject, pathFieldOffset); + + Field mapField = ucpField.getType().getDeclaredField("lmap"); + long mapFieldOffset = unsafe.objectFieldOffset(mapField); + Map map = (Map) unsafe.getObject(ucpObject, mapFieldOffset); + List all = new ArrayList<>(); + all.addAll(path.stream().map(URL -> toFile(URL)).collect(Collectors.toList())); + all.addAll(map.keySet().stream().map(url -> toFile(asUrl(url))).collect(Collectors.toSet())); + return all; + } + + return null; + } + + @NotNull + private static URL asUrl(String url) { + try { + return new URL(url); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + @NotNull + private static File toFile(URL url) { + try { + return new File(url.toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + + @NotNull + private static List buildDirs(File sampleGradleDir) { + List buildDirs = new ArrayList<>(); + buildDirs.add(FileUtils.getFile(sampleGradleDir, "build", "classes", "java", "main")); + buildDirs.add(FileUtils.getFile(sampleGradleDir, "build", "classes", "groovy", "main")); + buildDirs.add(FileUtils.getFile(sampleGradleDir, "build", "classes", "scala", "main")); + buildDirs.add(FileUtils.getFile(sampleGradleDir, "build", "classes", "kotlin", "main")); + + return buildDirs; + } + +} \ No newline at end of file diff --git a/typescript-generator-gradle-plugin/src/test/resources/build.gradle.template b/typescript-generator-gradle-plugin/src/test/resources/build.gradle.template new file mode 100644 index 000000000..6ad97c0a6 --- /dev/null +++ b/typescript-generator-gradle-plugin/src/test/resources/build.gradle.template @@ -0,0 +1,62 @@ + +buildscript { + dependencies { + classpath 'com.fasterxml.jackson.module:jackson-module-scala_2.13:2.14.2' + } +} + +plugins { + id 'java' + id "org.jetbrains.kotlin.jvm" version "1.9.0" + id 'scala' + id 'groovy' + id 'cz.habarta.typescript-generator' + +} + +version = '3.0' +sourceCompatibility = 11 +targetCompatibility = 11 + +repositories { + mavenCentral() +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + implementation 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.14.2' + implementation 'org.codehaus.groovy:groovy-all:3.0.16' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.10' + implementation 'org.scala-lang:scala-library:2.13.10' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2' +} + +generateTypeScript { + classes = [ + 'cz.habarta.typescript.generator.sample.Person', + 'cz.habarta.typescript.generator.sample.PersonGroovy', + 'cz.habarta.typescript.generator.sample.PersonKt', + 'cz.habarta.typescript.generator.sample.PersonScala', + ] + jsonLibrary = 'jackson2' + outputKind = 'module' + excludeClasses = [ + 'groovy.lang.GroovyObject', + 'groovy.lang.MetaClass', + 'java.io.Serializable', + 'scala.Equals', + 'scala.Product', + 'scala.Serializable', + ] + jackson2Modules = [ + 'com.fasterxml.jackson.module.scala.DefaultScalaModule', + 'com.fasterxml.jackson.module.kotlin.KotlinModule', + ] +} + +build.dependsOn generateTypeScript