diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c32d137 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Nebula Build +permissions: + contents: read + actions: write +on: + push: + branches: + - 'main' + pull_request: + +jobs: + buildmultijdk: + runs-on: ubuntu-latest + strategy: + matrix: + # test against latest update of some major Java version(s), as well as specific LTS version(s) + java: [17, 21, 25] + name: Gradle Build without Publish + steps: + - uses: actions/checkout@v4 + - name: Setup git user + run: | + git config --global user.name "Nebula Plugin Maintainers" + git config --global user.email "nebula-plugins-oss@netflix.com" + - name: Set up JDKs + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: | + 17 + 21 + ${{ matrix.java }} + java-package: jdk + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-overwrite-existing: true + - name: Gradle build + run: ./gradlew --stacktrace build + env: + JDK_VERSION_FOR_TESTS: ${{ matrix.java }} \ No newline at end of file diff --git a/.github/workflows/nebula.yml b/.github/workflows/nebula.yml deleted file mode 100644 index 889d210..0000000 --- a/.github/workflows/nebula.yml +++ /dev/null @@ -1,149 +0,0 @@ -name: Nebula Build -on: - push: - branches: - - '*' - tags: - - v*.*.* - - v*.*.*-rc.* - pull_request: - -jobs: - validation: - name: "Gradle Wrapper Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v1 - buildmultijdk: - if: (!startsWith(github.ref, 'refs/tags/v')) - needs: validation - runs-on: ubuntu-latest - strategy: - matrix: - # test against latest update of some major Java version(s), as well as specific LTS version(s) - java: [17, 21] - name: Gradle Build without Publish - steps: - - uses: actions/checkout@v4 - - name: Setup git user - run: | - git config --global user.name "Nebula Plugin Maintainers" - git config --global user.email "nebula-plugins-oss@netflix.com" - - name: Set up JDKs - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: | - 17 - ${{ matrix.java }} - java-package: jdk - - uses: actions/cache@v4 - id: gradle-cache - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} - restore-keys: | - - ${{ runner.os }}-gradle- - - uses: actions/cache@v4 - id: gradle-wrapper-cache - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} - restore-keys: | - - ${{ runner.os }}-gradlewrapper- - - name: Gradle build - run: ./gradlew --info --stacktrace build - env: - JDK_VERSION_FOR_TESTS: ${{ matrix.java }} - validatepluginpublication: - if: startsWith(github.ref, 'refs/tags/v') - needs: validation - runs-on: ubuntu-latest - name: Gradle Plugin Publication Validation - env: - NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} - NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} - steps: - - uses: actions/checkout@v4 - - name: Setup git user - run: | - git config --global user.name "Nebula Plugin Maintainers" - git config --global user.email "nebula-plugins-oss@netflix.com" - - name: Set up JDKs - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: | - 17 - 21 - java-package: jdk - - uses: actions/cache@v4 - id: gradle-cache - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} - restore-keys: | - - ${{ runner.os }}-gradle- - - uses: actions/cache@v4 - id: gradle-wrapper-cache - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} - restore-keys: | - - ${{ runner.os }}-gradlewrapper- - - name: Verify plugin publication - if: | - startsWith(github.ref, 'refs/tags/v') && - (!contains(github.ref, '-rc.')) - run: ./gradlew --stacktrace -Dgradle.publish.key=${{ secrets.gradlePublishKey }} -Dgradle.publish.secret=${{ secrets.gradlePublishSecret }} -Prelease.useLastTag=true final publishPlugin --validate-only -x check -x signPluginMavenPublication - publish: - if: startsWith(github.ref, 'refs/tags/v') - needs: validatepluginpublication - runs-on: ubuntu-latest - name: Gradle Build and Publish - env: - NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} - NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} - NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} - NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} - NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} - NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} - steps: - - uses: actions/checkout@v4 - - name: Setup git user - run: | - git config --global user.name "Nebula Plugin Maintainers" - git config --global user.email "nebula-plugins-oss@netflix.com" - - name: Set up JDKs - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: | - 17 - 21 - java-package: jdk - - uses: actions/cache@v4 - id: gradle-cache - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} - restore-keys: | - - ${{ runner.os }}-gradle- - - uses: actions/cache@v4 - id: gradle-wrapper-cache - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} - restore-keys: | - - ${{ runner.os }}-gradlewrapper- - - name: Publish candidate - if: | - startsWith(github.ref, 'refs/tags/v') && - contains(github.ref, '-rc.') - run: ./gradlew --info --stacktrace -Prelease.useLastTag=true candidate - - name: Publish release - if: | - startsWith(github.ref, 'refs/tags/v') && - (!contains(github.ref, '-rc.')) - run: ./gradlew --info --stacktrace -Dgradle.publish.key=${{ secrets.gradlePublishKey }} -Dgradle.publish.secret=${{ secrets.gradlePublishSecret }} -Prelease.useLastTag=true final diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c561add --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release +on: + push: + tags: + - v*.*.* + - v*.*.*-rc.* + +jobs: + publish: + runs-on: ubuntu-latest + name: Gradle Build and Publish + environment: + name: Publish + url: "https://repo1.maven.org/maven2/com/netflix/nebula/nebula-hollow-plugin/" + env: + NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} + NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} + NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} + NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} + NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} + NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} + GRADLE_PUBLISH_KEY: ${{ secrets.ORG_GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.ORG_GRADLE_PUBLISH_SECRET }} + steps: + - uses: actions/checkout@v4 + - name: Setup git user + run: | + git config --global user.name "Nebula Plugin Maintainers" + git config --global user.email "nebula-plugins-oss@netflix.com" + - name: Set up JDKs + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: | + 17 + 21 + java-package: jdk + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-overwrite-existing: true + - name: Verify plugin publication + if: (!contains(github.ref, '-rc.')) + run: ./gradlew --stacktrace -Prelease.useLastTag=true final publishPlugin --validate-only -x check + - name: Publish candidate + if: contains(github.ref, '-rc.') + run: ./gradlew --info --stacktrace -Prelease.useLastTag=true candidate + - name: Publish release + if: (!contains(github.ref, '-rc.')) + run: ./gradlew --stacktrace -Prelease.useLastTag=true final \ No newline at end of file diff --git a/src/main/java/com/netflix/nebula/hollow/ApiGeneratorExtension.java b/src/main/java/com/netflix/nebula/hollow/ApiGeneratorExtension.java index b37cf7c..1e7d2b5 100644 --- a/src/main/java/com/netflix/nebula/hollow/ApiGeneratorExtension.java +++ b/src/main/java/com/netflix/nebula/hollow/ApiGeneratorExtension.java @@ -15,24 +15,177 @@ */ package com.netflix.nebula.hollow; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; + +import javax.inject.Inject; import java.util.List; public class ApiGeneratorExtension { - public List packagesToScan; - public String apiClassName; - public String apiPackageName; - public String getterPrefix; - public String classPostfix; - public String destinationPath = ""; - public boolean parameterizeAllClassNames = false; - public boolean useAggressiveSubstitutions = false; - public boolean useErgonomicShortcuts = true; - public boolean usePackageGrouping = true; - public boolean useBooleanFieldErgonomics = true; - public boolean reservePrimaryKeyIndexForTypeWithPrimaryKey = true; - public boolean useHollowPrimitiveTypes = true; - public boolean restrictApiToFieldType = true; - public boolean useVerboseToString = true; - public boolean useGeneratedAnnotation = false; + private final ListProperty packagesToScan; + private final Property apiClassName; + private final Property apiPackageName; + private final Property getterPrefix; + private final Property classPostfix; + private final Property destinationPath; + private final Property parameterizeAllClassNames; + private final Property useAggressiveSubstitutions; + private final Property useErgonomicShortcuts; + private final Property usePackageGrouping; + private final Property useBooleanFieldErgonomics; + private final Property reservePrimaryKeyIndexForTypeWithPrimaryKey; + private final Property useHollowPrimitiveTypes; + private final Property restrictApiToFieldType; + private final Property useVerboseToString; + private final Property useGeneratedAnnotation; + + @Inject + public ApiGeneratorExtension(ObjectFactory objects) { + this.packagesToScan = objects.listProperty(String.class).empty(); + this.apiClassName = objects.property(String.class); + this.apiPackageName = objects.property(String.class); + this.getterPrefix = objects.property(String.class); + this.classPostfix = objects.property(String.class); + this.destinationPath = objects.property(String.class).convention(""); + this.parameterizeAllClassNames = objects.property(Boolean.class).convention(false); + this.useAggressiveSubstitutions = objects.property(Boolean.class).convention(false); + this.useErgonomicShortcuts = objects.property(Boolean.class).convention(true); + this.usePackageGrouping = objects.property(Boolean.class).convention(true); + this.useBooleanFieldErgonomics = objects.property(Boolean.class).convention(true); + this.reservePrimaryKeyIndexForTypeWithPrimaryKey = objects.property(Boolean.class).convention(true); + this.useHollowPrimitiveTypes = objects.property(Boolean.class).convention(true); + this.restrictApiToFieldType = objects.property(Boolean.class).convention(true); + this.useVerboseToString = objects.property(Boolean.class).convention(true); + this.useGeneratedAnnotation = objects.property(Boolean.class).convention(false); + } + + public ListProperty getPackagesToScan() { + return packagesToScan; + } + + public void setPackagesToScan(List value) { + packagesToScan.set(value); + } + + public Property getApiClassName() { + return apiClassName; + } + + public void setApiClassName(String value) { + apiClassName.set(value); + } + + public Property getApiPackageName() { + return apiPackageName; + } + + public void setApiPackageName(String value) { + apiPackageName.set(value); + } + + public Property getGetterPrefix() { + return getterPrefix; + } + + public void setGetterPrefix(String value) { + getterPrefix.set(value); + } + + public Property getClassPostfix() { + return classPostfix; + } + + public void setClassPostfix(String value) { + classPostfix.set(value); + } + + public Property getDestinationPath() { + return destinationPath; + } + + public void setDestinationPath(String value) { + destinationPath.set(value); + } + + public Property getParameterizeAllClassNames() { + return parameterizeAllClassNames; + } + + public void setParameterizeAllClassNames(boolean value) { + parameterizeAllClassNames.set(value); + } + + public Property getUseAggressiveSubstitutions() { + return useAggressiveSubstitutions; + } + + public void setUseAggressiveSubstitutions(boolean value) { + useAggressiveSubstitutions.set(value); + } + + public Property getUseErgonomicShortcuts() { + return useErgonomicShortcuts; + } + + public void setUseErgonomicShortcuts(boolean value) { + useErgonomicShortcuts.set(value); + } + + public Property getUsePackageGrouping() { + return usePackageGrouping; + } + + public void setUsePackageGrouping(boolean value) { + usePackageGrouping.set(value); + } + + public Property getUseBooleanFieldErgonomics() { + return useBooleanFieldErgonomics; + } + + public void setUseBooleanFieldErgonomics(boolean value) { + useBooleanFieldErgonomics.set(value); + } + + public Property getReservePrimaryKeyIndexForTypeWithPrimaryKey() { + return reservePrimaryKeyIndexForTypeWithPrimaryKey; + } + + public void setReservePrimaryKeyIndexForTypeWithPrimaryKey(boolean value) { + reservePrimaryKeyIndexForTypeWithPrimaryKey.set(value); + } + + public Property getUseHollowPrimitiveTypes() { + return useHollowPrimitiveTypes; + } + + public void setUseHollowPrimitiveTypes(boolean value) { + useHollowPrimitiveTypes.set(value); + } + + public Property getRestrictApiToFieldType() { + return restrictApiToFieldType; + } + + public void setRestrictApiToFieldType(boolean value) { + restrictApiToFieldType.set(value); + } + + public Property getUseVerboseToString() { + return useVerboseToString; + } + + public void setUseVerboseToString(boolean value) { + useVerboseToString.set(value); + } + + public Property getUseGeneratedAnnotation() { + return useGeneratedAnnotation; + } + + public void setUseGeneratedAnnotation(boolean value) { + useGeneratedAnnotation.set(value); + } } diff --git a/src/main/java/com/netflix/nebula/hollow/ApiGeneratorPlugin.java b/src/main/java/com/netflix/nebula/hollow/ApiGeneratorPlugin.java index d7675b2..2b3b04b 100644 --- a/src/main/java/com/netflix/nebula/hollow/ApiGeneratorPlugin.java +++ b/src/main/java/com/netflix/nebula/hollow/ApiGeneratorPlugin.java @@ -18,19 +18,18 @@ import com.netflix.hollow.core.write.objectmapper.HollowObjectMapper; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.Task; +import org.gradle.api.file.Directory; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.PluginContainer; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Delete; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.compile.JavaCompile; import java.io.File; import java.net.URLClassLoader; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.stream.Collectors; public class ApiGeneratorPlugin implements Plugin { @@ -44,53 +43,119 @@ public void apply(Project project) { PluginContainer plugins = project.getPlugins(); if (plugins.hasPlugin(JavaPlugin.class)) { - Map taskPropertiesMap = new HashMap<>(); - - taskPropertiesMap.put("name", "generateHollowConsumerApi"); - taskPropertiesMap.put("group", "hollow"); - taskPropertiesMap.put("type", ApiGeneratorTask.class); - - Task generateTask = project.getTasks().create(taskPropertiesMap); ApiGeneratorExtension extension = project.getExtensions().create("hollow", ApiGeneratorExtension.class); JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); SourceSet mainSourceSet = javaPluginExtension.getSourceSets().getByName("main"); - project.getTasks().register("compileDataModel", JavaCompile.class, javaCompile -> { - List packages = extension.packagesToScan - .stream() - .map(pkg -> pkg.replace(".", "/") + "/**") - .collect(Collectors.toList()); - + TaskProvider compileDataModelTask = project.getTasks().register("compileDataModel", JavaCompile.class, javaCompile -> { File destinationDir = mainSourceSet.getOutput().getClassesDirs() .filter(file -> file.toString().contains("java")) .getSingleFile(); - javaCompile.setSource(mainSourceSet.getJava().getSrcDirs()); - javaCompile.include(packages); + javaCompile.source(mainSourceSet.getJava().getSourceDirectories()); + // Use doFirst to lazily configure includes from the extension + javaCompile.doFirst(task -> { + JavaCompile compileTask = (JavaCompile) task; + compileTask.include( + extension.getPackagesToScan().get().stream() + .map(pkg -> pkg.replace(".", "/") + "/**") + .toArray(String[]::new) + ); + }); javaCompile.setClasspath(mainSourceSet.getCompileClasspath()); javaCompile.getDestinationDirectory().set(destinationDir); }); - project.getTasks().register("cleanDataModelApi", Delete.class, deleteTask -> { - if (!extension.destinationPath.isEmpty()) { - deleteTask.delete(extension.destinationPath); - return; - } - - String dataModelApiPath = extension.apiPackageName.replace(".", "/"); - - List paths = mainSourceSet.getJava().getSrcDirs() - .stream() - .map(srcDir -> srcDir.toPath().resolve(dataModelApiPath).toString()) - .collect(Collectors.toList()); + TaskProvider generateTask = project.getTasks().register("generateHollowConsumerApi", ApiGeneratorTask.class, task -> { + task.setGroup("hollow"); + task.setDescription("Generates Hollow consumer API from data model classes"); + + // Wire all extension properties to task properties + task.getPackagesToScan().set(extension.getPackagesToScan()); + task.getApiClassName().set(extension.getApiClassName()); + task.getApiPackageName().set(extension.getApiPackageName()); + task.getGetterPrefix().set(extension.getGetterPrefix()); + task.getClassPostfix().set(extension.getClassPostfix()); + task.getDestinationPath().set(extension.getDestinationPath()); + task.getParameterizeAllClassNames().set(extension.getParameterizeAllClassNames()); + task.getUseAggressiveSubstitutions().set(extension.getUseAggressiveSubstitutions()); + task.getUseErgonomicShortcuts().set(extension.getUseErgonomicShortcuts()); + task.getUsePackageGrouping().set(extension.getUsePackageGrouping()); + task.getUseBooleanFieldErgonomics().set(extension.getUseBooleanFieldErgonomics()); + task.getReservePrimaryKeyIndexForTypeWithPrimaryKey().set(extension.getReservePrimaryKeyIndexForTypeWithPrimaryKey()); + task.getUseHollowPrimitiveTypes().set(extension.getUseHollowPrimitiveTypes()); + task.getRestrictApiToFieldType().set(extension.getRestrictApiToFieldType()); + task.getUseVerboseToString().set(extension.getUseVerboseToString()); + task.getUseGeneratedAnnotation().set(extension.getUseGeneratedAnnotation()); + + // Set source directory to main java source directory + task.getSourceDirectory().set( + project.provider(() -> { + File mainJava = mainSourceSet.getJava().getSrcDirs().stream() + .findFirst() + .orElseThrow(() -> new IllegalStateException("No Java source directory found")); + return project.getLayout().getProjectDirectory().dir(mainJava.getAbsolutePath()); + }) + ); + + // Set classpath to compiled data model classes + task.getClasspath().from(compileDataModelTask.flatMap(javaCompile -> javaCompile.getDestinationDirectory())); + + // Set output directory based on destination path or default to source directory with API package path + task.getOutputDirectory().set( + project.provider(() -> { + String destPath = extension.getDestinationPath().getOrElse(""); + if (!destPath.isEmpty()) { + File destFile = new File(destPath); + if (destFile.isAbsolute()) { + return project.getLayout().getProjectDirectory().dir(destPath); + } + return project.getLayout().getProjectDirectory().dir(project.file(destPath).getAbsolutePath()); + } + // Use a fallback directory if apiPackageName is not set (task will fail with proper validation message during execution) + String apiPkg = extension.getApiPackageName().getOrElse("hollow-api-fallback"); + File mainJava = mainSourceSet.getJava().getSrcDirs().stream().findFirst().orElseThrow(); + File outputDir = new File(mainJava, apiPkg.replace(".", "/")); + return project.getLayout().getProjectDirectory().dir(outputDir.getAbsolutePath()); + }) + ); + + task.dependsOn(compileDataModelTask); + }); - deleteTask.delete(paths); + TaskProvider cleanDataModelApiTask = project.getTasks().register("cleanDataModelApi", Delete.class, deleteTask -> { + deleteTask.delete( + extension.getDestinationPath().map(destPath -> { + if (!destPath.isEmpty()) { + return project.files(destPath); + } + return project.files( + extension.getApiPackageName().map(apiPkg -> { + String dataModelApiPath = apiPkg.replace(".", "/"); + return mainSourceSet.getJava().getSrcDirs() + .stream() + .map(srcDir -> srcDir.toPath().resolve(dataModelApiPath).toString()) + .collect(Collectors.toList()); + }).getOrElse(java.util.Collections.emptyList()) + ); + }).orElse( + project.files( + extension.getApiPackageName().map(apiPkg -> { + String dataModelApiPath = apiPkg.replace(".", "/"); + return mainSourceSet.getJava().getSrcDirs() + .stream() + .map(srcDir -> srcDir.toPath().resolve(dataModelApiPath).toString()) + .collect(Collectors.toList()); + }).getOrElse(java.util.Collections.emptyList()) + ) + ) + ); }); - generateTask.dependsOn("compileDataModel"); - project.getTasks().getByName("compileJava").dependsOn(generateTask); - project.getTasks().getByName("clean").dependsOn("cleanDataModelApi"); + // Wire task dependencies using configuration avoidance + project.getTasks().named("compileJava").configure(task -> task.dependsOn(generateTask)); + project.getTasks().named("clean").configure(task -> task.dependsOn(cleanDataModelApiTask)); } } } diff --git a/src/main/java/com/netflix/nebula/hollow/ApiGeneratorTask.java b/src/main/java/com/netflix/nebula/hollow/ApiGeneratorTask.java index c9e06ef..ea8f36c 100644 --- a/src/main/java/com/netflix/nebula/hollow/ApiGeneratorTask.java +++ b/src/main/java/com/netflix/nebula/hollow/ApiGeneratorTask.java @@ -20,9 +20,15 @@ import com.netflix.hollow.core.write.objectmapper.HollowObjectMapper; import org.gradle.api.DefaultTask; import org.gradle.api.InvalidUserDataException; -import org.gradle.api.tasks.CacheableTask; -import org.gradle.api.tasks.TaskAction; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.*; +import org.gradle.api.tasks.Optional; +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; @@ -33,75 +39,217 @@ @CacheableTask public class ApiGeneratorTask extends DefaultTask { - private final File projectDirFile = getProject().getProjectDir(); - private final String projectDirPath = projectDirFile.getAbsolutePath(); - private final String relativeJavaSourcesPath = "/src/main/java/"; - private final String javaSourcesPath = projectDirPath + relativeJavaSourcesPath; - //TODO: perhaps allow the users to configure these paths? - private final String[] compiledClassesPaths = { - projectDirPath + "/build/classes/main/", - projectDirPath + "/build/classes/java/main/", - }; + private final ListProperty packagesToScan; + private final Property apiClassName; + private final Property apiPackageName; + private final Property getterPrefix; + private final Property classPostfix; + private final Property destinationPath; + private final Property parameterizeAllClassNames; + private final Property useAggressiveSubstitutions; + private final Property useErgonomicShortcuts; + private final Property usePackageGrouping; + private final Property useBooleanFieldErgonomics; + private final Property reservePrimaryKeyIndexForTypeWithPrimaryKey; + private final Property useHollowPrimitiveTypes; + private final Property restrictApiToFieldType; + private final Property useVerboseToString; + private final Property useGeneratedAnnotation; + private final DirectoryProperty sourceDirectory; + private final ConfigurableFileCollection classpath; + private final DirectoryProperty outputDirectory; private URLClassLoader urlClassLoader; + @Inject + public ApiGeneratorTask(ObjectFactory objects) { + this.packagesToScan = objects.listProperty(String.class); + this.apiClassName = objects.property(String.class); + this.apiPackageName = objects.property(String.class); + this.getterPrefix = objects.property(String.class); + this.classPostfix = objects.property(String.class); + this.destinationPath = objects.property(String.class); + this.parameterizeAllClassNames = objects.property(Boolean.class); + this.useAggressiveSubstitutions = objects.property(Boolean.class); + this.useErgonomicShortcuts = objects.property(Boolean.class); + this.usePackageGrouping = objects.property(Boolean.class); + this.useBooleanFieldErgonomics = objects.property(Boolean.class); + this.reservePrimaryKeyIndexForTypeWithPrimaryKey = objects.property(Boolean.class); + this.useHollowPrimitiveTypes = objects.property(Boolean.class); + this.restrictApiToFieldType = objects.property(Boolean.class); + this.useVerboseToString = objects.property(Boolean.class); + this.useGeneratedAnnotation = objects.property(Boolean.class); + this.sourceDirectory = objects.directoryProperty(); + this.classpath = objects.fileCollection(); + this.outputDirectory = objects.directoryProperty(); + } + + @Optional + @Input + public ListProperty getPackagesToScan() { + return packagesToScan; + } + + @Optional + @Input + public Property getApiClassName() { + return apiClassName; + } + + @Optional + @Input + public Property getApiPackageName() { + return apiPackageName; + } + + @Optional + @Input + public Property getGetterPrefix() { + return getterPrefix; + } + + @Optional + @Input + public Property getClassPostfix() { + return classPostfix; + } + + @Optional + @Input + public Property getDestinationPath() { + return destinationPath; + } + + @Input + public Property getParameterizeAllClassNames() { + return parameterizeAllClassNames; + } + + @Input + public Property getUseAggressiveSubstitutions() { + return useAggressiveSubstitutions; + } + + @Input + public Property getUseErgonomicShortcuts() { + return useErgonomicShortcuts; + } + + @Input + public Property getUsePackageGrouping() { + return usePackageGrouping; + } + + @Input + public Property getUseBooleanFieldErgonomics() { + return useBooleanFieldErgonomics; + } + + @Input + public Property getReservePrimaryKeyIndexForTypeWithPrimaryKey() { + return reservePrimaryKeyIndexForTypeWithPrimaryKey; + } + + @Input + public Property getUseHollowPrimitiveTypes() { + return useHollowPrimitiveTypes; + } + + @Input + public Property getRestrictApiToFieldType() { + return restrictApiToFieldType; + } + + @Input + public Property getUseVerboseToString() { + return useVerboseToString; + } + + @Input + public Property getUseGeneratedAnnotation() { + return useGeneratedAnnotation; + } + + @Optional + @InputDirectory + @PathSensitive(PathSensitivity.RELATIVE) + public DirectoryProperty getSourceDirectory() { + return sourceDirectory; + } + + @Classpath + public ConfigurableFileCollection getClasspath() { + return classpath; + } + + @OutputDirectory + public DirectoryProperty getOutputDirectory() { + return outputDirectory; + } + @TaskAction public void generateApi() throws IOException { - ApiGeneratorExtension extension = getProject().getExtensions().getByType(ApiGeneratorExtension.class); - validatePluginConfiguration(extension); + // Validate required configuration + if (!apiClassName.isPresent() || !apiPackageName.isPresent() || !packagesToScan.isPresent() || packagesToScan.get().isEmpty()) { + throw new InvalidUserDataException( + "Specify buildscript as per plugin readme | apiClassName, apiPackageName and packagesToScan configuration values must be present" + ); + } initClassLoader(); HollowWriteStateEngine writeEngine = new HollowWriteStateEngine(); HollowObjectMapper mapper = new HollowObjectMapper(writeEngine); - Collection> datamodelClasses = extractClasses(extension.packagesToScan); + Collection> datamodelClasses = extractClasses(packagesToScan.get()); for (Class clazz : datamodelClasses) { getLogger().debug("Initialize schema for class {}", clazz.getName()); mapper.initializeTypeState(clazz); } - String apiTargetPath = !extension.destinationPath.isEmpty() ? extension.destinationPath : buildPathToApiTargetFolder(extension.apiPackageName); + String apiTargetPath = destinationPath.isPresent() && !destinationPath.get().isEmpty() + ? destinationPath.get() + : buildPathToApiTargetFolder(apiPackageName.get()); + + HollowAPIGenerator generator = buildHollowAPIGenerator(writeEngine, apiTargetPath); - HollowAPIGenerator generator = buildHollowAPIGenerator(extension, writeEngine, apiTargetPath); - cleanupAndCreateFolders(apiTargetPath); generator.generateSourceFiles(); } - private HollowAPIGenerator buildHollowAPIGenerator(ApiGeneratorExtension extension, HollowWriteStateEngine writeStateEngine, String apiTargetPath) { + private HollowAPIGenerator buildHollowAPIGenerator(HollowWriteStateEngine writeStateEngine, String apiTargetPath) { HollowAPIGenerator.Builder builder = new HollowAPIGenerator.Builder() - .withAPIClassname(extension.apiClassName) - .withPackageName(extension.apiPackageName) + .withAPIClassname(apiClassName.get()) + .withPackageName(apiPackageName.get()) .withDataModel(writeStateEngine) .withDestination(apiTargetPath) - .withParameterizeAllClassNames(extension.parameterizeAllClassNames) - .withAggressiveSubstitutions(extension.useAggressiveSubstitutions) - .withBooleanFieldErgonomics(extension.useBooleanFieldErgonomics) - .reservePrimaryKeyIndexForTypeWithPrimaryKey(extension.reservePrimaryKeyIndexForTypeWithPrimaryKey) - .withHollowPrimitiveTypes(extension.useHollowPrimitiveTypes) - .withVerboseToString(extension.useVerboseToString); - if (extension.useGeneratedAnnotation) { + .withParameterizeAllClassNames(parameterizeAllClassNames.get()) + .withAggressiveSubstitutions(useAggressiveSubstitutions.get()) + .withBooleanFieldErgonomics(useBooleanFieldErgonomics.get()) + .reservePrimaryKeyIndexForTypeWithPrimaryKey(reservePrimaryKeyIndexForTypeWithPrimaryKey.get()) + .withHollowPrimitiveTypes(useHollowPrimitiveTypes.get()) + .withVerboseToString(useVerboseToString.get()); + if (useGeneratedAnnotation.get()) { builder.withGeneratedAnnotation(); } - if(extension.getterPrefix != null && !extension.getterPrefix.isEmpty()) { - builder.withGetterPrefix(extension.getterPrefix); + if(getterPrefix.isPresent() && !getterPrefix.get().isEmpty()) { + builder.withGetterPrefix(getterPrefix.get()); } - if(extension.classPostfix != null && !extension.classPostfix.isEmpty()) { - builder.withClassPostfix(extension.classPostfix); + if(classPostfix.isPresent() && !classPostfix.get().isEmpty()) { + builder.withClassPostfix(classPostfix.get()); } - if(extension.useErgonomicShortcuts) { + if(useErgonomicShortcuts.get()) { builder.withErgonomicShortcuts(); } - if(extension.usePackageGrouping) { + if(usePackageGrouping.get()) { builder.withPackageGrouping(); } - if(extension.restrictApiToFieldType) { + if(restrictApiToFieldType.get()) { builder.withRestrictApiToFieldType(); } @@ -110,6 +258,8 @@ private HollowAPIGenerator buildHollowAPIGenerator(ApiGeneratorExtension extensi private Collection> extractClasses(List packagesToScan) { Set> classes = new HashSet<>(); + File sourceDirFile = sourceDirectory.get().getAsFile(); + String sourceDirPath = sourceDirFile.getAbsolutePath(); for (String packageToScan : packagesToScan) { File packageFile = buildPackageFile(packageToScan); @@ -123,7 +273,7 @@ private Collection> extractClasses(List packagesToScan) { !filePath.endsWith("package-info.java") && !filePath.endsWith("module-info.java") ) { - String relativeFilePath = removeSubstrings(filePath, projectDirPath, relativeJavaSourcesPath); + String relativeFilePath = filePath.substring(sourceDirPath.length() + 1); classNames.add(convertFolderPathToPackageName(removeSubstrings(relativeFilePath, ".java"))); } } @@ -155,11 +305,11 @@ private List findFilesRecursively(File packageFile) { } private File buildPackageFile(String packageName) { - return new File(javaSourcesPath + convertPackageNameToFolderPath(packageName)); + return new File(sourceDirectory.get().getAsFile(), convertPackageNameToFolderPath(packageName)); } private String buildPathToApiTargetFolder(String apiPackageName) { - return javaSourcesPath + convertPackageNameToFolderPath(apiPackageName); + return new File(sourceDirectory.get().getAsFile(), convertPackageNameToFolderPath(apiPackageName)).getAbsolutePath(); } private String convertPackageNameToFolderPath(String packageName) { @@ -186,16 +336,10 @@ private void cleanupAndCreateFolders(String generatedApiTarget) { } private void initClassLoader() throws MalformedURLException { - URL[] urls = new URL[compiledClassesPaths.length]; - for (int i=0; i < compiledClassesPaths.length; i++){ - urls[i]= new File(compiledClassesPaths[i]).toURI().toURL() ; - } - urlClassLoader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader()); - } - - private void validatePluginConfiguration(ApiGeneratorExtension extension) { - if (extension.apiClassName == null || extension.apiPackageName == null || extension.packagesToScan.isEmpty()) { - throw new InvalidUserDataException("Specify buildscript as per plugin readme | apiClassName, apiPackageName and packagesToScan configuration values must be present"); + List urls = new ArrayList<>(); + for (File file : classpath.getFiles()) { + urls.add(file.toURI().toURL()); } + urlClassLoader = new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader()); } } diff --git a/src/test/groovy/com/netflix/nebula/hollow/ApiGeneratorIntegrationSpec.groovy b/src/test/groovy/com/netflix/nebula/hollow/ApiGeneratorIntegrationSpec.groovy index c066b6b..e982639 100644 --- a/src/test/groovy/com/netflix/nebula/hollow/ApiGeneratorIntegrationSpec.groovy +++ b/src/test/groovy/com/netflix/nebula/hollow/ApiGeneratorIntegrationSpec.groovy @@ -407,20 +407,27 @@ public class Movie { id 'java' id 'com.netflix.nebula.hollow' } - + hollow { packagesToScan = ['com.netflix.nebula.hollow.test'] } - + repositories { mavenCentral() } - + dependencies { implementation "com.netflix.hollow:hollow:3.+" } """.stripIndent() + // Create source directory and a dummy class to allow compileDataModel to succeed + def dummyFile = createFile('src/main/java/com/netflix/nebula/hollow/test/Dummy.java') + dummyFile << """package com.netflix.nebula.hollow.test; + +public class Dummy { +} + """.stripIndent() when: def result = runTasksAndFail('generateHollowConsumerApi')