diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..f200f5f9ba8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: davidmc24 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..655407d8bcc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,51 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Prerequisites** + +* [ ] Are you running the latest version of the plugin? (Check [releases](https://github.com/davidmc24/gradle-avro-plugin/releases)) +* [ ] Are you running a supported version of Gradle? (Check the [README](https://github.com/davidmc24/gradle-avro-plugin/blob/master/README.md)) +* [ ] Are you running a supported version of Apache Avro? (Check the [README](https://github.com/davidmc24/gradle-avro-plugin/blob/master/README.md)) +* [ ] Are you running a supported version of Java? (Check the [README](https://github.com/davidmc24/gradle-avro-plugin/blob/master/README.md)) +* [ ] Did you check to see if an [issue](https://github.com/davidmc24/gradle-avro-plugin/issues) has already been submitted? +* [ ] Are you reporting to the correct repository? If your schema doesn't work with the Apache Avro CLI tool either, it's not a problem with this plugin. Running your file through the `CLIComparisonTest` in the sample project under the `test-project` directory can help diagnose this. +* [ ] Did you perform a cursory search? + +For more information, see the [CONTRIBUTING](https://github.com/davidmc24/gradle-avro-plugin/blob/master/CONTRIBUTING.md) guide. + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior: + +1. Project set up like this... +2. Source files like this... +3. Ran this task... +4. See error + +Please provide complete input files that reproduce the problem, not fragments. +When possible, please express this using `test-project`. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** + - Gradle Version [e.g. 5.6.1] + - Apache Avro Version [e.g. 1.8.2] + - Gradle-Avro Plugin Version [e.g. 0.17.0] + - Java Version [e.g. 13.0.2] + - OS: [e.g. Mac OS X Mojave, Windows 10, Ubuntu 16.04] + +**Additional context** + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..ffca0ecaa45 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Feature requests and ideas + url: https://github.com/davidmc24/gradle-avro-plugin/discussions/categories/ideas + about: Suggest an idea for this project + - name: Questions + url: https://github.com/davidmc24/gradle-avro-plugin/discussions/categories/q-a + about: Please ask and answer questions here diff --git a/.github/workflows/avro-compatibility.yml b/.github/workflows/avro-compatibility.yml new file mode 100644 index 00000000000..9304f495872 --- /dev/null +++ b/.github/workflows/avro-compatibility.yml @@ -0,0 +1,20 @@ +name: Avro Compatibility Tests +on: [push, pull_request] +jobs: + test: + name: "Compatibility: avro ${{ matrix.avro }}/gradle ${{ matrix.gradle }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + avro: ["1.11.0", "1.11.1"] + gradle: ["5.1", "7.6"] + java: ["8"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + with: + arguments: testCompatibility -PavroVersion=${{ matrix.avro }} -PgradleVersion=${{ matrix.gradle }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..04029994afa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI Build +on: [push, pull_request] +jobs: + build: + name: "Build" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: 8 + - uses: gradle/gradle-build-action@v2 + with: + arguments: build +# - uses: codecov/codecov-action@v1 +# with: +# file: ./build/reports/jacoco/test/jacocoTestReport.xml +# fail_ci_if_error: true diff --git a/.github/workflows/gradle-compatibility.yml b/.github/workflows/gradle-compatibility.yml new file mode 100644 index 00000000000..a2a195ded7f --- /dev/null +++ b/.github/workflows/gradle-compatibility.yml @@ -0,0 +1,26 @@ +name: Gradle Compatibility Tests +on: [push, pull_request] +jobs: + test: + name: "Compatibility: gradle ${{ matrix.gradle }}/java ${{ matrix.java }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + avro: ["1.11.0"] + gradle: [ + "5.1", "5.1.1", "5.2", "5.2.1", "5.3", "5.3.1", "5.4", "5.4.1", "5.5", "5.5.1", "5.6", "5.6.1", "5.6.2", "5.6.3", "5.6.4", + "6.0", "6.0.1", "6.1", "6.1.1", "6.2", "6.2.1", "6.2.2", "6.3", "6.4", "6.4.1", "6.5", "6.5.1", "6.6", "6.6.1", "6.7", "6.7.1", + "6.8", "6.8.1", "6.8.2", "6.8.3", "6.9", "6.9.1", "6.9.2", "6.9.3", + "7.0", "7.0.1", "7.0.2", "7.1", "7.1.1", "7.2", "7.3", "7.3.1", "7.3.2", "7.3.3", "7.4", "7.4.1", "7.4.2", "7.5", "7.5.1", "7.6" + # See here for latest versions: https://services.gradle.org/versions/ + ] + java: ["8", "11"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + with: + arguments: testCompatibility -PavroVersion=${{ matrix.avro }} -PgradleVersion=${{ matrix.gradle }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 00000000000..a2b20bd7c02 --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,11 @@ +# See https://github.com/marketplace/actions/gradle-wrapper-validation +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/java-compatibility.yml b/.github/workflows/java-compatibility.yml new file mode 100644 index 00000000000..047e08e512d --- /dev/null +++ b/.github/workflows/java-compatibility.yml @@ -0,0 +1,92 @@ +# See https://docs.gradle.org/current/userguide/compatibility.html +name: Java Compatibility Tests +on: [push, pull_request] +jobs: + java8-12: + name: "Compatibility: java ${{ matrix.java }}/gradle ${{ matrix.gradle }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + avro: ["1.11.0"] + gradle: ["5.1", "7.6"] + java: ["8", "11"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + with: + arguments: testCompatibility -PavroVersion=${{ matrix.avro }} -PgradleVersion=${{ matrix.gradle }} + java17: + name: "Compatibility: java ${{ matrix.java }}/gradle ${{ matrix.gradle }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + avro: ["1.11.0"] + gradle: ["7.3", "7.6"] # See here for latest versions: https://services.gradle.org/versions/ + java: ["17"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + with: + arguments: testCompatibility -PavroVersion=${{ matrix.avro }} -PgradleVersion=${{ matrix.gradle }} + java18: + name: "Compatibility: java ${{ matrix.java }}/gradle ${{ matrix.gradle }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + avro: ["1.11.0"] + gradle: ["7.5", "7.6"] # See here for latest versions: https://services.gradle.org/versions/ + java: ["18"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + with: + arguments: testCompatibility -PavroVersion=${{ matrix.avro }} -PgradleVersion=${{ matrix.gradle }} + java-19: + name: "Compatibility: java ${{ matrix.java }}/gradle ${{ matrix.gradle }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + avro: ["1.11.0"] + gradle: ["7.6"] # See here for latest versions: https://services.gradle.org/versions/ + java: ["19"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + continue-on-error: true + with: + arguments: testCompatibility -PavroVersion=${{ matrix.avro }} -PgradleVersion=${{ matrix.gradle }} + java-ea: + name: "Compatibility: java ${{ matrix.java }}/gradle ${{ matrix.gradle }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + avro: ["1.11.0"] + gradle: ["7.6"] # See here for latest versions: https://services.gradle.org/versions/ + java: ["20-ea"] + fail-fast: false + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + continue-on-error: true + with: + arguments: testCompatibility -PavroVersion=${{ matrix.avro }} -PgradleVersion=${{ matrix.gradle }} diff --git a/.github/workflows/os-compatibility.yml b/.github/workflows/os-compatibility.yml new file mode 100644 index 00000000000..58f8e30dd76 --- /dev/null +++ b/.github/workflows/os-compatibility.yml @@ -0,0 +1,19 @@ +name: OS Compatibility +on: [push, pull_request] +jobs: + build: + name: "Compatibility: ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + java: [8] # Minimum supported major version + os: [ubuntu-latest, windows-latest, macOS-latest] # All supported OS + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ matrix.java }} + - uses: gradle/gradle-build-action@v2 + with: + arguments: test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000000..69e66b6638f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,20 @@ +name: Publish package to the Maven Central Repository +on: + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: 8 + - uses: gradle/gradle-build-action@v2 + with: + arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository -PsonatypeUsername=${{ secrets.SONATYPE_USERNAME }} -PsonatypePassword=${{ secrets.SONATYPE_PASSWORD }} + env: + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} diff --git a/lang/java/gradle-plugin/.editorconfig b/lang/java/gradle-plugin/.editorconfig new file mode 100644 index 00000000000..c285d17b2ae --- /dev/null +++ b/lang/java/gradle-plugin/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 + +[*.yml] +indent_size = 2 diff --git a/lang/java/gradle-plugin/CHANGES.md b/lang/java/gradle-plugin/CHANGES.md new file mode 100644 index 00000000000..ecb01369c1e --- /dev/null +++ b/lang/java/gradle-plugin/CHANGES.md @@ -0,0 +1,422 @@ +# Change Log + +## Unreleased + +## 1.7.1 +* Fix vulnerabilities in transitive dependencies (contribution from [BlacCello](https://github.com/BlacCello)); see https://github.com/davidmc24/gradle-avro-plugin/pull/229 + +## 1.7.0 +* Support for using conversions and type factories located outside of build classpath (contribution from [erdi](https://github.com/erdi)); see https://github.com/davidmc24/gradle-avro-plugin/pull/228 + +## 1.6.0 +* Add support for configuring classpath for `GenerateAvroJavaTask` (thanks to [crtlib](https://github.com/crtlib)); see https://github.com/davidmc24/gradle-avro-plugin/pull/222 +* Drop compatibility testing for old versions of Java (9, 10, 12, 13, 14, 15, 16) +* Built using Gradle 7.6 +* Updated compatibility testing through Gradle 7.6 +* Updated compatibility testing through Java 19 + +## 1.5.0 +* Added support for `additionalVelocityTool` thanks to a contribution from [dcracauer](https://github.com/dcracauer); see https://github.com/davidmc24/gradle-avro-plugin/pull/211 +* Built using Avro 1.11.1 +* Built using Gradle 7.5.1 +* Updated compatibility testing through Gradle 7.5.1 +* Updated compatibility testing through Java 18 + +## 1.4.0 +* Drop support for Kotlin plugin integration + +## 1.3.0 +* Built using Avro 1.11.0 +* Dropped support for Avro 1.9.0-1.10.2 due to use of new SpecificRecordBuilderBase constructor in Avro 1.11.0 +* Default field visibility is now "PRIVATE" to match Avro's new default, as "PUBLIC_DEPRECATED" is no longer supported in Avro 1.11.0 +* Built using Gradle 7.3 +* Updated compatibility testing through Gradle 7.3 +* Updated compatibility testing through Kotlin 1.5.31 +* Added compatibility with Java 17 +* `GenerateAvroProtocolTask` now has a debug log to output its classpath +* `GenerateAvroProtocolTask` will no longer delegate to the system classloader + +## 1.2.1 +* Built using Gradle 7.1.1 +* Updated compatibility testing through Gradle 7.1.1 +* When `sourcesJar` is used, declares dependency on `GenerateAvroJavaTask`s to avoid disabling execution optimizations introduced in Gradle 7.1. (see #167) + +## 1.2.0 +* Avro 1.9.0-1.9.2 is supported again (no changed needed; just change in support policy and testing) +* `generateAvroProtocol` task fails if avpr file will get overwritten (due to multiple IDL files using the same namespace and protocol) + +## 1.1.0 +* Built using Avro 1.10.2 +* Built using Gradle 7.0-rc-1 +* Updated compatibility testing through Gradle 6.8.3/7.0-rc-1 +* Updated compatibility testing through Kotlin 1.4.32 +* Updated compatibility testing to include Java 16/17-ea +* Adopted Github Actions for compatibility testing + +## 1.0.0 +* Published to Maven Central (no longer published to JCenter) +* New plugin IDs: `com.github.davidmc24.gradle.plugin.avro` and `com.github.davidmc24.gradle.plugin.avro-base` +* New package for tasks: `com.github.davidmc24.gradle.plugin.avro` + +# Pre-1.0 Versions + +These versions used a different publishing process. They use different coordinates/packages and may no longer be available in a traditional Maven repository. It is strongly recommended to upgrade to a newer version. + +If you still need to use them, the artifacts can be downloaded from [GitHub Releases](https://github.com/davidmc24/gradle-avro-plugin/releases) or accessed via [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin). +The plugin IDs are `com.commercehub.gradle.plugin.avro` and `com.commercehub.gradle.plugin.avro-base`, with all tasks in the package `com.commercehub.gradle.plugin.avro`. + +## 0.22.0 +* Add [Configuration Cache](https://docs.gradle.org/6.6/userguide/configuration_cache.html) support (#129; thanks to [dcabasson](https://github.com/dcabasson) and [eskatos](https://github.com/eskatos)) +* Add coverage reporting via JaCoco/Codecov to the plugin's build pipeline +* Add support for multiple IDL files with the same name in different directories (#123) + * The `.avpr` file generated by `GenerateAvroProtocolTask` is now based on the namespace and name of the protocol, rather than the name of the `.avdl` file. +* Built using Avro 1.10.1 +* Built using Gradle 6.7.1 +* Updated compatibility testing to include Java 15 +* Updated compatibility testing through Gradle 6.7.1 +* Updated compatibility testing through Kotlin 1.4.20 + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.22.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.22.0) + +## 0.21.0 +* Built using Avro 1.10.0 +* Drop support for Avro 1.9.X +* Removed support for `dateTimeLogicalType`; The behavior is now as if it were always `JSR-310` due to an upstream change +* Add support for `optionalGettersForNullableFieldsOnly` +* Apply @Classpath annotation to classpath on `GenerateAvroProtocolTask` + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.21.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.21.0) + +## 0.20.0 +* Built using Gradle 6.5 +* Updated compatibility testing to include Java 14 +* Updated compatibility testing through Gradle 6.5 +* Add `ResolveAvroDependenciesTask` (#115) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.20.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.20.0) + +## 0.19.1 +* Fix schema dependency resolution when types are referenced with a `{ "type": NAME }` block rather than just `NAME` (#107) +* Eliminate `NullPointerException` handling in schema dependency resolution, as it no longer appears to be needed. + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.19.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.19.1) + +## 0.19.0 +* Add support for Gradle 6.0-6.2.2 (#101) +* Drop support for Gradle versions prior to 5.1 +* Update version of kotlin plugin in tests/example +* Built using Avro 1.9.2 (#104) +* Add support for Java 13 +* Add support for testing multiple Kotlin versions +* Update plugin's own build to address some deprecation warnings of APIs being removed in Gradle 7 +* Add tests for Kotlin DSL usage (#61) +* Support [Task Configuration Avoidance](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html) (#97); thanks to [dcabasson](https://github.com/dcabasson) for the collaboration +* Upgrade Codenarc from 1.4 to 1.5 +* Preliminary Java 14 support + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.19.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.19.0) + +## 0.18.0 +* Use reproducible file order for plugin archives +* Eliminate usage of internal conventions API, using new Lazy Configuration approach instead; requires Gradle 4.4+ + * Technically, the APIs needed are available in Gradle 4.3, but there is a bug related to un-set `Property` instances in 4.3 and 4.3.1; see https://github.com/gradle/gradle/issues/3879 +* Cleaned up compatibility code for older versions of Gradle +* Built using Gradle 5.6.2 +* Upgrade Spock from 1.2 to 1.3 +* Upgrade Checkstyle from 6.1.1 to 8.23 and adjust rules used +* Upgrade Codenarc from 1.0 to 1.4 and adjust rules used +* Change source compatibility to 8 +* Modernized for Java 8 +* Built using Avro 1.9.1 +* GenerateAvroProtocolTask now has a `classpath` property; defaults to the runtime configuration when the Avro plugin is applied +* GenerateAvroProtocolTask now properly declares the `classpath` as an input; fixes #86; thanks to [RichSteele](https://github.com/RichSteele) for the bug report +* Fix handling of default `outputCharacterEncoding` (use of system default character set to match Java compiler) +* Add support for generating getters that return Optional (#90); contribution from [bspeakmon](https://github.com/bspeakmon) +* Add support for `logicalTypeFactories` and `customConversions`; fixes #92 + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.18.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.18.0) + +## 0.17.0 +* Built using Avro 1.9.0 +* Removed configuration setting `validateDefaults`; defaults are now always validated due to an upstream change +* Java 7 is no longer supported, as Avro 1.9.0 is now Java 8+ +* Began testing using Java 12 + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.17.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.17.0) + +## 0.16.0 +* Built using Gradle 4.10.2 +* Updated compatibility testing through Gradle 4.10.2 +* Added support for the Gradle [Build Cache](https://docs.gradle.org/current/userguide/build_cache.html) (#48); contribution from [dcabasson](https://github.com/dcabasson) +* Upgrade Spock from 1.0 to 1.2 +* Update plugin publishing mode to address Gradle 5.0 deprecation warning + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.16.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.16.0) + +## 0.15.1 +* Fix "Boolean configuration cannot be set with boolean values from Kotlin DSL" (#60) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.15.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.15.1) + +## 0.15.0 +* Built using Gradle 4.9 +* Updated compatibility testing through Gradle 4.9 +* Began testing using Java 11 +* Add support for generating schema files (#56) +* Fix bug where `GenerateAvroProtocolTask` can't be used without a runtime configuration + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.15.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.15.0) + +## 0.14.2 +* Stop creating default generated output directories when `outputDir` is customized and IntelliJ integration is used (#52) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.14.2) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.14.2) + +## 0.14.1 +* Built using Gradle 4.6 +* Updated compatibility testing through Gradle 4.6 +* Began testing using Java 10 +* Began testing using Kotlin 1.2.31 +* Fixed infinite loop when a schema file contains multiple definitions of the same type (#47) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.14.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.14.1) + +## 0.14.0 +* Built using Gradle 4.5 +* Updated compatibility testing through Gradle 4.5 +* Support for validation of default values in schema (#42) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.14.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.14.0) + +## 0.13.0 +* Remove pre-cleaning behavior from `GenerateAvroJavaTask` (#41) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.13.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.13.0) + +## 0.12.0 +* Improve support for Kotlin (#36) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.12.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.12.0) + +## 0.11.0 +* Built using Gradle 4.2.1 +* Began testing using Java 9 +* Built using Avro 1.8.2 +* Breaking backward compatibility with Avro versions older than 1.8.2 +* Add new configuration option "enableDecimalLogicalType" to generate `BigDecimal` for fields annotated with `logicalType` equals to `decimal` +* Breaking backward compatibility caused by "enableDecimalLogicalType" default value set `true`. `BigDecimal` will be used instead of old usage of `ByteBuffer` + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.11.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.11.0) + +## 0.10.0 +* Drop support for Gradle 2.x +* As Gradle 3.0+ has a minimum Java version requiremenet of Java 7, drop support for Java 6 +* Update source compatibility to Java 7 +* Reduce access to utility methods not intended for re-use +* Stopped publishing to [Gradle plugin portal](https://plugins.gradle.org) +* Published to [Bintray](https://bintray.com/commercehub-oss/main/gradle-avro-plugin) +* MapUtils class is no longer public + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.10.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.10.0) + +## 0.9.1 +* Built using Gradle 4.1 +* Updated versions for cross-compatibility testing + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.9.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.9.1) + +## 0.9.0 +* Built using Avro 1.8.1 (#23) +* Built using Gradle 2.13 +* Added version cross-compatibility testing + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.9.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.9.0) + +## 0.8.1 +* Compatible at runtime with Gradle 5; no functional changes. Compiled with Gradle 5.6. + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.8.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.8.1) + +## 0.8.0 +* Add support for Java 6 (#21) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.8.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.8.0) + +## 0.7.0 +* Remove usage of Apache Commons IO (#19) +* Add ability to retry processing of duplicate type definitions (#13) +* Renamed "encoding" option to "outputCharacterEncoding" to match Avro compiler +* Allowed setting "outputCharacterEncoding" to a `java.nio.charset.Charset` (in addition to a `String` charset name) +* Allowed setting "stringType" to a `org.apache.avro.generic.GenericData.StringType` (in addition to a String) +* Allowed setting "fieldVisibility" to a `org.apache.avro.compiler.specific.SpecificCompiler.FieldVisibility` (in addition to a String) +* Fixed handling of non-"true" String settings for "createSetters" option +* Automatically use encoding from `JavaCompile` task as "outputCharacterEncoding", if set +* Change default "outputCharacterEncoding" to system default to match `JavaCompile` task behavior (#20) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.7.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.7.0) + +## 0.6.1 +* Add Checkstyle ImportControl to prevent accidentally adding dependencies on libraries that Gradle makes available for build but not runtime. +* Remove usage of Guava (#18) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.6.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.6.1) + +## 0.6.0 +* Add new configuration option "templateDirectory" to set source directory for the Avro compiler's Velocity templates. +* Add new configuration option "createSetters" to allow suppressing the Avro compiler's creation of setters in created domain objects. +* Matching of fieldVisibility settings is now case-insensitive. +* Removed some excessive debug logging +* Built against Gradle 2.7 +* Added Checkstyle and Codenarc to build +* Known Bug: doesn't work properly unless you manually add a dependency on guava; please upgrade to 0.6.1 + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.6.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.6.0) + +## 0.5.0 +* Add support for schemas/protocols/IDL in subdirectories of `src/main/avro`, etc. (#11) +* Expose original error messages from `avro-compiler` when compilation fails + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.5.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.5.0) + +## 0.4.0 +* Add ability to specify fieldVisibility for generated Java source; contribution from [wooder79](https://github.com/wooder79) +* Removed support for unqualified plugin ID (just "avro") +* Published via new mechanism to [Gradle plugin portal](https://plugins.gradle.org) +* Stopped publishing to previous location on Bintray +* Built against Gradle 2.6; uses [test kit](https://docs.gradle.org/current/userguide/test_kit.html) for functional testing + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.4.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.4.0) + +## 0.3.4 +* Fix registration of generated sources for compilation (#8) +* Change classloader handling to better support import of external dependencies (#9) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.3.4) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.3.4) + +## 0.3.3 +* Fix generation of Java files from .avdl files; contribution from [viacoban](https://github.com/viacoban) + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.3.3) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.3.3) + +## 0.3.2 +* Improve handling when custom buildDir is used + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.3.2) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.3.2) + +## 0.3.1 +* Fix extension support for configuring encoding +* Make default encoding UTF-8 + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.3.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.3.1) + +## 0.3.0 +* IntelliJ: register generated source directories even if they don't already exist. +* Add avro-base plugin, which exposes tasks and the extension without creating tasks, defaults, etc. +* Add support for configuring encoding + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.3.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.3.0) + +## 0.2.0 +* Build against Gradle 1.12 +* Compile using Avro 1.7.6 +* Support for qualified plugin ID +* Deprecate unqualified plugin ID + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.2.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.2.0) + +## 0.1.3 +* Always regenerate all Java classes when any schema file changes to avoid some classes having outdated schema information. + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.1.3) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.1.3) + +## 0.1.2 +* Eliminate dependency on guava, make dependency on commons-io explicit + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.1.2) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.1.2) + +## 0.1.1 +* Fixed NullPointerException when performing clean builds + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.1.1) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.1.1) + +## 0.1.0 +* Add support for converting IDL files to JSON protocol declaration files +* Add support for generating Java classes from JSON protocol declaration files +* Add support for generating Java classes from JSON schema declaration files +* Add support for inter-dependent JSON schema declaration files +* Add support for tweaking source/exclude directories in IntelliJ +* Add support for specifying the string type to use in generated classes + +Links: +* [Release](https://github.com/davidmc24/gradle-avro-plugin/releases/tag/0.1.0) +* [JitPack](https://jitpack.io/#davidmc24/gradle-avro-plugin/0.1.0) diff --git a/lang/java/gradle-plugin/CODE_OF_CONDUCT.md b/lang/java/gradle-plugin/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..a4157ac46eb --- /dev/null +++ b/lang/java/gradle-plugin/CODE_OF_CONDUCT.md @@ -0,0 +1,78 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at david@carrclan.us. +We will endeavor to review submitted complaints and respond in a manner that +CommerceHub (in its sole discretion) deems necessary and appropriate to the +circumstances. In enforcing this Code of Conduct, Project maintainers may +(but shall not be obligated to) remove, edit, or reject comments, commits, +code, wiki edits, issues, and other contributions that are not aligned to +this Code of Conduct, or to ban temporarily or permanently any contributor +for other behaviors that they deem inappropriate, threatening, offensive, +or harmful. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/lang/java/gradle-plugin/CONTRIBUTING.md b/lang/java/gradle-plugin/CONTRIBUTING.md new file mode 100644 index 00000000000..96b183f23e4 --- /dev/null +++ b/lang/java/gradle-plugin/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# Contributing + +> Before contributing, please read our [code of conduct](https://github.com/davidmc24/gradle-avro-plugin/blob/master/CODE_OF_CONDUCT.md). + +Before starting work on an enhancement, it's highly recommended to open an [issue](https://github.com/davidmc24/gradle-avro-plugin/issues) to describe the intended change. +This allows for the project maintainers to provide feedback before you've done work that may not fit the project's vision. + +Note that this plugin is primarily focussed on exposing functionality from the [Apache Avro Java API](https://avro.apache.org/docs/current/api/java/index.html) in the ways most commonly used in Gradle builds. +If the capability that you are looking for doesn't currently exist in said upstream API, you're likely better off requesting the feature from the [Apache Avro project](https://avro.apache.org/) than requesting it here. + +Some possible enhancements may have already been considered and documented. Check the design-docs folder for the design specification for such features. + +To run the project's build, run: + +* (Mac/Linux): `./gradlew build` +* (Windows): `gradlew.bat build` + +This will run static analysis against the project, run the project's tests, and build the project. +If any failures are detected, please correct them prior to submitting your pull request. + +All enhancements should be accompanied by test coverage. +Our tests are based on [Spock](https://github.com/spockframework/spock). +Generally, it's best to extend our `FunctionalSpec` class, which provides useful functions for running the plugin within Gradle. + +Note that the "build" task only tests the plugin against a single version of Gradle/Avro. +If you want to test compatibility with a larger range, consider using the `testRecentVersionCompatibility` task or `testVersionCompatibility` task. + +For information on how to use GitHub to submit a pull request, see [Collaborating on projects using issues and pull requests](https://help.github.com/categories/collaborating-on-projects-using-issues-and-pull-requests/). diff --git a/lang/java/gradle-plugin/LICENSE b/lang/java/gradle-plugin/LICENSE new file mode 100644 index 00000000000..37ec93a14fd --- /dev/null +++ b/lang/java/gradle-plugin/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/lang/java/gradle-plugin/README.md b/lang/java/gradle-plugin/README.md new file mode 100644 index 00000000000..dab55f446e6 --- /dev/null +++ b/lang/java/gradle-plugin/README.md @@ -0,0 +1,471 @@ +# Seeking New Maintainer + +For more details, see [the discussion](https://github.com/davidmc24/gradle-avro-plugin/discussions/208). + +If no new maintainer is found, this project will be archived on October 22, 2023 (the 10 year anniversary of this project's first public commit). + +# Overview + +This is a [Gradle](http://www.gradle.org/) plugin to allow easily performing Java code generation for [Apache Avro](http://avro.apache.org/). It supports JSON schema declaration files, JSON protocol declaration files, and Avro IDL files. + +[![Build Status](https://github.com/davidmc24/gradle-avro-plugin/workflows/CI%20Build/badge.svg)](https://github.com/davidmc24/gradle-avro-plugin/actions) + +# Compatibility + +**NOTE**: Pre-1.0 versions used a different publishing process/namespace. It is strongly recommended to upgrade to a newer version. Further details can be found in the [change log](CHANGES.md). + +* Currently tested against Java 8, 11, and 17-19 + * Though not supported yet, tests are also run against Java 20 to provide early notification of potential incompatibilities. + * Java 19 support requires Gradle 7.6 or higher (as per Gradle's release notes) + * Java 18 support requires Gradle 7.5 or higher (as per Gradle's release notes) + * Java 17 support requires Gradle 7.3 or higher (as per Gradle's release notes) + * Java 16 support requires Gradle 7.0 or higher (as per Gradle's release notes) + * Java 15 support requires Gradle 6.7 or higher (as per Gradle's release notes) + * Java 14 support requires Gradle 6.3 or higher (as per Gradle's release notes) + * Java 13 support requires Gradle 6.0 or higher + * Java 8-12 support requires Gradle 5.1 or higher (versions lower than 5.1 are no longer supported) +* Currently built against Gradle 7.6 + * Currently tested against Gradle 5.1-5.6.4 and 6.0-7.6 +* Currently built against Avro 1.11.1 + * Currently tested against Avro 1.11.0-1.11.1 + * Avro 1.9.0-1.10.2 were last supported in version 1.2.1 +* Support for Kotlin + * Dropped integration with the Kotlin plugin in plugin version 1.4.0, as Kotlin 1.7.x would require compile-time dependency on a specific Kotlin version + * Wiring between the tasks added by the plugin and the Kotlin compilation tasks can either be added by your build logic, or a derived plugin + * Plugin version 1.3.0 was the last version with tested support for Kotlin + * It is believed to work with Kotlin 1.6.x as well + * It was tested against Kotlin plugin versions 1.3.20-1.3.72 and 1.4.0-1.4.32 and 1.5.0-1.5.31 using the latest compatible version of Gradle + * It was tested against Kotlin plugin versions 1.2.20-1.2.71 and 1.3.0-1.3.11 using Gradle 5.1 + * Kotlin plugin versions 1.4.20-1.4.32 require special settings to work with Java 17+; see [KT-43704](https://youtrack.jetbrains.com/issue/KT-43704#focus=Comments-27-4639603.0-0) + * Kotlin plugin version 1.3.30 is not compatible with Gradle 7.0+ + * Kotlin plugin versions starting with 1.4.0 require Gradle 5.3+ + * Kotlin plugin versions prior to 1.3.20 do not support Gradle 6.0+ + * Kotlin plugin versions prior to 1.2.30 do not support Java 10+ + * Version of the Kotlin plugin prior to 1.2.20 are unlikely to work +* Support for Gradle Kotlin DSL + +# Usage + +Add the following to your build files. Substitute the desired version based on [CHANGES.md](https://github.com/davidmc24/gradle-avro-plugin/blob/master/CHANGES.md). + +`settings.gradle`: +```groovy +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} +``` + +`build.gradle`: +```groovy +plugins { + id "com.github.davidmc24.gradle.plugin.avro" version "VERSION" +} +``` + +Additionally, ensure that you have an implementation dependency on Avro, such as: + +```groovy +repositories { + mavenCentral() +} +dependencies { + implementation "org.apache.avro:avro:1.11.0" +} +``` + +If you now run `gradle build`, Java classes will be compiled from Avro files in `src/main/avro`. +Actually, it will attempt to process an "avro" directory in every `SourceSet` (main, test, etc.) + +# Configuration + +There are a number of configuration options supported in the `avro` block. + +| option | default | description | +|--------------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| createSetters | `true` | `createSetters` passed to Avro compiler | +| createOptionalGetters | `false` | `createOptionalGetters` passed to Avro compiler | +| gettersReturnOptional | `false` | `gettersReturnOptional` passed to Avro compiler | +| optionalGettersForNullableFieldsOnly | `false` | `optionalGettersForNullableFieldsOnly` passed to Avro compiler | +| fieldVisibility | `"PRIVATE"` | `fieldVisibility` passed to Avro compiler | +| outputCharacterEncoding | see below | `outputCharacterEncoding` passed to Avro compiler | +| stringType | `"String"` | `stringType` passed to Avro compiler | +| templateDirectory | see below | `templateDir` passed to Avro compiler | +| additionalVelocityToolClasses | see below | `additionalVelocityTools` passed to Avro compiler | +| enableDecimalLogicalType | `true` | `enableDecimalLogicalType` passed to Avro compiler | +| conversionsAndTypeFactoriesClasspath | empty `ConfigurableFileCollection` | used for loading custom conversions and logical type factories | +| logicalTypeFactoryClassNames | empty `Map` | map from names to class names of logical types factories to be loaded from `conversionsAndTypeFactoriesClasspath` | +| customConversionClassNames | empty `List` | class names of custom conversions to be loaded from `conversionsAndTypeFactoriesClasspath` | + +Additionally, the `avro` extension exposes the following methods: + +* `logicalTypeFactory(String typeName, Class typeFactoryClass)`: register an additional logical type factory +* `customConversion(Class conversionClass)`: register a custom conversion + +## createSetters + +Valid values: `true` (default), `false`; supports equivalent `String` values + +Set to `false` to not create setter methods in the generated classes. + +Example: + +```groovy +avro { + createSetters = false +} +``` + +## createOptionalGetters + +Valid values: `false` (default), `true`; supports equivalent `String` values + +Set to `true` to create additional getter methods that return their fields wrapped in an +[Optional](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html). For a field with +name `abc` and type `string`, this setting will create a method +`Optional getOptionalAbc()`. + +Example: + +```groovy +avro { + createOptionalGetters = false +} +``` + +## gettersReturnOptional + +Valid values: `false` (default), `true`; supports equivalent `String` values + +Set to `true` to cause getter methods to return +[Optional](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) wrappers of the +underlying type. Where [`createOptionalGetters`](#createoptionalgetters) generates an additional +method, this one replaces the existing getter. + +Example: + +```groovy +avro { + gettersReturnOptional = false +} +``` + +## optionalGettersForNullableFieldsOnly + +Valid values: `false` (default), `true`; supports equivalent `String` values + +Set to `true` in conjuction with `gettersReturnOptional` to `true` to return +[Optional](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) wrappers of the +underlying type. Where [`gettersReturnOptional`](#gettersReturnOptional) alone changes all getters to +return `Optional`, this one only returns Optional for nullable (null union) field definitions. +Setting this to `true` without setting `gettersReturnOptional` to `true` will result in this flag having no effect. + +Example: +```groovy +avro { + gettersReturnOptional = true + optionalGettersForNullableFieldsOnly = true +} +``` + +## fieldVisibility + +Valid values: any [FieldVisibility](https://avro.apache.org/docs/1.11.0/api/java/org/apache/avro/compiler/specific/SpecificCompiler.FieldVisibility.html) or equivalent `String` name (matched case-insensitively); default `"PRIVATE"` (default) + +By default, the fields in generated Java files will have private visibility. +Set to `"PRIVATE"` to explicitly specify private visibility of the fields, or `"PUBLIC"` to specify public visibility of the fields. + +Example: + +```groovy +avro { + fieldVisibility = "PUBLIC" +} +``` + +## outputCharacterEncoding + +Valid values: any [Charset](http://docs.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html) or equivalent `String` name + +Controls the character encoding of generated Java files. +If using the plugin's conventions (i.e., not just the base plugin), the associated `JavaCompile` task's encoding will be used automatically. +Otherwise, it will use the value configured in the `avro` block, defaulting to `"UTF-8"`. + +Examples: + +```groovy +// Option 1: configure compilation task (avro plugin will automatically match) +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} +// Option 2: just configure avro plugin +avro { + outputCharacterEncoding = "UTF-8" +} +``` + +## stringType + +Valid values: any [StringType](http://avro.apache.org/docs/1.8.1/api/java/org/apache/avro/generic/GenericData.StringType.html) or equivalent `String` name (matched case-insensitively); default `"String"` (default) + +By default, the generated Java files will use [`java.lang.String`](http://docs.oracle.com/javase/7/docs/api/java/lang/String.html) to represent string types. +Alternatively, you can set it to `"Utf8"` to use [`org.apache.avro.util.Utf8`](https://avro.apache.org/docs/1.8.1/api/java/org/apache/avro/util/Utf8.html) or `"charSequence"` to use [`java.lang.CharSequence`](http://docs.oracle.com/javase/7/docs/api/java/lang/CharSequence.html). + +```groovy +avro { + stringType = "CharSequence" +} +``` + +## templateDirectory + +By default, files will be generated using Avro's default templates. +If desired, you can override the template set used by either setting this property or the `"org.apache.avro.specific.templates"` System property. + +```groovy +avro { + templateDirectory = "/path/to/velocity/templates" +} +``` + +## additionalVelocityToolClasses + +When overriding the default set of Velocity templates provided with Avro, it is often desirable to provide additional tools to use during generation. +The class names you provide will be made available for use in your Velocity templates. An instance of each class provided will be created using +the default constructor (required). When registered, they will be available as $class.simpleName(). Given the example configuration below, +two tools would be registered, and be available as escape and json. + + +```groovy +avro { + additionalVelocityToolClasses = ['com.yourpackage.Escape', 'com.yourpackage.JSON'] +} +``` + +## enableDecimalLogicalType + +Valid values: `true` (default), `false`; supports equivalent `String` values + +By default, generated Java files will use [`java.math.BigDecimal`](https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html) +for representing `fixed` or `bytes` fields annotated with `"logicalType": "decimal"`. +Set to `false` to use [`java.nio.ByteBuffer`](https://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html) in generated classes. + +Example: + +```groovy +avro { + enableDecimalLogicalType = false +} +``` + +## conversionsAndTypeFactoriesClasspath, logicalTypeFactoryClassNames and customConversionClassNames + +Properties that can be used for loading [Conversion](https://avro.apache.org/docs/current/api/java/org/apache/avro/Conversion.html) and [LogicalTypeFactory](https://avro.apache.org/docs/current/api/java/org/apache/avro/LogicalTypes.LogicalTypeFactory.html) classes from outside of the build classpath. + +Example: + +```groovy +configurations { + customConversions +} + +dependencies { + customConversions(project(":custom-conversions")) +} + +avro { + conversionsAndTypeFactoriesClasspath.from(configurations.customConversions) + logicalTypeFactoryClassNames.put("timezone", "com.github.davidmc24.gradle.plugin.avro.test.custom.TimeZoneLogicalTypeFactory") + customConversionClassNames.add("com.github.davidmc24.gradle.plugin.avro.test.custom.TimeZoneConversion") +} +``` + +# IntelliJ Integration + +The plugin attempts to make IntelliJ play more smoothly with generated sources when using Gradle-generated project files. +However, there are still some rough edges. It will work best if you first run `gradle build`, and _after_ that run `gradle idea`. +If you do it in the other order, IntelliJ may not properly exclude some directories within your `build` directory. + +# Alternate Usage + +If the defaults used by the plugin don't work for you, you can still use the tasks by themselves. +In this case, use the `com.github.davidmc24.gradle.plugin.avro-base` plugin instead, and create tasks of type `GenerateAvroJavaTask` and/or `GenerateAvroProtocolTask`. + +Here's a short example of what this might look like: + +```groovy +import com.github.davidmc24.gradle.plugin.avro.GenerateAvroJavaTask + +apply plugin: "java" +apply plugin: "com.github.davidmc24.gradle.plugin.avro-base" + +dependencies { + implementation "org.apache.avro:avro:1.11.0" +} + +def generateAvro = tasks.register("generateAvro", GenerateAvroJavaTask) { + source("src/avro") + outputDir = file("dest/avro") +} + +tasks.named("compileJava").configure { + source(generateAvro) +} +``` + +# File Processing + +When using this plugin, it is recommended to define each record/enum/fixed type in its own file rather than using inline type definitions. +This approach allows defining any type of schema structure, and eliminates the potential for conflicting definitions of a type between multiple files. +The plugin will automatically recognize the dependency and compile the files in the correct order. +For example, instead of `Cat.avsc`: + +```json +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields" : [ + { + "name": "breed", + "type": { + "name": "Breed", + "type": "enum", + "symbols" : [ + "ABYSSINIAN", "AMERICAN_SHORTHAIR", "BIRMAN", "MAINE_COON", "ORIENTAL", "PERSIAN", "RAGDOLL", "SIAMESE", "SPHYNX" + ] + } + } + ] +} +``` + +use `Breed.avsc`: + +```json +{ + "name": "Breed", + "namespace": "example", + "type": "enum", + "symbols" : ["ABYSSINIAN", "AMERICAN_SHORTHAIR", "BIRMAN", "MAINE_COON", "ORIENTAL", "PERSIAN", "RAGDOLL", "SIAMESE", "SPHYNX"] +} +``` + + +and `Cat.avsc`: + +```json +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields" : [ + {"name": "breed", "type": "Breed"} + ] +} +``` + +There may be cases where the schema files contain inline type definitions and it is undesirable to modify them. +In this case, the plugin will automatically recognize any duplicate type definitions and check if they match. +If any conflicts are identified, it will cause a build failure. + +# Kotlin Support + +The Java classes generated from your Avro files should be automatically accessible in the classpath to Kotlin classes in the same sourceset, and transitively to any sourcesets that depend on that sourceset. +This is accomplished by this plugin detecting that the Kotlin plugin has been applied, and informing the Kotlin compilation tasks of the presence of the generated sources directories for cross-compilation. + +This support does *not* support producing the Avro generated classes as Kotlin classes, as that functionality is not currently provided by the upstream Avro library. + +# Kotlin DSL Support + +Special notes relevant to using this plugin via the Gradle Kotlin DSL: + +* Apply the plugin declaratively using the `plugins {}` block. Otherwise, various features may not work as intended. See [Configuring Plugins in the Gradle Kotlin DSL](https://github.com/gradle/kotlin-dsl/blob/master/doc/getting-started/Configuring-Plugins.md) for more details. +* Configuration in the `avro {}` block must be applied differently than in the Groovy DSL. See the example below for details. + +### Example Kotlin DSL Setup: + +In `gradle.build.kts` add: + +```kotlin +plugins { + // Find latest release here: https://github.com/davidmc24/gradle-avro-plugin/releases + id("com.github.davidmc24.gradle.plugin.avro") version "VERSION" +} +``` + +And then in your `settings.gradle.kts` add: + +```kotlin +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} +``` + +The syntax for configuring the extension looks like this: + +```kotlin +avro { + isCreateSetters.set(true) + isCreateOptionalGetters.set(false) + isGettersReturnOptional.set(false) + isOptionalGettersForNullableFieldsOnly.set(false) + fieldVisibility.set("PUBLIC_DEPRECATED") + outputCharacterEncoding.set("UTF-8") + stringType.set("String") + templateDirectory.set(null as String?) + isEnableDecimalLogicalType.set(true) +} +``` + +# Resolving schema dependencies + +If desired, you can generate JSON schema with dependencies resolved. + +Example build: + +```groovy +import com.github.davidmc24.gradle.plugin.avro.ResolveAvroDependenciesTask + +apply plugin: "com.github.davidmc24.gradle.plugin.avro-base" + +tasks.register("resolveAvroDependencies", ResolveAvroDependenciesTask) { + source file("src/avro/normalized") + outputDir = file("build/avro/resolved") +} +``` + +# Generating schema files from protocol/IDL + +If desired, you can generate JSON schema files. +To do this, apply the plugin (either `avro` or `avro-base`), and define custom tasks as needed for the schema generation. +From JSON protocol files, all that's needed is the `GenerateAvroSchemaTask`. +From IDL files, first use `GenerateAvroProtocolTask` to convert the IDL files to JSON protocol files, then use `GenerateAvroSchemaTask`. + +Example using base plugin with support for both IDL and JSON protocol files in `src/main/avro`: + +```groovy +import com.github.davidmc24.gradle.plugin.avro.GenerateAvroProtocolTask +import com.github.davidmc24.gradle.plugin.avro.GenerateAvroSchemaTask + +apply plugin: "com.github.davidmc24.gradle.plugin.avro-base" + +def generateProtocol = tasks.register("generateProtocol", GenerateAvroProtocolTask) { + source file("src/main/avro") + include("**/*.avdl") + outputDir = file("build/generated-avro-main-avpr") +} + +tasks.register("generateSchema", GenerateAvroSchemaTask) { + dependsOn generateProtocol + source file("src/main/avro") + source file("build/generated-avro-main-avpr") + include("**/*.avpr") + outputDir = file("build/generated-main-avro-avsc") +} +``` diff --git a/lang/java/gradle-plugin/RELEASING.md b/lang/java/gradle-plugin/RELEASING.md new file mode 100644 index 00000000000..f0a540cdea1 --- /dev/null +++ b/lang/java/gradle-plugin/RELEASING.md @@ -0,0 +1,12 @@ +# Release Process + +1. Update `CHANGES.md` +1. Ensure that there is a milestone for the version, and that appropriate issues are associated with the milestone. +1. Update the plugin version in `build.gradle` under "version" +1. Commit and tag with the version number (don't push yet) +1. Run `./gradlew clean build` to make sure it looks good. +1. Update the version in `build.gradle` to the next SNAPSHOT and commit. +1. Push +1. If there was a issue requesting the release, close it. +1. Close the milestone. +1. Go to the [GitHub Releases page](https://github.com/davidmc24/gradle-avro-plugin/releases), click "Draft a new release", select the tag version, use the version number as the title, copy the relevant segment from `CHANGES.md` into the description, and click "Publish release". This will trigger the CI job that does the actual publishing. diff --git a/lang/java/gradle-plugin/build.gradle b/lang/java/gradle-plugin/build.gradle new file mode 100644 index 00000000000..7b47e5048b5 --- /dev/null +++ b/lang/java/gradle-plugin/build.gradle @@ -0,0 +1,257 @@ +plugins { + id "groovy" + id "checkstyle" + id "codenarc" + id "idea" +// id "jacoco" + id "maven-publish" + id "signing" + id "java-gradle-plugin" + id "org.nosphere.gradle.github.actions" version "1.2.0" + id "io.github.gradle-nexus.publish-plugin" version "1.0.0" +// id "pl.droidsonroids.jacoco.testkit" version "1.0.8" +} + +// Gradle TestKit (which most of this plugins tests run using) runs the builds in a separate +// JVM than the tests. Thus, for Jacoco to pick up on it, you need to do some extra legwork. +// https://github.com/koral--/jacoco-gradle-testkit-plugin seeks to solve that +// However, Gradle 7.1 doesn't currently support the combination of the configuration cache +// with a Java agent (such as Jacoco) and TestKit. +// Thus, for now, no coverage reporting, as I place a higher value on testing configuration cache +// support. + +group = "com.github.davidmc24.gradle.plugin" +version = "1.7.2-SNAPSHOT" + +def isCI = System.getenv("CI") == "true" + +repositories { + mavenCentral() + maven { + // Used for snapshot builds for some libraries + name 'Sonatype OSS' + url 'https://oss.sonatype.org/content/repositories/snapshots' + } +} + +def compileAvroVersion = "1.11.1" + +// Write the plugin's classpath to a file to share with the tests +task createClasspathManifest { + def outputDir = file("$buildDir/$name") + + inputs.files sourceSets.main.runtimeClasspath + outputs.dir outputDir + + doLast { + outputDir.mkdirs() + file("$outputDir/plugin-classpath.txt").text = sourceSets.main.runtimeClasspath.join("\n") + } +} + +dependencies { + implementation localGroovy() + implementation "org.apache.avro:avro-compiler:${compileAvroVersion}" + constraints { + implementation ('com.fasterxml.jackson.core:jackson-databind:2.12.7.1') { + because 'previous versions have vulnerabilities: CVE-2022-42004, CVE-2022-42003' + } + implementation ('org.apache.commons:commons-text:1.10.0') { + because 'previous versions have vulnerability: CVE-2022-42889' + } + } + testImplementation "org.spockframework:spock-core:2.0-M5-groovy-3.0" + testImplementation gradleTestKit() + testImplementation "uk.co.datumedge:hamcrest-json:0.2" + testImplementation "com.vdurmont:semver4j:3.1.0" + testRuntimeOnly files(createClasspathManifest) // Add the classpath file to the test runtime classpath + // tool version specified in dependencies in order to override Groovy version for Java compatibility + codenarc "org.codenarc:CodeNarc:2.2.0" + codenarc "org.codehaus.groovy:groovy-all:3.0.9" +} + +tasks.withType(AbstractCompile) { + options.encoding = "UTF-8" +} +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-Xlint:-options" << "-Werror" +} + +tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +java { + withJavadocJar() + withSourcesJar() +} + +javadoc { + if(JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } +} + +publishing { + publications.withType(MavenPublication) { + pom { + name = "gradle-avro-plugin" + description = "A Gradle plugin to allow easily performing Java code generation for Apache Avro. It supports JSON schema declaration files, JSON protocol declaration files, and Avro IDL files." + url = "https://github.com/davidmc24/gradle-avro-plugin" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = 'davidmc24' + name = 'David M. Carr' + email = 'david@carrclan.us' + } + } + scm { + connection = 'scm:git:https://github.com/davidmc24/gradle-avro-plugin.git' + developerConnection = 'scm:git:ssh://github.com/davidmc24/gradle-avro-plugin.git' + url = 'https://github.com/davidmc24/gradle-avro-plugin' + } + } + } +} + +nexusPublishing { + repositories { + sonatype() + } +} + +signing { + if (isCI) { + def signingKeyId = System.getenv("SIGNING_KEY_ID") + def signingKey = System.getenv("SIGNING_KEY") + def signingPassword = System.getenv("SIGNING_PASSWORD") + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + } + publishing.publications.all { publication -> + sign publication + } +} + +gradlePlugin { + plugins { + avro { + id = "com.github.davidmc24.gradle.plugin.avro" + implementationClass = "com.github.davidmc24.gradle.plugin.avro.AvroPlugin" + displayName = "avro" + description = "Conventions plugin for gradle-avro-plugin" + } + avroBase { + id = "com.github.davidmc24.gradle.plugin.avro-base" + implementationClass = "com.github.davidmc24.gradle.plugin.avro.AvroBasePlugin" + displayName = "avro-base" + description = "Capabilities plugin for gradle-avro-plugin" + } + } +} + +idea { + project { + vcs = "Git" + ipr { + withXml { provider -> + def node = provider.asNode() + node.append(new XmlParser().parseText(""" + + + + """.stripIndent())) + } + } + } +} + +checkstyle { + ignoreFailures = false + maxErrors = 0 + maxWarnings = 0 + showViolations = true + toolVersion = "8.23" +} +// In Gradle 4.8 the checkstyle basedir changed to no longer be the project root by default; thus we need to specify +checkstyleMain { + configProperties = ['basedir': "$rootDir/config/checkstyle"] +} +checkstyleTest { + configProperties = ['basedir': "$rootDir/config/checkstyle"] +} + +codenarc { + config = project.resources.text.fromFile("config/codenarc/codenarc.groovy") + ignoreFailures = false + maxPriority1Violations = 0 + maxPriority2Violations = 0 + maxPriority3Violations = 0 + // tool version specified in dependencies in order to override Groovy version for Java compatibility +// toolVersion = "2.2.0" +} + +// Java 8+ is required due to requirements introduced in Avro 1.9.0 +// Java 8+ is also required by Gradle 5.x +if (JavaVersion.current().java10Compatible) { + compileJava { + options.release = 8 + } +} else { + sourceCompatibility = 8 +} + +test { + useJUnitPlatform() + systemProperties = [ + avroVersion: compileAvroVersion, + gradleVersion: gradle.gradleVersion, + ] +// finalizedBy jacocoTestReport // report is always generated after tests run +} + +tasks.create(name: "testCompatibility", type: Test) { + description = "Test cross-compatibility of the plugin with Avro/Gradle" + group = "Verification" + useJUnitPlatform() + systemProperties = [ + avroVersion: findProperty("avroVersion"), + gradleVersion: findProperty("gradleVersion"), + ] + reports { + html.destination = file("$buildDir/reports/tests/compatibility") + junitXml.destination = file("$buildDir/reports/tests/compatibility") + } +} + +//jacoco { +// // 0.8.7+ needed for Java 15 support +// // See https://www.jacoco.org/jacoco/trunk/doc/changes.html +// toolVersion = "0.8.7-SNAPSHOT" +//} + +//jacocoTestReport { +// reports { +// html.enabled true +// xml.enabled true +// } +//} + +tasks.withType(Test) { + jvmArgs "-Xss320k" + minHeapSize "120m" + maxHeapSize "280m" + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} diff --git a/lang/java/gradle-plugin/config/checkstyle/checkstyle.xml b/lang/java/gradle-plugin/config/checkstyle/checkstyle.xml new file mode 100644 index 00000000000..ce9b2420e69 --- /dev/null +++ b/lang/java/gradle-plugin/config/checkstyle/checkstyle.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lang/java/gradle-plugin/config/checkstyle/import-control.xml b/lang/java/gradle-plugin/config/checkstyle/import-control.xml new file mode 100644 index 00000000000..920e5ea3e29 --- /dev/null +++ b/lang/java/gradle-plugin/config/checkstyle/import-control.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lang/java/gradle-plugin/config/codenarc/codenarc.groovy b/lang/java/gradle-plugin/config/codenarc/codenarc.groovy new file mode 100644 index 00000000000..9265e6a8fe5 --- /dev/null +++ b/lang/java/gradle-plugin/config/codenarc/codenarc.groovy @@ -0,0 +1,329 @@ +ruleset { + + // rulesets/basic.xml + AssertWithinFinallyBlock + AssignmentInConditional + BigDecimalInstantiation + BitwiseOperatorInConditional + BooleanGetBoolean + BrokenNullCheck + BrokenOddnessCheck + ClassForName + ComparisonOfTwoConstants + ComparisonWithSelf + ConstantAssertExpression + ConstantIfExpression + ConstantTernaryExpression + DeadCode + DoubleNegative + DuplicateCaseStatement + DuplicateMapKey + DuplicateSetValue + EmptyCatchBlock + EmptyClass + EmptyElseBlock + EmptyFinallyBlock + EmptyForStatement + EmptyIfStatement + EmptyInstanceInitializer + EmptyMethod + EmptyStaticInitializer + EmptySwitchStatement + EmptySynchronizedStatement + EmptyTryBlock + EmptyWhileStatement + EqualsAndHashCode + EqualsOverloaded + ExplicitGarbageCollection + ForLoopShouldBeWhileLoop + HardCodedWindowsFileSeparator + HardCodedWindowsRootDirectory + IntegerGetInteger + RandomDoubleCoercedToZero + RemoveAllOnSelf + ReturnFromFinallyBlock + ThrowExceptionFromFinallyBlock + + // rulesets/braces.xml + ElseBlockBraces + ForStatementBraces + IfStatementBraces + WhileStatementBraces + + // rulesets/concurrency.xml + BusyWait + DoubleCheckedLocking + InconsistentPropertyLocking + InconsistentPropertySynchronization + NestedSynchronization + StaticCalendarField + StaticConnection + StaticDateFormatField + StaticMatcherField + StaticSimpleDateFormatField + SynchronizedMethod + SynchronizedOnBoxedPrimitive + SynchronizedOnGetClass + SynchronizedOnReentrantLock + SynchronizedOnString + SynchronizedOnThis + SynchronizedReadObjectMethod + SystemRunFinalizersOnExit + ThisReferenceEscapesConstructor + ThreadGroup + ThreadLocalNotStaticFinal + ThreadYield + UseOfNotifyMethod + VolatileArrayField + VolatileLongOrDoubleField + WaitOutsideOfWhileLoop + + // rulesets/convention.xml + ConfusingTernary + CouldBeElvis + HashtableIsObsolete + IfStatementCouldBeTernary + InvertedIfElse + LongLiteralWithLowerCaseL + ParameterReassignment + TernaryCouldBeElvis + VectorIsObsolete + + // rulesets/design.xml + AbstractClassWithPublicConstructor + BooleanMethodReturnsNull + BuilderMethodWithSideEffects + CloneableWithoutClone + CloseWithoutCloseable + CompareToWithoutComparable + ConstantsOnlyInterface + EmptyMethodInAbstractClass + FinalClassWithProtectedMember + ImplementationAsType + LocaleSetDefault + PrivateFieldCouldBeFinal + PublicInstanceField + ReturnsNullInsteadOfEmptyArray + ReturnsNullInsteadOfEmptyCollection + SimpleDateFormatMissingLocale + StatelessSingleton + + // rulesets/exceptions.xml + CatchArrayIndexOutOfBoundsException + CatchError + CatchException + CatchIllegalMonitorStateException + CatchIndexOutOfBoundsException + CatchNullPointerException + CatchRuntimeException + CatchThrowable + ConfusingClassNamedException + ExceptionExtendsError + ExceptionNotThrown + MissingNewInThrowStatement + ReturnNullFromCatchBlock + SwallowThreadDeath + ThrowError + ThrowException + ThrowNullPointerException + ThrowRuntimeException + ThrowThrowable + + // rulesets/formatting.xml + BracesForClass + BracesForForLoop + BracesForIfElse + BracesForMethod + BracesForTryCatchFinally + ClosureStatementOnOpeningLineOfMultipleLineClosure + LineLength (length: 140) + SpaceAfterCatch + SpaceAfterClosingBrace + SpaceAfterComma + SpaceAfterFor + SpaceAfterIf + SpaceAfterOpeningBrace + SpaceAfterSemicolon + SpaceAfterSwitch + SpaceAfterWhile + SpaceAroundClosureArrow + SpaceAroundMapEntryColon (characterAfterColonRegex: /\s/) + SpaceAroundOperator + SpaceBeforeClosingBrace + SpaceBeforeOpeningBrace + + // rulesets/generic.xml + IllegalClassMember + IllegalClassReference + IllegalPackageReference + IllegalRegex + IllegalString + RequiredRegex + RequiredString + StatelessClass + + // rulesets/groovyism.xml + AssignCollectionSort + AssignCollectionUnique + ClosureAsLastMethodParameter + CollectAllIsDeprecated + ConfusingMultipleReturns + ExplicitArrayListInstantiation + ExplicitCallToAndMethod + ExplicitCallToCompareToMethod + ExplicitCallToDivMethod + ExplicitCallToEqualsMethod + ExplicitCallToGetAtMethod + ExplicitCallToLeftShiftMethod + ExplicitCallToMinusMethod + ExplicitCallToModMethod + ExplicitCallToMultiplyMethod + ExplicitCallToOrMethod + ExplicitCallToPlusMethod + ExplicitCallToPowerMethod + ExplicitCallToRightShiftMethod + ExplicitCallToXorMethod + ExplicitHashMapInstantiation + ExplicitHashSetInstantiation + ExplicitLinkedHashMapInstantiation + ExplicitLinkedListInstantiation + ExplicitStackInstantiation + ExplicitTreeSetInstantiation + GStringAsMapKey + GStringExpressionWithinString + GetterMethodCouldBeProperty + GroovyLangImmutable + UseCollectMany + UseCollectNested + + // rulesets/imports.xml + DuplicateImport + ImportFromSamePackage + ImportFromSunPackages + MisorderedStaticImports (comesBefore: false) + UnnecessaryGroovyImport + UnusedImport + + // rulesets/junit.xml + ChainedTest + CoupledTestCase + JUnitAssertAlwaysFails + JUnitAssertAlwaysSucceeds + JUnitFailWithoutMessage + JUnitLostTest + JUnitPublicField + // Triggers incorrectly for Spock methods + // JUnitPublicNonTestMethod + JUnitSetUpCallsSuper + JUnitStyleAssertions + JUnitTearDownCallsSuper + JUnitTestMethodWithoutAssert + JUnitUnnecessarySetUp + JUnitUnnecessaryTearDown + JUnitUnnecessaryThrowsException + SpockIgnoreRestUsed + UnnecessaryFail + UseAssertEqualsInsteadOfAssertTrue + UseAssertFalseInsteadOfNegation + UseAssertNullInsteadOfAssertEquals + UseAssertSameInsteadOfAssertTrue + UseAssertTrueInsteadOfAssertEquals + UseAssertTrueInsteadOfNegation + + // rulesets/logging.xml + LoggerForDifferentClass + LoggerWithWrongModifiers + LoggingSwallowsStacktrace + MultipleLoggers + PrintStackTrace + Println + SystemErrPrint + SystemOutPrint + + // rulesets/naming.xml + AbstractClassName + ClassName + ClassNameSameAsFilename + ConfusingMethodName + FieldName + InterfaceName + ObjectOverrideMisspelledMethodName + PackageName + ParameterName + PropertyName + VariableName + + // rulesets/security.xml + FileCreateTempFile + InsecureRandom + NonFinalPublicField + NonFinalSubclassOfSensitiveInterface + ObjectFinalize + PublicFinalizeMethod + SystemExit + UnsafeArrayDeclaration + + // rulesets/serialization.xml + EnumCustomSerializationIgnored + SerialPersistentFields + SerialVersionUID + SerializableClassMustDefineSerialVersionUID + + // rulesets/size.xml + ClassSize + MethodCount + MethodSize + NestedBlockDepth + + // rulesets/unnecessary.xml + AddEmptyString + ConsecutiveLiteralAppends + ConsecutiveStringConcatenation + UnnecessaryBigDecimalInstantiation + UnnecessaryBigIntegerInstantiation + UnnecessaryBooleanExpression + UnnecessaryBooleanInstantiation + UnnecessaryCallForLastElement + UnnecessaryCallToSubstring + UnnecessaryCatchBlock + UnnecessaryCollectCall + UnnecessaryCollectionCall + UnnecessaryConstructor + UnnecessaryDefInFieldDeclaration + UnnecessaryDefInMethodDeclaration + UnnecessaryDefInVariableDeclaration + UnnecessaryDotClass + UnnecessaryDoubleInstantiation + UnnecessaryElseStatement + UnnecessaryFinalOnPrivateMethod + UnnecessaryFloatInstantiation + UnnecessaryGetter + UnnecessaryIfStatement + UnnecessaryInstanceOfCheck + UnnecessaryInstantiationToGetClass + UnnecessaryIntegerInstantiation + UnnecessaryLongInstantiation + UnnecessaryModOne + UnnecessaryNullCheck + UnnecessaryNullCheckBeforeInstanceOf + // Unnecessarily complicates Spock assertions + // UnnecessaryObjectReferences + UnnecessaryOverridingMethod + UnnecessaryPackageReference + UnnecessaryParenthesesForMethodCallWithClosure + UnnecessaryPublicModifier + UnnecessarySelfAssignment + UnnecessarySemicolon + UnnecessaryStringInstantiation + UnnecessaryTernaryExpression + UnnecessaryTransientModifier + + // rulesets/unused.xml + UnusedArray + UnusedMethodParameter + UnusedObject + UnusedPrivateField + UnusedPrivateMethod + UnusedPrivateMethodParameter + UnusedVariable +} diff --git a/lang/java/gradle-plugin/design-docs/configurations-for-additional-schema.md b/lang/java/gradle-plugin/design-docs/configurations-for-additional-schema.md new file mode 100644 index 00000000000..d87364ab7c0 --- /dev/null +++ b/lang/java/gradle-plugin/design-docs/configurations-for-additional-schema.md @@ -0,0 +1,18 @@ +Periodically, we get requests for help figuring out how to +load schema files from a JAR, whether it is a JAR from a +repository or the outputs of a subproject. + +`examples/avsc-from-subproject` gives an example of how +this can be configured based on a new configuration. +We might want to consider having the Avro plugin create +and configure such configurations as part of the +conventional usage pattern. + +If we do this, a few gotchas: + +1. We need to take into account all sourceSets (main, test, etc.) +2. We need to consider the naming so it's clear what the + configurations are for and don't conflict with other plugins. + For example, `additionalSchema` is too ambiguous. + `additionalAvroSchema` might be better. +3. Whether it makes sense to exclude generated classes from jars diff --git a/lang/java/gradle-plugin/design-docs/external-schemata-and-protocols.md b/lang/java/gradle-plugin/design-docs/external-schemata-and-protocols.md new file mode 100644 index 00000000000..50861b7d423 --- /dev/null +++ b/lang/java/gradle-plugin/design-docs/external-schemata-and-protocols.md @@ -0,0 +1,9 @@ +Originally requested as [#4](https://github.com/davidmc24/gradle-avro-plugin/issues/4). +Some users would like the ability to have JAR files that contain Avro schema/protocol files, and have a way to declare a dependency on these, such that the plugin's generation capability can use them without needing to manual extract the archives. + +Intended approach: + +* The plugin defines a new `Avro` configuration for every source-set that it is working with. +* As with any configuration, the build script can define dependencies in the configuration, and the resolution mechanism will resolve the configuration against the configured repositories when it is resolved. +* A task would be added to extract all such archives to a `/unpacked--avro` directory, per source-set +* The plugin's generation tasks would be configured to use the relevant unpacked directories as additional source directories. diff --git a/lang/java/gradle-plugin/design-docs/run-avro-as-an-external-process.md b/lang/java/gradle-plugin/design-docs/run-avro-as-an-external-process.md new file mode 100644 index 00000000000..eff15f70342 --- /dev/null +++ b/lang/java/gradle-plugin/design-docs/run-avro-as-an-external-process.md @@ -0,0 +1,16 @@ +Originally reported as [#27](https://github.com/davidmc24/gradle-avro-plugin/issues/27). +Currently, Avro generation takes by running the avro-compiler library as part of the Gradle plugin process. +This is simple and works, but has a few drawbacks: + +* Custom templates need to be available on the classpath for the plugin, which isn't compatible with the new style of Gradle plugin declarations. +* The Gradle plugin may use a different version of Avro for generation than you're using on the compile classpath for compilation. + +Instead, here is an alternative view of how it could work. + +* There is an enhanced-avro-compiler library that externalizes most of the logic currently present in GenerateAvroJavaTask/GenerateAvroProtocolTask, and makes those calls accessible as JVM entry points (via `main` methods). + * This library would be published on Maven Central, and potentially have multiple versions as needed for compatibility with multiple versions of Avro + * It's possible we might be able to get this logic pushed upstream into avro-compiler, in which case the need for this library would be eliminated. +* For a source-set, the plugin would take a single declaration of the desired Avro version, which is then used for both generation and compilation +* The plugin would use a configuration per source-set to resolve the appropriate version of enhanced-avro-compiler + * The build script could add additional dependencies to this configuration in order to pull in custom templates +* When doing generation, the plugin would use [JavaExec](https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/JavaExec.html) to spawn a child JVM and execute the appropriate logic in enhanced-avro-compiler. diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/README.md b/lang/java/gradle-plugin/examples/avsc-from-external-jar/README.md new file mode 100644 index 00000000000..8f6cb2ab77b --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/README.md @@ -0,0 +1,8 @@ +# Purpose + +An example project for having dependencies on .avsc schema files loaded from an external JAR file +(not produced by the current Gradle project). + +# Maintainer Notes + +* Command to create JAR: `(cd external-files && jar --create --no-manifest --file ../external-libs/schema.jar Breed.avsc)` diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/build.gradle b/lang/java/gradle-plugin/examples/avsc-from-external-jar/build.gradle new file mode 100644 index 00000000000..c7aafeeb146 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "com.github.davidmc24.gradle.plugin.avro" version "1.2.1" +} + +repositories { + mavenCentral() +} +dependencies { + implementation "org.apache.avro:avro:1.10.1" +} + +generateAvroJava { + source zipTree("external-libs/schema.jar") +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-files/Breed.avsc b/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-files/Breed.avsc new file mode 100644 index 00000000000..ce752ac4ea1 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-files/Breed.avsc @@ -0,0 +1,6 @@ +{ + "name": "Breed", + "namespace": "example", + "type": "enum", + "symbols" : ["ABYSSINIAN", "AMERICAN_SHORTHAIR", "BIRMAN", "MAINE_COON", "ORIENTAL", "PERSIAN", "RAGDOLL", "SIAMESE", "SPHYNX"] +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-files/Cat.avsc b/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-files/Cat.avsc new file mode 100644 index 00000000000..cb5aa8be4f3 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-files/Cat.avsc @@ -0,0 +1,8 @@ +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields" : [ + {"name": "breed", "type": "Breed"} + ] +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-libs/schema.jar b/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-libs/schema.jar new file mode 100644 index 00000000000..f512f36a489 Binary files /dev/null and b/lang/java/gradle-plugin/examples/avsc-from-external-jar/external-libs/schema.jar differ diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradle/wrapper/gradle-wrapper.jar b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..7454180f2ae Binary files /dev/null and b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradle/wrapper/gradle-wrapper.properties b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..e750102e092 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradlew b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradlew new file mode 100755 index 00000000000..1b6c787337f --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradlew.bat b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradlew.bat new file mode 100644 index 00000000000..107acd32c4e --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/settings.gradle b/lang/java/gradle-plugin/examples/avsc-from-external-jar/settings.gradle new file mode 100644 index 00000000000..731be1519c4 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + mavenLocal() + } +} + +rootProject.name = "avsc-from-external-jar" diff --git a/lang/java/gradle-plugin/examples/avsc-from-external-jar/src/main/avro/Cat.avsc b/lang/java/gradle-plugin/examples/avsc-from-external-jar/src/main/avro/Cat.avsc new file mode 100644 index 00000000000..cb5aa8be4f3 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-external-jar/src/main/avro/Cat.avsc @@ -0,0 +1,8 @@ +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields" : [ + {"name": "breed", "type": "Breed"} + ] +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/README.md b/lang/java/gradle-plugin/examples/avsc-from-subproject/README.md new file mode 100644 index 00000000000..73a4610c249 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/README.md @@ -0,0 +1,11 @@ +# Purpose + +An example project for having dependencies on .avsc schema files loaded from an JAR file +produced by a subproject of the current multi-project Gradle build. + +# Variants + +## schema project JAR doesn't contain classes + +If you'd rather have the `schema` project **not** generate Java classes, you can rename `src/main/avro` to `src/main/resources`. +In that case, you can also replace the Avro plugin with the `java` plugin. diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/cat/build.gradle b/lang/java/gradle-plugin/examples/avsc-from-subproject/cat/build.gradle new file mode 100644 index 00000000000..8984807202b --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/cat/build.gradle @@ -0,0 +1,47 @@ +import java.util.zip.ZipFile + +plugins { + id "com.github.davidmc24.gradle.plugin.avro" version "1.2.1" +} + +repositories { + mavenCentral() +} +configurations { + additionalSchema +} +dependencies { + implementation "org.apache.avro:avro:1.10.1" + additionalSchema project(":schema") +} + +generateAvroJava { + dependsOn configurations.additionalSchema + source { + // As of Gradle 7.2, Using zipTree within source appears to disable build caching + configurations.additionalSchema.collect { zipTree(it) } + } +} + +def configureJar = tasks.register("configureJar") { + it.doLast { + // Exclude classes that are already in schema.jar from this jar + tasks.jar.exclude( + configurations.additionalSchema + .findAll { it.name.endsWith("jar") } + .collect { File file -> + new ZipFile(file).entries() + .findAll { it.name.endsWith(".class") } + .collect { it.name } + } + .flatten() + ) + } + // otherwise the jars of dependent projects might not have been built + // TODO is there a way to copy the dependencies of the jar task? classes is not part of tasks.jar.dependsOn + it.dependsOn(tasks.classes) +} + +tasks.named("jar") { + it.dependsOn(configureJar) +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/cat/src/main/avro/Cat.avsc b/lang/java/gradle-plugin/examples/avsc-from-subproject/cat/src/main/avro/Cat.avsc new file mode 100644 index 00000000000..cb5aa8be4f3 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/cat/src/main/avro/Cat.avsc @@ -0,0 +1,8 @@ +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields" : [ + {"name": "breed", "type": "Breed"} + ] +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/gradle/wrapper/gradle-wrapper.jar b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..7454180f2ae Binary files /dev/null and b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/gradle/wrapper/gradle-wrapper.properties b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..e750102e092 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/gradlew b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradlew new file mode 100755 index 00000000000..1b6c787337f --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/gradlew.bat b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradlew.bat new file mode 100644 index 00000000000..107acd32c4e --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/schema/build.gradle b/lang/java/gradle-plugin/examples/avsc-from-subproject/schema/build.gradle new file mode 100644 index 00000000000..de7e12dcadd --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/schema/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "com.github.davidmc24.gradle.plugin.avro" version "1.2.1" +} + +repositories { + mavenCentral() +} +dependencies { + compileOnly "org.apache.avro:avro:1.10.1" +} + +sourceSets { + main { + resources { + srcDirs "src/main/avro" + } + } +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/schema/src/main/avro/Breed.avsc b/lang/java/gradle-plugin/examples/avsc-from-subproject/schema/src/main/avro/Breed.avsc new file mode 100644 index 00000000000..ce752ac4ea1 --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/schema/src/main/avro/Breed.avsc @@ -0,0 +1,6 @@ +{ + "name": "Breed", + "namespace": "example", + "type": "enum", + "symbols" : ["ABYSSINIAN", "AMERICAN_SHORTHAIR", "BIRMAN", "MAINE_COON", "ORIENTAL", "PERSIAN", "RAGDOLL", "SIAMESE", "SPHYNX"] +} diff --git a/lang/java/gradle-plugin/examples/avsc-from-subproject/settings.gradle b/lang/java/gradle-plugin/examples/avsc-from-subproject/settings.gradle new file mode 100644 index 00000000000..2023d97da8e --- /dev/null +++ b/lang/java/gradle-plugin/examples/avsc-from-subproject/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + mavenLocal() + } +} + +rootProject.name = "avsc-from-subproject" + +include "schema" +include "cat" diff --git a/lang/java/gradle-plugin/examples/default-custom-types/README.md b/lang/java/gradle-plugin/examples/default-custom-types/README.md new file mode 100644 index 00000000000..cad11f77323 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/README.md @@ -0,0 +1,4 @@ +This example demonstrates a custom plugin that registers a logical type factory and custom conversion. + +To simplify the example, the custom conversion/logicalTypeFactory classes are duplicated in both buildSrc and the project. +In the real world, you would likely put them in a separate project, publish them as a JAR, and depend on them in both places. diff --git a/lang/java/gradle-plugin/examples/default-custom-types/build.gradle b/lang/java/gradle-plugin/examples/default-custom-types/build.gradle new file mode 100644 index 00000000000..8643d410fef --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/build.gradle @@ -0,0 +1,9 @@ +apply plugin: custom.AvroConventionPlugin + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.apache.avro:avro:1.11.0' +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/build.gradle b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/build.gradle new file mode 100644 index 00000000000..b55abe34eb0 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/build.gradle @@ -0,0 +1,8 @@ +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.github.davidmc24.gradle.plugin:gradle-avro-plugin:1.2.0' + implementation 'org.apache.avro:avro:1.11.0' +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/AvroConventionPlugin.java b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/AvroConventionPlugin.java new file mode 100644 index 00000000000..40734d866e7 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/AvroConventionPlugin.java @@ -0,0 +1,15 @@ +package custom; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import com.github.davidmc24.gradle.plugin.avro.AvroPlugin; +import com.github.davidmc24.gradle.plugin.avro.AvroExtension; + +public class AvroConventionPlugin implements Plugin { + public void apply(Project project) { + project.getPluginManager().apply(AvroPlugin.class); + AvroExtension avroExtension = project.getExtensions().findByType(AvroExtension.class); + avroExtension.logicalTypeFactory("timezone", TimeZoneLogicalTypeFactory.class); + avroExtension.customConversion(TimeZoneConversion.class); + } +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneConversion.java b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneConversion.java new file mode 100644 index 00000000000..bc89a6255aa --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneConversion.java @@ -0,0 +1,36 @@ +package custom; + +import java.util.TimeZone; +import org.apache.avro.Conversion; +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; + +@SuppressWarnings("unused") +public class TimeZoneConversion extends Conversion { + public static final String LOGICAL_TYPE_NAME = "timezone"; + + @Override + public Class getConvertedType() { + return TimeZone.class; + } + + @Override + public String getLogicalTypeName() { + return LOGICAL_TYPE_NAME; + } + + @Override + public TimeZone fromCharSequence(CharSequence value, Schema schema, LogicalType type) { + return TimeZone.getTimeZone(value.toString()); + } + + @Override + public CharSequence toCharSequence(TimeZone value, Schema schema, LogicalType type) { + return value.getID(); + } + + @Override + public Schema getRecommendedSchema() { + return TimeZoneLogicalType.INSTANCE.addToSchema(Schema.create(Schema.Type.STRING)); + } +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneLogicalType.java b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneLogicalType.java new file mode 100644 index 00000000000..bf9361780cb --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneLogicalType.java @@ -0,0 +1,20 @@ +package custom; + +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; + +public class TimeZoneLogicalType extends LogicalType { + static final TimeZoneLogicalType INSTANCE = new TimeZoneLogicalType(); + + private TimeZoneLogicalType() { + super(TimeZoneConversion.LOGICAL_TYPE_NAME); + } + + @Override + public void validate(Schema schema) { + super.validate(schema); + if (schema.getType() != Schema.Type.STRING) { + throw new IllegalArgumentException("Timezone can only be used with an underlying string type"); + } + } +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneLogicalTypeFactory.java b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneLogicalTypeFactory.java new file mode 100644 index 00000000000..f5e16553be6 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/buildSrc/src/main/java/custom/TimeZoneLogicalTypeFactory.java @@ -0,0 +1,12 @@ +package custom; + +import org.apache.avro.LogicalType; +import org.apache.avro.LogicalTypes; +import org.apache.avro.Schema; + +public class TimeZoneLogicalTypeFactory implements LogicalTypes.LogicalTypeFactory { + @Override + public LogicalType fromSchema(Schema schema) { + return TimeZoneLogicalType.INSTANCE; + } +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/gradle/wrapper/gradle-wrapper.jar b/lang/java/gradle-plugin/examples/default-custom-types/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..e708b1c023e Binary files /dev/null and b/lang/java/gradle-plugin/examples/default-custom-types/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lang/java/gradle-plugin/examples/default-custom-types/gradle/wrapper/gradle-wrapper.properties b/lang/java/gradle-plugin/examples/default-custom-types/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..f371643eed7 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lang/java/gradle-plugin/examples/default-custom-types/gradlew b/lang/java/gradle-plugin/examples/default-custom-types/gradlew new file mode 100755 index 00000000000..4f906e0c811 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/lang/java/gradle-plugin/examples/default-custom-types/gradlew.bat b/lang/java/gradle-plugin/examples/default-custom-types/gradlew.bat new file mode 100644 index 00000000000..107acd32c4e --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lang/java/gradle-plugin/examples/default-custom-types/settings.gradle b/lang/java/gradle-plugin/examples/default-custom-types/settings.gradle new file mode 100644 index 00000000000..beb40b1cbf2 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'default-custom-types' diff --git a/lang/java/gradle-plugin/examples/default-custom-types/src/main/avro/customConversion.avsc b/lang/java/gradle-plugin/examples/default-custom-types/src/main/avro/customConversion.avsc new file mode 100644 index 00000000000..eca16922833 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/src/main/avro/customConversion.avsc @@ -0,0 +1,9 @@ +{"namespace": "example", + "type": "record", + "name": "Event", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "start", "type": {"type": "long", "logicalType": "timestamp-millis"} }, + {"name": "timezone", "type": {"type": "string", "logicalType": "timezone"} } + ] +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneConversion.java b/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneConversion.java new file mode 100644 index 00000000000..bc89a6255aa --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneConversion.java @@ -0,0 +1,36 @@ +package custom; + +import java.util.TimeZone; +import org.apache.avro.Conversion; +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; + +@SuppressWarnings("unused") +public class TimeZoneConversion extends Conversion { + public static final String LOGICAL_TYPE_NAME = "timezone"; + + @Override + public Class getConvertedType() { + return TimeZone.class; + } + + @Override + public String getLogicalTypeName() { + return LOGICAL_TYPE_NAME; + } + + @Override + public TimeZone fromCharSequence(CharSequence value, Schema schema, LogicalType type) { + return TimeZone.getTimeZone(value.toString()); + } + + @Override + public CharSequence toCharSequence(TimeZone value, Schema schema, LogicalType type) { + return value.getID(); + } + + @Override + public Schema getRecommendedSchema() { + return TimeZoneLogicalType.INSTANCE.addToSchema(Schema.create(Schema.Type.STRING)); + } +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneLogicalType.java b/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneLogicalType.java new file mode 100644 index 00000000000..bf9361780cb --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneLogicalType.java @@ -0,0 +1,20 @@ +package custom; + +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; + +public class TimeZoneLogicalType extends LogicalType { + static final TimeZoneLogicalType INSTANCE = new TimeZoneLogicalType(); + + private TimeZoneLogicalType() { + super(TimeZoneConversion.LOGICAL_TYPE_NAME); + } + + @Override + public void validate(Schema schema) { + super.validate(schema); + if (schema.getType() != Schema.Type.STRING) { + throw new IllegalArgumentException("Timezone can only be used with an underlying string type"); + } + } +} diff --git a/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneLogicalTypeFactory.java b/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneLogicalTypeFactory.java new file mode 100644 index 00000000000..f5e16553be6 --- /dev/null +++ b/lang/java/gradle-plugin/examples/default-custom-types/src/main/java/custom/TimeZoneLogicalTypeFactory.java @@ -0,0 +1,12 @@ +package custom; + +import org.apache.avro.LogicalType; +import org.apache.avro.LogicalTypes; +import org.apache.avro.Schema; + +public class TimeZoneLogicalTypeFactory implements LogicalTypes.LogicalTypeFactory { + @Override + public LogicalType fromSchema(Schema schema) { + return TimeZoneLogicalType.INSTANCE; + } +} diff --git a/lang/java/gradle-plugin/gradle.properties b/lang/java/gradle-plugin/gradle.properties new file mode 100644 index 00000000000..e51e97e1bc5 --- /dev/null +++ b/lang/java/gradle-plugin/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.warning.mode=all +org.gradle.parallel=true +org.gradle.vfs.watch=true +systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false diff --git a/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.jar b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..943f0cbfa75 Binary files /dev/null and b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..f398c33c4b0 --- /dev/null +++ b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lang/java/gradle-plugin/gradlew b/lang/java/gradle-plugin/gradlew new file mode 100755 index 00000000000..65dcd68d65c --- /dev/null +++ b/lang/java/gradle-plugin/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lang/java/gradle-plugin/gradlew.bat b/lang/java/gradle-plugin/gradlew.bat new file mode 100644 index 00000000000..93e3f59f135 --- /dev/null +++ b/lang/java/gradle-plugin/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lang/java/gradle-plugin/scripts/run-avro-cli.sh b/lang/java/gradle-plugin/scripts/run-avro-cli.sh new file mode 100755 index 00000000000..3267dc38117 --- /dev/null +++ b/lang/java/gradle-plugin/scripts/run-avro-cli.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -ex +avroVersion=${1?"Usage: $0 AVRO_VERSION ARGUMENTS"}; +shift +mkdir -p downloads +wget --timestamping --directory-prefix=downloads/ http://archive.apache.org/dist/avro/avro-${avroVersion}/java/avro-tools-${avroVersion}.jar +java -jar downloads/avro-tools-${avroVersion}.jar $* diff --git a/lang/java/gradle-plugin/scripts/run-compile-schema.sh b/lang/java/gradle-plugin/scripts/run-compile-schema.sh new file mode 100755 index 00000000000..15d46cd75c8 --- /dev/null +++ b/lang/java/gradle-plugin/scripts/run-compile-schema.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -ex +avroVersion=${1?"Usage: $0 AVRO_VERSION SCHEMA_FILE"}; +schemaFile=${2?"Usage: $0 AVRO_VERSION SCHEMA_FILE"}; +mkdir -p input/ +mkdir -p output/ +./run-avro-cli.sh $1 compile schema input/$2 output/ diff --git a/lang/java/gradle-plugin/settings.gradle b/lang/java/gradle-plugin/settings.gradle new file mode 100644 index 00000000000..b3d572ee837 --- /dev/null +++ b/lang/java/gradle-plugin/settings.gradle @@ -0,0 +1,19 @@ +plugins { + id "com.gradle.enterprise" version "3.3.4" +} + +rootProject.name = "gradle-avro-plugin" + +gradleEnterprise { + buildScan { + if (System.getenv("CI")) { + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" + publishAlways() + uploadInBackground = false + tag "CI" + } else { + tag "Local" + } + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroBasePlugin.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroBasePlugin.java new file mode 100644 index 00000000000..e83971c116f --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroBasePlugin.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2014-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +public class AvroBasePlugin implements Plugin { + @Override + public void apply(final Project project) { + configureExtension(project); + } + + @SuppressWarnings("deprecation") + private static void configureExtension(final Project project) { + final AvroExtension avroExtension = + GradleCompatibility.createExtensionWithObjectFactory(project, Constants.AVRO_EXTENSION_NAME, DefaultAvroExtension.class); + project.getTasks().withType(GenerateAvroJavaTask.class).configureEach(task -> { + task.getOutputCharacterEncoding().convention(avroExtension.getOutputCharacterEncoding()); + task.getStringType().convention(avroExtension.getStringType()); + task.getFieldVisibility().convention(avroExtension.getFieldVisibility()); + task.getTemplateDirectory().convention(avroExtension.getTemplateDirectory()); + task.getAdditionalVelocityToolClasses().convention(avroExtension.getAdditionalVelocityToolClasses()); + task.isCreateSetters().convention(avroExtension.isCreateSetters()); + task.isCreateOptionalGetters().convention(avroExtension.isCreateOptionalGetters()); + task.isGettersReturnOptional().convention(avroExtension.isGettersReturnOptional()); + task.isOptionalGettersForNullableFieldsOnly().convention(avroExtension.isOptionalGettersForNullableFieldsOnly()); + task.isEnableDecimalLogicalType().convention(avroExtension.isEnableDecimalLogicalType()); + task.getConversionsAndTypeFactoriesClasspath().from(avroExtension.getConversionsAndTypeFactoriesClasspath()); + task.getLogicalTypeFactories().convention(avroExtension.getLogicalTypeFactories()); + task.getLogicalTypeFactoryClassNames().convention(avroExtension.getLogicalTypeFactoryClassNames()); + task.getCustomConversions().convention(avroExtension.getCustomConversions()); + task.getCustomConversionClassNames().convention(avroExtension.getCustomConversionClassNames()); + }); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroExtension.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroExtension.java new file mode 100644 index 00000000000..e0ef45cd707 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroExtension.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2013-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import org.apache.avro.Conversion; +import org.apache.avro.LogicalTypes; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; + +@SuppressWarnings("unused") +public interface AvroExtension { + Property getOutputCharacterEncoding(); + Property getStringType(); + Property getFieldVisibility(); + Property getTemplateDirectory(); + ListProperty getAdditionalVelocityToolClasses(); + Property isCreateSetters(); + Property isCreateOptionalGetters(); + Property isGettersReturnOptional(); + Property isOptionalGettersForNullableFieldsOnly(); + Property isEnableDecimalLogicalType(); + ConfigurableFileCollection getConversionsAndTypeFactoriesClasspath(); + + /** + * @deprecated use {@link #getLogicalTypeFactoryClassNames()} instead + */ + @Deprecated + MapProperty> getLogicalTypeFactories(); + MapProperty getLogicalTypeFactoryClassNames(); + + /** + * @deprecated use {@link #getCustomConversionClassNames()} instead + */ + @Deprecated + ListProperty>> getCustomConversions(); + ListProperty getCustomConversionClassNames(); + + /** + * @deprecated use {@link #logicalTypeFactory(String, String)} + */ + @Deprecated + AvroExtension logicalTypeFactory(String typeName, Class typeFactoryClass); + AvroExtension logicalTypeFactory(String typeName, String typeFactoryClassName); + + /** + * @deprecated use {@link #customConversion(String)} instead + */ + @Deprecated + AvroExtension customConversion(Class> conversionClass); + AvroExtension customConversion(String conversionClassName); +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroPlugin.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroPlugin.java new file mode 100644 index 00000000000..bc288e24864 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroPlugin.java @@ -0,0 +1,169 @@ +/** + * Copyright © 2013-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.io.FileFilter; +import java.nio.charset.Charset; +import java.util.Optional; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.Directory; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.SourceTask; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.plugins.ide.idea.GenerateIdeaModule; +import org.gradle.plugins.ide.idea.IdeaPlugin; +import org.gradle.plugins.ide.idea.model.IdeaModule; + +import static org.gradle.api.plugins.JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME; + +public class AvroPlugin implements Plugin { + @Override + public void apply(final Project project) { + project.getPlugins().apply(JavaPlugin.class); + project.getPlugins().apply(AvroBasePlugin.class); + configureTasks(project); + configureIntelliJ(project); + } + + private static void configureTasks(final Project project) { + getSourceSets(project).configureEach(sourceSet -> { + TaskProvider protoTaskProvider = configureProtocolGenerationTask(project, sourceSet); + TaskProvider javaTaskProvider = configureJavaGenerationTask(project, sourceSet, protoTaskProvider); + configureTaskDependencies(project, sourceSet, javaTaskProvider); + }); + } + + private static void configureIntelliJ(final Project project) { + project.getPlugins().withType(IdeaPlugin.class).configureEach(ideaPlugin -> { + SourceSet mainSourceSet = getMainSourceSet(project); + SourceSet testSourceSet = getTestSourceSet(project); + IdeaModule module = ideaPlugin.getModel().getModule(); + module.setSourceDirs(new SetBuilder() + .addAll(module.getSourceDirs()) + .add(getAvroSourceDir(project, mainSourceSet)) + .add(getGeneratedOutputDir(project, mainSourceSet, Constants.JAVA_EXTENSION).map(Directory::getAsFile).get()) + .build()); + GradleCompatibility.addTestSources(module, + getAvroSourceDir(project, testSourceSet), + getGeneratedOutputDir(project, testSourceSet, Constants.JAVA_EXTENSION).map(Directory::getAsFile).get() + ); + // IntelliJ doesn't allow source directories beneath an excluded directory. + // Thus, we remove the build directory exclude and add all non-generated sub-directories as excludes. + SetBuilder excludeDirs = new SetBuilder<>(); + excludeDirs.addAll(module.getExcludeDirs()).remove(project.getBuildDir()); + File buildDir = project.getBuildDir(); + if (buildDir.isDirectory()) { + excludeDirs.addAll(project.getBuildDir().listFiles(new NonGeneratedDirectoryFileFilter())); + } + module.setExcludeDirs(excludeDirs.build()); + }); + project.getTasks().withType(GenerateIdeaModule.class).configureEach(generateIdeaModule -> + generateIdeaModule.doFirst(task -> + project.getTasks().withType(GenerateAvroJavaTask.class, generateAvroJavaTask -> + project.mkdir(generateAvroJavaTask.getOutputDir().get())))); + } + + private static TaskProvider configureProtocolGenerationTask(final Project project, + final SourceSet sourceSet) { + String taskName = sourceSet.getTaskName("generate", "avroProtocol"); + return project.getTasks().register(taskName, GenerateAvroProtocolTask.class, task -> { + task.setDescription( + String.format("Generates %s Avro protocol definition files from IDL files.", sourceSet.getName())); + task.setGroup(Constants.GROUP_SOURCE_GENERATION); + task.source(getAvroSourceDir(project, sourceSet)); + task.include("**/*." + Constants.IDL_EXTENSION); + task.setClasspath(project.getConfigurations().getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME)); + task.getOutputDir().convention(getGeneratedOutputDir(project, sourceSet, Constants.PROTOCOL_EXTENSION)); + }); + } + + private static TaskProvider configureJavaGenerationTask(final Project project, final SourceSet sourceSet, + TaskProvider protoTaskProvider) { + String taskName = sourceSet.getTaskName("generate", "avroJava"); + TaskProvider javaTaskProvider = project.getTasks().register(taskName, GenerateAvroJavaTask.class, task -> { + task.setDescription(String.format("Generates %s Avro Java source files from schema/protocol definition files.", + sourceSet.getName())); + task.setGroup(Constants.GROUP_SOURCE_GENERATION); + task.source(getAvroSourceDir(project, sourceSet)); + task.source(protoTaskProvider); + task.include("**/*." + Constants.SCHEMA_EXTENSION, "**/*." + Constants.PROTOCOL_EXTENSION); + task.getOutputDir().convention(getGeneratedOutputDir(project, sourceSet, Constants.JAVA_EXTENSION)); + + sourceSet.getJava().srcDir(task.getOutputDir()); + + JavaCompile compileJavaTask = project.getTasks().named(sourceSet.getCompileJavaTaskName(), JavaCompile.class).get(); + task.getOutputCharacterEncoding().convention(project.provider(() -> + Optional.ofNullable(compileJavaTask.getOptions().getEncoding()).orElse(Charset.defaultCharset().name()))); + }); + project.getTasks().named(sourceSet.getCompileJavaTaskName(), JavaCompile.class, compileJavaTask -> { + compileJavaTask.source(javaTaskProvider); + }); + // When the Gradle's JVM plugin's withSourcesJar capability is used, it automatically includes all directories listed in the + // SourceSet's `allSource`. However, in Gradle 7.1, they started including a warning that execution optimizations are disabled + // unless you explicitly declare what task produced the directory you're using. Gradle doesn't currently have a way to declare a + // source directory and the task that creates it, so for now we need to manually declare the task dependency. + project.getTasks() + .matching(task -> GradleCompatibility.getSourcesJarTaskName(sourceSet).equals(task.getName())) + .configureEach(sourcesJarTask -> sourcesJarTask.dependsOn(javaTaskProvider)); + return javaTaskProvider; + } + + private static void configureTaskDependencies(final Project project, final SourceSet sourceSet, + final TaskProvider javaTaskProvider) { + project.getPluginManager().withPlugin("org.jetbrains.kotlin.jvm", appliedPlugin -> + project.getTasks() + .withType(SourceTask.class) + .matching(task -> sourceSet.getCompileTaskName("kotlin").equals(task.getName())) + .configureEach(task -> + task.source(javaTaskProvider.get().getOutputs()) + ) + ); + } + + private static File getAvroSourceDir(Project project, SourceSet sourceSet) { + return project.file(String.format("src/%s/avro", sourceSet.getName())); + } + + private static Provider getGeneratedOutputDir(Project project, SourceSet sourceSet, String extension) { + String generatedOutputDirName = String.format("generated-%s-avro-%s", sourceSet.getName(), extension); + return project.getLayout().getBuildDirectory().dir(generatedOutputDirName); + } + + private static SourceSetContainer getSourceSets(Project project) { + return project.getExtensions().getByType(SourceSetContainer.class); + } + + private static SourceSet getMainSourceSet(Project project) { + return getSourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME); + } + + private static SourceSet getTestSourceSet(Project project) { + return getSourceSets(project).getByName(SourceSet.TEST_SOURCE_SET_NAME); + } + + private static class NonGeneratedDirectoryFileFilter implements FileFilter { + @Override + public boolean accept(File file) { + return file.isDirectory() && !file.getName().startsWith("generated-"); + } + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroUtils.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroUtils.java new file mode 100644 index 00000000000..a753ffc7ab2 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/AvroUtils.java @@ -0,0 +1,70 @@ +package com.github.davidmc24.gradle.plugin.avro; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import org.apache.avro.Protocol; +import org.apache.avro.Schema; + +/** + * Utility method for working with Avro objects. + */ +class AvroUtils { + /** + * The namespace separator. + */ + private static final String NAMESPACE_SEPARATOR = "."; + + /** + * The extension separator. + */ + private static final String EXTENSION_SEPARATOR = "."; + + /** + * The Unix separator. + */ + private static final String UNIX_SEPARATOR = "/"; + + /** + * Not intended for instantiation. + */ + private AvroUtils() { } + + /** + * Assembles a file path based on the namespace and name of the provided {@link Schema}. + * + * @param schema the schema for which to assemble a path + * @return a file path + */ + static String assemblePath(Schema schema) { + return assemblePath(schema.getNamespace(), schema.getName(), Constants.SCHEMA_EXTENSION); + } + + /** + * Assembles a file path based on the namespace and name of the provided {@link Protocol}. + * + * @param protocol the protocol for which to assemble a path + * @return a file path + */ + static String assemblePath(Protocol protocol) { + return assemblePath(protocol.getNamespace(), protocol.getName(), Constants.PROTOCOL_EXTENSION); + } + + /** + * Assembles a file path based on the provided arguments. + * + * @param namespace the namespace for the path; may be null + * @param name the name for the path; will result in an exception if null or empty + * @param extension the extension for the path + * @return the assembled path + */ + private static String assemblePath(String namespace, String name, String extension) { + Strings.requireNotEmpty(name, "Path cannot be assembled for nameless objects"); + List parts = new ArrayList<>(); + if (Strings.isNotEmpty(namespace)) { + parts.add(namespace.replaceAll(Pattern.quote(NAMESPACE_SEPARATOR), UNIX_SEPARATOR)); + } + parts.add(name + EXTENSION_SEPARATOR + extension); + return String.join(UNIX_SEPARATOR, parts); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Constants.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Constants.java new file mode 100644 index 00000000000..919ec35326a --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Constants.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2013-2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.avro.Conversion; +import org.apache.avro.LogicalTypes; +import org.apache.avro.compiler.specific.SpecificCompiler.FieldVisibility; +import org.apache.avro.generic.GenericData.StringType; +import org.gradle.api.reflect.TypeOf; + +/** + * Various constants needed by the plugin. + * + *

The default values from {@code avro-compiler} aren't exposed in a way that's easily accessible, so even default + * values that we want to match are still reproduced here.

+ */ +class Constants { + static final String UTF8_ENCODING = "UTF-8"; + + static final String DEFAULT_STRING_TYPE = StringType.String.name(); + static final String DEFAULT_FIELD_VISIBILITY = FieldVisibility.PRIVATE.name(); + static final boolean DEFAULT_CREATE_SETTERS = true; + static final boolean DEFAULT_CREATE_OPTIONAL_GETTERS = false; + static final boolean DEFAULT_GETTERS_RETURN_OPTIONAL = false; + static final boolean DEFAULT_OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY = false; + static final boolean DEFAULT_ENABLE_DECIMAL_LOGICAL_TYPE = true; + static final Map> DEFAULT_LOGICAL_TYPE_FACTORIES = Collections.emptyMap(); + static final Map DEFAULT_LOGICAL_TYPE_FACTORY_CLASS_NAMES = Collections.emptyMap(); + static final List>> DEFAULT_CUSTOM_CONVERSIONS = Collections.emptyList(); + static final List DEFAULT_CUSTOM_CONVERSION_CLASS_NAMES = Collections.emptyList(); + + static final String SCHEMA_EXTENSION = "avsc"; + static final String PROTOCOL_EXTENSION = "avpr"; + static final String IDL_EXTENSION = "avdl"; + static final String JAVA_EXTENSION = "java"; + + static final String GROUP_SOURCE_GENERATION = "Source Generation"; + + static final String AVRO_EXTENSION_NAME = "avro"; + + static final String OPTION_FIELD_VISIBILITY = "fieldVisibility"; + static final String OPTION_STRING_TYPE = "stringType"; + + static final TypeOf> LOGICAL_TYPE_FACTORY_TYPE = + new TypeOf>() { }; + + static final TypeOf>> CONVERSION_TYPE = + new TypeOf>>() { }; +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/DefaultAvroExtension.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/DefaultAvroExtension.java new file mode 100644 index 00000000000..2182da4160b --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/DefaultAvroExtension.java @@ -0,0 +1,318 @@ +/** + * Copyright © 2013-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import org.apache.avro.Conversion; +import org.apache.avro.LogicalTypes; +import org.apache.avro.compiler.specific.SpecificCompiler; +import org.apache.avro.generic.GenericData; +import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class DefaultAvroExtension implements AvroExtension { + private final Property outputCharacterEncoding; + private final Property stringType; + private final Property fieldVisibility; + private final Property templateDirectory; + private final ListProperty additionalVelocityToolClasses; + private final Property createSetters; + private final Property createOptionalGetters; + private final Property gettersReturnOptional; + private final Property optionalGettersForNullableFieldsOnly; + private final Property enableDecimalLogicalType; + private final ConfigurableFileCollection conversionsAndTypeFactoriesClasspath; + private final MapProperty> logicalTypeFactories; + private final MapProperty logicalTypeFactoryClassNames; + private final ListProperty>> customConversions; + private final ListProperty customConversionClassNames; + + @Inject + public DefaultAvroExtension(Project project, ObjectFactory objects) { + this.outputCharacterEncoding = objects.property(String.class); + this.stringType = objects.property(String.class).convention(Constants.DEFAULT_STRING_TYPE); + this.fieldVisibility = objects.property(String.class).convention(Constants.DEFAULT_FIELD_VISIBILITY); + this.templateDirectory = objects.property(String.class); + this.additionalVelocityToolClasses = + objects.listProperty(String.class).convention(Collections.emptyList()); + this.createSetters = objects.property(Boolean.class).convention(Constants.DEFAULT_CREATE_SETTERS); + this.createOptionalGetters = objects.property(Boolean.class).convention(Constants.DEFAULT_CREATE_OPTIONAL_GETTERS); + this.gettersReturnOptional = objects.property(Boolean.class).convention(Constants.DEFAULT_GETTERS_RETURN_OPTIONAL); + this.optionalGettersForNullableFieldsOnly = objects.property(Boolean.class) + .convention(Constants.DEFAULT_OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY); + this.enableDecimalLogicalType = objects.property(Boolean.class).convention(Constants.DEFAULT_ENABLE_DECIMAL_LOGICAL_TYPE); + this.conversionsAndTypeFactoriesClasspath = GradleCompatibility.createConfigurableFileCollection(project); + this.logicalTypeFactories = objects.mapProperty(String.class, Constants.LOGICAL_TYPE_FACTORY_TYPE.getConcreteClass()) + .convention(Constants.DEFAULT_LOGICAL_TYPE_FACTORIES); + this.logicalTypeFactoryClassNames = objects.mapProperty(String.class, String.class) + .convention(Constants.DEFAULT_LOGICAL_TYPE_FACTORY_CLASS_NAMES); + this.customConversions = + objects.listProperty(Constants.CONVERSION_TYPE.getConcreteClass()).convention(Constants.DEFAULT_CUSTOM_CONVERSIONS); + this.customConversionClassNames = + objects.listProperty(String.class).convention(Constants.DEFAULT_CUSTOM_CONVERSION_CLASS_NAMES); + } + + @Override + public Property getOutputCharacterEncoding() { + return outputCharacterEncoding; + } + + public void setOutputCharacterEncoding(String outputCharacterEncoding) { + this.outputCharacterEncoding.set(outputCharacterEncoding); + } + + public void setOutputCharacterEncoding(Charset outputCharacterEncoding) { + setOutputCharacterEncoding(outputCharacterEncoding.name()); + } + + @Override + public Property getStringType() { + return stringType; + } + + public void setStringType(String stringType) { + this.stringType.set(stringType); + } + + public void setStringType(GenericData.StringType stringType) { + setStringType(stringType.name()); + } + + @Override + public Property getFieldVisibility() { + return fieldVisibility; + } + + public void setFieldVisibility(String fieldVisibility) { + this.fieldVisibility.set(fieldVisibility); + } + + public void setFieldVisibility(SpecificCompiler.FieldVisibility fieldVisibility) { + setFieldVisibility(fieldVisibility.name()); + } + + @Override + public Property getTemplateDirectory() { + return templateDirectory; + } + + public void setTemplateDirectory(String templateDirectory) { + this.templateDirectory.set(templateDirectory); + } + + @Optional + @Input + public ListProperty getAdditionalVelocityToolClasses() { + return additionalVelocityToolClasses; + } + + public void setAdditionalVelocityToolClasses(List additionalVelocityToolClasses) { + this.additionalVelocityToolClasses.set(additionalVelocityToolClasses); + } + + @Override + public Property isCreateSetters() { + return createSetters; + } + + public void setCreateSetters(String createSetters) { + setCreateSetters(Boolean.parseBoolean(createSetters)); + } + + public void setCreateSetters(boolean createSetters) { + this.createSetters.set(createSetters); + } + + @Override + public Property isCreateOptionalGetters() { + return createOptionalGetters; + } + + public void setCreateOptionalGetters(String createOptionalGetters) { + setCreateOptionalGetters(Boolean.parseBoolean(createOptionalGetters)); + } + + public void setCreateOptionalGetters(boolean createOptionalGetters) { + this.createOptionalGetters.set(createOptionalGetters); + } + + @Override + public Property isGettersReturnOptional() { + return gettersReturnOptional; + } + + public void setGettersReturnOptional(String gettersReturnOptional) { + setGettersReturnOptional(Boolean.parseBoolean(gettersReturnOptional)); + } + + public void setGettersReturnOptional(boolean gettersReturnOptional) { + this.gettersReturnOptional.set(gettersReturnOptional); + } + + @Override + public Property isOptionalGettersForNullableFieldsOnly() { + return optionalGettersForNullableFieldsOnly; + } + + public void setOptionalGettersForNullableFieldsOnly(String optionalGettersForNullableFieldsOnly) { + setOptionalGettersForNullableFieldsOnly(Boolean.parseBoolean(optionalGettersForNullableFieldsOnly)); + } + + public void setOptionalGettersForNullableFieldsOnly(boolean optionalGettersForNullableFieldsOnly) { + this.optionalGettersForNullableFieldsOnly.set(optionalGettersForNullableFieldsOnly); + } + + @Override + public Property isEnableDecimalLogicalType() { + return enableDecimalLogicalType; + } + + public void setEnableDecimalLogicalType(String enableDecimalLogicalType) { + setEnableDecimalLogicalType(Boolean.parseBoolean(enableDecimalLogicalType)); + } + + public void setEnableDecimalLogicalType(boolean enableDecimalLogicalType) { + this.enableDecimalLogicalType.set(enableDecimalLogicalType); + } + + @Override + public ConfigurableFileCollection getConversionsAndTypeFactoriesClasspath() { + return conversionsAndTypeFactoriesClasspath; + } + + /** + * @deprecated use {@link #getLogicalTypeFactoryClassNames()} instead + */ + @Deprecated + @Override + public MapProperty> getLogicalTypeFactories() { + return logicalTypeFactories; + } + + /** + * @deprecated use {@link #setLogicalTypeFactoryClassNames(Provider)} ()} instead + */ + @Deprecated + public void setLogicalTypeFactories(Provider>> provider) { + this.logicalTypeFactories.set(provider); + } + + /** + * @deprecated use {@link #setLogicalTypeFactoryClassNames(Map)} ()} instead + */ + @Deprecated + public void setLogicalTypeFactories(Map> logicalTypeFactories) { + this.logicalTypeFactories.set(logicalTypeFactories); + } + + @Override + public MapProperty getLogicalTypeFactoryClassNames() { + return logicalTypeFactoryClassNames; + } + + public void setLogicalTypeFactoryClassNames(Provider> provider) { + this.logicalTypeFactoryClassNames.set(provider); + } + + public void setLogicalTypeFactoryClassNames(Map logicalTypeFactoryClassNames) { + this.logicalTypeFactoryClassNames.set(logicalTypeFactoryClassNames); + } + + /** + * @deprecated use {@link #getCustomConversionClassNames()} instead + */ + @Deprecated + @Override + public ListProperty>> getCustomConversions() { + return customConversions; + } + + /** + * @deprecated use {@link #setCustomConversionClassNames(Provider)} ()} instead + */ + @Deprecated + public void setCustomConversions(Provider>>> provider) { + this.customConversions.set(provider); + } + + /** + * @deprecated use {@link #setCustomConversionClassNames(Iterable)} instead + */ + @Deprecated + public void setCustomConversions(Iterable>> customConversions) { + this.customConversions.set(customConversions); + } + + public ListProperty getCustomConversionClassNames() { + return customConversionClassNames; + } + + public void setCustomConversionClassNames(Provider> provider) { + this.customConversionClassNames.set(provider); + } + + public void setCustomConversionClassNames(Iterable customConversionClassNames) { + this.customConversionClassNames.set(customConversionClassNames); + } + + /** + * @deprecated use {@link #logicalTypeFactory(String, String)} ()} instead + */ + @Deprecated + @Override + public AvroExtension logicalTypeFactory(String typeName, Class typeFactoryClass) { + logicalTypeFactories.put(typeName, typeFactoryClass); + return this; + } + + @Override + public AvroExtension logicalTypeFactory(String typeName, String typeFactoryClassName) { + logicalTypeFactoryClassNames.put(typeName, typeFactoryClassName); + return this; + } + + /** + * @deprecated use {@link #customConversion(String)} ()} instead + */ + @Deprecated + @Override + public AvroExtension customConversion(Class> conversionClass) { + customConversions.add(conversionClass); + return this; + } + + @Override + public AvroExtension customConversion(String conversionClassName) { + customConversionClassNames.add(conversionClassName); + return this; + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Enums.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Enums.java new file mode 100644 index 00000000000..252e7f3f6ad --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Enums.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.util.Arrays; + +class Enums { + static > T parseCaseInsensitive(String label, T[] values, String input) { + for (T value : values) { + if (value.name().equalsIgnoreCase(input)) { + return value; + } + } + throw new IllegalArgumentException(String.format("Invalid %s '%s'. Value values are: %s", + label, input, Arrays.asList(values))); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileExtensionSpec.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileExtensionSpec.java new file mode 100644 index 00000000000..a9545a86d85 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileExtensionSpec.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2013-2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.gradle.api.specs.Spec; + +class FileExtensionSpec implements Spec { + private final Set extensions; + + FileExtensionSpec(String... extensions) { + this.extensions = new HashSet<>(Arrays.asList(extensions)); + } + + FileExtensionSpec(Collection extensions) { + this.extensions = new HashSet<>(extensions); + } + + @Override + public boolean isSatisfiedBy(File file) { + return extensions.contains(FilenameUtils.getExtension(file.getName())); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileState.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileState.java new file mode 100644 index 00000000000..e65243ff889 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileState.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.util.Set; +import java.util.TreeSet; + +class FileState implements Comparable { + private final File file; + private final String path; + private String errorMessage; + private Set duplicateTypeNames = new TreeSet<>(); + + FileState(File file, String path) { + this.file = file; + this.path = path; + } + + File getFile() { + return file; + } + + Set getDuplicateTypeNames() { + return duplicateTypeNames; + } + + void clearError() { + errorMessage = null; + } + + void setError(Throwable ex) { + this.errorMessage = ex.getMessage(); + } + + void addDuplicateTypeName(String typeName) { + duplicateTypeNames.add(typeName); + } + + public boolean containsDuplicateTypeName(String typeName) { + return duplicateTypeNames.contains(typeName); + } + + public String getPath() { + return path; + } + + public String getErrorMessage() { + return errorMessage; + } + + @Override + public int compareTo(FileState o) { + return path.compareTo(o.getPath()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FileState fileState = (FileState) o; + return path.equals(fileState.path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileUtils.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileUtils.java new file mode 100644 index 00000000000..a7266ead0bc --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FileUtils.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import org.gradle.api.file.ProjectLayout; + +/** + * General file manipulation utilities. + * + *

This copy from Apache Commons IO has been pruned down to just what is needed for gradle-avro-plugin.

+ * + *

+ * Facilities are provided in the following areas: + *

    + *
  • writing to a file + *
  • reading from a file + *
  • make a directory including parent directories + *
  • copying files and directories + *
  • deleting files and directories + *
  • converting to and from a URL + *
  • listing files and directories by filter and extension + *
  • comparing file content + *
  • file last changed date + *
  • calculating a checksum + *
+ *

+ * Origin of code: Excalibur, Alexandria, Commons-Utils + * + * @author Kevin A. Burton + * @author Scott Sanders + * @author Daniel Rall + * @author Christoph.Reck + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Jeremias Maerki + * @author Stephen Colebourne + * @author Ian Springer + * @author Chris Eldredge + * @author Jim Harrington + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id: FileUtils.java 507684 2007-02-14 20:38:25Z bayard $ + */ +class FileUtils { + /** + * Opens a {@link FileOutputStream} for the specified file, checking and + * creating the parent directory if it does not exist. + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * The parent directory will be created if it does not exist. + * The file will be created if it does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be written to. + * An exception is thrown if the parent directory cannot be created. + * + * @param file the file to open for output, must not be null + * @return a new {@link FileOutputStream} for the specified file + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be written to + * @throws IOException if a parent directory needs creating but that fails + * @since Commons IO 1.3 + */ + private static FileOutputStream openOutputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (!file.canWrite()) { + throw new IOException("File '" + file + "' cannot be written to"); + } + } else { + File parent = file.getParentFile(); + if (parent != null && !parent.exists()) { + if (!parent.mkdirs()) { + throw new IOException("File '" + file + "' could not be created"); + } + } + } + return new FileOutputStream(file); + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, null means platform default + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + */ + @SuppressWarnings("SameParameterValue") + private static void writeStringToFile(File file, String data, String encoding) throws IOException { + if (encoding == null) { + throw new IllegalArgumentException("Must specify encoding"); + } + try (OutputStream out = openOutputStream(file)) { + if (data != null) { + out.write(data.getBytes(encoding)); + } + } + } + + /** + * Writes a file in a manner appropriate for a JSON file. UTF-8 will be used, as it is the default encoding for JSON, and should be + * maximally interoperable. + * + * @see JSON Character Encoding + */ + static void writeJsonFile(File file, String data) throws IOException { + writeStringToFile(file, data, Constants.UTF8_ENCODING); + } + + /** + * Acts as a replacement for {@link org.gradle.api.Project#relativePath(Object)}, as Configuration Cache support doesn't allow + * maintaining references to the {@link org.gradle.api.Project}. + */ + static String projectRelativePath(ProjectLayout projectLayout, File file) { + Path path = file.toPath(); + if (path.isAbsolute()) { + Path projectDirectoryPath = projectLayout.getProjectDirectory().getAsFile().toPath(); + path = projectDirectoryPath.relativize(path); + } else { + path = file.toPath(); + } + return path.toString(); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FilenameUtils.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FilenameUtils.java new file mode 100644 index 00000000000..dae1e0c38b6 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/FilenameUtils.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.github.davidmc24.gradle.plugin.avro; + +/** + * General filename and filepath manipulation utilities. + * + *

This copy from Apache Commons IO has been pruned down to just what is needed for gradle-avro-plugin.

+ * + *

+ * When dealing with filenames you can hit problems when moving from a Windows + * based development machine to a Unix based production machine. + * This class aims to help avoid those problems. + *

+ * NOTE: You may be able to avoid using this class entirely simply by + * using JDK {@link java.io.File File} objects and the two argument constructor + * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}. + *

+ * Most methods on this class are designed to work the same on both Unix and Windows. + * Those that don't include 'System', 'Unix' or 'Windows' in their name. + *

+ * Most methods recognise both separators (forward and back), and both + * sets of prefixes. See the javadoc of each method for details. + *

+ * This class defines six components within a filename + * (example C:\dev\project\file.txt): + *

    + *
  • the prefix - C:\
  • + *
  • the path - dev\project\
  • + *
  • the full path - C:\dev\project\
  • + *
  • the name - file.txt
  • + *
  • the base name - file
  • + *
  • the extension - txt
  • + *
+ * Note that this class works best if directory filenames end with a separator. + * If you omit the last separator, it is impossible to determine if the filename + * corresponds to a file or a directory. As a result, we have chosen to say + * it corresponds to a file. + *

+ * This class only supports Unix and Windows style names. + * Prefixes are matched as follows: + *

+ * Windows:
+ * a\b\c.txt           --> ""          --> relative
+ * \a\b\c.txt          --> "\"         --> current drive absolute
+ * C:a\b\c.txt         --> "C:"        --> drive relative
+ * C:\a\b\c.txt        --> "C:\"       --> absolute
+ * \\server\a\b\c.txt  --> "\\server\" --> UNC
+ *
+ * Unix:
+ * a/b/c.txt           --> ""          --> relative
+ * /a/b/c.txt          --> "/"         --> absolute
+ * ~/a/b/c.txt         --> "~/"        --> current user
+ * ~                   --> "~/"        --> current user (slash added)
+ * ~user/a/b/c.txt     --> "~user/"    --> named user
+ * ~user               --> "~user/"    --> named user (slash added)
+ * 
+ * Both prefix styles are matched always, irrespective of the machine that you are + * currently running on. + *

+ * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils. + * + * @author Kevin A. Burton + * @author Scott Sanders + * @author Daniel Rall + * @author Christoph.Reck + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Martin Cooper + * @author Jeremias Maerki + * @author Stephen Colebourne + * @version $Id: FilenameUtils.java 490424 2006-12-27 01:20:43Z bayard $ + * @since Commons IO 1.1 + */ +class FilenameUtils { + /** + * The extension separator character. + */ + private static final char EXTENSION_SEPARATOR = '.'; + + /** + * The Unix separator character. + */ + private static final char UNIX_SEPARATOR = '/'; + + /** + * The Windows separator character. + */ + private static final char WINDOWS_SEPARATOR = '\\'; + + /** + * Returns the index of the last directory separator character. + *

+ * This method will handle a file in either Unix or Windows format. + * The position of the last forward or backslash is returned. + *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to find the last path separator in, null returns -1 + * @return the index of the last separator character, or -1 if there + * is no such character + */ + private static int indexOfLastSeparator(String filename) { + if (filename == null) { + return -1; + } + int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR); + int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR); + return Math.max(lastUnixPos, lastWindowsPos); + } + + /** + * Returns the index of the last extension separator character, which is a dot. + *

+ * This method also checks that there is no directory separator after the last dot. + * To do this it uses {@link #indexOfLastSeparator(String)} which will + * handle a file in either Unix or Windows format. + *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to find the last path separator in, null returns -1 + * @return the index of the last separator character, or -1 if there + * is no such character + */ + private static int indexOfExtension(String filename) { + if (filename == null) { + return -1; + } + int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR); + int lastSeparator = indexOfLastSeparator(filename); + return lastSeparator > extensionPos ? -1 : extensionPos; + } + + /** + * Gets the name minus the path from a full filename. + *

+ * This method will handle a file in either Unix or Windows format. + * The text after the last forward or backslash is returned. + *

+     * a/b/c.txt --> c.txt
+     * a.txt     --> a.txt
+     * a/b/c     --> c
+     * a/b/c/    --> ""
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the name of the file without the path, or an empty string if none exists + */ + private static String getName(String filename) { + if (filename == null) { + return null; + } + int index = indexOfLastSeparator(filename); + return filename.substring(index + 1); + } + + /** + * Gets the base name, minus the full path and extension, from a full filename. + *

+ * This method will handle a file in either Unix or Windows format. + * The text after the last forward or backslash and before the last dot is returned. + *

+     * a/b/c.txt --> c
+     * a.txt     --> a
+     * a/b/c     --> c
+     * a/b/c/    --> ""
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the name of the file without the path, or an empty string if none exists + */ + static String getBaseName(String filename) { + return removeExtension(getName(filename)); + } + + /** + * Gets the extension of a filename. + *

+ * This method returns the textual part of the filename after the last dot. + * There must be no directory separator after the dot. + *

+     * foo.txt      --> "txt"
+     * a/b/c.jpg    --> "jpg"
+     * a/b.txt/c    --> ""
+     * a/b/c        --> ""
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to retrieve the extension of. + * @return the extension of the file or an empty string if none exists. + */ + static String getExtension(String filename) { + if (filename == null) { + return null; + } + int index = indexOfExtension(filename); + if (index == -1) { + return ""; + } else { + return filename.substring(index + 1); + } + } + + //----------------------------------------------------------------------- + /** + * Removes the extension from a filename. + *

+ * This method returns the textual part of the filename before the last dot. + * There must be no directory separator after the dot. + *

+     * foo.txt    --> foo
+     * a\b\c.jpg  --> a\b\c
+     * a\b\c      --> a\b\c
+     * a.b\c      --> a.b\c
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the filename minus the extension + */ + private static String removeExtension(String filename) { + if (filename == null) { + return null; + } + int index = indexOfExtension(filename); + if (index == -1) { + return filename; + } else { + return filename.substring(0, index); + } + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroJavaTask.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroJavaTask.java new file mode 100644 index 00000000000..a9fe3796d24 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroJavaTask.java @@ -0,0 +1,554 @@ +/** + * Copyright © 2013-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.apache.avro.Conversion; +import org.apache.avro.LogicalTypes; +import org.apache.avro.LogicalTypes.LogicalTypeFactory; +import org.apache.avro.Protocol; +import org.apache.avro.Schema; +import org.apache.avro.compiler.specific.SpecificCompiler; +import org.apache.avro.compiler.specific.SpecificCompiler.FieldVisibility; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericData.StringType; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.specs.NotSpec; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; + +/** + * Task to generate Java source files based on Avro protocol files and Avro schema files using {@link Protocol} and + * {@link SpecificCompiler}. + */ +@SuppressWarnings("WeakerAccess") +@CacheableTask +public class GenerateAvroJavaTask extends OutputDirTask { + private static Set SUPPORTED_EXTENSIONS = + new SetBuilder().add(Constants.PROTOCOL_EXTENSION).add(Constants.SCHEMA_EXTENSION).build(); + + private final Property outputCharacterEncoding; + private final Property stringType; + private final Property fieldVisibility; + private final Property templateDirectory; + private final ListProperty additionalVelocityToolClasses; + private final Property createOptionalGetters; + private final Property gettersReturnOptional; + private final Property optionalGettersForNullableFieldsOnly; + private final Property createSetters; + private final Property enableDecimalLogicalType; + private FileCollection classpath; + private final ConfigurableFileCollection conversionsAndTypeFactoriesClasspath; + private final MapProperty> logicalTypeFactories; + private final MapProperty logicalTypeFactoryClassNames; + private final ListProperty>> customConversions; + private final ListProperty customConversionClassNames; + + private final Provider stringTypeProvider; + private final Provider fieldVisibilityProvider; + + private final ProjectLayout projectLayout; + private final SchemaResolver resolver; + + @Inject + public GenerateAvroJavaTask(ObjectFactory objects) { + super(); + this.outputCharacterEncoding = objects.property(String.class); + this.stringType = objects.property(String.class).convention(Constants.DEFAULT_STRING_TYPE); + this.fieldVisibility = objects.property(String.class).convention(Constants.DEFAULT_FIELD_VISIBILITY); + this.templateDirectory = objects.property(String.class); + this.additionalVelocityToolClasses = + objects.listProperty(String.class).convention(Collections.emptyList()); + this.createOptionalGetters = objects.property(Boolean.class).convention(Constants.DEFAULT_CREATE_OPTIONAL_GETTERS); + this.gettersReturnOptional = objects.property(Boolean.class).convention(Constants.DEFAULT_GETTERS_RETURN_OPTIONAL); + this.optionalGettersForNullableFieldsOnly = objects.property(Boolean.class) + .convention(Constants.DEFAULT_OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY); + this.createSetters = objects.property(Boolean.class).convention(Constants.DEFAULT_CREATE_SETTERS); + this.enableDecimalLogicalType = objects.property(Boolean.class).convention(Constants.DEFAULT_ENABLE_DECIMAL_LOGICAL_TYPE); + this.classpath = GradleCompatibility.createConfigurableFileCollection(getProject()); + this.conversionsAndTypeFactoriesClasspath = GradleCompatibility.createConfigurableFileCollection(getProject()); + this.logicalTypeFactories = objects.mapProperty(String.class, Constants.LOGICAL_TYPE_FACTORY_TYPE.getConcreteClass()) + .convention(Constants.DEFAULT_LOGICAL_TYPE_FACTORIES); + this.logicalTypeFactoryClassNames = objects.mapProperty(String.class, String.class) + .convention(Constants.DEFAULT_LOGICAL_TYPE_FACTORY_CLASS_NAMES); + this.customConversions = + objects.listProperty(Constants.CONVERSION_TYPE.getConcreteClass()).convention(Constants.DEFAULT_CUSTOM_CONVERSIONS); + this.customConversionClassNames = + objects.listProperty(String.class).convention(Constants.DEFAULT_CUSTOM_CONVERSION_CLASS_NAMES); + this.stringTypeProvider = getStringType() + .map(input -> Enums.parseCaseInsensitive(Constants.OPTION_STRING_TYPE, StringType.values(), input)); + this.fieldVisibilityProvider = getFieldVisibility() + .map(input -> Enums.parseCaseInsensitive(Constants.OPTION_FIELD_VISIBILITY, FieldVisibility.values(), input)); + this.projectLayout = getProject().getLayout(); + this.resolver = new SchemaResolver(projectLayout, getLogger()); + } + + public void setClasspath(FileCollection classpath) { + this.classpath = classpath; + } + + public void classpath(Object... paths) { + this.classpath.plus(getProject().files(paths)); + } + + @Classpath + public FileCollection getClasspath() { + return this.classpath; + } + + @Optional + @Input + public Property getOutputCharacterEncoding() { + return outputCharacterEncoding; + } + + public void setOutputCharacterEncoding(String outputCharacterEncoding) { + this.outputCharacterEncoding.set(outputCharacterEncoding); + } + + public void setOutputCharacterEncoding(Charset outputCharacterEncoding) { + setOutputCharacterEncoding(outputCharacterEncoding.name()); + } + + @Input + public Property getStringType() { + return stringType; + } + + public void setStringType(GenericData.StringType stringType) { + setStringType(stringType.name()); + } + + public void setStringType(String stringType) { + this.stringType.set(stringType); + } + + @Input + public Property getFieldVisibility() { + return fieldVisibility; + } + + public void setFieldVisibility(String fieldVisibility) { + this.fieldVisibility.set(fieldVisibility); + } + + public void setFieldVisibility(SpecificCompiler.FieldVisibility fieldVisibility) { + setFieldVisibility(fieldVisibility.name()); + } + + @Optional + @Input + public Property getTemplateDirectory() { + return templateDirectory; + } + + public void setTemplateDirectory(String templateDirectory) { + this.templateDirectory.set(templateDirectory); + } + + @Optional + @Input + public ListProperty getAdditionalVelocityToolClasses() { + return additionalVelocityToolClasses; + } + + public void setAdditionalVelocityToolClasses(List additionalVelocityToolClasses) { + this.additionalVelocityToolClasses.set(additionalVelocityToolClasses); + } + + public Property isCreateSetters() { + return createSetters; + } + + @Input + public Property getCreateSetters() { + return createSetters; + } + + public void setCreateSetters(String createSetters) { + this.createSetters.set(Boolean.parseBoolean(createSetters)); + } + + public Property isCreateOptionalGetters() { + return createOptionalGetters; + } + + @Input + public Property getCreateOptionalGetters() { + return createOptionalGetters; + } + + public void setCreateOptionalGetters(String createOptionalGetters) { + this.createOptionalGetters.set(Boolean.parseBoolean(createOptionalGetters)); + } + + public Property isGettersReturnOptional() { + return gettersReturnOptional; + } + + @Input + public Property getGettersReturnOptional() { + return gettersReturnOptional; + } + + public void setGettersReturnOptional(String gettersReturnOptional) { + this.gettersReturnOptional.set(Boolean.parseBoolean(gettersReturnOptional)); + } + + public Property isOptionalGettersForNullableFieldsOnly() { + return optionalGettersForNullableFieldsOnly; + } + + @Input + public Property getOptionalGettersForNullableFieldsOnly() { + return optionalGettersForNullableFieldsOnly; + } + + public void setOptionalGettersForNullableFieldsOnly(String optionalGettersForNullableFieldsOnly) { + this.optionalGettersForNullableFieldsOnly.set(Boolean.parseBoolean(optionalGettersForNullableFieldsOnly)); + } + + public Property isEnableDecimalLogicalType() { + return enableDecimalLogicalType; + } + + @Input + public Property getEnableDecimalLogicalType() { + return enableDecimalLogicalType; + } + + public void setEnableDecimalLogicalType(String enableDecimalLogicalType) { + this.enableDecimalLogicalType.set(Boolean.parseBoolean(enableDecimalLogicalType)); + } + + @Optional + @Classpath + public ConfigurableFileCollection getConversionsAndTypeFactoriesClasspath() { + return conversionsAndTypeFactoriesClasspath; + } + + /** + * @deprecated use {@link #getLogicalTypeFactoryClassNames()} ()} instead + */ + @Deprecated + @Optional + @Input + public MapProperty> getLogicalTypeFactories() { + return logicalTypeFactories; + } + + /** + * @deprecated use {@link #setLogicalTypeFactoryClassNames(Provider)} ()} instead + */ + @Deprecated + public void setLogicalTypeFactories(Provider>> provider) { + this.logicalTypeFactories.set(provider); + } + + /** + * @deprecated use {@link #setLogicalTypeFactoryClassNames(Map)} ()} instead + */ + @Deprecated + public void setLogicalTypeFactories(Map> logicalTypeFactories) { + this.logicalTypeFactories.set(logicalTypeFactories); + } + + @Input + @Optional + public MapProperty getLogicalTypeFactoryClassNames() { + return logicalTypeFactoryClassNames; + } + + public void setLogicalTypeFactoryClassNames(Provider> provider) { + this.logicalTypeFactoryClassNames.set(provider); + } + + public void setLogicalTypeFactoryClassNames(Map logicalTypeFactoryClassNames) { + this.logicalTypeFactoryClassNames.set(logicalTypeFactoryClassNames); + } + + /** + * @deprecated use {@link #getCustomConversions()} ()} instead + */ + @Deprecated + @Optional + @Input + public ListProperty>> getCustomConversions() { + return customConversions; + } + + /** + * @deprecated use {@link #setCustomConversionClassNames(Provider)} ()} instead + */ + @Deprecated + public void setCustomConversions(Provider>>> provider) { + this.customConversions.set(provider); + } + + /** + * @deprecated use {@link #setCustomConversionClassNames(Iterable)} ()} instead + */ + @Deprecated + public void setCustomConversions(Iterable>> customConversions) { + this.customConversions.set(customConversions); + } + + @Optional + @Input + public ListProperty getCustomConversionClassNames() { + return customConversionClassNames; + } + + public void setCustomConversionClassNames(Provider> provider) { + this.customConversionClassNames.set(provider); + } + + public void setCustomConversionClassNames(Iterable customConversionClassNames) { + this.customConversionClassNames.set(customConversionClassNames); + } + + @TaskAction + protected void process() { + getLogger().debug("Using outputCharacterEncoding {}", getOutputCharacterEncoding().getOrNull()); + getLogger().debug("Using stringType {}", stringTypeProvider.get().name()); + getLogger().debug("Using fieldVisibility {}", fieldVisibilityProvider.get().name()); + getLogger().debug("Using templateDirectory '{}'", getTemplateDirectory().getOrNull()); + getLogger().debug("Using additionalVelocityToolClasses '{}'", getAdditionalVelocityToolClasses().getOrNull()); + getLogger().debug("Using createSetters {}", isCreateSetters().get()); + getLogger().debug("Using createOptionalGetters {}", isCreateOptionalGetters().get()); + getLogger().debug("Using gettersReturnOptional {}", isGettersReturnOptional().get()); + getLogger().debug("Using optionalGettersForNullableFieldsOnly {}", isOptionalGettersForNullableFieldsOnly().get()); + getLogger().debug("Using enableDecimalLogicalType {}", isEnableDecimalLogicalType().get()); + getLogger().debug("Using logicalTypeFactories {}", + logicalTypeFactories.get().entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + (Map.Entry> e) -> e.getValue().getName() + ))); + getLogger().debug("Using customConversions {}", + customConversions.get().stream().map(v -> ((Class) v).getName()).collect(Collectors.toList())); + getLogger().info("Found {} files", getInputs().getSourceFiles().getFiles().size()); + failOnUnsupportedFiles(); + processFiles(); + } + + private void failOnUnsupportedFiles() { + FileCollection unsupportedFiles = filterSources(new NotSpec<>(new FileExtensionSpec(SUPPORTED_EXTENSIONS))); + if (!unsupportedFiles.isEmpty()) { + throw new GradleException( + String.format("Unsupported file extension for the following files: %s", unsupportedFiles)); + } + } + + private void processFiles() { + registerLogicalTypes(); + int processedFileCount = 0; + processedFileCount += processProtoFiles(); + processedFileCount += processSchemaFiles(); + setDidWork(processedFileCount > 0); + } + + private int processProtoFiles() { + int processedFileCount = 0; + for (File sourceFile : filterSources(new FileExtensionSpec(Constants.PROTOCOL_EXTENSION))) { + processProtoFile(sourceFile); + processedFileCount++; + } + return processedFileCount; + } + + private void processProtoFile(File sourceFile) { + getLogger().info("Processing {}", sourceFile); + try { + compile(new SpecificCompiler(Protocol.parse(sourceFile)), sourceFile); + } catch (IOException ex) { + throw new GradleException(String.format("Failed to compile protocol definition file %s", sourceFile), ex); + } + } + + private int processSchemaFiles() { + Set files = filterSources(new FileExtensionSpec(Constants.SCHEMA_EXTENSION)).getFiles(); + ProcessingState processingState = resolver.resolve(files); + for (File file : files) { + String path = FileUtils.projectRelativePath(projectLayout, file); + for (Schema schema : processingState.getSchemasForLocation(path)) { + try { + compile(new SpecificCompiler(schema), file); + } catch (IOException ex) { + throw new GradleException(String.format("Failed to compile schema definition file %s", path), ex); + } + } + } + return processingState.getProcessedTotal(); + } + + private void compile(SpecificCompiler compiler, File sourceFile) throws IOException { + compiler.setOutputCharacterEncoding(getOutputCharacterEncoding().getOrNull()); + compiler.setStringType(stringTypeProvider.get()); + compiler.setFieldVisibility(fieldVisibilityProvider.get()); + if (getTemplateDirectory().isPresent()) { + compiler.setTemplateDir(getTemplateDirectory().get()); + } + if (getAdditionalVelocityToolClasses().isPresent()) { + ClassLoader loader = assembleClassLoader(); + List tools = getAdditionalVelocityToolClasses().get().stream() + .map(s -> { + try { + return Class.forName(s, true, loader); + } catch (ClassNotFoundException e) { + throw new RuntimeException("unable to load velocity tool class " + s, e); + } + }) + .map(aClass -> { + try { + return aClass.getConstructor().newInstance(); + } catch (InstantiationException + | NoSuchMethodException + | InvocationTargetException + | IllegalAccessException e) { + throw new RuntimeException("Unable to instantiate velocity tool class using default constructor: " + aClass, e); + } + }).collect(Collectors.toList()); + compiler.setAdditionalVelocityTools(tools); + } + compiler.setCreateOptionalGetters(createOptionalGetters.get()); + compiler.setGettersReturnOptional(gettersReturnOptional.get()); + compiler.setOptionalGettersForNullableFieldsOnly(optionalGettersForNullableFieldsOnly.get()); + compiler.setCreateSetters(isCreateSetters().get()); + compiler.setEnableDecimalLogicalType(isEnableDecimalLogicalType().get()); + registerCustomConversions(compiler); + + compiler.compileToDestination(sourceFile, getOutputDir().get().getAsFile()); + } + + /** + * Registers the logical types to be used in this run. + * This must be called before the Schemas are parsed, or they will not be applied correctly. + * Since {@link LogicalTypes} is a static registry, this may result in side-effects. + */ + private void registerLogicalTypes() { + Map> logicalTypeFactoryMap = resolveLocalTypeFactories(); + Set>> logicalTypeFactoryEntries = + logicalTypeFactoryMap.entrySet(); + for (Map.Entry> entry : logicalTypeFactoryEntries) { + String logicalTypeName = entry.getKey(); + Class logicalTypeFactoryClass = entry.getValue(); + try { + LogicalTypes.LogicalTypeFactory logicalTypeFactory = logicalTypeFactoryClass.getDeclaredConstructor().newInstance(); + LogicalTypes.register(logicalTypeName, logicalTypeFactory); + } catch (ReflectiveOperationException ex) { + getLogger().error("Could not instantiate logicalTypeFactory class \"" + logicalTypeFactoryClass.getName() + "\""); + } + } + } + + @SuppressWarnings("unchecked") + private Map> resolveLocalTypeFactories() { + Map> result = new HashMap<>(); + if (logicalTypeFactoryClassNames.isPresent()) { + ClassLoader typeFactoriesClassLoader = createConversionsAndTypeFactoriesClassLoader(); + for (Entry entry : logicalTypeFactoryClassNames.get().entrySet()) { + String logicalTypeFactoryClassName = entry.getValue(); + try { + Class aClass = Class.forName(logicalTypeFactoryClassName, true, typeFactoriesClassLoader); + result.put(entry.getKey(), (Class) aClass); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load logical type factory class " + logicalTypeFactoryClassName, e); + } + } + } + result.putAll(logicalTypeFactories.get()); + return result; + } + + private void registerCustomConversions(SpecificCompiler compiler) { + loadCustomConversionClasses().forEach(compiler::addCustomConversion); + customConversions.get().forEach(compiler::addCustomConversion); + } + + private List> loadCustomConversionClasses() { + if (customConversionClassNames.isPresent()) { + ClassLoader customConversionsClassLoader = createConversionsAndTypeFactoriesClassLoader(); + return customConversionClassNames.get().stream() + .map(conversionClassName -> { + try { + return Class.forName(conversionClassName, true, customConversionsClassLoader); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load custom conversion class " + conversionClassName, e); + } + }).collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + private ClassLoader createConversionsAndTypeFactoriesClassLoader() { + URL[] urls = conversionsAndTypeFactoriesClasspath.getFiles().stream() + .map(File::toURI) + .map(uri -> { + try { + return uri.toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException("Unable to resolve URL in conversions and type factories classpath", e); + } + }) + .toArray(URL[]::new); + + return new URLClassLoader(urls, getClass().getClassLoader()); + } + + private ClassLoader assembleClassLoader() { + getLogger().debug("Using additional classpath: {}", classpath.getFiles()); + List urls = new LinkedList<>(); + for (File file : classpath) { + try { + urls.add(file.toURI().toURL()); + } catch (MalformedURLException e) { + getLogger().debug(e.getMessage()); + } + } + return new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader()); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroProtocolTask.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroProtocolTask.java new file mode 100644 index 00000000000..725a758ebda --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroProtocolTask.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2013-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import org.apache.avro.Protocol; +import org.apache.avro.compiler.idl.Idl; +import org.apache.avro.compiler.idl.ParseException; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; +import org.gradle.api.specs.NotSpec; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.TaskAction; + +import static com.github.davidmc24.gradle.plugin.avro.Constants.IDL_EXTENSION; + +/** + * Task to convert Avro IDL files into Avro protocol files using {@link Idl}. + */ +@CacheableTask +public class GenerateAvroProtocolTask extends OutputDirTask { + + private FileCollection classpath; + private Set processedFiles; + + public GenerateAvroProtocolTask() { + super(); + this.classpath = GradleCompatibility.createConfigurableFileCollection(getProject()); + this.processedFiles = new HashSet(); + } + + public void setClasspath(FileCollection classpath) { + this.classpath = classpath; + } + + public void classpath(Object... paths) { + this.classpath.plus(getProject().files(paths)); + } + + @Classpath + public FileCollection getClasspath() { + return this.classpath; + } + + @TaskAction + protected void process() { + getLogger().info("Found {} files", getSource().getFiles().size()); + failOnUnsupportedFiles(); + processFiles(); + } + + private void failOnUnsupportedFiles() { + FileCollection unsupportedFiles = filterSources(new NotSpec<>(new FileExtensionSpec(IDL_EXTENSION))); + if (!unsupportedFiles.isEmpty()) { + throw new GradleException( + String.format("Unsupported file extension for the following files: %s", unsupportedFiles)); + } + } + + private void processFiles() { + int processedFileCount = 0; + ClassLoader loader = assembleClassLoader(); + for (File sourceFile : filterSources(new FileExtensionSpec(IDL_EXTENSION))) { + processIDLFile(sourceFile, loader); + processedFileCount++; + } + setDidWork(processedFileCount > 0); + } + + private void processIDLFile(File idlFile, ClassLoader loader) { + getLogger().info("Processing {}", idlFile); + try (Idl idl = new Idl(idlFile, loader)) { + File outputDir = getOutputDir().get().getAsFile(); + Protocol protocol = idl.CompilationUnit(); + String filePath = AvroUtils.assemblePath(protocol); + if (!processedFiles.add(filePath)) { + throw new GradleException("File already processed with same namespace and protocol name."); + } + File protoFile = new File(outputDir, filePath); + String protoJson = protocol.toString(true); + FileUtils.writeJsonFile(protoFile, protoJson); + getLogger().debug("Wrote {}", protoFile.getPath()); + } catch (IOException | ParseException | GradleException ex) { + throw new GradleException(String.format("Failed to compile IDL file %s", idlFile), ex); + } + } + + private ClassLoader assembleClassLoader() { + getLogger().debug("Using classpath: {}", classpath.getFiles()); + List urls = new LinkedList<>(); + for (File file : classpath) { + try { + urls.add(file.toURI().toURL()); + } catch (MalformedURLException e) { + getLogger().debug(e.getMessage()); + } + } + // No parent classloader; either it's in the specified classpath or it shouldn't be resolved. + return new URLClassLoader(urls.toArray(new URL[0]), null); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroSchemaTask.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroSchemaTask.java new file mode 100644 index 00000000000..fe51733788b --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GenerateAvroSchemaTask.java @@ -0,0 +1,68 @@ +/* + * Copyright © 2018-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.io.IOException; +import org.apache.avro.Protocol; +import org.apache.avro.Schema; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; +import org.gradle.api.specs.NotSpec; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.TaskAction; + +@CacheableTask +public class GenerateAvroSchemaTask extends OutputDirTask { + @TaskAction + protected void process() { + getLogger().info("Found {} files", getSource().getFiles().size()); + failOnUnsupportedFiles(); + processFiles(); + } + + private void failOnUnsupportedFiles() { + FileCollection unsupportedFiles = filterSources(new NotSpec<>(new FileExtensionSpec(Constants.PROTOCOL_EXTENSION))); + if (!unsupportedFiles.isEmpty()) { + throw new GradleException( + String.format("Unsupported file extension for the following files: %s", unsupportedFiles)); + } + } + + private void processFiles() { + int processedFileCount = 0; + for (File sourceFile : filterSources(new FileExtensionSpec(Constants.PROTOCOL_EXTENSION))) { + processProtoFile(sourceFile); + processedFileCount++; + } + setDidWork(processedFileCount > 0); + } + + private void processProtoFile(File sourceFile) { + getLogger().info("Processing {}", sourceFile); + try { + Protocol protocol = Protocol.parse(sourceFile); + for (Schema schema : protocol.getTypes()) { + File schemaFile = new File(getOutputDir().get().getAsFile(), AvroUtils.assemblePath(schema)); + String schemaJson = schema.toString(true); + FileUtils.writeJsonFile(schemaFile, schemaJson); + getLogger().debug("Wrote {}", schemaFile.getPath()); + } + } catch (IOException ex) { + throw new GradleException(String.format("Failed to process protocol definition file %s", sourceFile), ex); + } + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleCompatibility.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleCompatibility.java new file mode 100644 index 00000000000..c62c983246e --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleCompatibility.java @@ -0,0 +1,84 @@ +/* + * Copyright © 2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.tasks.SourceSet; +import org.gradle.plugins.ide.idea.model.IdeaModule; + +class GradleCompatibility { + private static final Class[] NO_PARAMETERS = {}; + private static final Object[] NO_ARGUMENTS = {}; + + static T createExtensionWithObjectFactory(Project project, String extensionName, Class extensionType) { + if (GradleFeatures.projectIntoExtensionInjection.isSupported()) { + return project.getExtensions().create(extensionName, extensionType); + } else { + return project.getExtensions().create(extensionName, extensionType, project, project.getObjects()); + } + } + + @SuppressWarnings("deprecation") + static ConfigurableFileCollection createConfigurableFileCollection(Project project) { + if (GradleFeatures.objectFactoryFileCollection.isSupported()) { + return project.getObjects().fileCollection(); + } else { + Class[] parameterTypes = {Object[].class}; + Object[] args = {new Object[0]}; + return invokeMethod(project.getLayout(), "configurableFiles", parameterTypes, args); + } + } + + static String getSourcesJarTaskName(SourceSet sourceSet) { + if (GradleFeatures.getSourcesJarTaskName.isSupported()) { + return sourceSet.getSourcesJarTaskName(); + } else { + return sourceSet.getTaskName(null, "sourcesJar"); + } + } + + @SuppressWarnings("deprecation") + static void addTestSources(IdeaModule module, File... files) { + if (GradleFeatures.ideaModuleTestSources.isSupported()) { + // Can't use these methods directly as they didn't exist until 7.4 + Object testSources = invokeMethod(module, "getTestSources", NO_PARAMETERS, NO_ARGUMENTS); + Class[] parameterTypes = {Object[].class}; + Object[] args = {files}; + invokeMethod(testSources, "from", parameterTypes, args); + } else { + // Deprecated in 7.6 + module.setTestSourceDirs(new SetBuilder() + .addAll(module.getTestSourceDirs()) + .addAll(files) + .build()); + } + } + + @SuppressWarnings("unchecked") + private static T invokeMethod(Object object, String methodName, Class[] parameterTypes, Object[] args) { + try { + Method method = object.getClass().getMethod(methodName, parameterTypes); + return (T) method.invoke(object, args); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + throw new RuntimeException("Failed to invoke method via reflection", ex); + } + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleFeatures.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleFeatures.java new file mode 100644 index 00000000000..c918f962664 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleFeatures.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import org.gradle.util.GradleVersion; + +enum GradleFeatures { + projectIntoExtensionInjection() { + boolean isSupportedBy(GradleVersion version) { + return version.compareTo(GradleVersions.v7_1) >= 0; + } + }, + objectFactoryFileCollection() { + @Override + boolean isSupportedBy(GradleVersion version) { + return version.compareTo(GradleVersions.v5_3) >= 0; + } + }, + configCache() { + @Override + boolean isSupportedBy(GradleVersion version) { + return version.compareTo(GradleVersions.v6_6) >= 0; + } + }, + getSourcesJarTaskName() { + @Override + boolean isSupportedBy(GradleVersion version) { + return version.compareTo(GradleVersions.v6_0) >= 0; + } + }, + ideaModuleTestSources() { + @Override + boolean isSupportedBy(GradleVersion version) { + return version.compareTo(GradleVersions.v7_4) >= 0; + } + }; + + abstract boolean isSupportedBy(GradleVersion version); + boolean isSupported() { + return isSupportedBy(GradleVersion.current()); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleVersions.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleVersions.java new file mode 100644 index 00000000000..42367748a10 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/GradleVersions.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import org.gradle.util.GradleVersion; + +class GradleVersions { + static final GradleVersion v5_3 = GradleVersion.version("5.3"); + static final GradleVersion v6_0 = GradleVersion.version("6.0"); + static final GradleVersion v6_6 = GradleVersion.version("6.6"); + static final GradleVersion v7_1 = GradleVersion.version("7.1"); + static final GradleVersion v7_4 = GradleVersion.version("7.4"); +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/MapUtils.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/MapUtils.java new file mode 100644 index 00000000000..70e75dc0eb8 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/MapUtils.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.util.LinkedHashMap; +import java.util.Map; + +class MapUtils { + /** + * Returns the map of all entries present in the first map but not present in the second map (by key). + */ + static Map asymmetricDifference(Map a, Map b) { + if (b == null || b.isEmpty()) { + return a; + } + Map result = new LinkedHashMap<>(a); + result.keySet().removeAll(b.keySet()); + return result; + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/OutputDirTask.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/OutputDirTask.java new file mode 100644 index 00000000000..6eab997caca --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/OutputDirTask.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2013-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import javax.annotation.Nonnull; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.SourceTask; + +import static org.gradle.api.tasks.PathSensitivity.RELATIVE; + +class OutputDirTask extends SourceTask { + private final DirectoryProperty outputDir; + + OutputDirTask() { + this.outputDir = getProject().getObjects().directoryProperty(); + } + + public void setOutputDir(File outputDir) { + this.outputDir.set(outputDir); + getOutputs().dir(outputDir); + } + + @Nonnull + @PathSensitive(value = RELATIVE) + public FileTree getSource() { + return super.getSource(); + } + + @OutputDirectory + protected DirectoryProperty getOutputDir() { + return outputDir; + } + + FileCollection filterSources(Spec spec) { + return getSource().filter(spec); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/ProcessingState.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/ProcessingState.java new file mode 100644 index 00000000000..9d9f905243d --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/ProcessingState.java @@ -0,0 +1,110 @@ +/** + * Copyright © 2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.avro.Schema; +import org.gradle.api.file.ProjectLayout; + +class ProcessingState { + private final Map typeStates = new HashMap<>(); + private final Set delayedFiles = new LinkedHashSet<>(); + private final Queue filesToProcess = new LinkedList<>(); + private int processedTotal; + + ProcessingState(Iterable files, ProjectLayout projectLayout) { + for (File file : files) { + filesToProcess.add(new FileState(file, FileUtils.projectRelativePath(projectLayout, file))); + } + } + + Map determineParserTypes(FileState fileState) { + Set duplicateTypeNames = fileState.getDuplicateTypeNames(); + Map types = new HashMap<>(); + for (TypeState typeState : typeStates.values()) { + String typeName = typeState.getName(); + if (!duplicateTypeNames.contains(typeName)) { + types.put(typeState.getName(), typeState.getSchema()); + } + } + return types; + } + + void processTypeDefinitions(FileState fileState, Map newTypes) { + String path = fileState.getPath(); + for (Map.Entry entry : newTypes.entrySet()) { + String typeName = entry.getKey(); + Schema schema = entry.getValue(); + getTypeState(typeName).processTypeDefinition(path, schema); + } + fileState.clearError(); + processedTotal++; + queueDelayedFilesForProcessing(); + } + + Set getFailedFiles() { + return delayedFiles; + } + + private TypeState getTypeState(String typeName) { + TypeState typeState = typeStates.get(typeName); + if (typeState == null) { + typeState = new TypeState(typeName); + typeStates.put(typeName, typeState); + } + return typeState; + } + + void queueForProcessing(FileState fileState) { + filesToProcess.add(fileState); + } + + void queueForDelayedProcessing(FileState fileState) { + delayedFiles.add(fileState); + } + + private void queueDelayedFilesForProcessing() { + filesToProcess.addAll(delayedFiles); + delayedFiles.clear(); + } + + FileState nextFileState() { + return filesToProcess.poll(); + } + + boolean isWorkRemaining() { + return !filesToProcess.isEmpty(); + } + + int getProcessedTotal() { + return processedTotal; + } + + Iterable getSchemasForLocation(String path) { + return typeStates.values().stream().filter(it -> it.hasLocation(path)).map(TypeState::getSchema).collect(Collectors.toList()); + } + + Iterable getSchemas() { + return typeStates.values().stream().map(TypeState::getSchema).collect(Collectors.toSet()); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/ResolveAvroDependenciesTask.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/ResolveAvroDependenciesTask.java new file mode 100644 index 00000000000..9edd68d2644 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/ResolveAvroDependenciesTask.java @@ -0,0 +1,55 @@ +package com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.io.IOException; +import java.util.Set; +import org.apache.avro.Schema; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; +import org.gradle.api.specs.NotSpec; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.TaskAction; + +/** + * Task to read Avro schema files, resolve their dependencies, and write out dependency-free Avro schema files. + */ +@CacheableTask +public class ResolveAvroDependenciesTask extends OutputDirTask { + private final SchemaResolver resolver = new SchemaResolver(getProject().getLayout(), getLogger()); + + @TaskAction + protected void process() { + getLogger().info("Found {} files", getInputs().getSourceFiles().getFiles().size()); + failOnUnsupportedFiles(); + processFiles(); + } + + private void failOnUnsupportedFiles() { + FileCollection unsupportedFiles = filterSources(new NotSpec<>(new FileExtensionSpec(Constants.SCHEMA_EXTENSION))); + if (!unsupportedFiles.isEmpty()) { + throw new GradleException( + String.format("Unsupported file extension for the following files: %s", unsupportedFiles)); + } + } + + private void processFiles() { + int processedFileCount = processSchemaFiles(); + setDidWork(processedFileCount > 0); + } + + private int processSchemaFiles() { + Set inputFiles = filterSources(new FileExtensionSpec(Constants.SCHEMA_EXTENSION)).getFiles(); + ProcessingState processingState = resolver.resolve(inputFiles); + for (Schema schema : processingState.getSchemas()) { + try { + File outputFile = new File(getOutputDir().get().getAsFile(), AvroUtils.assemblePath(schema)); + String schemaJson = schema.toString(true); + FileUtils.writeJsonFile(outputFile, schemaJson); + getLogger().debug("Wrote {}", outputFile.getPath()); + } catch (IOException ex) { + throw new GradleException(String.format("Failed to write resolved schema definition for %s", schema.getFullName()), ex); + } + } + return processingState.getProcessedTotal(); + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/SchemaResolver.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/SchemaResolver.java new file mode 100644 index 00000000000..59f60def7ea --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/SchemaResolver.java @@ -0,0 +1,88 @@ +package com.github.davidmc24.gradle.plugin.avro; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.avro.Schema; +import org.apache.avro.SchemaParseException; +import org.gradle.api.GradleException; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.logging.Logger; + +class SchemaResolver { + private static Pattern ERROR_UNKNOWN_TYPE = Pattern.compile("(?i).*(undefined name|not a defined name|type not supported).*"); + private static Pattern ERROR_DUPLICATE_TYPE = Pattern.compile("Can't redefine: (.*)"); + + private final ProjectLayout projectLayout; + private final Logger logger; + + SchemaResolver(ProjectLayout projectLayout, Logger logger) { + this.projectLayout = projectLayout; + this.logger = logger; + } + + ProcessingState resolve(Iterable files) { + ProcessingState processingState = new ProcessingState(files, projectLayout); + while (processingState.isWorkRemaining()) { + processSchemaFile(processingState, processingState.nextFileState()); + } + Set failedFiles = processingState.getFailedFiles(); + if (!failedFiles.isEmpty()) { + StringBuilder errorMessage = new StringBuilder("Could not resolve schema definition files:"); + for (FileState fileState : failedFiles) { + String path = fileState.getPath(); + String fileErrorMessage = fileState.getErrorMessage(); + errorMessage.append(System.lineSeparator()).append("* ").append(path).append(": ").append(fileErrorMessage); + } + throw new GradleException(errorMessage.toString()); + } + return processingState; + } + + private void processSchemaFile(ProcessingState processingState, FileState fileState) { + String path = fileState.getPath(); + logger.debug("Processing {}, excluding types {}", path, fileState.getDuplicateTypeNames()); + File sourceFile = fileState.getFile(); + Map parserTypes = processingState.determineParserTypes(fileState); + try { + Schema.Parser parser = new Schema.Parser(); + parser.addTypes(parserTypes); + parser.parse(sourceFile); + Map typesDefinedInFile = MapUtils.asymmetricDifference(parser.getTypes(), parserTypes); + processingState.processTypeDefinitions(fileState, typesDefinedInFile); + if (logger.isDebugEnabled()) { + logger.debug("Processed {}; contained types {}", path, typesDefinedInFile.keySet()); + } else { + logger.info("Processed {}", path); + } + } catch (SchemaParseException ex) { + String errorMessage = ex.getMessage(); + Matcher unknownTypeMatcher = ERROR_UNKNOWN_TYPE.matcher(errorMessage); + Matcher duplicateTypeMatcher = ERROR_DUPLICATE_TYPE.matcher(errorMessage); + if (unknownTypeMatcher.matches()) { + fileState.setError(ex); + processingState.queueForDelayedProcessing(fileState); + logger.debug("Found undefined name in {} ({}); will try again", path, errorMessage); + } else if (duplicateTypeMatcher.matches()) { + String typeName = duplicateTypeMatcher.group(1); + if (fileState.containsDuplicateTypeName(typeName)) { + throw new GradleException( + String.format("Failed to resolve schema definition file %s; contains duplicate type definition %s", path, typeName), + ex); + } else { + fileState.setError(ex); + fileState.addDuplicateTypeName(typeName); + processingState.queueForProcessing(fileState); + logger.debug("Identified duplicate type {} in {}; will re-process excluding it", typeName, path); + } + } else { + throw new GradleException(String.format("Failed to resolve schema definition file %s", path), ex); + } + } catch (IOException ex) { + throw new GradleException(String.format("Failed to resolve schema definition file %s", path), ex); + } + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/SetBuilder.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/SetBuilder.java new file mode 100644 index 00000000000..1d3202e9e5a --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/SetBuilder.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2013-2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings("UnusedReturnValue") +class SetBuilder { + private Set set = new HashSet(); + + SetBuilder add(T e) { + set.add(e); + return this; + } + + final SetBuilder addAll(T[] c) { + Collections.addAll(set, c); + return this; + } + + SetBuilder addAll(Collection c) { + set.addAll(c); + return this; + } + + SetBuilder remove(T e) { + set.remove(e); + return this; + } + + Set build() { + return set; + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Strings.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Strings.java new file mode 100644 index 00000000000..605c2944c49 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/Strings.java @@ -0,0 +1,48 @@ +package com.github.davidmc24.gradle.plugin.avro; + +/** + * Utility methods for working with {@link String}s. + */ +class Strings { + /** + * Not intended for instantiation. + */ + private Strings() { } + + /** + * Checks if a {@link String} is empty ({@code ""}) or {@code null}. + * + * @param str the String to check, may be {@code null} + * @return true if the String is empty or {@code null} + */ + static boolean isEmpty(String str) { + return str == null || str.isEmpty(); + } + + /** + * Checks if a {@link String} is not empty ({@code ""}) and not {@code null}. + * + * @param str the String to check, may be {@code null} + * @return true if the String is not empty and not {@code null} + */ + static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * Requires that a {@link String} is not empty ({@code ""}) and not {@code null}. + * If the requirement is violated, an {@link IllegalArgumentException} will be thrown. + * + * @param str the String to check, may be {@code null} + * @param message the message to include in + * @return the String, if the requirement was not violated + * @throws IllegalArgumentException if the requirement was violated + */ + @SuppressWarnings({"UnusedReturnValue", "SameParameterValue"}) + static String requireNotEmpty(String str, String message) { + if (isEmpty(str)) { + throw new IllegalArgumentException(message); + } + return str; + } +} diff --git a/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/TypeState.java b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/TypeState.java new file mode 100644 index 00000000000..9094029b2a2 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/java/com/github/davidmc24/gradle/plugin/avro/TypeState.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2015 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro; + +import java.util.Set; +import java.util.TreeSet; +import org.apache.avro.Schema; +import org.gradle.api.GradleException; + +class TypeState { + private final String name; + private final Set locations = new TreeSet<>(); + private Schema schema; + + TypeState(String name) { + this.name = name; + } + + void processTypeDefinition(String path, Schema schemaToProcess) { + locations.add(path); + if (this.schema == null) { + this.schema = schemaToProcess; + } else if (!this.schema.equals(schemaToProcess)) { + throw new GradleException(String.format("Found conflicting definition of type %s in %s", name, locations)); + } // Otherwise duplicate declaration of identical schema; nothing to do + } + + String getName() { + return name; + } + + Schema getSchema() { + return schema; + } + + boolean hasLocation(String location) { + return locations.contains(location); + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroBasePluginFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroBasePluginFunctionalSpec.groovy new file mode 100644 index 00000000000..241782a0edf --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroBasePluginFunctionalSpec.groovy @@ -0,0 +1,177 @@ +/* + * Copyright © 2018 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import com.github.davidmc24.gradle.plugin.avro.test.custom.CommentGenerator +import com.github.davidmc24.gradle.plugin.avro.test.custom.TimestampGenerator + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class AvroBasePluginFunctionalSpec extends FunctionalSpec { + def "setup"() { + applyAvroBasePlugin() + } + + def "can generate java files from json schema"() { + given: + buildFile << """ + |tasks.register("generateAvroJava", com.github.davidmc24.gradle.plugin.avro.GenerateAvroJavaTask) { + | source file("src/main/avro") + | include("**/*.avsc") + | outputDir = file("build/generated-main-avro-java") + |} + |""".stripMargin() + + copyResource("user.avsc", avroDir) + + when: + def result = run("generateAvroJava") + + then: + result.task(":generateAvroJava").outcome == SUCCESS + projectFile("build/generated-main-avro-java/example/avro/User.java").file + } + + def "can generate json schema files from json protocol"() { + given: + buildFile << """ + |tasks.register("generateSchema", com.github.davidmc24.gradle.plugin.avro.GenerateAvroSchemaTask) { + | source file("src/main/avro") + | include("**/*.avpr") + | outputDir = file("build/generated-main-avro-avsc") + |} + |""".stripMargin() + + copyResource("mail.avpr", avroDir) + + when: + def result = run("generateSchema") + + then: + result.task(":generateSchema").outcome == SUCCESS + def expectedFileContents = getClass().getResource("Message.avsc").text.trim() + def generateFileContents = projectFile("build/generated-main-avro-avsc/org/apache/avro/test/Message.avsc").text.trim() + expectedFileContents == generateFileContents + } + + def "can generate json schema files from IDL"() { + given: + buildFile << """ + |tasks.register("generateProtocol", com.github.davidmc24.gradle.plugin.avro.GenerateAvroProtocolTask) { + | source file("src/main/avro") + | outputDir = file("build/generated-avro-main-avpr") + |} + |tasks.register("generateSchema", com.github.davidmc24.gradle.plugin.avro.GenerateAvroSchemaTask) { + | dependsOn generateProtocol + | source file("build/generated-avro-main-avpr") + | include("**/*.avpr") + | outputDir = file("build/generated-main-avro-avsc") + |} + |""".stripMargin() + + copyResource("interop.avdl", avroDir) + + when: + def result = run("generateSchema") + + then: + result.task(":generateSchema").outcome == SUCCESS + projectFile("build/generated-main-avro-avsc/org/apache/avro/Foo.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/Kind.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/MD5.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/Node.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/Interop.avsc").file + } + + def "example of converting both IDL and json protocol simultaneously"() { + given: + buildFile << """ + |tasks.register("generateProtocol", com.github.davidmc24.gradle.plugin.avro.GenerateAvroProtocolTask) { + | source file("src/main/avro") + | include("**/*.avdl") + | outputDir = file("build/generated-avro-main-avpr") + |} + |tasks.register("generateSchema", com.github.davidmc24.gradle.plugin.avro.GenerateAvroSchemaTask) { + | dependsOn generateProtocol + | source file("src/main/avro") + | source file("build/generated-avro-main-avpr") + | include("**/*.avpr") + | outputDir = file("build/generated-main-avro-avsc") + |} + |""".stripMargin() + + copyResource("mail.avpr", avroDir) + copyResource("interop.avdl", avroDir) + + when: + def result = run("generateSchema") + + then: + result.task(":generateSchema").outcome == SUCCESS + projectFile("build/generated-main-avro-avsc/org/apache/avro/test/Message.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/Foo.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/Kind.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/MD5.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/Node.avsc").file + projectFile("build/generated-main-avro-avsc/org/apache/avro/Interop.avsc").file + } + + def "supports classpath property for instantiating of velocity tools"() { + given: + copyAvroTools("src/main/java") + def templatesDir = projectFolder("templates") + copyResource("user.avsc", avroDir) + copyResource("record-tools.vm", templatesDir, "record.vm") + applyPlugin("java") + buildFile << """ + |avro { + | templateDirectory = "${templatesDir.toString().replace('\\', '\\\\')}/" + | additionalVelocityToolClasses = ['com.github.davidmc24.gradle.plugin.avro.test.custom.TimestampGenerator', + | 'com.github.davidmc24.gradle.plugin.avro.test.custom.CommentGenerator'] + |} + |tasks.register("compileTools", JavaCompile) { + | source = sourceSets.main.java + | classpath = sourceSets.main.compileClasspath + | destinationDir = file("build/classes/java/main") + |} + |tasks.register("generateAvro", com.github.davidmc24.gradle.plugin.avro.GenerateAvroJavaTask) { + | dependsOn compileTools + | classpath = files("build/classes/java/main") + | source file("src/main/avro") + | include("**/*.avsc") + | outputDir = file("build/generated-main-avro-java") + |} + |""".stripMargin() + + when: + def result = run("generateAvro") + + then: "the task succeeds" + result.task(":generateAvro").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + + and: "the velocity tools have been applied" + content.contains(CommentGenerator.CUSTOM_COMMENT) + content.contains(TimestampGenerator.MESSAGE_PREFIX) + } + + private void copyAvroTools(String destDir) { + copyFile("src/test/java", destDir, + "com/github/davidmc24/gradle/plugin/avro/test/custom/CommentGenerator.java") + copyFile("src/test/java", destDir, + "com/github/davidmc24/gradle/plugin/avro/test/custom/TimestampGenerator.java") + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroPluginFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroPluginFunctionalSpec.groovy new file mode 100644 index 00000000000..5e7532e73af --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroPluginFunctionalSpec.groovy @@ -0,0 +1,182 @@ +/* + * Copyright © 2015-2017 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import static org.gradle.testkit.runner.TaskOutcome.FAILED +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class AvroPluginFunctionalSpec extends FunctionalSpec { + def "setup"() { + applyAvroPlugin() + addDefaultRepository() + addAvroDependency() + } + + def "can generate and compile java files from json schema"() { + given: + copyResource("user.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/User.class")).file + } + + def "can generate and compile java files from json protocol"() { + given: + addAvroIpcDependency() + copyResource("mail.avpr", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("org/apache/avro/test/Mail.class")).file + projectFile(buildOutputClassPath("org/apache/avro/test/Message.class")).file + } + + def "can generate and compile java files from IDL"() { + given: + copyResource(interopIDLResourceName, avroDir) + + when: + def result = run() + def interopJavaContent = projectFile("build/generated-main-avro-java/org/apache/avro/Interop.java").text + + then: + result.task(":generateAvroProtocol").outcome == SUCCESS + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile("build/generated-main-avro-java/org/apache/avro/Interop.java").file + projectFile(buildOutputClassPath("org/apache/avro/Foo.class")).file + projectFile(buildOutputClassPath("org/apache/avro/Interop.class")).file + projectFile(buildOutputClassPath("org/apache/avro/Kind.class")).file + projectFile(buildOutputClassPath("org/apache/avro/MD5.class")).file + projectFile(buildOutputClassPath("org/apache/avro/Node.class")).file + interopJavaContent + interopJavaContent.contains("int intField") + interopJavaContent.contains("long longField") + interopJavaContent.contains("String stringField") + interopJavaContent.contains("boolean boolField") + interopJavaContent.contains("float floatField") + interopJavaContent.contains("double doubleField") + interopJavaContent.contains("java.lang.Void nullField") + interopJavaContent.contains("java.util.List arrayField") + interopJavaContent =~ /Map mapField/ + interopJavaContent.contains("Object unionField") + interopJavaContent.contains("Kind enumField") + interopJavaContent.contains("MD5 fixedField") + interopJavaContent.contains("Node recordField") + interopJavaContent.contains("BigDecimal decimalField") + interopJavaContent.contains("LocalDate dateField") + interopJavaContent.contains("LocalTime timeField") + interopJavaContent.contains("Instant timeStampField") + if (localTimestampConversionSupported) { + interopJavaContent.contains("LocalDateTime localTimeStampField") + } + } + + def "supports json schema files in subdirectories"() { + given: + copyResource("user.avsc", avroSubDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/User.class")).file + } + + def "supports json protocol files in subdirectories"() { + given: + addAvroIpcDependency() + copyResource("mail.avpr", avroSubDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("org/apache/avro/test/Mail.class")).file + projectFile(buildOutputClassPath("org/apache/avro/test/Message.class")).file + } + + def "supports IDL files in subdirectories"() { + given: + copyResource(interopIDLResourceName, avroSubDir) + + when: + def result = run() + + then: + result.task(":generateAvroProtocol").outcome == SUCCESS + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("org/apache/avro/Foo.class")).file + projectFile(buildOutputClassPath("org/apache/avro/Interop.class")).file + projectFile(buildOutputClassPath("org/apache/avro/Kind.class")).file + projectFile(buildOutputClassPath("org/apache/avro/MD5.class")).file + projectFile(buildOutputClassPath("org/apache/avro/Node.class")).file + } + + def "gives a meaningful error message when presented a malformed schema file"() { + given: + copyResource("enumMalformed.avsc", avroDir) + def errorFilePath = new File("src/main/avro/enumMalformed.avsc").path + + when: + def result = runAndFail() + + then: + result.task(":generateAvroJava").outcome == FAILED + result.output.contains("> Could not resolve schema definition files:") + result.output.contains("* $errorFilePath: \"enum\" is not a defined name. The type of the \"gender\" " + + "field must be a defined name or a {\"type\": ...} expression.") + } + + @SuppressWarnings(["GStringExpressionWithinString"]) + def "avro plugin correctly uses task configuration avoidance"() { + given: + buildFile << """ + |def configuredTasks = [] + |tasks.configureEach { + | println "Configured task: \${it.path}" + |} + |""".stripMargin() + + when: + def result = run("help") + + then: + def expectedConfiguredTasks = [":help"] + if (GradleFeatures.configCache.isSupportedBy(gradleVersion)) { + // Not sure why, but when configuration caching was introduced, the base plugin started configuring the + // clean task even if it wasn't called. + expectedConfiguredTasks << ":clean" + } + def actualConfiguredTasks = [] + result.output.findAll(/(?m)^Configured task: (.*)$/) { match, taskPath -> actualConfiguredTasks << taskPath } + actualConfiguredTasks == expectedConfiguredTasks + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroPluginSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroPluginSpec.groovy new file mode 100644 index 00000000000..28d3c515183 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroPluginSpec.groovy @@ -0,0 +1,74 @@ +/* + * Copyright © 2013-2019 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification + +class AvroPluginSpec extends Specification { + Project project = ProjectBuilder.builder().build() + + def "avro protocol generation tasks are registered"() { + when: "the plugin is applied to a project" + project.apply(plugin: AvroPlugin) + + then: "avro protocol generation tasks are registered with the project with appropriate configuration" + project.tasks.withType(GenerateAvroProtocolTask)*.name.sort() == ["generateAvroProtocol", "generateTestAvroProtocol"] + mainGenerateAvroProtoTask.description == "Generates main Avro protocol definition files from IDL files." + testGenerateAvroProtoTask.description == "Generates test Avro protocol definition files from IDL files." + mainGenerateAvroProtoTask.group == Constants.GROUP_SOURCE_GENERATION + testGenerateAvroProtoTask.group == Constants.GROUP_SOURCE_GENERATION + // Can't easily test the sources + mainGenerateAvroProtoTask.outputDir.get().asFile == project.file("build/generated-main-avro-avpr") + testGenerateAvroProtoTask.outputDir.get().asFile == project.file("build/generated-test-avro-avpr") + } + + def "avro java generation tasks are registered"() { + when: "the plugin is applied to a project" + project.apply(plugin: AvroPlugin) + + then: "avro java generation tasks are registered with the project with appropriate configuration" + project.tasks.withType(GenerateAvroJavaTask)*.name.sort() == ["generateAvroJava", "generateTestAvroJava"] + mainGenerateAvroJavaTask.description == "Generates main Avro Java source files from schema/protocol definition files." + testGenerateAvroJavaTask.description == "Generates test Avro Java source files from schema/protocol definition files." + mainGenerateAvroJavaTask.group == Constants.GROUP_SOURCE_GENERATION + testGenerateAvroJavaTask.group == Constants.GROUP_SOURCE_GENERATION + // Can't easily test the sources + mainGenerateAvroJavaTask.outputDir.get().asFile == project.file("build/generated-main-avro-java") + testGenerateAvroJavaTask.outputDir.get().asFile == project.file("build/generated-test-avro-java") + } + + OutputDirTask getTask(String name) { + return project.tasks.getByName(name) as OutputDirTask + } + + OutputDirTask getMainGenerateAvroProtoTask() { + return getTask("generateAvroProtocol") + } + + OutputDirTask getTestGenerateAvroProtoTask() { + return getTask("generateTestAvroProtocol") + } + + OutputDirTask getMainGenerateAvroJavaTask() { + return getTask("generateAvroJava") + } + + OutputDirTask getTestGenerateAvroJavaTask() { + return getTask("generateTestAvroJava") + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroUtilsSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroUtilsSpec.groovy new file mode 100644 index 00000000000..993f8a245fc --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/AvroUtilsSpec.groovy @@ -0,0 +1,71 @@ +package com.github.davidmc24.gradle.plugin.avro + +import org.apache.avro.Protocol +import org.apache.avro.Schema +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static Constants.PROTOCOL_EXTENSION +import static Constants.SCHEMA_EXTENSION + +@Subject(AvroUtils) +class AvroUtilsSpec extends Specification { + private static final String EMPTY_STRING = "" + private static final String SINGLE_LEVEL_NAMESPACE = "avro" + private static final String MULTI_LEVEL_NAMESPACE = "org.example" + private static final String MULTI_LEVEL_NAMESPACE_PATH = "org/example" + private static final String SCHEMA_NAME = "SchemaName" + private static final String PROTOCOL_NAME = "ProtocolName" + + @SuppressWarnings("ParameterName") + @Unroll + def "assemblePath rejects unnamed arguments (#arg)"(def arg, def _) { + when: + //noinspection GroovyAssignabilityCheck + AvroUtils.assemblePath(arg) + then: + def ex = thrown(IllegalArgumentException) + ex.message == "Path cannot be assembled for nameless objects" + where: + arg | _ + createSchema(null, null, true) | _ + createSchema(null, EMPTY_STRING, true) | _ + createProtocol(null, null) | _ + createProtocol(null, EMPTY_STRING) | _ + } + + @Unroll + def "assemblePath(#arg)"(def arg, String expectedPath) { + when: + //noinspection GroovyAssignabilityCheck + def actualPath = AvroUtils.assemblePath(arg) + then: + actualPath == expectedPath + where: + arg | expectedPath + createSchema(null, SCHEMA_NAME) | "${SCHEMA_NAME}.${SCHEMA_EXTENSION}" + createSchema(EMPTY_STRING, SCHEMA_NAME) | "${SCHEMA_NAME}.${SCHEMA_EXTENSION}" + createSchema(SINGLE_LEVEL_NAMESPACE, SCHEMA_NAME) | "${SINGLE_LEVEL_NAMESPACE}/${SCHEMA_NAME}.${SCHEMA_EXTENSION}" + createSchema(MULTI_LEVEL_NAMESPACE, SCHEMA_NAME) | "${MULTI_LEVEL_NAMESPACE_PATH}/${SCHEMA_NAME}.${SCHEMA_EXTENSION}" + createProtocol(null, PROTOCOL_NAME) | "${PROTOCOL_NAME}.${PROTOCOL_EXTENSION}" + createProtocol(EMPTY_STRING, PROTOCOL_NAME) | "${PROTOCOL_NAME}.${PROTOCOL_EXTENSION}" + createProtocol(SINGLE_LEVEL_NAMESPACE, PROTOCOL_NAME) | "${SINGLE_LEVEL_NAMESPACE}/${PROTOCOL_NAME}.${PROTOCOL_EXTENSION}" + createProtocol(MULTI_LEVEL_NAMESPACE, PROTOCOL_NAME) | "${MULTI_LEVEL_NAMESPACE_PATH}/${PROTOCOL_NAME}.${PROTOCOL_EXTENSION}" + } + + Schema createSchema(String namespace, String name, boolean disableNameValidation = false) { + if (disableNameValidation) { + Schema.validateNames.set(false) + } + def schema = Schema.createRecord(name, null, namespace, false, Collections.emptyList()) + if (disableNameValidation) { + Schema.validateNames.set(true) + } + return schema + } + + Protocol createProtocol(String namespace, String name) { + return new Protocol(name, null, namespace) + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/BuildCacheSupportFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/BuildCacheSupportFunctionalSpec.groovy new file mode 100644 index 00000000000..bcf7b06c188 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/BuildCacheSupportFunctionalSpec.groovy @@ -0,0 +1,101 @@ +/* + * Copyright © 2018 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import spock.lang.IgnoreIf +import spock.util.environment.OperatingSystem + +import static org.gradle.testkit.runner.TaskOutcome.FROM_CACHE + +/** + * Testing for Build Cache feature support. + */ +class BuildCacheSupportFunctionalSpec extends FunctionalSpec { + def "setup"() { + applyAvroPlugin() + addDefaultRepository() + addAvroDependency() + } + + def "supports build cache for schema/protocol java source generation"() { + given: "a project is built once with build cache enabled" + copyResource("user.avsc", avroDir) + copyResource("mail.avpr", avroDir) + addAvroIpcDependency() + run("build", "--build-cache") + + and: "the project is cleaned" + run("clean") + + when: "the project is built again with build cache enabled" + def result = run("build", "--build-cache") + + then: "the expected outputs were produced from the build cache" + result.task(":generateAvroJava").outcome == FROM_CACHE + result.task(":compileJava").outcome == FROM_CACHE + projectFile("build/generated-main-avro-java/example/avro/User.java").file + projectFile("build/generated-main-avro-java/org/apache/avro/test/Mail.java").file + projectFile(buildOutputClassPath("example/avro/User.class")).file + projectFile(buildOutputClassPath("org/apache/avro/test/Mail.class")).file + } + + /** + * This test appears to fail on Windows due to clean being unable to delete interop.avpr. + */ + @IgnoreIf({ OperatingSystem.current.windows }) + def "supports build cache for IDL to protocol conversion"() { + given: "a project is built once with build cache enabled" + copyResource(interopIDLResourceName, avroDir) + run("build", "--build-cache") + + and: "the project is cleaned" + run("clean") + + when: "the project is built again with build cache enabled" + def result = run("build", "--build-cache") + + then: "the expected outputs were produced from the build cache" + result.task(":generateAvroProtocol").outcome == FROM_CACHE + result.task(":generateAvroJava").outcome == FROM_CACHE + result.task(":compileJava").outcome == FROM_CACHE + projectFile("build/generated-main-avro-avpr/org/apache/avro/InteropProtocol.avpr").file + projectFile("build/generated-main-avro-java/org/apache/avro/Interop.java").file + projectFile(buildOutputClassPath("org/apache/avro/Interop.class")).file + } + + def "supports build cache for protocol to schema conversion"() { + given: "a project is built once with build cache enabled" + copyResource("mail.avpr", avroDir) + buildFile << """ + |tasks.register("generateSchema", com.github.davidmc24.gradle.plugin.avro.GenerateAvroSchemaTask) { + | source file("src/main/avro") + | include("**/*.avpr") + | outputDir = file("build/generated-main-avro-avsc") + |} + |""".stripMargin() + run("generateSchema", "--build-cache") + + and: "the project is cleaned" + run("clean") + + when: "the project is built again with build cache enabled" + def result = run("generateSchema", "--build-cache") + + then: "the expected outputs were produced from the build cache" + result.task(":generateSchema").outcome == FROM_CACHE + projectFile("build/generated-main-avro-avsc/org/apache/avro/test/Message.avsc").file + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/CustomConversionFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/CustomConversionFunctionalSpec.groovy new file mode 100644 index 00000000000..94b9354db96 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/CustomConversionFunctionalSpec.groovy @@ -0,0 +1,239 @@ +/* + * Copyright © 2019 David M. Carr + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class CustomConversionFunctionalSpec extends FunctionalSpec { + private void copyCustomConversion(String destDir) { + copyFile("src/test/java", destDir, + "com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneConversion.java") + copyFile("src/test/java", destDir, + "com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalType.java") + copyFile("src/test/java", destDir, + "com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalTypeFactory.java") + } + + def "can use a custom conversion when generating java from a schema with stringType = \"String\""() { + // since Avro 1.9.2 https://issues.apache.org/jira/browse/AVRO-2548 is fixed + // This is a behavior of the buildscript version of avro rather than the compile-time one, + // so our version compatibility tests won't cover the difference + given: + copyResource("customConversion.avsc", avroDir) + // This functionality doesn't work with the plugins DSL syntax. + // To load files from the buildSrc classpath you need to load the plugin from the buildscript classpath. + buildFile << """ + |buildscript { + | dependencies { + | classpath files(${readPluginClasspath()}) + | } + |} + |apply plugin: "com.github.davidmc24.gradle.plugin.avro" + |import com.github.davidmc24.gradle.plugin.avro.test.custom.* + |avro { + | stringType = "String" + | logicalTypeFactory("timezone", TimeZoneLogicalTypeFactory) + | customConversion(TimeZoneConversion) + |} + |""".stripMargin() + addDefaultRepository() + addAvroDependency() + projectFile("buildSrc/build.gradle") << """ + |repositories { + | mavenCentral() + |} + |dependencies { + | implementation "org.apache.avro:avro:${avroVersion}" + |} + |""".stripMargin() + copyCustomConversion("buildSrc/src/main/java") + copyCustomConversion("src/main/java") + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("test/Event.class")).file + def javaSource = projectFile("build/generated-main-avro-java/test/Event.java").text + javaSource.contains("java.time.Instant start;") + javaSource.contains("java.util.TimeZone timezone;") + } + + def "can use a custom conversion when generating java from a schema"() { + // As of Avro 1.9.1, custom conversions have an undesirable interaction with stringType=String. + // See https://issues.apache.org/jira/browse/AVRO-2548 + given: + copyResource("customConversion.avsc", avroDir) + // This functionality doesn't work with the plugins DSL syntax. + // To load files from the buildSrc classpath you need to load the plugin from the buildscript classpath. + buildFile << """ + |buildscript { + | dependencies { + | classpath files(${readPluginClasspath()}) + | } + |} + |apply plugin: "com.github.davidmc24.gradle.plugin.avro" + |import com.github.davidmc24.gradle.plugin.avro.test.custom.* + |avro { + | stringType = "CharSequence" + | logicalTypeFactory("timezone", TimeZoneLogicalTypeFactory) + | customConversion(TimeZoneConversion) + |} + |""".stripMargin() + addDefaultRepository() + addAvroDependency() + projectFile("buildSrc/build.gradle") << """ + |repositories { + | mavenCentral() + |} + |dependencies { + | implementation "org.apache.avro:avro:${avroVersion}" + |} + |""".stripMargin() + copyCustomConversion("buildSrc/src/main/java") + copyCustomConversion("src/main/java") + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("test/Event.class")).file + def javaSource = projectFile("build/generated-main-avro-java/test/Event.java").text + javaSource.contains("java.time.Instant start;") + javaSource.contains("java.util.TimeZone timezone;") + } + + def "can use a custom conversion when generating java from a protocol"() { + // As of Avro 1.9.1, custom conversions have an undesirable interaction with stringType=String. + // See https://issues.apache.org/jira/browse/AVRO-2548 + given: + copyResource("customConversion.avpr", avroDir) + // This functionality doesn't work with the plugins DSL syntax. + // To load files from the buildSrc classpath you need to load the plugin from the buildscript classpath. + buildFile << """ + |buildscript { + | dependencies { + | classpath files(${readPluginClasspath()}) + | } + |} + |apply plugin: "com.github.davidmc24.gradle.plugin.avro" + |import com.github.davidmc24.gradle.plugin.avro.test.custom.* + |avro { + | stringType = "CharSequence" + | logicalTypeFactory("timezone", TimeZoneLogicalTypeFactory) + | customConversion(TimeZoneConversion) + |} + |""".stripMargin() + addDefaultRepository() + addAvroDependency() + projectFile("buildSrc/build.gradle") << """ + |repositories { + | mavenCentral() + |} + |dependencies { + | implementation "org.apache.avro:avro:${avroVersion}" + |} + |""".stripMargin() + copyCustomConversion("buildSrc/src/main/java") + copyCustomConversion("src/main/java") + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("test/Event.class")).file + def javaSource = projectFile("build/generated-main-avro-java/test/Event.java").text + javaSource.contains("java.time.Instant start;") + javaSource.contains("java.util.TimeZone timezone;") + } + + def "can use a custom conversion from outside of the build classpath when generating java from a protocol"() { + given: + copyResource("customConversion.avpr", avroDir) + applyAvroPlugin() + buildFile << """ + |configurations { + | customConversions + | implementation.extendsFrom(customConversions) + |} + |dependencies { + | customConversions(project(":custom-conversions")) + |} + |avro { + | stringType = "CharSequence" + | conversionsAndTypeFactoriesClasspath.from(configurations.customConversions) + | logicalTypeFactory("timezone", "com.github.davidmc24.gradle.plugin.avro.test.custom.TimeZoneLogicalTypeFactory") + | customConversion("com.github.davidmc24.gradle.plugin.avro.test.custom.TimeZoneConversion") + |} + |""".stripMargin() + addDefaultRepository() + addAvroDependency() + projectFile("custom-conversions/build.gradle") << """ + |plugins { + | id "java-library" + |} + |repositories { + | mavenCentral() + |} + |dependencies { + | implementation "org.apache.avro:avro:${avroVersion}" + |} + |""".stripMargin() + projectFile("settings.gradle") << """ + |include("custom-conversions") + |""".stripMargin() + copyCustomConversion("custom-conversions/src/main/java") + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("test/Event.class")).file + def javaSource = projectFile("build/generated-main-avro-java/test/Event.java").text + javaSource.contains("java.time.Instant start;") + javaSource.contains("java.util.TimeZone timezone;") + } + + def "can use a custom logical type while generating a schema from a protocol"() { + given: + copyResource("customConversion.avpr", avroDir) + applyAvroPlugin() + buildFile << """ + |tasks.register("generateSchema", com.github.davidmc24.gradle.plugin.avro.GenerateAvroSchemaTask) { + | source file("src/main/avro") + | include("**/*.avpr") + | outputDir = file("build/generated-main-avro-avsc") + |} + |""".stripMargin() + + when: + def result = run("generateSchema") + + then: + result.task(":generateSchema").outcome == SUCCESS + def schemaFile = projectFile("build/generated-main-avro-avsc/test/Event.avsc").text + schemaFile.contains('"logicalType" : "timestamp-millis"') + schemaFile.contains('"logicalType" : "timezone"') + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/DuplicateHandlingFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/DuplicateHandlingFunctionalSpec.groovy new file mode 100644 index 00000000000..03f396f0916 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/DuplicateHandlingFunctionalSpec.groovy @@ -0,0 +1,166 @@ +/* + * Copyright © 2015-2016 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import static org.gradle.testkit.runner.TaskOutcome.FAILED +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +/** + * Functional tests related to handling of duplicate type definitions. + * + *

This situation is generally encountered when schema files define records with inline record/enum definitions, and those inline types + * are used in more than one file.

+ */ +class DuplicateHandlingFunctionalSpec extends FunctionalSpec { + def "setup"() { + applyAvroPlugin() + addDefaultRepository() + addAvroDependency() + } + + def "Duplicate record definition succeeds if definition identical"() { + given: + copyIdenticalRecord() + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/Person.class")).file + projectFile(buildOutputClassPath("example/Fish.class")).file + projectFile(buildOutputClassPath("example/Gender.class")).file + } + + def "Duplicate enum definition succeeds if definition identical"() { + given: + copyIdenticalEnum() + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/Person.class")).file + projectFile(buildOutputClassPath("example/Cat.class")).file + projectFile(buildOutputClassPath("example/Gender.class")).file + } + + def "Duplicate fixed definition succeeds if definition identical"() { + given: + copyIdenticalFixed() + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/ContainsFixed1.class")).file + projectFile(buildOutputClassPath("example/ContainsFixed2.class")).file + projectFile(buildOutputClassPath("example/Picture.class")).file + } + + def "Duplicate record definition fails if definition differs"() { + given: + copyDifferentRecord() + def errorFilePath1 = new File("src/main/avro/duplicate/Person.avsc").path + def errorFilePath2 = new File("src/main/avro/duplicate/Spider.avsc").path + when: + def result = runAndFail() + + then: + result.task(":generateAvroJava").outcome == FAILED + result.output.contains("Found conflicting definition of type example.Person in " + + "[$errorFilePath1, $errorFilePath2]") + } + + def "Duplicate enum definition fails if definition differs"() { + given: + copyDifferentEnum() + def errorFilePath1 = new File("src/main/avro/duplicate/Dog.avsc").path + def errorFilePath2 = new File("src/main/avro/duplicate/Person.avsc").path + + when: + def result = runAndFail() + + then: + result.task(":generateAvroJava").outcome == FAILED + result.output.contains("Found conflicting definition of type example.Gender in " + + "[$errorFilePath1, $errorFilePath2]") + } + + def "Duplicate fixed definition fails if definition differs"() { + given: + copyDifferentFixed() + def errorFilePath1 = new File("src/main/avro/duplicate/ContainsFixed1.avsc").path + def errorFilePath2 = new File("src/main/avro/duplicate/ContainsFixed3.avsc").path + + when: + def result = runAndFail() + + then: + result.task(":generateAvroJava").outcome == FAILED + result.output.contains("Found conflicting definition of type example.Picture in " + + "[$errorFilePath1, $errorFilePath2]") + } + + def "Duplicate record definition in single file fails with clear error"() { + given: + copyResource("duplicate/duplicateInSingleFile.avsc", avroDir) + def errorFilePath = new File("src/main/avro/duplicate/duplicateInSingleFile.avsc").path + + when: + def result = runAndFail() + + then: + result.task(":generateAvroJava").outcome == FAILED + result.output.contains("Failed to resolve schema definition file $errorFilePath; " + + "contains duplicate type definition example.avro.date") + } + + private void copyIdenticalRecord() { + copyResource("duplicate/Person.avsc", avroDir) + copyResource("duplicate/Fish.avsc", avroDir) + } + + private void copyIdenticalEnum() { + copyResource("duplicate/Person.avsc", avroDir) + copyResource("duplicate/Cat.avsc", avroDir) + } + + private void copyIdenticalFixed() { + copyResource("duplicate/ContainsFixed1.avsc", avroDir) + copyResource("duplicate/ContainsFixed2.avsc", avroDir) + } + + private void copyDifferentRecord() { + copyResource("duplicate/Person.avsc", avroDir) + copyResource("duplicate/Spider.avsc", avroDir) + } + + private void copyDifferentEnum() { + copyResource("duplicate/Person.avsc", avroDir) + copyResource("duplicate/Dog.avsc", avroDir) + } + + private void copyDifferentFixed() { + copyResource("duplicate/ContainsFixed1.avsc", avroDir) + copyResource("duplicate/ContainsFixed3.avsc", avroDir) + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/EncodingFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/EncodingFunctionalSpec.groovy new file mode 100644 index 00000000000..1716cda1465 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/EncodingFunctionalSpec.groovy @@ -0,0 +1,110 @@ +/* + * Copyright © 2015-2016 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import spock.lang.Unroll + +import java.nio.charset.Charset + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class EncodingFunctionalSpec extends FunctionalSpec { + private static final List LANGUAGES = ["alemán", "chino", "español", "francés", "inglés", "japonés"] + /* Not all encodings have the characters needed for the test file, and not all encoding may be supported by any given JRE */ + private static final List AVAILABLE_ENCODINGS = + ["UTF-8", "UTF-16", "UTF-32", "windows-1252", "X-MacRoman"].findAll { Charset.isSupported(it) } + private static final String SYSTEM_ENCODING = Charset.defaultCharset().name() + + def "with convention plugin, default encoding matches default compilation behavior"() { + given: + applyAvroPlugin() + addDefaultRepository() + addAvroDependency() + copyResource("idioma.avsc", avroDir) + + when: + def result = run() + + then: "compilation succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + + and: "the system default encoding is used" + def content = projectFile("build/generated-main-avro-java/example/avro/Idioma.java").getText(SYSTEM_ENCODING) + LANGUAGES.collect { content.contains(it) }.every { it } + } + + @Unroll + def "with convention plugin, configuring Java compilation task with encoding=#encoding will use it for outputCharacterEncoding"() { + given: + applyAvroPlugin() + addDefaultRepository() + addAvroDependency() + copyResource("idioma.avsc", avroDir) + buildFile << """ + |tasks.named("compileJava").configure { + | options.encoding = '${encoding}' + |} + |""".stripMargin() + + when: + def result = run() + + then: "compilation succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + + and: "the specified encoding is used" + def content = projectFile("build/generated-main-avro-java/example/avro/Idioma.java").getText(encoding) + LANGUAGES.collect { content.contains(it) }.every { it } + + where: + encoding << AVAILABLE_ENCODINGS + } + + @Unroll + def "with base plugin, configuring outputCharacterEncoding=#outputCharacterEncoding is supported"() { + given: + applyAvroBasePlugin() + copyResource("idioma.avsc", avroDir) + buildFile << """ + |avro { + | outputCharacterEncoding = ${outputCharacterEncoding} + |} + |tasks.register("generateAvroJava", com.github.davidmc24.gradle.plugin.avro.GenerateAvroJavaTask) { + | source file("src/main/avro") + | include("**/*.avsc") + | outputDir = file("build/generated-main-avro-java") + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "compilation succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + + and: "the specified encoding is used" + def content = projectFile("build/generated-main-avro-java/example/avro/Idioma.java").getText(expectedEncoding) + LANGUAGES.collect { content.contains(it) }.every { it } + + where: + outputCharacterEncoding | expectedEncoding + "'UTF-16'" | "UTF-16" + "'utf-8'" | "UTF-8" + "java.nio.charset.Charset.forName('UTF-16')" | "UTF-16" + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/EnumHandlingFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/EnumHandlingFunctionalSpec.groovy new file mode 100644 index 00000000000..618d7f38a73 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/EnumHandlingFunctionalSpec.groovy @@ -0,0 +1,85 @@ +/* + * Copyright © 2015-2016 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +/** + * Functional tests relating to handling of enums. + */ +class EnumHandlingFunctionalSpec extends FunctionalSpec { + def "setup"() { + applyAvroPlugin() + addDefaultRepository() + addAvroDependency() + } + + def "supports simple enums"() { + given: + copyResource("enumSimple.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/MyEnum.class")).file + } + + def "supports enums defined within a record field"() { + given: + copyResource("enumField.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/Test.class")).file + projectFile(buildOutputClassPath("example/avro/Gender.class")).file + } + + def "supports enums defined within a union"() { + given: + copyResource("enumUnion.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/Test.class")).file + projectFile(buildOutputClassPath("example/avro/Kind.class")).file + } + + def "supports using enums defined in a separate schema file"() { + given: + copyResource("enumSimple.avsc", avroDir) + copyResource("enumUseSimple.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/User.class")).file + projectFile(buildOutputClassPath("example/avro/MyEnum.class")).file + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/ExamplesFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/ExamplesFunctionalSpec.groovy new file mode 100644 index 00000000000..bc89ae18255 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/ExamplesFunctionalSpec.groovy @@ -0,0 +1,55 @@ +/* + * Copyright © 2018 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class ExamplesFunctionalSpec extends FunctionalSpec { + def "setup"() { + applyAvroPlugin() + addDefaultRepository() + addAvroDependency() + } + + def "inline example is valid"() { + given: + copyResource("/examples/inline/Cat.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/Cat.class")).file + projectFile(buildOutputClassPath("example/Breed.class")).file + } + + def "separate example is valid"() { + given: + copyResource("/examples/separate/Breed.avsc", avroDir) + copyResource("/examples/separate/Cat.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/Cat.class")).file + projectFile(buildOutputClassPath("example/Breed.class")).file + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/FunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/FunctionalSpec.groovy new file mode 100644 index 00000000000..9585392c276 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/FunctionalSpec.groovy @@ -0,0 +1,175 @@ +/* + * Copyright © 2015-2018 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import com.vdurmont.semver4j.Semver +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.util.GradleVersion +import spock.lang.Specification +import spock.lang.TempDir + +@SuppressWarnings(["Println"]) +abstract class FunctionalSpec extends Specification { + protected Semver getAvroVersion() { + def version = System.getProperty("avroVersion") + if (!version) { + throw new IllegalArgumentException("avroVersion project property is required") + } + return new Semver(version) + } + protected GradleVersion getGradleVersion() { + def version = System.getProperty("gradleVersion") + if (!version) { + throw new IllegalArgumentException("gradleVersion project property is required") + } + return GradleVersion.version(version) + } + + @TempDir + File testProjectDir + + File buildFile + File avroDir + File avroSubDir + + def setup() { + println "Testing using Avro version ${avroVersion}." + println "Testing using Gradle version ${gradleVersion}." + + buildFile = projectFile("build.gradle") + avroDir = projectFile("src/main/avro") + avroSubDir = projectFile("src/main/avro/foo") + } + + protected String readPluginClasspath() { + def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt") + if (pluginClasspathResource == null) { + throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.") + } + + // escape backslashes in Windows paths and assemble + return pluginClasspathResource.readLines()*.replace('\\', '\\\\').collect { "\"$it\"" }.join(", ") + } + + protected void applyAvroPlugin() { + applyPlugin("com.github.davidmc24.gradle.plugin.avro") + } + + protected void applyAvroBasePlugin() { + applyPlugin("com.github.davidmc24.gradle.plugin.avro-base") + } + + protected void applyPlugin(String pluginId) { + buildFile << "plugins { id \"${pluginId}\" }\n" + } + + protected void applyPlugin(String pluginId, String version) { + buildFile << "plugins { id \"${pluginId}\" version \"${version}\" }\n" + } + + protected void addDefaultRepository() { + buildFile << "repositories { mavenCentral() }\n" + } + + protected void addImplementationDependency(String dependencySpec) { + addDependency("implementation", dependencySpec) + } + + protected void addRuntimeDependency(String dependencySpec) { + addDependency("runtimeOnly", dependencySpec) + } + + protected void addDependency(String configuration, String dependencySpec) { + buildFile << "dependencies { ${configuration} \"${dependencySpec}\" }\n" + } + + protected void addAvroDependency() { + addImplementationDependency("org.apache.avro:avro:${avroVersion}") + } + + protected void addAvroIpcDependency() { + addImplementationDependency("org.apache.avro:avro-ipc:${avroVersion}") + } + + protected void copyResource(String name, File targetFolder) { + copyResource(name, targetFolder, name) + } + + protected void copyResource(String name, File targetFolder, String targetName) { + def resource = getClass().getResourceAsStream(name) + def file = new File(targetFolder, targetName) + if (resource == null) { + throw new FileNotFoundException("Could not resource with name ${name}") + } + file.parentFile.mkdirs() + file << getClass().getResourceAsStream(name) + } + + protected void copyFile(String srcDir, String destDir, String path) { + def destFile = new File(projectFile(destDir), path) + def srcFile = new File(srcDir, path) + destFile.parentFile.mkdirs() + destFile << srcFile.bytes + } + + protected File projectFile(String path) { + File file = new File(testProjectDir, path) + file.parentFile.mkdirs() + return file + } + + protected File projectFolder(String path) { + File file = new File(testProjectDir, path) + file.mkdirs() + return file + } + + protected GradleRunner createGradleRunner() { +// // Set up code coverage reporting based on https://github.com/koral--/jacoco-gradle-testkit-plugin +// copyResource("/testkit-gradle.properties", testProjectDir, "gradle.properties") + return GradleRunner.create().withProjectDir(testProjectDir).withGradleVersion(gradleVersion.version).withPluginClasspath() + } + + protected BuildResult run(String... args = ["build"]) { + return createGradleRunner().withArguments(determineGradleArguments(args)).build() + } + + protected BuildResult runAndFail(String... args = ["build"]) { + return createGradleRunner().withArguments(determineGradleArguments(args)).buildAndFail() + } + + protected String buildOutputClassPath(String suffix) { + return "build/classes/java/main/${suffix}" + } + + private List determineGradleArguments(String... args) { + def arguments = ["--stacktrace"] + arguments.addAll(Arrays.asList(args)) + if (GradleFeatures.configCache.isSupportedBy(gradleVersion) && !arguments.contains("--no-configuration-cache")) { + arguments << "--configuration-cache" + } + return arguments + } + + protected boolean isLocalTimestampConversionSupported() { + return avroVersion.isGreaterThanOrEqualTo(new Semver("1.10.0")) + } + + protected String getInteropIDLResourceName() { + return localTimestampConversionSupported ? "interop.avdl" : "interop-1.9.avdl" + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/GenerateAvroProtocolTaskFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/GenerateAvroProtocolTaskFunctionalSpec.groovy new file mode 100644 index 00000000000..ad7e17b118e --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/GenerateAvroProtocolTaskFunctionalSpec.groovy @@ -0,0 +1,110 @@ +/* + * Copyright © 2019 David M. Carr + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import spock.lang.Subject + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +@Subject(GenerateAvroProtocolTask) +class GenerateAvroProtocolTaskFunctionalSpec extends FunctionalSpec { + def "With base plugin, declares input on classpath"() { + given: "a build that declares another task's output in the classpath" + applyAvroBasePlugin() + applyPlugin("java") // Jar task appears to only work with the java plugin applied + buildFile << """ + |configurations.create("shared") + |tasks.register("sharedIdlJar", Jar) { + | from "src/shared" + |} + |dependencies { + | shared sharedIdlJar.outputs.files + |} + |tasks.register("generateProtocol", com.github.davidmc24.gradle.plugin.avro.GenerateAvroProtocolTask) { + | classpath = configurations.shared + | source file("src/dependent") + | outputDir = file("build/protocol") + |} + |""".stripMargin() + + copyResource("shared.avdl", projectFolder("src/shared")) + copyResource("dependent.avdl", projectFolder("src/dependent")) + + when: "running the task" + def result = run("generateProtocol") + + then: "running the generate protocol task occurs after running the producing task" + result.tasks*.path == [":sharedIdlJar", ":generateProtocol"] + result.task(":generateProtocol").outcome == SUCCESS + projectFile("build/protocol/com/example/dependent/DependentProtocol.avpr").file + } + + def "With avro plugin, declares input on classpath (runtime configuration by default)"() { + given: "a build that declares another task's output in the classpath" + applyAvroPlugin() + buildFile << """ + |tasks.register("sharedIdlJar", Jar) { + | from "src/shared" + |} + |dependencies { + | runtimeOnly sharedIdlJar.outputs.files + |} + |""".stripMargin() + + copyResource("shared.avdl", projectFolder("src/shared")) + copyResource("dependent.avdl", avroDir) + + when: "running the task" + def result = run("generateAvroProtocol") + + then: "running the generate protocol task occurs after running the producing task" + result.tasks*.path == [":sharedIdlJar", ":generateAvroProtocol"] + result.task(":generateAvroProtocol").outcome == SUCCESS + projectFile("build/generated-main-avro-avpr/com/example/dependent/DependentProtocol.avpr").file + } + + def "supports files with the same name in different directories"() { + given: "a project with two IDL files with the same name, but in different directories" + applyAvroPlugin() + + copyResource("namespaced-idl/v1/test.avdl", projectFolder("src/main/avro/v1")) + copyResource("namespaced-idl/v2/test.avdl", projectFolder("src/main/avro/v2")) + + when: "running the task" + def result = run("generateAvroProtocol") + + then: "avpr files are generated for each IDL file" + result.task(":generateAvroProtocol").outcome == SUCCESS + projectFile("build/generated-main-avro-avpr/org/example/v1/TestProtocol.avpr").file + projectFile("build/generated-main-avro-avpr/org/example/v2/TestProtocol.avpr").file + } + + def "fails if avpr will be overwritten"() { + given: "a project with two IDL files with the same protocol name and namespace" + applyAvroPlugin() + + copyResource("namespaced-idl/v1/test.avdl", projectFolder("src/main/avro/v1")) + copyResource("namespaced-idl/v1/test_same_protocol.avdl", projectFolder("src/main/avro/v1")) + + when: "running the task" + run("generateAvroProtocol") + + then: + def ex = thrown(Exception) + ex.message.contains("Failed to compile IDL file") + ex.message.contains("File already processed with same namespace and protocol name.") + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/IntellijFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/IntellijFunctionalSpec.groovy new file mode 100644 index 00000000000..f6362148e19 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/IntellijFunctionalSpec.groovy @@ -0,0 +1,92 @@ +/* + * Copyright © 2018 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import org.gradle.testkit.runner.BuildResult + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class IntellijFunctionalSpec extends FunctionalSpec { + def "setup"() { + applyAvroPlugin() + applyPlugin("idea") + } + + def "generated intellij project files include source directories for generated source"() { + given: + copyResource("user.avsc", avroDir) + projectFolder("src/main/java") + projectFolder("src/test/java") + projectFolder("src/test/avro") + + when: + runIdea() + + then: + def moduleFile = projectFile("${testProjectDir.name}.iml") + def module = new XmlSlurper().parseText(moduleFile.text) + module.component.content.sourceFolder.findAll { it.@isTestSource.text() == "false" }.@url*.text().sort() == [ + 'file://$MODULE_DIR\$/build/generated-main-avro-java', + 'file://$MODULE_DIR\$/src/main/avro', 'file://$MODULE_DIR\$/src/main/java', + ] + module.component.content.sourceFolder.findAll { it.@isTestSource.text() == "true" }.@url*.text().sort() == [ + 'file://$MODULE_DIR\$/build/generated-test-avro-java', + 'file://$MODULE_DIR\$/src/test/avro', 'file://$MODULE_DIR\$/src/test/java', + ] + } + + def "generated output directories are created by default"() { + when: + def result = runIdea() + + then: + result.task(":idea").outcome == SUCCESS + projectFile("build/generated-main-avro-java").directory + projectFile("build/generated-test-avro-java").directory + } + + def "overriding task's outputDir doesn't result in default directory still being created"() { + given: + buildFile << """ + |tasks.named("generateAvroJava").configure { + | outputDir = file("build/generatedMainAvro") + |} + |tasks.named("generateTestAvroJava").configure { + | outputDir = file("build/generatedTestAvro") + |} + |""".stripMargin() + + when: + def result = runIdea() + + then: + result.task(":idea").outcome == SUCCESS + !projectFile("build/generated-main-avro-java").directory + !projectFile("build/generated-test-avro-java").directory + projectFile("build/generatedMainAvro").directory + projectFile("build/generatedTestAvro").directory + } + + private BuildResult runIdea() { + def args = ["idea"] + if (GradleFeatures.configCache.isSupportedBy(gradleVersion)) { + // As of Gradle 6.7.1, idea plugin doesn't support configuration cache yet. + // Thus, don't try to use it in this spec. + args << "--no-configuration-cache" + } + return run(args as String[]) + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/KotlinDSLCompatibilityFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/KotlinDSLCompatibilityFunctionalSpec.groovy new file mode 100644 index 00000000000..b0b612685d3 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/KotlinDSLCompatibilityFunctionalSpec.groovy @@ -0,0 +1,63 @@ +package com.github.davidmc24.gradle.plugin.avro + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class KotlinDSLCompatibilityFunctionalSpec extends FunctionalSpec { + File kotlinBuildFile + + def "setup"() { + buildFile.delete() // Don't use the Groovy build file created by the superclass + kotlinBuildFile = projectFile("build.gradle.kts") + kotlinBuildFile << """ + |plugins { + | java + | id("com.github.davidmc24.gradle.plugin.avro") + |} + |repositories { + | mavenCentral() + |} + |dependencies { + | implementation("org.apache.avro:avro:${avroVersion}") + |} + |""".stripMargin() + } + + def "works with kotlin DSL"() { + given: + copyResource("user.avsc", avroDir) + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/User.class")).file + } + + def "extension supports configuring all supported properties"() { + given: + copyResource("user.avsc", avroDir) + kotlinBuildFile << """ + |avro { + | isCreateSetters.set(true) + | isCreateOptionalGetters.set(false) + | isGettersReturnOptional.set(false) + | isOptionalGettersForNullableFieldsOnly.set(false) + | fieldVisibility.set("PUBLIC") + | outputCharacterEncoding.set("UTF-8") + | stringType.set("String") + | templateDirectory.set(null as String?) + | isEnableDecimalLogicalType.set(true) + |} + |""".stripMargin() + + when: + def result = run() + + then: + result.task(":generateAvroJava").outcome == SUCCESS + result.task(":compileJava").outcome == SUCCESS + projectFile(buildOutputClassPath("example/avro/User.class")).file + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/OptionsFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/OptionsFunctionalSpec.groovy new file mode 100644 index 00000000000..4b9efc8dc8b --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/OptionsFunctionalSpec.groovy @@ -0,0 +1,396 @@ +/* + * Copyright © 2015-2016 Commerce Technologies, LLC. + * + * 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 com.github.davidmc24.gradle.plugin.avro + +import com.github.davidmc24.gradle.plugin.avro.test.custom.CommentGenerator +import com.github.davidmc24.gradle.plugin.avro.test.custom.TimestampGenerator +import org.apache.avro.compiler.specific.SpecificCompiler.FieldVisibility +import org.apache.avro.generic.GenericData.StringType +import spock.lang.Unroll + +import java.nio.ByteBuffer + +import static org.gradle.testkit.runner.TaskOutcome.FAILED +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +/** + * Functional tests for most functions. Encoding tests have been pulled out into {@link EncodingFunctionalSpec} + */ +class OptionsFunctionalSpec extends FunctionalSpec { + + def "works with default options"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + + and: "the stringType is string" + content.contains("public java.lang.String getName()") + + and: "the fieldVisibility is PRIVATE" + content.contains("private java.lang.String name;") + + and: "the default template is used" + !content.contains("Custom template") + + and: "createSetters is enabled" + content.contains("public void setName(java.lang.String value)") + + and: "createOptionalGetters is disabled" + !content.contains("Optional") + + and: "gettersReturnOptional is disabled" + !content.contains("Optional") + + and: "enableDecimalLogicalType is enabled" + content.contains("public void setSalary(${BigDecimal.name} value)") + } + + @Unroll + def "supports configuring stringType to #stringType"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | stringType = ${stringType} + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + def mainClassContent = getMainClassContent(content) + + and: "the specified stringType is used" + mainClassContent.contains(expectedContent) + + where: + stringType | expectedContent + "'${StringType.String.name()}'" | "public java.lang.String getName()" + "'${StringType.CharSequence.name()}'" | "public java.lang.CharSequence getName()" + "'${StringType.Utf8.name()}'" | "public org.apache.avro.util.Utf8 getName()" + "'${StringType.Utf8.name().toUpperCase()}'" | "public org.apache.avro.util.Utf8 getName()" + "'${StringType.Utf8.name().toLowerCase()}'" | "public org.apache.avro.util.Utf8 getName()" + "${StringType.name}.${StringType.Utf8.name()}" | "public org.apache.avro.util.Utf8 getName()" + } + + @Unroll + def "supports configuring fieldVisibility to #fieldVisibility"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | fieldVisibility = "${fieldVisibility}" + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + def mainClassContent = getMainClassContent(content) + + and: "the specified fieldVisibility is used" + mainClassContent.contains(expectedContent) + + where: + fieldVisibility | expectedContent + FieldVisibility.PRIVATE.name().toLowerCase() | "private java.lang.String name;" + FieldVisibility.PRIVATE.name() | "private java.lang.String name;" + FieldVisibility.PUBLIC.name() | "public java.lang.String name;" + } + + @Unroll + def "supports configuring createSetters to #createSetters"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | createSetters = ${createSetters} + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + def mainClassContent = getMainClassContent(content) + + and: "the specified createSetters is used" + mainClassContent.contains("public void setName(java.lang.String value)") == expectedPresent + + where: + createSetters | expectedPresent + "Boolean.TRUE" | true + "Boolean.FALSE" | false + "true" | true + "false" | false + "'true'" | true + "'false'" | false + } + + @Unroll + def "supports configuring createOptionalGetters to #createOptionalGetters"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | createOptionalGetters = ${createOptionalGetters} + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + def mainClassContent = getMainClassContent(content) + + and: "the nullable getter is generated" + mainClassContent.contains("public java.lang.String getFavoriteColor()") + + and: "the specified createOptionalGetters is used" + mainClassContent.contains("public Optional getOptionalFavoriteColor()") == expectedPresent + + where: + createOptionalGetters | expectedPresent + "Boolean.TRUE" | true + "Boolean.FALSE" | false + "true" | true + "false" | false + "'true'" | true + "'false'" | false + } + + @SuppressWarnings("LineLength") + @Unroll + def "supports configuring gettersReturnOptional/optionalGettersForNullableFieldsOnly to #gettersReturnOptional/#optionalGettersForNullableFieldsOnly"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | gettersReturnOptional = ${gettersReturnOptional} + | optionalGettersForNullableFieldsOnly = ${optionalGettersForNullableFieldsOnly} + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + def mainClassContent = getMainClassContent(content) + + and: "the specified optionalGettersForNullableFieldsOnly is used" + mainClassContent.contains("public Optional getFavoriteColor()") == expectedNullableOptionalGetter + mainClassContent.contains("public java.lang.String getFavoriteColor()") != expectedNullableOptionalGetter + mainClassContent.contains("public Optional getName()") == expectedRequiredOptionalGetter + mainClassContent.contains("public java.lang.String getName()") != expectedRequiredOptionalGetter + + + where: + gettersReturnOptional | optionalGettersForNullableFieldsOnly | expectedNullableOptionalGetter | expectedRequiredOptionalGetter + "Boolean.TRUE" | "Boolean.TRUE" | true | false + "Boolean.TRUE" | "Boolean.FALSE" | true | true + "Boolean.FALSE" | "Boolean.TRUE" | false | false + "Boolean.FALSE" | "Boolean.FALSE" | false | false + "true" | "true" | true | false + "true" | "false" | true | true + "false" | "true" | false | false + "false" | "false" | false | false + "'true'" | "'true'" | true | false + "'true'" | "'false'" | true | true + "'false'" | "'true'" | false | false + "'false'" | "'false'" | false | false + } + + def "supports configuring templateDirectory"() { + given: + def templatesDir = projectFolder("templates/alternateTemplates") + copyResource("user.avsc", avroDir) + copyResource("record.vm", templatesDir) + // This functionality doesn't work with the plugins DSL syntax. + // To load files from the buildscript classpath you need to load the plugin from it as well. + buildFile << """ + |buildscript { + | dependencies { + | classpath files(${readPluginClasspath()}) + | classpath files(["${templatesDir.parentFile.toURI()}"]) + | } + |} + |apply plugin: "com.github.davidmc24.gradle.plugin.avro" + |avro { + | templateDirectory = "/alternateTemplates/" + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + + and: "the specified templates are used" + content.contains("Custom template") + } + + def "supports configuring velocity tool classes"() { + given: + copyAvroTools("buildSrc/src/main/java") + copyAvroTools("src/main/java") + def templatesDir = projectFolder("templates/alternateTemplates") + copyResource("user.avsc", avroDir) + copyResource("record-tools.vm", templatesDir, "record.vm") + // This functionality doesn't work with the plugins DSL syntax. + // To load files from the buildscript classpath you need to load the plugin from it as well. + buildFile << """ + |buildscript { + | dependencies { + | classpath files(${readPluginClasspath()}) + | classpath files(["${templatesDir.parentFile.toURI()}"]) + | } + |} + |apply plugin: "com.github.davidmc24.gradle.plugin.avro" + |avro { + | templateDirectory = "/alternateTemplates/" + | additionalVelocityToolClasses = ['com.github.davidmc24.gradle.plugin.avro.test.custom.TimestampGenerator', + | 'com.github.davidmc24.gradle.plugin.avro.test.custom.CommentGenerator'] + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + + and: "the velocity tools have been applied" + content.contains(CommentGenerator.CUSTOM_COMMENT) + content.contains(TimestampGenerator.MESSAGE_PREFIX) + } + + def "rejects unsupported stringType values"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | stringType = "badValue" + |} + |""".stripMargin() + + when: + def result = runAndFail("generateAvroJava") + + then: + result.task(":generateAvroJava").outcome == FAILED + result.output.contains("Invalid stringType 'badValue'. Value values are: [CharSequence, String, Utf8]") + } + + def "rejects unsupported fieldVisibility values"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | fieldVisibility = "badValue" + |} + |""".stripMargin() + + when: + def result = runAndFail("generateAvroJava") + + then: + result.task(":generateAvroJava").outcome == FAILED + result.output.contains("Invalid fieldVisibility 'badValue'. Value values are: [PUBLIC, PRIVATE]") + } + + @Unroll + def "supports configuring enableDecimalLogicalType to #enableDecimalLogicalType"() { + given: + copyResource("user.avsc", avroDir) + applyAvroPlugin() + buildFile << """ + |avro { + | enableDecimalLogicalType = $enableDecimalLogicalType + |} + |""".stripMargin() + + when: + def result = run("generateAvroJava") + + then: "the task succeeds" + result.task(":generateAvroJava").outcome == SUCCESS + def content = projectFile("build/generated-main-avro-java/example/avro/User.java").text + def mainClassContent = getMainClassContent(content) + + and: "the specified enableDecimalLogicalType is used" + mainClassContent.contains("public void setSalary(${fieldClz.name} value)") + + where: + enableDecimalLogicalType | fieldClz + "Boolean.TRUE" | BigDecimal + "Boolean.FALSE" | ByteBuffer + "true" | BigDecimal + "false" | ByteBuffer + "'true'" | BigDecimal + "'false'" | ByteBuffer + } + + /** + * Returns just the portion of a file that relates to the main class. + * This is used in order to allow assertions on the getters/setters/fields of the generated class itself, as opposed to a Builder. + * + * @param content the file content for which to get the main content + * @return the content of the class, from the start of the class body to the first inner class definition + */ + @SuppressWarnings("LineLength") + private static String getMainClassContent(String content) { + def className = "User" + def matcher = content =~ /(?s)public class ${className} extends org\.apache\.avro\.specific\.\SpecificRecordBase implements org\.apache\.avro\.specific\.SpecificRecord \{(?.*)public static class Builder/ + assert matcher.find() + return matcher.group("mainClassContent") + } + + private void copyAvroTools(String destDir) { + copyFile("src/test/java", destDir, + "com/github/davidmc24/gradle/plugin/avro/test/custom/CommentGenerator.java") + copyFile("src/test/java", destDir, + "com/github/davidmc24/gradle/plugin/avro/test/custom/TimestampGenerator.java") + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/ResolveAvroDependenciesTaskFunctionalSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/ResolveAvroDependenciesTaskFunctionalSpec.groovy new file mode 100644 index 00000000000..b9204417c36 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/ResolveAvroDependenciesTaskFunctionalSpec.groovy @@ -0,0 +1,39 @@ +package com.github.davidmc24.gradle.plugin.avro + +import org.hamcrest.MatcherAssert +import spock.lang.Subject +import uk.co.datumedge.hamcrest.json.SameJSONAs + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +@Subject(ResolveAvroDependenciesTask) +class ResolveAvroDependenciesTaskFunctionalSpec extends FunctionalSpec { + def "resolves dependencies"() { + def srcDir = projectFolder("src/avro/normalized") + + given: "a build with the task declared" + applyAvroBasePlugin() + buildFile << """ + |tasks.register("resolveAvroDependencies", com.github.davidmc24.gradle.plugin.avro.ResolveAvroDependenciesTask) { + | source file("src/avro/normalized") + | outputDir = file("build/avro/resolved") + |} + |""".stripMargin() + + and: "some normalized schema files" + copyResource("/examples/separate/Breed.avsc", srcDir) + copyResource("/examples/separate/Cat.avsc", srcDir) + + when: "running the task" + def result = run("resolveAvroDependencies") + + then: "the resolved schema files are generated" + result.task(":resolveAvroDependencies").outcome == SUCCESS + MatcherAssert.assertThat( + projectFile("build/avro/resolved/example/Cat.avsc").text, + SameJSONAs.sameJSONAs(getClass().getResourceAsStream("/examples/inline/Cat.avsc").text)) + MatcherAssert.assertThat( + projectFile("build/avro/resolved/example/Breed.avsc").text, + SameJSONAs.sameJSONAs(getClass().getResourceAsStream("/examples/separate/Breed.avsc").text)) + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/SchemaResolverSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/SchemaResolverSpec.groovy new file mode 100644 index 00000000000..76b67152c66 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/SchemaResolverSpec.groovy @@ -0,0 +1,200 @@ +package com.github.davidmc24.gradle.plugin.avro + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification +import spock.lang.Unroll + +class SchemaResolverSpec extends Specification { + private Project project + private SchemaResolver resolver + + def setup() { + project = ProjectBuilder.builder().build() + resolver = new SchemaResolver(project.layout, project.logger) + } + + @Unroll + def "Can resolve records that use a separate record type (#resourceNames)"(List resourceNames) { + when: + def files = resourceNames.collect { new File("src/test/resources/resolver/${it}") } + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == resourceNames.size() + + where: + resourceNames << ( + ["SimpleRecord.avsc", "UseRecord.avsc"].permutations() + + ["SimpleRecord.avsc", "UseRecordWithType.avsc"].permutations() + ) + } + + @Unroll + def "Can resolve records that use a separate enum type (#resourceNames)"(List resourceNames) { + when: + def files = resourceNames.collect { new File("src/test/resources/resolver/${it}") } + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == resourceNames.size() + + where: + resourceNames << ( + ["SimpleEnum.avsc", "UseEnum.avsc"].permutations() + + ["SimpleEnum.avsc", "UseEnumWithType.avsc"].permutations() + ) + } + + @Unroll + def "Can resolve records that use a separate fixed type (#resourceNames)"(List resourceNames) { + when: + def files = resourceNames.collect { new File("src/test/resources/resolver/${it}") } + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == resourceNames.size() + + where: + resourceNames << ( + ["SimpleFixed.avsc", "UseFixed.avsc"].permutations() + + ["SimpleFixed.avsc", "UseFixedWithType.avsc"].permutations() + ) + } + + @Unroll + def "Can resolve records that use a separate type in an array (#resourceNames)"(List resourceNames) { + when: + def files = resourceNames.collect { new File("src/test/resources/resolver/${it}") } + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == resourceNames.size() + + where: + resourceNames << ( + ["SimpleEnum.avsc", "SimpleRecord.avsc", "SimpleFixed.avsc", "UseArray.avsc"].permutations() + + ["SimpleEnum.avsc", "SimpleRecord.avsc", "SimpleFixed.avsc", "UseArrayWithType.avsc"].permutations() + ) + } + + @Unroll + def "Can resolve records that use a separate type in an map value (#resourceNames)"(List resourceNames) { + when: + def files = resourceNames.collect { new File("src/test/resources/resolver/${it}") } + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == resourceNames.size() + + where: + resourceNames << ( + ["SimpleEnum.avsc", "SimpleRecord.avsc", "SimpleFixed.avsc", "UseMap.avsc"].permutations() + + ["SimpleEnum.avsc", "SimpleRecord.avsc", "SimpleFixed.avsc", "UseMapWithType.avsc"].permutations() + ) + } + + def "Duplicate record definition succeeds if definition identical"() { + given: + def resourceNames = ["Person.avsc", "Fish.avsc"] + def files = resourceNames.collect { new File("src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/${it}") } + + when: + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == files.size() + } + + def "Duplicate enum definition succeeds if definition identical"() { + given: + def resourceNames = ["Person.avsc", "Cat.avsc"] + def files = resourceNames.collect { new File("src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/${it}") } + + when: + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == files.size() + } + + def "Duplicate fixed definition succeeds if definition identical"() { + given: + def resourceNames = ["ContainsFixed1.avsc", "ContainsFixed2.avsc"] + def files = resourceNames.collect { new File("src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/${it}") } + + when: + def processingState = resolver.resolve(files) + + then: + noExceptionThrown() + processingState.failedFiles.empty + processingState.processedTotal == files.size() + } + + def "Duplicate record definition fails if definition differs"() { + given: + def resourceNames = ["Person.avsc", "Spider.avsc"] + def files = resourceNames.collect { new File("src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/${it}") } + + when: + resolver.resolve(files) + + then: + def ex = thrown(GradleException) + ex.message == "Found conflicting definition of type example.Person in [${files[0].path}, ${files[1].path}]" + } + + def "Duplicate enum definition fails if definition differs"() { + given: + def resourceNames = ["Dog.avsc", "Person.avsc"] + def files = resourceNames.collect { new File("src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/${it}") } + + when: + resolver.resolve(files) + + then: + def ex = thrown(GradleException) + ex.message == "Found conflicting definition of type example.Gender in [${files[0].path}, ${files[1].path}]" + } + + def "Duplicate fixed definition fails if definition differs"() { + given: + def resourceNames = ["ContainsFixed1.avsc", "ContainsFixed3.avsc"] + def files = resourceNames.collect { new File("src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/${it}") } + + when: + resolver.resolve(files) + + then: + def ex = thrown(GradleException) + ex.message == "Found conflicting definition of type example.Picture in [${files[0].path}, ${files[1].path}]" + } + + def "Duplicate record definition in single file fails with clear error"() { + given: + def file = new File("src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/duplicateInSingleFile.avsc") + + when: + resolver.resolve([file]) + + then: + def ex = thrown(GradleException) + ex.message == "Failed to resolve schema definition file ${file.path}; contains duplicate type definition example.avro.date" + } +} diff --git a/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/StringsSpec.groovy b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/StringsSpec.groovy new file mode 100644 index 00000000000..a2e98f773ef --- /dev/null +++ b/lang/java/gradle-plugin/src/test/groovy/com/github/davidmc24/gradle/plugin/avro/StringsSpec.groovy @@ -0,0 +1,57 @@ +package com.github.davidmc24.gradle.plugin.avro + +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +@Subject(Strings) +class StringsSpec extends Specification { + @Unroll + def "isEmpty(#str)"() { + when: + def actual = Strings.isEmpty(str) + then: + actual == expected + where: + str | expected + null | true + "" | true + " " | false + "abc" | false + } + + @Unroll + def "isNotEmpty(#str)"() { + when: + def actual = Strings.isNotEmpty(str) + then: + actual == expected + where: + str | expected + null | false + "" | false + " " | true + "abc" | true + } + + @Unroll + def "when not empty, requireNotEmpty returns argument (#str)"() { + def message = "testMessage" + expect: + Strings.requireNotEmpty(str, message) == str + where: + str << [" ", "abc"] + } + + @Unroll + def "when empty, requireNotEmpty throws exception (#str)"() { + def message = "testMessage" + when: + Strings.requireNotEmpty(str, message) + then: + def ex = thrown(IllegalArgumentException) + ex.message == message + where: + str << [null, ""] + } +} diff --git a/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/CommentGenerator.java b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/CommentGenerator.java new file mode 100644 index 00000000000..767763bebaf --- /dev/null +++ b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/CommentGenerator.java @@ -0,0 +1,11 @@ +package com.github.davidmc24.gradle.plugin.avro.test.custom; + +public class CommentGenerator { + + public static final String CUSTOM_COMMENT = "/** Custom generated comment. */"; + + public String generateComment() { + return CommentGenerator.CUSTOM_COMMENT; + } + +} diff --git a/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneConversion.java b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneConversion.java new file mode 100644 index 00000000000..c80922294a0 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneConversion.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2019 David M. Carr + * + * 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 com.github.davidmc24.gradle.plugin.avro.test.custom; + +import java.util.TimeZone; +import org.apache.avro.Conversion; +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; + +@SuppressWarnings("unused") +public class TimeZoneConversion extends Conversion { + public static final String LOGICAL_TYPE_NAME = "timezone"; + + @Override + public Class getConvertedType() { + return TimeZone.class; + } + + @Override + public String getLogicalTypeName() { + return LOGICAL_TYPE_NAME; + } + + @Override + public TimeZone fromCharSequence(CharSequence value, Schema schema, LogicalType type) { + return TimeZone.getTimeZone(value.toString()); + } + + @Override + public CharSequence toCharSequence(TimeZone value, Schema schema, LogicalType type) { + return value.getID(); + } + + @Override + public Schema getRecommendedSchema() { + return TimeZoneLogicalType.INSTANCE.addToSchema(Schema.create(Schema.Type.STRING)); + } +} diff --git a/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalType.java b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalType.java new file mode 100644 index 00000000000..72ba6e54419 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalType.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2019 David M. Carr + * + * 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 com.github.davidmc24.gradle.plugin.avro.test.custom; + +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; + +public class TimeZoneLogicalType extends LogicalType { + static final TimeZoneLogicalType INSTANCE = new TimeZoneLogicalType(); + + private TimeZoneLogicalType() { + super(TimeZoneConversion.LOGICAL_TYPE_NAME); + } + + @Override + public void validate(Schema schema) { + super.validate(schema); + if (schema.getType() != Schema.Type.STRING) { + throw new IllegalArgumentException("Timezone can only be used with an underlying string type"); + } + } +} diff --git a/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalTypeFactory.java b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalTypeFactory.java new file mode 100644 index 00000000000..5e35dac20a8 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimeZoneLogicalTypeFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2019 David M. Carr + * + * 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 com.github.davidmc24.gradle.plugin.avro.test.custom; + +import org.apache.avro.LogicalType; +import org.apache.avro.LogicalTypes; +import org.apache.avro.Schema; + +public class TimeZoneLogicalTypeFactory implements LogicalTypes.LogicalTypeFactory { + @Override + public LogicalType fromSchema(Schema schema) { + return TimeZoneLogicalType.INSTANCE; + } +} diff --git a/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimestampGenerator.java b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimestampGenerator.java new file mode 100644 index 00000000000..1fa300bf6e4 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/java/com/github/davidmc24/gradle/plugin/avro/test/custom/TimestampGenerator.java @@ -0,0 +1,13 @@ +package com.github.davidmc24.gradle.plugin.avro.test.custom; + + +public class TimestampGenerator { + + public static final String MESSAGE_PREFIX = "Current timestamp is"; + + public String generateTimestampMessage() { + return String.format("%s %s", MESSAGE_PREFIX, + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ISO_DATE_TIME)); + } + +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/Message.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/Message.avsc new file mode 100644 index 00000000000..fefda8fe4a8 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/Message.avsc @@ -0,0 +1,15 @@ +{ + "type" : "record", + "name" : "Message", + "namespace" : "org.apache.avro.test", + "fields" : [ { + "name" : "to", + "type" : "string" + }, { + "name" : "from", + "type" : "string" + }, { + "name" : "body", + "type" : "string" + } ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/customConversion.avpr b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/customConversion.avpr new file mode 100644 index 00000000000..1b32e8fae7c --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/customConversion.avpr @@ -0,0 +1,13 @@ +{"namespace": "test", + "protocol": "CustomConversion", + "types": [ + {"name": "Event", "type": "record", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "start", "type": {"type": "long", "logicalType": "timestamp-millis"} }, + {"name": "timezone", "type": {"type": "string", "logicalType": "timezone"} } + ] + } + ], + "messages": { } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/customConversion.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/customConversion.avsc new file mode 100644 index 00000000000..7f345f2e666 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/customConversion.avsc @@ -0,0 +1,9 @@ +{"namespace": "test", + "type": "record", + "name": "Event", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "start", "type": {"type": "long", "logicalType": "timestamp-millis"} }, + {"name": "timezone", "type": {"type": "string", "logicalType": "timezone"} } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/dependent.avdl b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/dependent.avdl new file mode 100644 index 00000000000..c2ef56b1464 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/dependent.avdl @@ -0,0 +1,26 @@ +/** + * Copyright 2019 Paychex, Inc. + * Licensed pursuant to the terms of the Apache License, Version 2.0 (the "License"); + * your use of the Work is subject to the terms and conditions of the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor + * provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, + * without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible + * for determining the appropriateness of using or redistributing the Work and assume + * any risks associated with your exercise of permissions under this License. + */ + +@namespace("com.example.dependent") +protocol DependentProtocol { + + import idl "shared.avdl"; + + record ThisDependsOnTemporal { + com.example.shared.SomethingShared a; + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Cat.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Cat.avsc new file mode 100644 index 00000000000..cdf2c71b42a --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Cat.avsc @@ -0,0 +1,16 @@ +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "name", "type": "string" }, + { + "name": "gender", + "type": { + "name": "Gender", + "type": "enum", + "symbols": [ "MALE", "FEMALE", "OTHER" ] + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed1.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed1.avsc new file mode 100644 index 00000000000..137ea4eca6a --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed1.avsc @@ -0,0 +1,8 @@ +{ + "name": "ContainsFixed1", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "picture", "type": { "type": "fixed", "name": "Picture", "size": 16 } } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed2.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed2.avsc new file mode 100644 index 00000000000..6f55e12703b --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed2.avsc @@ -0,0 +1,8 @@ +{ + "name": "ContainsFixed2", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "picture", "type": { "type": "fixed", "name": "Picture", "size": 16 } } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed3.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed3.avsc new file mode 100644 index 00000000000..9b8f65aaf2b --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/ContainsFixed3.avsc @@ -0,0 +1,8 @@ +{ + "name": "ContainsFixed3", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "picture", "type": { "type": "fixed", "name": "Picture", "size": 12 } } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Dog.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Dog.avsc new file mode 100644 index 00000000000..a206a724acb --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Dog.avsc @@ -0,0 +1,16 @@ +{ + "name": "Dog", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "name", "type": "string" }, + { + "name": "gender", + "type": { + "name": "Gender", + "type": "enum", + "symbols": [ "MALE", "FEMALE" ] + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Fish.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Fish.avsc new file mode 100644 index 00000000000..5f759351c23 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Fish.avsc @@ -0,0 +1,26 @@ +{ + "name": "Fish", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "name", "type": "string" }, + { + "name": "owner", + "type": { + "name": "Person", + "type": "record", + "fields": [ + { "name": "name", "type": "string" }, + { + "name": "gender", + "type": { + "name": "Gender", + "type": "enum", + "symbols": [ "MALE", "FEMALE", "OTHER" ] + } + } + ] + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Person.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Person.avsc new file mode 100644 index 00000000000..c9e5dc27229 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Person.avsc @@ -0,0 +1,16 @@ +{ + "name": "Person", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "name", "type": "string" }, + { + "name": "gender", + "type": { + "name": "Gender", + "type": "enum", + "symbols": [ "MALE", "FEMALE", "OTHER" ] + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Spider.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Spider.avsc new file mode 100644 index 00000000000..5ecc5334d75 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/Spider.avsc @@ -0,0 +1,18 @@ +{ + "name": "Spider", + "namespace": "example", + "type": "record", + "fields": [ + { "name": "name", "type": "string" }, + { + "name": "owner", + "type": { + "name": "Person", + "type": "record", + "fields": [ + { "name": "name", "type": "string" } + ] + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/duplicateInSingleFile.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/duplicateInSingleFile.avsc new file mode 100644 index 00000000000..581ca1175f1 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/duplicate/duplicateInSingleFile.avsc @@ -0,0 +1,38 @@ +{ + "type" : "record", + "name" : "Fail", + "namespace" : "example.avro", + "fields" : [ { + "name" : "start_date", + "type" : { + "type" : "record", + "name" : "date", + "fields" : [ { + "name" : "day", + "type" : ["null", "int"] + }, { + "name" : "month", + "type" : ["null", "int"] + }, { + "name" : "year", + "type" : ["null", "int"] + } ] + } + }, { + "name" : "end_date", + "type" : { + "type" : "record", + "name" : "date", + "fields" : [ { + "name" : "day", + "type" : ["null", "int"] + }, { + "name" : "month", + "type" : ["null", "int"] + }, { + "name" : "year", + "type" : ["null", "int"] + } ] + } + } ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumField.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumField.avsc new file mode 100644 index 00000000000..05ec79e5a40 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumField.avsc @@ -0,0 +1,15 @@ +{ + "namespace": "example.avro", + "type": "record", + "name": "Test", + "fields": [ + { + "name": "gender", + "type": { + "name": "Gender", + "type": "enum", + "symbols": ["MALE", "FEMALE"] + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumMalformed.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumMalformed.avsc new file mode 100644 index 00000000000..7b86671b805 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumMalformed.avsc @@ -0,0 +1,12 @@ +{ + "namespace": "example.avro", + "type": "record", + "name": "Test", + "fields": [ + { + "name": "gender", + "type": "enum", + "symbols": ["MALE", "FEMALE"] + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumSimple.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumSimple.avsc new file mode 100644 index 00000000000..db570b2e48b --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumSimple.avsc @@ -0,0 +1,11 @@ +{ + "namespace": "example.avro", + "type": "enum", + "symbols": [ + "zero", + "int", + "two", + "three" + ], + "name": "MyEnum" +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumUnion.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumUnion.avsc new file mode 100644 index 00000000000..ff4c6894ecc --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumUnion.avsc @@ -0,0 +1,18 @@ +{ + "namespace": "example.avro", + "type": "record", + "name": "Test", + "fields": [ + { + "name": "kind", + "type": [ + "null", + { + "name": "Kind", + "type": "enum", + "symbols": ["X", "Y", "Z"] + } + ] + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumUseSimple.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumUseSimple.avsc new file mode 100644 index 00000000000..15741804f16 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/enumUseSimple.avsc @@ -0,0 +1,8 @@ +{"namespace": "example.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "kind", "type": "MyEnum"} + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/helloWorld.kt b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/helloWorld.kt new file mode 100644 index 00000000000..7557b3de4a2 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/helloWorld.kt @@ -0,0 +1,25 @@ +/* + * Copyright © 2017 Commerce Technologies, LLC. + * + * 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 demo + +import example.avro.User +import java.time.LocalDate + +fun main(args : Array) { + val user = User("David", 24, "blue", null, LocalDate.of(2019, 1, 1)) + println("Hello, ${user.getName()}") +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/idioma.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/idioma.avsc new file mode 100644 index 00000000000..a246218ba58 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/idioma.avsc @@ -0,0 +1,13 @@ +{ + "namespace": "example.avro", + "type": "enum", + "symbols": [ + "alemán", + "chino", + "español", + "francés", + "inglés", + "japonés" + ], + "name": "Idioma" +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/interop-1.9.avdl b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/interop-1.9.avdl new file mode 100644 index 00000000000..9adcb7d7bc5 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/interop-1.9.avdl @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// Currently genavro only does Protocols. +@namespace("org.apache.avro") +protocol InteropProtocol { + record Foo { + string label; + } + + enum Kind { A, B, C } + fixed MD5(16); + + record Node { + string label; + array children = []; + } + + record Interop { + int intField = 1; + long longField = -1; + string stringField; + boolean boolField = false; + float floatField = 0.0; + double doubleField = -1.0e12; + null nullField; + array arrayField = []; + map mapField; + union { boolean, double, array } unionField; + Kind enumField; + MD5 fixedField; + Node recordField; + decimal(10,2) decimalField; + date dateField; + time_ms timeField; + timestamp_ms timeStampField; +// local_timestamp_ms is not supported in Avro 1.9.x +// local_timestamp_ms localTimeStampField; + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/interop.avdl b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/interop.avdl new file mode 100644 index 00000000000..a290af330db --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/interop.avdl @@ -0,0 +1,54 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// Currently genavro only does Protocols. +@namespace("org.apache.avro") +protocol InteropProtocol { + record Foo { + string label; + } + + enum Kind { A, B, C } + fixed MD5(16); + + record Node { + string label; + array children = []; + } + + record Interop { + int intField = 1; + long longField = -1; + string stringField; + boolean boolField = false; + float floatField = 0.0; + double doubleField = -1.0e12; + null nullField; + array arrayField = []; + map mapField; + union { boolean, double, array } unionField; + Kind enumField; + MD5 fixedField; + Node recordField; + decimal(10,2) decimalField; + date dateField; + time_ms timeField; + timestamp_ms timeStampField; + local_timestamp_ms localTimeStampField; + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/mail.avpr b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/mail.avpr new file mode 100644 index 00000000000..74105922b4b --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/mail.avpr @@ -0,0 +1,26 @@ +{"namespace": "org.apache.avro.test", + "protocol": "Mail", + + "types": [ + {"name": "Message", "type": "record", + "fields": [ + {"name": "to", "type": "string"}, + {"name": "from", "type": "string"}, + {"name": "body", "type": "string"} + ] + } + ], + + "messages": { + "send": { + "request": [{"name": "message", "type": "Message"}], + "response": "string" + }, + "fireandforget": { + "request": [{"name": "message", "type": "Message"}], + "response": "null", + "one-way": true + } + + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v1/test.avdl b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v1/test.avdl new file mode 100644 index 00000000000..242cb42184a --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v1/test.avdl @@ -0,0 +1,6 @@ +@namespace("org.example.v1") +protocol TestProtocol { + record TestRecord { + string field1; + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v1/test_same_protocol.avdl b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v1/test_same_protocol.avdl new file mode 100644 index 00000000000..f388f899ad6 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v1/test_same_protocol.avdl @@ -0,0 +1,6 @@ +@namespace("org.example.v1") +protocol TestProtocol { + record SomeOtherTestRecord { + string field1; + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v2/test.avdl b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v2/test.avdl new file mode 100644 index 00000000000..e6ec904bc2c --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/namespaced-idl/v2/test.avdl @@ -0,0 +1,7 @@ +@namespace("org.example.v2") +protocol TestProtocol { + record TestRecord { + string field1; + string field2; + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/record-tools.vm b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/record-tools.vm new file mode 100644 index 00000000000..1e8b6891268 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/record-tools.vm @@ -0,0 +1,872 @@ +## +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you 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 +## +## https://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. +## +#if ($schema.getNamespace()) +package $this.mangle($schema.getNamespace()); +#end + +import org.apache.avro.generic.GenericArray; +import org.apache.avro.specific.SpecificData; +import org.apache.avro.util.Utf8; +#if (!$schema.isError()) +import org.apache.avro.message.BinaryMessageEncoder; +import org.apache.avro.message.BinaryMessageDecoder; +import org.apache.avro.message.SchemaStore; +#end +#if (${this.gettersReturnOptional} || ${this.createOptionalGetters})import java.util.Optional;#end + +#if ($schema.getDoc()) +/** $schema.getDoc() */ +#end +#foreach ($annotation in $this.javaAnnotations($schema)) +@$annotation +#end +/** +${timestampgenerator.generateTimestampMessage()} +*/ +@org.apache.avro.specific.AvroGenerated +public class ${this.mangle($schema.getName())}#if ($schema.isError()) extends org.apache.avro.specific.SpecificExceptionBase#else extends org.apache.avro.specific.SpecificRecordBase#end implements org.apache.avro.specific.SpecificRecord { + ${commentgenerator.generateComment()} + private static final long serialVersionUID = ${this.fingerprint64($schema)}L; + +#set ($schemaString = $this.javaSplit($schema.toString())) +#set ($customLogicalTypeFactories = $this.getUsedCustomLogicalTypeFactories($schema).entrySet()) +#if (!$customLogicalTypeFactories.isEmpty()) + static { +#foreach ($customLogicalTypeFactory in $customLogicalTypeFactories) + org.apache.avro.LogicalTypes.register("${customLogicalTypeFactory.getKey()}", new ${customLogicalTypeFactory.getValue()}()); +#end + } +#end + + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse($schemaString); + public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } + + private static final SpecificData MODEL$ = new SpecificData(); +#set ($usedConversions = $this.getUsedConversionClasses($schema)) +#if (!$usedConversions.isEmpty()) + static { +#foreach ($conversion in $usedConversions) + MODEL$.addLogicalTypeConversion(new ${conversion}()); +#end + } +#end + +#if (!$schema.isError()) + private static final BinaryMessageEncoder<${this.mangle($schema.getName())}> ENCODER = + new BinaryMessageEncoder<${this.mangle($schema.getName())}>(MODEL$, SCHEMA$); + + private static final BinaryMessageDecoder<${this.mangle($schema.getName())}> DECODER = + new BinaryMessageDecoder<${this.mangle($schema.getName())}>(MODEL$, SCHEMA$); + + /** + * Return the BinaryMessageEncoder instance used by this class. + * @return the message encoder used by this class + */ + public static BinaryMessageEncoder<${this.mangle($schema.getName())}> getEncoder() { + return ENCODER; + } + + /** + * Return the BinaryMessageDecoder instance used by this class. + * @return the message decoder used by this class + */ + public static BinaryMessageDecoder<${this.mangle($schema.getName())}> getDecoder() { + return DECODER; + } + + /** + * Create a new BinaryMessageDecoder instance for this class that uses the specified {@link SchemaStore}. + * @param resolver a {@link SchemaStore} used to find schemas by fingerprint + * @return a BinaryMessageDecoder instance for this class backed by the given SchemaStore + */ + public static BinaryMessageDecoder<${this.mangle($schema.getName())}> createDecoder(SchemaStore resolver) { + return new BinaryMessageDecoder<${this.mangle($schema.getName())}>(MODEL$, SCHEMA$, resolver); + } + + /** + * Serializes this ${schema.getName()} to a ByteBuffer. + * @return a buffer holding the serialized data for this instance + * @throws java.io.IOException if this instance could not be serialized + */ + public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { + return ENCODER.encode(this); + } + + /** + * Deserializes a ${schema.getName()} from a ByteBuffer. + * @param b a byte buffer holding serialized data for an instance of this class + * @return a ${schema.getName()} instance decoded from the given buffer + * @throws java.io.IOException if the given bytes could not be deserialized into an instance of this class + */ + public static ${this.mangle($schema.getName())} fromByteBuffer( + java.nio.ByteBuffer b) throws java.io.IOException { + return DECODER.decode(b); + } +#end + +#foreach ($field in $schema.getFields()) +#if ($field.doc()) + /** $field.doc() */ +#end +#foreach ($annotation in $this.javaAnnotations($field)) + @$annotation +#end + #if (${this.publicFields()})public#elseif (${this.privateFields()})private#end ${this.javaUnbox($field.schema(), false)} ${this.mangle($field.name(), $schema.isError())}; +#end +#if ($schema.isError()) + + public ${this.mangle($schema.getName())}() { + super(); + } + + public ${this.mangle($schema.getName())}(Object value) { + super(value); + } + + public ${this.mangle($schema.getName())}(Throwable cause) { + super(cause); + } + + public ${this.mangle($schema.getName())}(Object value, Throwable cause) { + super(value, cause); + } + +#else +#if ($schema.getFields().size() > 0) + + /** + * Default constructor. Note that this does not initialize fields + * to their default values from the schema. If that is desired then + * one should use newBuilder(). + */ + public ${this.mangle($schema.getName())}() {} +#if ($this.isCreateAllArgsConstructor()) + + /** + * All-args constructor. +#foreach ($field in $schema.getFields()) +#if ($field.doc()) * @param ${this.mangle($field.name())} $field.doc() +#else * @param ${this.mangle($field.name())} The new value for ${field.name()} +#end +#end + */ + public ${this.mangle($schema.getName())}(#foreach($field in $schema.getFields())${this.javaType($field.schema())} ${this.mangle($field.name())}#if($foreach.count < $schema.getFields().size()), #end#end) { +#foreach ($field in $schema.getFields()) + ${this.generateSetterCode($field.schema(), ${this.mangle($field.name())}, ${this.mangle($field.name())})} +#end + } +#else + /** + * This schema contains more than 254 fields which exceeds the maximum number + * of permitted constructor parameters in the JVM. An all-args constructor + * will not be generated. Please use newBuilder() to instantiate + * objects instead. + */ +#end +#end + +#end + public org.apache.avro.specific.SpecificData getSpecificData() { return MODEL$; } + public org.apache.avro.Schema getSchema() { return SCHEMA$; } + // Used by DatumWriter. Applications should not call. + public java.lang.Object get(int field$) { + switch (field$) { +#set ($i = 0) +#foreach ($field in $schema.getFields()) + case $i: return ${this.mangle($field.name(), $schema.isError())}; +#set ($i = $i + 1) +#end + default: throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + +#if ($this.hasLogicalTypeField($schema)) + private static final org.apache.avro.Conversion[] conversions = + new org.apache.avro.Conversion[] { +#foreach ($field in $schema.getFields()) + ${this.conversionInstance($field.schema())}, +#end + null + }; + + @Override + public org.apache.avro.Conversion getConversion(int field) { + return conversions[field]; + } + +#end + // Used by DatumReader. Applications should not call. + @SuppressWarnings(value="unchecked") + public void put(int field$, java.lang.Object value$) { + switch (field$) { +#set ($i = 0) +#foreach ($field in $schema.getFields()) + case $i: ${this.mangle($field.name(), $schema.isError())} = #if(${this.javaType($field.schema())} != "java.lang.Object" && ${this.javaType($field.schema())} != "java.lang.String")(${this.javaType($field.schema())})#{end}value$#if(${this.javaType($field.schema())} == "java.lang.String") != null ? value$.toString() : null#{end}; break; +#set ($i = $i + 1) +#end + default: throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + +#foreach ($field in $schema.getFields()) +#if (${this.gettersReturnOptional} && (!${this.optionalGettersForNullableFieldsOnly} || ${field.schema().isNullable()})) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>. +#if ($field.doc()) * $field.doc() +#end + * @return The value wrapped in an Optional<${this.javaType($field.schema())}>. + */ + public Optional<${this.javaType($field.schema())}> ${this.generateGetMethod($schema, $field)}() { + return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())}); + } +#else + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * @return $field.doc() +#else * @return The value of the '${this.mangle($field.name(), $schema.isError())}' field. +#end + */ + public ${this.javaUnbox($field.schema(), false)} ${this.generateGetMethod($schema, $field)}() { + return ${this.mangle($field.name(), $schema.isError())}; + } +#end + +#if (${this.createOptionalGetters}) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>. +#if ($field.doc()) * $field.doc() +#end + * @return The value wrapped in an Optional<${this.javaType($field.schema())}>. + */ + public Optional<${this.javaType($field.schema())}> ${this.generateGetOptionalMethod($schema, $field)}() { + return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())}); + } +#end + +#if ($this.createSetters) + /** + * Sets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @param value the value to set. + */ + public void ${this.generateSetMethod($schema, $field)}(${this.javaUnbox($field.schema(), false)} value) { + ${this.generateSetterCode($field.schema(), ${this.mangle($field.name(), $schema.isError())}, "value")} + } +#end + +#end + /** + * Creates a new ${this.mangle($schema.getName())} RecordBuilder. + * @return A new ${this.mangle($schema.getName())} RecordBuilder + */ + public static #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder newBuilder() { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(); + } + + /** + * Creates a new ${this.mangle($schema.getName())} RecordBuilder by copying an existing Builder. + * @param other The existing builder to copy. + * @return A new ${this.mangle($schema.getName())} RecordBuilder + */ + public static #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder newBuilder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder other) { + if (other == null) { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(); + } else { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(other); + } + } + + /** + * Creates a new ${this.mangle($schema.getName())} RecordBuilder by copying an existing $this.mangle($schema.getName()) instance. + * @param other The existing instance to copy. + * @return A new ${this.mangle($schema.getName())} RecordBuilder + */ + public static #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder newBuilder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())} other) { + if (other == null) { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(); + } else { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(other); + } + } + + /** + * RecordBuilder for ${this.mangle($schema.getName())} instances. + */ + @org.apache.avro.specific.AvroGenerated + public static class Builder extends#if ($schema.isError()) org.apache.avro.specific.SpecificErrorBuilderBase<${this.mangle($schema.getName())}>#else org.apache.avro.specific.SpecificRecordBuilderBase<${this.mangle($schema.getName())}>#end + + implements#if ($schema.isError()) org.apache.avro.data.ErrorBuilder<${this.mangle($schema.getName())}>#else org.apache.avro.data.RecordBuilder<${this.mangle($schema.getName())}>#end { + +#foreach ($field in $schema.getFields()) +#if ($field.doc()) + /** $field.doc() */ +#end + private ${this.javaUnbox($field.schema(), false)} ${this.mangle($field.name(), $schema.isError())}; +#if (${this.hasBuilder($field.schema())}) + private ${this.javaUnbox($field.schema(), false)}.Builder ${this.mangle($field.name(), $schema.isError())}Builder; +#end +#end + + /** Creates a new Builder */ + private Builder() { + super(SCHEMA$, MODEL$); + } + + /** + * Creates a Builder by copying an existing Builder. + * @param other The existing Builder to copy. + */ + private Builder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder other) { + super(other); +#foreach ($field in $schema.getFields()) + if (isValidValue(fields()[$field.pos()], other.${this.mangle($field.name(), $schema.isError())})) { + this.${this.mangle($field.name(), $schema.isError())} = data().deepCopy(fields()[$field.pos()].schema(), other.${this.mangle($field.name(), $schema.isError())}); + fieldSetFlags()[$field.pos()] = other.fieldSetFlags()[$field.pos()]; + } +#if (${this.hasBuilder($field.schema())}) + if (other.${this.generateHasBuilderMethod($schema, $field)}()) { + this.${this.mangle($field.name(), $schema.isError())}Builder = ${this.javaType($field.schema())}.newBuilder(other.${this.generateGetBuilderMethod($schema, $field)}()); + } +#end +#end + } + + /** + * Creates a Builder by copying an existing $this.mangle($schema.getName()) instance + * @param other The existing instance to copy. + */ + private Builder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())} other) { +#if ($schema.isError()) super(other)#else + super(SCHEMA$, MODEL$)#end; +#foreach ($field in $schema.getFields()) + if (isValidValue(fields()[$field.pos()], other.${this.mangle($field.name(), $schema.isError())})) { + this.${this.mangle($field.name(), $schema.isError())} = data().deepCopy(fields()[$field.pos()].schema(), other.${this.mangle($field.name(), $schema.isError())}); + fieldSetFlags()[$field.pos()] = true; + } +#if (${this.hasBuilder($field.schema())}) + this.${this.mangle($field.name(), $schema.isError())}Builder = null; +#end +#end + } +#if ($schema.isError()) + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder setValue(Object value) { + super.setValue(value); + return this; + } + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder clearValue() { + super.clearValue(); + return this; + } + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder setCause(Throwable cause) { + super.setCause(cause); + return this; + } + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder clearCause() { + super.clearCause(); + return this; + } +#end + +#foreach ($field in $schema.getFields()) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @return The value. + */ + public ${this.javaUnbox($field.schema(), false)} ${this.generateGetMethod($schema, $field)}() { + return ${this.mangle($field.name(), $schema.isError())}; + } + +#if (${this.createOptionalGetters}) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>. +#if ($field.doc()) * $field.doc() +#end + * @return The value wrapped in an Optional<${this.javaType($field.schema())}>. + */ + public Optional<${this.javaType($field.schema())}> ${this.generateGetOptionalMethod($schema, $field)}() { + return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())}); + } +#end + + /** + * Sets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @param value The value of '${this.mangle($field.name(), $schema.isError())}'. + * @return This builder. + */ + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder ${this.generateSetMethod($schema, $field)}(${this.javaUnbox($field.schema(), false)} value) { + validate(fields()[$field.pos()], value); +#if (${this.hasBuilder($field.schema())}) + this.${this.mangle($field.name(), $schema.isError())}Builder = null; +#end + ${this.generateSetterCode($field.schema(), ${this.mangle($field.name(), $schema.isError())}, "value")} + fieldSetFlags()[$field.pos()] = true; + return this; + } + + /** + * Checks whether the '${this.mangle($field.name(), $schema.isError())}' field has been set. +#if ($field.doc()) * $field.doc() +#end + * @return True if the '${this.mangle($field.name(), $schema.isError())}' field has been set, false otherwise. + */ + public boolean ${this.generateHasMethod($schema, $field)}() { + return fieldSetFlags()[$field.pos()]; + } + +#if (${this.hasBuilder($field.schema())}) + /** + * Gets the Builder instance for the '${this.mangle($field.name(), $schema.isError())}' field and creates one if it doesn't exist yet. +#if ($field.doc()) * $field.doc() +#end + * @return This builder. + */ + public ${this.javaType($field.schema())}.Builder ${this.generateGetBuilderMethod($schema, $field)}() { + if (${this.mangle($field.name(), $schema.isError())}Builder == null) { + if (${this.generateHasMethod($schema, $field)}()) { + ${this.generateSetBuilderMethod($schema, $field)}(${this.javaType($field.schema())}.newBuilder(${this.mangle($field.name(), $schema.isError())})); + } else { + ${this.generateSetBuilderMethod($schema, $field)}(${this.javaType($field.schema())}.newBuilder()); + } + } + return ${this.mangle($field.name(), $schema.isError())}Builder; + } + + /** + * Sets the Builder instance for the '${this.mangle($field.name(), $schema.isError())}' field +#if ($field.doc()) * $field.doc() +#end + * @param value The builder instance that must be set. + * @return This builder. + */ + + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder ${this.generateSetBuilderMethod($schema, $field)}(${this.javaUnbox($field.schema(), false)}.Builder value) { + ${this.generateClearMethod($schema, $field)}(); + ${this.mangle($field.name(), $schema.isError())}Builder = value; + return this; + } + + /** + * Checks whether the '${this.mangle($field.name(), $schema.isError())}' field has an active Builder instance +#if ($field.doc()) * $field.doc() +#end + * @return True if the '${this.mangle($field.name(), $schema.isError())}' field has an active Builder instance + */ + public boolean ${this.generateHasBuilderMethod($schema, $field)}() { + return ${this.mangle($field.name(), $schema.isError())}Builder != null; + } +#end + + /** + * Clears the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @return This builder. + */ + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder ${this.generateClearMethod($schema, $field)}() { +#if (${this.isUnboxedJavaTypeNullable($field.schema())}) + ${this.mangle($field.name(), $schema.isError())} = null; +#end +#if (${this.hasBuilder($field.schema())}) + ${this.mangle($field.name(), $schema.isError())}Builder = null; +#end + fieldSetFlags()[$field.pos()] = false; + return this; + } + +#end + @Override + @SuppressWarnings("unchecked") + public ${this.mangle($schema.getName())} build() { + try { + ${this.mangle($schema.getName())} record = new ${this.mangle($schema.getName())}(#if ($schema.isError())getValue(), getCause()#end); +#foreach ($field in $schema.getFields()) +#if (${this.hasBuilder($field.schema())}) + if (${this.mangle($field.name(), $schema.isError())}Builder != null) { + try { + record.${this.mangle($field.name(), $schema.isError())} = this.${this.mangle($field.name(), $schema.isError())}Builder.build(); + } catch (org.apache.avro.AvroMissingFieldException e) { + e.addParentField(record.getSchema().getField("${this.mangle($field.name(), $schema.isError())}")); + throw e; + } + } else { + record.${this.mangle($field.name(), $schema.isError())} = fieldSetFlags()[$field.pos()] ? this.${this.mangle($field.name(), $schema.isError())} : #if(${this.javaType($field.schema())} != "java.lang.Object")(${this.javaType($field.schema())})#{end} defaultValue(fields()[$field.pos()]); + } +#else + record.${this.mangle($field.name(), $schema.isError())} = fieldSetFlags()[$field.pos()] ? this.${this.mangle($field.name(), $schema.isError())} : #if(${this.javaType($field.schema())} != "java.lang.Object")(${this.javaType($field.schema())})#{end} defaultValue(fields()[$field.pos()]); +#end +#end + return record; + } catch (org.apache.avro.AvroMissingFieldException e) { + throw e; + } catch (java.lang.Exception e) { + throw new org.apache.avro.AvroRuntimeException(e); + } + } + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumWriter<${this.mangle($schema.getName())}> + WRITER$ = (org.apache.avro.io.DatumWriter<${this.mangle($schema.getName())}>)MODEL$.createDatumWriter(SCHEMA$); + + @Override public void writeExternal(java.io.ObjectOutput out) + throws java.io.IOException { + WRITER$.write(this, SpecificData.getEncoder(out)); + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumReader<${this.mangle($schema.getName())}> + READER$ = (org.apache.avro.io.DatumReader<${this.mangle($schema.getName())}>)MODEL$.createDatumReader(SCHEMA$); + + @Override public void readExternal(java.io.ObjectInput in) + throws java.io.IOException { + READER$.read(this, SpecificData.getDecoder(in)); + } + +#if ($this.isCustomCodable($schema)) + @Override protected boolean hasCustomCoders() { return true; } + + @Override public void customEncode(org.apache.avro.io.Encoder out) + throws java.io.IOException + { +#set ($nv = 0)## Counter to ensure unique var-names +#set ($maxnv = 0)## Holds high-water mark during recursion +#foreach ($field in $schema.getFields()) +#set ($n = $this.mangle($field.name(), $schema.isError())) +#set ($s = $field.schema()) +#encodeVar(0 "this.${n}" $s) + +#set ($nv = $maxnv) +#end + } + + @Override public void customDecode(org.apache.avro.io.ResolvingDecoder in) + throws java.io.IOException + { + org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); + if (fieldOrder == null) { +## Common case: order of fields hasn't changed, so read them in a +## fixed order according to reader's schema +#set ($nv = 0)## Counter to ensure unique var-names +#set ($maxnv = 0)## Holds high-water mark during recursion +#foreach ($field in $schema.getFields()) +#set ($n = $this.mangle($field.name(), $schema.isError())) +#set ($s = $field.schema()) +#set ($rs = "SCHEMA$.getField(""${n}"").schema()") +#decodeVar(2 "this.${n}" $s $rs) + +#set ($nv = $maxnv) +#end + } else { + for (int i = 0; i < $schema.getFields().size(); i++) { + switch (fieldOrder[i].pos()) { +#set ($fieldno = 0) +#set ($nv = 0)## Counter to ensure unique var-names +#set ($maxnv = 0)## Holds high-water mark during recursion +#foreach ($field in $schema.getFields()) + case $fieldno: +#set ($n = $this.mangle($field.name(), $schema.isError())) +#set ($s = $field.schema()) +#set ($rs = "SCHEMA$.getField(""${n}"").schema()") +#decodeVar(6 "this.${n}" $s $rs) + break; + +#set ($nv = $maxnv) +#set ($fieldno = $fieldno + 1) +#end + default: + throw new java.io.IOException("Corrupt ResolvingDecoder."); + } + } + } + } +#end +} + +#macro( encodeVar $indent $var $s ) +#set ($I = $this.indent($indent)) +##### Compound types (array, map, and union) require calls +##### that will recurse back into this encodeVar macro: +#if ($s.Type.Name.equals("array")) +#encodeArray($indent $var $s) +#elseif ($s.Type.Name.equals("map")) +#encodeMap($indent $var $s) +#elseif ($s.Type.Name.equals("union")) +#encodeUnion($indent $var $s) +##### Use the generated "encode" method as fast way to write +##### (specific) record types: +#elseif ($s.Type.Name.equals("record")) +$I ${var}.customEncode(out); +##### For rest of cases, generate calls out.writeXYZ: +#elseif ($s.Type.Name.equals("null")) +$I out.writeNull(); +#elseif ($s.Type.Name.equals("boolean")) +$I out.writeBoolean(${var}); +#elseif ($s.Type.Name.equals("int")) +$I out.writeInt(${var}); +#elseif ($s.Type.Name.equals("long")) +$I out.writeLong(${var}); +#elseif ($s.Type.Name.equals("float")) +$I out.writeFloat(${var}); +#elseif ($s.Type.Name.equals("double")) +$I out.writeDouble(${var}); +#elseif ($s.Type.Name.equals("string")) +#if ($this.isStringable($s)) +$I out.writeString(${var}.toString()); +#else +$I out.writeString(${var}); +#end +#elseif ($s.Type.Name.equals("bytes")) +$I out.writeBytes(${var}); +#elseif ($s.Type.Name.equals("fixed")) +$I out.writeFixed(${var}.bytes(), 0, ${s.FixedSize}); +#elseif ($s.Type.Name.equals("enum")) +$I out.writeEnum(${var}.ordinal()); +#else +## TODO -- singal a code-gen-time error +#end +#end + +#macro( encodeArray $indent $var $s ) +#set ($I = $this.indent($indent)) +#set ($et = $this.javaType($s.ElementType)) +$I long size${nv} = ${var}.size(); +$I out.writeArrayStart(); +$I out.setItemCount(size${nv}); +$I long actualSize${nv} = 0; +$I for ($et e${nv}: ${var}) { +$I actualSize${nv}++; +$I out.startItem(); +#set ($var = "e${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 2) +#encodeVar($indent $var $s.ElementType) +#set ($nv = $nv - 1) +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +$I out.writeArrayEnd(); +$I if (actualSize${nv} != size${nv}) +$I throw new java.util.ConcurrentModificationException("Array-size written was " + size${nv} + ", but element count was " + actualSize${nv} + "."); +#end + +#macro( encodeMap $indent $var $s ) +#set ($I = $this.indent($indent)) +#set ($kt = $this.getStringType($s)) +#set ($vt = $this.javaType($s.ValueType)) +$I long size${nv} = ${var}.size(); +$I out.writeMapStart(); +$I out.setItemCount(size${nv}); +$I long actualSize${nv} = 0; +$I for (java.util.Map.Entry<$kt, $vt> e${nv}: ${var}.entrySet()) { +$I actualSize${nv}++; +$I out.startItem(); +#if ($this.isStringable($s)) +$I out.writeString(e${nv}.getKey().toString()); +#else +$I out.writeString(e${nv}.getKey()); +#end +$I $vt v${nv} = e${nv}.getValue(); +#set ($var = "v${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 2) +#encodeVar($indent $var $s.ValueType) +#set ($nv = $nv - 1) +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +$I out.writeMapEnd(); +$I if (actualSize${nv} != size${nv}) + throw new java.util.ConcurrentModificationException("Map-size written was " + size${nv} + ", but element count was " + actualSize${nv} + "."); +#end + +#macro( encodeUnion $indent $var $s ) +#set ($I = $this.indent($indent)) +#set ($et = $this.javaType($s.Types.get($this.getNonNullIndex($s)))) +$I if (${var} == null) { +$I out.writeIndex(#if($this.getNonNullIndex($s)==0)1#{else}0#end); +$I out.writeNull(); +$I } else { +$I out.writeIndex(${this.getNonNullIndex($s)}); +#set ($indent = $indent + 2) +#encodeVar($indent $var $s.Types.get($this.getNonNullIndex($s))) +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +#end + + +#macro( decodeVar $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +##### Compound types (array, map, and union) require calls +##### that will recurse back into this decodeVar macro: +#if ($s.Type.Name.equals("array")) +#decodeArray($indent $var $s $rs) +#elseif ($s.Type.Name.equals("map")) +#decodeMap($indent $var $s $rs) +#elseif ($s.Type.Name.equals("union")) +#decodeUnion($indent $var $s $rs) +##### Use the generated "decode" method as fast way to write +##### (specific) record types: +#elseif ($s.Type.Name.equals("record")) +$I if (${var} == null) { +$I ${var} = new ${this.javaType($s)}(); +$I } +$I ${var}.customDecode(in); +##### For rest of cases, generate calls in.readXYZ: +#elseif ($s.Type.Name.equals("null")) +$I in.readNull(); +#elseif ($s.Type.Name.equals("boolean")) +$I $var = in.readBoolean(); +#elseif ($s.Type.Name.equals("int")) +$I $var = in.readInt(); +#elseif ($s.Type.Name.equals("long")) +$I $var = in.readLong(); +#elseif ($s.Type.Name.equals("float")) +$I $var = in.readFloat(); +#elseif ($s.Type.Name.equals("double")) +$I $var = in.readDouble(); +#elseif ($s.Type.Name.equals("string")) +#decodeString( "$I" $var $s ) +#elseif ($s.Type.Name.equals("bytes")) +$I $var = in.readBytes(${var}); +#elseif ($s.Type.Name.equals("fixed")) +$I if (${var} == null) { +$I ${var} = new ${this.javaType($s)}(); +$I } +$I in.readFixed(${var}.bytes(), 0, ${s.FixedSize}); +#elseif ($s.Type.Name.equals("enum")) +$I $var = ${this.javaType($s)}.values()[in.readEnum()]; +#else +## TODO -- singal a code-gen-time error +#end +#end + +#macro( decodeString $II $var $s ) +#set ($st = ${this.getStringType($s)}) +#if ($this.isStringable($s)) +#if ($st.equals("java.net.URI")) +$II try { +$II ${var} = new ${st}(in.readString()); +$II } catch (java.net.URISyntaxException e) { +$II throw new java.io.IOException(e.getMessage()); +$II } +#elseif ($st.equals("java.net.URL")) +$II try { +$II ${var} = new ${st}(in.readString()); +$II } catch (java.net.MalformedURLException e) { +$II throw new java.io.IOException(e.getMessage()); +$II } +#else +$II ${var} = new ${st}(in.readString()); +#end +#elseif ($st.equals("java.lang.String")) +$II $var = in.readString(); +#elseif ($st.equals("org.apache.avro.util.Utf8")) +$II $var = in.readString(${var}); +#else +$II $var = in.readString(${var} instanceof Utf8 ? (Utf8)${var} : null); +#end +#end + +#macro( decodeArray $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +#set ($t = $this.javaType($s)) +#set ($et = $this.javaType($s.ElementType)) +#set ($gat = "SpecificData.Array<${et}>") +$I long size${nv} = in.readArrayStart(); +## Need fresh variable name due to limitation of macro system +$I $t a${nv} = ${var}; +$I if (a${nv} == null) { +$I a${nv} = new ${gat}((int)size${nv}, ${rs}); +$I $var = a${nv}; +$I } else a${nv}.clear(); +$I $gat ga${nv} = (a${nv} instanceof SpecificData.Array ? (${gat})a${nv} : null); +$I for ( ; 0 < size${nv}; size${nv} = in.arrayNext()) { +$I for ( ; size${nv} != 0; size${nv}--) { +$I $et e${nv} = (ga${nv} != null ? ga${nv}.peek() : null); +#set ($var = "e${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 4) +#decodeVar($indent $var $s.ElementType "${rs}.getElementType()") +#set ($nv = $nv - 1) +#set ($indent = $indent - 4) +#set ($I = $this.indent($indent)) +$I a${nv}.add(e${nv}); +$I } +$I } +#end + +#macro( decodeMap $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +#set ($t = $this.javaType($s)) +#set ($kt = $this.getStringType($s)) +#set ($vt = $this.javaType($s.ValueType)) +$I long size${nv} = in.readMapStart(); +$I $t m${nv} = ${var}; // Need fresh name due to limitation of macro system +$I if (m${nv} == null) { +$I m${nv} = new java.util.HashMap<${kt},${vt}>((int)size${nv}); +$I $var = m${nv}; +$I } else m${nv}.clear(); +$I for ( ; 0 < size${nv}; size${nv} = in.mapNext()) { +$I for ( ; size${nv} != 0; size${nv}--) { +$I $kt k${nv} = null; +#decodeString( "$I " "k${nv}" $s ) +$I $vt v${nv} = null; +#set ($var = "v${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 4) +#decodeVar($indent $var $s.ValueType "${rs}.getValueType()") +#set ($nv = $nv - 1) +#set ($indent = $indent - 4) +#set ($I = $this.indent($indent)) +$I m${nv}.put(k${nv}, v${nv}); +$I } +$I } +#end + +#macro( decodeUnion $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +#set ($et = $this.javaType($s.Types.get($this.getNonNullIndex($s)))) +#set ($si = $this.getNonNullIndex($s)) +$I if (in.readIndex() != ${si}) { +$I in.readNull(); +$I ${var} = null; +$I } else { +#set ($indent = $indent + 2) +#decodeVar($indent $var $s.Types.get($si) "${rs}.getTypes().get(${si})") +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +#end diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/record.vm b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/record.vm new file mode 100644 index 00000000000..ae8f0f08d5f --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/record.vm @@ -0,0 +1,869 @@ +## +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you 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 +## +## https://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. +## +#if ($schema.getNamespace()) +package $this.mangle($schema.getNamespace()); +#end + +import org.apache.avro.generic.GenericArray; +import org.apache.avro.specific.SpecificData; +import org.apache.avro.util.Utf8; +#if (!$schema.isError()) +import org.apache.avro.message.BinaryMessageEncoder; +import org.apache.avro.message.BinaryMessageDecoder; +import org.apache.avro.message.SchemaStore; +#end +#if (${this.gettersReturnOptional} || ${this.createOptionalGetters})import java.util.Optional;#end + +#if ($schema.getDoc()) +/** $schema.getDoc() */ +#end +#foreach ($annotation in $this.javaAnnotations($schema)) +@$annotation +#end +@org.apache.avro.specific.AvroGenerated +public class ${this.mangle($schema.getName())}#if ($schema.isError()) extends org.apache.avro.specific.SpecificExceptionBase#else extends org.apache.avro.specific.SpecificRecordBase#end implements org.apache.avro.specific.SpecificRecord { + // Custom template + private static final long serialVersionUID = ${this.fingerprint64($schema)}L; + +#set ($schemaString = $this.javaSplit($schema.toString())) +#set ($customLogicalTypeFactories = $this.getUsedCustomLogicalTypeFactories($schema).entrySet()) +#if (!$customLogicalTypeFactories.isEmpty()) + static { +#foreach ($customLogicalTypeFactory in $customLogicalTypeFactories) + org.apache.avro.LogicalTypes.register("${customLogicalTypeFactory.getKey()}", new ${customLogicalTypeFactory.getValue()}()); +#end + } +#end + + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse($schemaString); + public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } + + private static final SpecificData MODEL$ = new SpecificData(); +#set ($usedConversions = $this.getUsedConversionClasses($schema)) +#if (!$usedConversions.isEmpty()) + static { +#foreach ($conversion in $usedConversions) + MODEL$.addLogicalTypeConversion(new ${conversion}()); +#end + } +#end + +#if (!$schema.isError()) + private static final BinaryMessageEncoder<${this.mangle($schema.getName())}> ENCODER = + new BinaryMessageEncoder<${this.mangle($schema.getName())}>(MODEL$, SCHEMA$); + + private static final BinaryMessageDecoder<${this.mangle($schema.getName())}> DECODER = + new BinaryMessageDecoder<${this.mangle($schema.getName())}>(MODEL$, SCHEMA$); + + /** + * Return the BinaryMessageEncoder instance used by this class. + * @return the message encoder used by this class + */ + public static BinaryMessageEncoder<${this.mangle($schema.getName())}> getEncoder() { + return ENCODER; + } + + /** + * Return the BinaryMessageDecoder instance used by this class. + * @return the message decoder used by this class + */ + public static BinaryMessageDecoder<${this.mangle($schema.getName())}> getDecoder() { + return DECODER; + } + + /** + * Create a new BinaryMessageDecoder instance for this class that uses the specified {@link SchemaStore}. + * @param resolver a {@link SchemaStore} used to find schemas by fingerprint + * @return a BinaryMessageDecoder instance for this class backed by the given SchemaStore + */ + public static BinaryMessageDecoder<${this.mangle($schema.getName())}> createDecoder(SchemaStore resolver) { + return new BinaryMessageDecoder<${this.mangle($schema.getName())}>(MODEL$, SCHEMA$, resolver); + } + + /** + * Serializes this ${schema.getName()} to a ByteBuffer. + * @return a buffer holding the serialized data for this instance + * @throws java.io.IOException if this instance could not be serialized + */ + public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { + return ENCODER.encode(this); + } + + /** + * Deserializes a ${schema.getName()} from a ByteBuffer. + * @param b a byte buffer holding serialized data for an instance of this class + * @return a ${schema.getName()} instance decoded from the given buffer + * @throws java.io.IOException if the given bytes could not be deserialized into an instance of this class + */ + public static ${this.mangle($schema.getName())} fromByteBuffer( + java.nio.ByteBuffer b) throws java.io.IOException { + return DECODER.decode(b); + } +#end + +#foreach ($field in $schema.getFields()) +#if ($field.doc()) + /** $field.doc() */ +#end +#foreach ($annotation in $this.javaAnnotations($field)) + @$annotation +#end + #if (${this.publicFields()})public#elseif (${this.privateFields()})private#end ${this.javaUnbox($field.schema(), false)} ${this.mangle($field.name(), $schema.isError())}; +#end +#if ($schema.isError()) + + public ${this.mangle($schema.getName())}() { + super(); + } + + public ${this.mangle($schema.getName())}(Object value) { + super(value); + } + + public ${this.mangle($schema.getName())}(Throwable cause) { + super(cause); + } + + public ${this.mangle($schema.getName())}(Object value, Throwable cause) { + super(value, cause); + } + +#else +#if ($schema.getFields().size() > 0) + + /** + * Default constructor. Note that this does not initialize fields + * to their default values from the schema. If that is desired then + * one should use newBuilder(). + */ + public ${this.mangle($schema.getName())}() {} +#if ($this.isCreateAllArgsConstructor()) + + /** + * All-args constructor. +#foreach ($field in $schema.getFields()) +#if ($field.doc()) * @param ${this.mangle($field.name())} $field.doc() +#else * @param ${this.mangle($field.name())} The new value for ${field.name()} +#end +#end + */ + public ${this.mangle($schema.getName())}(#foreach($field in $schema.getFields())${this.javaType($field.schema())} ${this.mangle($field.name())}#if($foreach.count < $schema.getFields().size()), #end#end) { +#foreach ($field in $schema.getFields()) + ${this.generateSetterCode($field.schema(), ${this.mangle($field.name())}, ${this.mangle($field.name())})} +#end + } +#else + /** + * This schema contains more than 254 fields which exceeds the maximum number + * of permitted constructor parameters in the JVM. An all-args constructor + * will not be generated. Please use newBuilder() to instantiate + * objects instead. + */ +#end +#end + +#end + public org.apache.avro.specific.SpecificData getSpecificData() { return MODEL$; } + public org.apache.avro.Schema getSchema() { return SCHEMA$; } + // Used by DatumWriter. Applications should not call. + public java.lang.Object get(int field$) { + switch (field$) { +#set ($i = 0) +#foreach ($field in $schema.getFields()) + case $i: return ${this.mangle($field.name(), $schema.isError())}; +#set ($i = $i + 1) +#end + default: throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + +#if ($this.hasLogicalTypeField($schema)) + private static final org.apache.avro.Conversion[] conversions = + new org.apache.avro.Conversion[] { +#foreach ($field in $schema.getFields()) + ${this.conversionInstance($field.schema())}, +#end + null + }; + + @Override + public org.apache.avro.Conversion getConversion(int field) { + return conversions[field]; + } + +#end + // Used by DatumReader. Applications should not call. + @SuppressWarnings(value="unchecked") + public void put(int field$, java.lang.Object value$) { + switch (field$) { +#set ($i = 0) +#foreach ($field in $schema.getFields()) + case $i: ${this.mangle($field.name(), $schema.isError())} = #if(${this.javaType($field.schema())} != "java.lang.Object" && ${this.javaType($field.schema())} != "java.lang.String")(${this.javaType($field.schema())})#{end}value$#if(${this.javaType($field.schema())} == "java.lang.String") != null ? value$.toString() : null#{end}; break; +#set ($i = $i + 1) +#end + default: throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + +#foreach ($field in $schema.getFields()) +#if (${this.gettersReturnOptional} && (!${this.optionalGettersForNullableFieldsOnly} || ${field.schema().isNullable()})) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>. +#if ($field.doc()) * $field.doc() +#end + * @return The value wrapped in an Optional<${this.javaType($field.schema())}>. + */ + public Optional<${this.javaType($field.schema())}> ${this.generateGetMethod($schema, $field)}() { + return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())}); + } +#else + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * @return $field.doc() +#else * @return The value of the '${this.mangle($field.name(), $schema.isError())}' field. +#end + */ + public ${this.javaUnbox($field.schema(), false)} ${this.generateGetMethod($schema, $field)}() { + return ${this.mangle($field.name(), $schema.isError())}; + } +#end + +#if (${this.createOptionalGetters}) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>. +#if ($field.doc()) * $field.doc() +#end + * @return The value wrapped in an Optional<${this.javaType($field.schema())}>. + */ + public Optional<${this.javaType($field.schema())}> ${this.generateGetOptionalMethod($schema, $field)}() { + return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())}); + } +#end + +#if ($this.createSetters) + /** + * Sets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @param value the value to set. + */ + public void ${this.generateSetMethod($schema, $field)}(${this.javaUnbox($field.schema(), false)} value) { + ${this.generateSetterCode($field.schema(), ${this.mangle($field.name(), $schema.isError())}, "value")} + } +#end + +#end + /** + * Creates a new ${this.mangle($schema.getName())} RecordBuilder. + * @return A new ${this.mangle($schema.getName())} RecordBuilder + */ + public static #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder newBuilder() { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(); + } + + /** + * Creates a new ${this.mangle($schema.getName())} RecordBuilder by copying an existing Builder. + * @param other The existing builder to copy. + * @return A new ${this.mangle($schema.getName())} RecordBuilder + */ + public static #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder newBuilder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder other) { + if (other == null) { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(); + } else { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(other); + } + } + + /** + * Creates a new ${this.mangle($schema.getName())} RecordBuilder by copying an existing $this.mangle($schema.getName()) instance. + * @param other The existing instance to copy. + * @return A new ${this.mangle($schema.getName())} RecordBuilder + */ + public static #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder newBuilder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())} other) { + if (other == null) { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(); + } else { + return new #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder(other); + } + } + + /** + * RecordBuilder for ${this.mangle($schema.getName())} instances. + */ + @org.apache.avro.specific.AvroGenerated + public static class Builder extends#if ($schema.isError()) org.apache.avro.specific.SpecificErrorBuilderBase<${this.mangle($schema.getName())}>#else org.apache.avro.specific.SpecificRecordBuilderBase<${this.mangle($schema.getName())}>#end + + implements#if ($schema.isError()) org.apache.avro.data.ErrorBuilder<${this.mangle($schema.getName())}>#else org.apache.avro.data.RecordBuilder<${this.mangle($schema.getName())}>#end { + +#foreach ($field in $schema.getFields()) +#if ($field.doc()) + /** $field.doc() */ +#end + private ${this.javaUnbox($field.schema(), false)} ${this.mangle($field.name(), $schema.isError())}; +#if (${this.hasBuilder($field.schema())}) + private ${this.javaUnbox($field.schema(), false)}.Builder ${this.mangle($field.name(), $schema.isError())}Builder; +#end +#end + + /** Creates a new Builder */ + private Builder() { + super(SCHEMA$, MODEL$); + } + + /** + * Creates a Builder by copying an existing Builder. + * @param other The existing Builder to copy. + */ + private Builder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder other) { + super(other); +#foreach ($field in $schema.getFields()) + if (isValidValue(fields()[$field.pos()], other.${this.mangle($field.name(), $schema.isError())})) { + this.${this.mangle($field.name(), $schema.isError())} = data().deepCopy(fields()[$field.pos()].schema(), other.${this.mangle($field.name(), $schema.isError())}); + fieldSetFlags()[$field.pos()] = other.fieldSetFlags()[$field.pos()]; + } +#if (${this.hasBuilder($field.schema())}) + if (other.${this.generateHasBuilderMethod($schema, $field)}()) { + this.${this.mangle($field.name(), $schema.isError())}Builder = ${this.javaType($field.schema())}.newBuilder(other.${this.generateGetBuilderMethod($schema, $field)}()); + } +#end +#end + } + + /** + * Creates a Builder by copying an existing $this.mangle($schema.getName()) instance + * @param other The existing instance to copy. + */ + private Builder(#if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())} other) { +#if ($schema.isError()) super(other)#else + super(SCHEMA$, MODEL$)#end; +#foreach ($field in $schema.getFields()) + if (isValidValue(fields()[$field.pos()], other.${this.mangle($field.name(), $schema.isError())})) { + this.${this.mangle($field.name(), $schema.isError())} = data().deepCopy(fields()[$field.pos()].schema(), other.${this.mangle($field.name(), $schema.isError())}); + fieldSetFlags()[$field.pos()] = true; + } +#if (${this.hasBuilder($field.schema())}) + this.${this.mangle($field.name(), $schema.isError())}Builder = null; +#end +#end + } +#if ($schema.isError()) + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder setValue(Object value) { + super.setValue(value); + return this; + } + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder clearValue() { + super.clearValue(); + return this; + } + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder setCause(Throwable cause) { + super.setCause(cause); + return this; + } + + @Override + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder clearCause() { + super.clearCause(); + return this; + } +#end + +#foreach ($field in $schema.getFields()) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @return The value. + */ + public ${this.javaUnbox($field.schema(), false)} ${this.generateGetMethod($schema, $field)}() { + return ${this.mangle($field.name(), $schema.isError())}; + } + +#if (${this.createOptionalGetters}) + /** + * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>. +#if ($field.doc()) * $field.doc() +#end + * @return The value wrapped in an Optional<${this.javaType($field.schema())}>. + */ + public Optional<${this.javaType($field.schema())}> ${this.generateGetOptionalMethod($schema, $field)}() { + return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())}); + } +#end + + /** + * Sets the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @param value The value of '${this.mangle($field.name(), $schema.isError())}'. + * @return This builder. + */ + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder ${this.generateSetMethod($schema, $field)}(${this.javaUnbox($field.schema(), false)} value) { + validate(fields()[$field.pos()], value); +#if (${this.hasBuilder($field.schema())}) + this.${this.mangle($field.name(), $schema.isError())}Builder = null; +#end + ${this.generateSetterCode($field.schema(), ${this.mangle($field.name(), $schema.isError())}, "value")} + fieldSetFlags()[$field.pos()] = true; + return this; + } + + /** + * Checks whether the '${this.mangle($field.name(), $schema.isError())}' field has been set. +#if ($field.doc()) * $field.doc() +#end + * @return True if the '${this.mangle($field.name(), $schema.isError())}' field has been set, false otherwise. + */ + public boolean ${this.generateHasMethod($schema, $field)}() { + return fieldSetFlags()[$field.pos()]; + } + +#if (${this.hasBuilder($field.schema())}) + /** + * Gets the Builder instance for the '${this.mangle($field.name(), $schema.isError())}' field and creates one if it doesn't exist yet. +#if ($field.doc()) * $field.doc() +#end + * @return This builder. + */ + public ${this.javaType($field.schema())}.Builder ${this.generateGetBuilderMethod($schema, $field)}() { + if (${this.mangle($field.name(), $schema.isError())}Builder == null) { + if (${this.generateHasMethod($schema, $field)}()) { + ${this.generateSetBuilderMethod($schema, $field)}(${this.javaType($field.schema())}.newBuilder(${this.mangle($field.name(), $schema.isError())})); + } else { + ${this.generateSetBuilderMethod($schema, $field)}(${this.javaType($field.schema())}.newBuilder()); + } + } + return ${this.mangle($field.name(), $schema.isError())}Builder; + } + + /** + * Sets the Builder instance for the '${this.mangle($field.name(), $schema.isError())}' field +#if ($field.doc()) * $field.doc() +#end + * @param value The builder instance that must be set. + * @return This builder. + */ + + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder ${this.generateSetBuilderMethod($schema, $field)}(${this.javaUnbox($field.schema(), false)}.Builder value) { + ${this.generateClearMethod($schema, $field)}(); + ${this.mangle($field.name(), $schema.isError())}Builder = value; + return this; + } + + /** + * Checks whether the '${this.mangle($field.name(), $schema.isError())}' field has an active Builder instance +#if ($field.doc()) * $field.doc() +#end + * @return True if the '${this.mangle($field.name(), $schema.isError())}' field has an active Builder instance + */ + public boolean ${this.generateHasBuilderMethod($schema, $field)}() { + return ${this.mangle($field.name(), $schema.isError())}Builder != null; + } +#end + + /** + * Clears the value of the '${this.mangle($field.name(), $schema.isError())}' field. +#if ($field.doc()) * $field.doc() +#end + * @return This builder. + */ + public #if ($schema.getNamespace())$this.mangle($schema.getNamespace()).#end${this.mangle($schema.getName())}.Builder ${this.generateClearMethod($schema, $field)}() { +#if (${this.isUnboxedJavaTypeNullable($field.schema())}) + ${this.mangle($field.name(), $schema.isError())} = null; +#end +#if (${this.hasBuilder($field.schema())}) + ${this.mangle($field.name(), $schema.isError())}Builder = null; +#end + fieldSetFlags()[$field.pos()] = false; + return this; + } + +#end + @Override + @SuppressWarnings("unchecked") + public ${this.mangle($schema.getName())} build() { + try { + ${this.mangle($schema.getName())} record = new ${this.mangle($schema.getName())}(#if ($schema.isError())getValue(), getCause()#end); +#foreach ($field in $schema.getFields()) +#if (${this.hasBuilder($field.schema())}) + if (${this.mangle($field.name(), $schema.isError())}Builder != null) { + try { + record.${this.mangle($field.name(), $schema.isError())} = this.${this.mangle($field.name(), $schema.isError())}Builder.build(); + } catch (org.apache.avro.AvroMissingFieldException e) { + e.addParentField(record.getSchema().getField("${this.mangle($field.name(), $schema.isError())}")); + throw e; + } + } else { + record.${this.mangle($field.name(), $schema.isError())} = fieldSetFlags()[$field.pos()] ? this.${this.mangle($field.name(), $schema.isError())} : #if(${this.javaType($field.schema())} != "java.lang.Object")(${this.javaType($field.schema())})#{end} defaultValue(fields()[$field.pos()]); + } +#else + record.${this.mangle($field.name(), $schema.isError())} = fieldSetFlags()[$field.pos()] ? this.${this.mangle($field.name(), $schema.isError())} : #if(${this.javaType($field.schema())} != "java.lang.Object")(${this.javaType($field.schema())})#{end} defaultValue(fields()[$field.pos()]); +#end +#end + return record; + } catch (org.apache.avro.AvroMissingFieldException e) { + throw e; + } catch (java.lang.Exception e) { + throw new org.apache.avro.AvroRuntimeException(e); + } + } + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumWriter<${this.mangle($schema.getName())}> + WRITER$ = (org.apache.avro.io.DatumWriter<${this.mangle($schema.getName())}>)MODEL$.createDatumWriter(SCHEMA$); + + @Override public void writeExternal(java.io.ObjectOutput out) + throws java.io.IOException { + WRITER$.write(this, SpecificData.getEncoder(out)); + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumReader<${this.mangle($schema.getName())}> + READER$ = (org.apache.avro.io.DatumReader<${this.mangle($schema.getName())}>)MODEL$.createDatumReader(SCHEMA$); + + @Override public void readExternal(java.io.ObjectInput in) + throws java.io.IOException { + READER$.read(this, SpecificData.getDecoder(in)); + } + +#if ($this.isCustomCodable($schema)) + @Override protected boolean hasCustomCoders() { return true; } + + @Override public void customEncode(org.apache.avro.io.Encoder out) + throws java.io.IOException + { +#set ($nv = 0)## Counter to ensure unique var-names +#set ($maxnv = 0)## Holds high-water mark during recursion +#foreach ($field in $schema.getFields()) +#set ($n = $this.mangle($field.name(), $schema.isError())) +#set ($s = $field.schema()) +#encodeVar(0 "this.${n}" $s) + +#set ($nv = $maxnv) +#end + } + + @Override public void customDecode(org.apache.avro.io.ResolvingDecoder in) + throws java.io.IOException + { + org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); + if (fieldOrder == null) { +## Common case: order of fields hasn't changed, so read them in a +## fixed order according to reader's schema +#set ($nv = 0)## Counter to ensure unique var-names +#set ($maxnv = 0)## Holds high-water mark during recursion +#foreach ($field in $schema.getFields()) +#set ($n = $this.mangle($field.name(), $schema.isError())) +#set ($s = $field.schema()) +#set ($rs = "SCHEMA$.getField(""${n}"").schema()") +#decodeVar(2 "this.${n}" $s $rs) + +#set ($nv = $maxnv) +#end + } else { + for (int i = 0; i < $schema.getFields().size(); i++) { + switch (fieldOrder[i].pos()) { +#set ($fieldno = 0) +#set ($nv = 0)## Counter to ensure unique var-names +#set ($maxnv = 0)## Holds high-water mark during recursion +#foreach ($field in $schema.getFields()) + case $fieldno: +#set ($n = $this.mangle($field.name(), $schema.isError())) +#set ($s = $field.schema()) +#set ($rs = "SCHEMA$.getField(""${n}"").schema()") +#decodeVar(6 "this.${n}" $s $rs) + break; + +#set ($nv = $maxnv) +#set ($fieldno = $fieldno + 1) +#end + default: + throw new java.io.IOException("Corrupt ResolvingDecoder."); + } + } + } + } +#end +} + +#macro( encodeVar $indent $var $s ) +#set ($I = $this.indent($indent)) +##### Compound types (array, map, and union) require calls +##### that will recurse back into this encodeVar macro: +#if ($s.Type.Name.equals("array")) +#encodeArray($indent $var $s) +#elseif ($s.Type.Name.equals("map")) +#encodeMap($indent $var $s) +#elseif ($s.Type.Name.equals("union")) +#encodeUnion($indent $var $s) +##### Use the generated "encode" method as fast way to write +##### (specific) record types: +#elseif ($s.Type.Name.equals("record")) +$I ${var}.customEncode(out); +##### For rest of cases, generate calls out.writeXYZ: +#elseif ($s.Type.Name.equals("null")) +$I out.writeNull(); +#elseif ($s.Type.Name.equals("boolean")) +$I out.writeBoolean(${var}); +#elseif ($s.Type.Name.equals("int")) +$I out.writeInt(${var}); +#elseif ($s.Type.Name.equals("long")) +$I out.writeLong(${var}); +#elseif ($s.Type.Name.equals("float")) +$I out.writeFloat(${var}); +#elseif ($s.Type.Name.equals("double")) +$I out.writeDouble(${var}); +#elseif ($s.Type.Name.equals("string")) +#if ($this.isStringable($s)) +$I out.writeString(${var}.toString()); +#else +$I out.writeString(${var}); +#end +#elseif ($s.Type.Name.equals("bytes")) +$I out.writeBytes(${var}); +#elseif ($s.Type.Name.equals("fixed")) +$I out.writeFixed(${var}.bytes(), 0, ${s.FixedSize}); +#elseif ($s.Type.Name.equals("enum")) +$I out.writeEnum(${var}.ordinal()); +#else +## TODO -- singal a code-gen-time error +#end +#end + +#macro( encodeArray $indent $var $s ) +#set ($I = $this.indent($indent)) +#set ($et = $this.javaType($s.ElementType)) +$I long size${nv} = ${var}.size(); +$I out.writeArrayStart(); +$I out.setItemCount(size${nv}); +$I long actualSize${nv} = 0; +$I for ($et e${nv}: ${var}) { +$I actualSize${nv}++; +$I out.startItem(); +#set ($var = "e${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 2) +#encodeVar($indent $var $s.ElementType) +#set ($nv = $nv - 1) +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +$I out.writeArrayEnd(); +$I if (actualSize${nv} != size${nv}) +$I throw new java.util.ConcurrentModificationException("Array-size written was " + size${nv} + ", but element count was " + actualSize${nv} + "."); +#end + +#macro( encodeMap $indent $var $s ) +#set ($I = $this.indent($indent)) +#set ($kt = $this.getStringType($s)) +#set ($vt = $this.javaType($s.ValueType)) +$I long size${nv} = ${var}.size(); +$I out.writeMapStart(); +$I out.setItemCount(size${nv}); +$I long actualSize${nv} = 0; +$I for (java.util.Map.Entry<$kt, $vt> e${nv}: ${var}.entrySet()) { +$I actualSize${nv}++; +$I out.startItem(); +#if ($this.isStringable($s)) +$I out.writeString(e${nv}.getKey().toString()); +#else +$I out.writeString(e${nv}.getKey()); +#end +$I $vt v${nv} = e${nv}.getValue(); +#set ($var = "v${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 2) +#encodeVar($indent $var $s.ValueType) +#set ($nv = $nv - 1) +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +$I out.writeMapEnd(); +$I if (actualSize${nv} != size${nv}) + throw new java.util.ConcurrentModificationException("Map-size written was " + size${nv} + ", but element count was " + actualSize${nv} + "."); +#end + +#macro( encodeUnion $indent $var $s ) +#set ($I = $this.indent($indent)) +#set ($et = $this.javaType($s.Types.get($this.getNonNullIndex($s)))) +$I if (${var} == null) { +$I out.writeIndex(#if($this.getNonNullIndex($s)==0)1#{else}0#end); +$I out.writeNull(); +$I } else { +$I out.writeIndex(${this.getNonNullIndex($s)}); +#set ($indent = $indent + 2) +#encodeVar($indent $var $s.Types.get($this.getNonNullIndex($s))) +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +#end + + +#macro( decodeVar $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +##### Compound types (array, map, and union) require calls +##### that will recurse back into this decodeVar macro: +#if ($s.Type.Name.equals("array")) +#decodeArray($indent $var $s $rs) +#elseif ($s.Type.Name.equals("map")) +#decodeMap($indent $var $s $rs) +#elseif ($s.Type.Name.equals("union")) +#decodeUnion($indent $var $s $rs) +##### Use the generated "decode" method as fast way to write +##### (specific) record types: +#elseif ($s.Type.Name.equals("record")) +$I if (${var} == null) { +$I ${var} = new ${this.javaType($s)}(); +$I } +$I ${var}.customDecode(in); +##### For rest of cases, generate calls in.readXYZ: +#elseif ($s.Type.Name.equals("null")) +$I in.readNull(); +#elseif ($s.Type.Name.equals("boolean")) +$I $var = in.readBoolean(); +#elseif ($s.Type.Name.equals("int")) +$I $var = in.readInt(); +#elseif ($s.Type.Name.equals("long")) +$I $var = in.readLong(); +#elseif ($s.Type.Name.equals("float")) +$I $var = in.readFloat(); +#elseif ($s.Type.Name.equals("double")) +$I $var = in.readDouble(); +#elseif ($s.Type.Name.equals("string")) +#decodeString( "$I" $var $s ) +#elseif ($s.Type.Name.equals("bytes")) +$I $var = in.readBytes(${var}); +#elseif ($s.Type.Name.equals("fixed")) +$I if (${var} == null) { +$I ${var} = new ${this.javaType($s)}(); +$I } +$I in.readFixed(${var}.bytes(), 0, ${s.FixedSize}); +#elseif ($s.Type.Name.equals("enum")) +$I $var = ${this.javaType($s)}.values()[in.readEnum()]; +#else +## TODO -- singal a code-gen-time error +#end +#end + +#macro( decodeString $II $var $s ) +#set ($st = ${this.getStringType($s)}) +#if ($this.isStringable($s)) +#if ($st.equals("java.net.URI")) +$II try { +$II ${var} = new ${st}(in.readString()); +$II } catch (java.net.URISyntaxException e) { +$II throw new java.io.IOException(e.getMessage()); +$II } +#elseif ($st.equals("java.net.URL")) +$II try { +$II ${var} = new ${st}(in.readString()); +$II } catch (java.net.MalformedURLException e) { +$II throw new java.io.IOException(e.getMessage()); +$II } +#else +$II ${var} = new ${st}(in.readString()); +#end +#elseif ($st.equals("java.lang.String")) +$II $var = in.readString(); +#elseif ($st.equals("org.apache.avro.util.Utf8")) +$II $var = in.readString(${var}); +#else +$II $var = in.readString(${var} instanceof Utf8 ? (Utf8)${var} : null); +#end +#end + +#macro( decodeArray $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +#set ($t = $this.javaType($s)) +#set ($et = $this.javaType($s.ElementType)) +#set ($gat = "SpecificData.Array<${et}>") +$I long size${nv} = in.readArrayStart(); +## Need fresh variable name due to limitation of macro system +$I $t a${nv} = ${var}; +$I if (a${nv} == null) { +$I a${nv} = new ${gat}((int)size${nv}, ${rs}); +$I $var = a${nv}; +$I } else a${nv}.clear(); +$I $gat ga${nv} = (a${nv} instanceof SpecificData.Array ? (${gat})a${nv} : null); +$I for ( ; 0 < size${nv}; size${nv} = in.arrayNext()) { +$I for ( ; size${nv} != 0; size${nv}--) { +$I $et e${nv} = (ga${nv} != null ? ga${nv}.peek() : null); +#set ($var = "e${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 4) +#decodeVar($indent $var $s.ElementType "${rs}.getElementType()") +#set ($nv = $nv - 1) +#set ($indent = $indent - 4) +#set ($I = $this.indent($indent)) +$I a${nv}.add(e${nv}); +$I } +$I } +#end + +#macro( decodeMap $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +#set ($t = $this.javaType($s)) +#set ($kt = $this.getStringType($s)) +#set ($vt = $this.javaType($s.ValueType)) +$I long size${nv} = in.readMapStart(); +$I $t m${nv} = ${var}; // Need fresh name due to limitation of macro system +$I if (m${nv} == null) { +$I m${nv} = new java.util.HashMap<${kt},${vt}>((int)size${nv}); +$I $var = m${nv}; +$I } else m${nv}.clear(); +$I for ( ; 0 < size${nv}; size${nv} = in.mapNext()) { +$I for ( ; size${nv} != 0; size${nv}--) { +$I $kt k${nv} = null; +#decodeString( "$I " "k${nv}" $s ) +$I $vt v${nv} = null; +#set ($var = "v${nv}") +#set ($nv = $nv + 1) +#set ($maxnv = $nv) +#set ($indent = $indent + 4) +#decodeVar($indent $var $s.ValueType "${rs}.getValueType()") +#set ($nv = $nv - 1) +#set ($indent = $indent - 4) +#set ($I = $this.indent($indent)) +$I m${nv}.put(k${nv}, v${nv}); +$I } +$I } +#end + +#macro( decodeUnion $indent $var $s $rs ) +#set ($I = $this.indent($indent)) +#set ($et = $this.javaType($s.Types.get($this.getNonNullIndex($s)))) +#set ($si = $this.getNonNullIndex($s)) +$I if (in.readIndex() != ${si}) { +$I in.readNull(); +$I ${var} = null; +$I } else { +#set ($indent = $indent + 2) +#decodeVar($indent $var $s.Types.get($si) "${rs}.getTypes().get(${si})") +#set ($indent = $indent - 2) +#set ($I = $this.indent($indent)) +$I } +#end diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/shared.avdl b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/shared.avdl new file mode 100644 index 00000000000..b1c4f4dfb7b --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/shared.avdl @@ -0,0 +1,23 @@ +/** + * Copyright 2019 Paychex, Inc. + * Licensed pursuant to the terms of the Apache License, Version 2.0 (the "License"); + * your use of the Work is subject to the terms and conditions of the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor + * provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, + * without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible + * for determining the appropriateness of using or redistributing the Work and assume + * any risks associated with your exercise of permissions under this License. + */ + +@namespace("com.example.shared") +protocol SharedProtocol { + record SomethingShared { + string greeting; + } +} diff --git a/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/user.avsc b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/user.avsc new file mode 100644 index 00000000000..5e3f4d01670 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/com/github/davidmc24/gradle/plugin/avro/user.avsc @@ -0,0 +1,17 @@ +{"namespace": "example.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "favorite_number", "type": ["int", "null"]}, + {"name": "favorite_color", "type": ["string", "null"]}, + { + "name": "salary", + "type": { "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2 } + }, + { + "name": "birth_date", + "type": { "type": "int", "logicalType": "date" } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/examples/inline/Cat.avsc b/lang/java/gradle-plugin/src/test/resources/examples/inline/Cat.avsc new file mode 100644 index 00000000000..0752abad722 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/examples/inline/Cat.avsc @@ -0,0 +1,17 @@ +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields" : [ + { + "name": "breed", + "type": { + "name": "Breed", + "type": "enum", + "symbols" : [ + "ABYSSINIAN", "AMERICAN_SHORTHAIR", "BIRMAN", "MAINE_COON", "ORIENTAL", "PERSIAN", "RAGDOLL", "SIAMESE", "SPHYNX" + ] + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/examples/separate/Breed.avsc b/lang/java/gradle-plugin/src/test/resources/examples/separate/Breed.avsc new file mode 100644 index 00000000000..ce752ac4ea1 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/examples/separate/Breed.avsc @@ -0,0 +1,6 @@ +{ + "name": "Breed", + "namespace": "example", + "type": "enum", + "symbols" : ["ABYSSINIAN", "AMERICAN_SHORTHAIR", "BIRMAN", "MAINE_COON", "ORIENTAL", "PERSIAN", "RAGDOLL", "SIAMESE", "SPHYNX"] +} diff --git a/lang/java/gradle-plugin/src/test/resources/examples/separate/Cat.avsc b/lang/java/gradle-plugin/src/test/resources/examples/separate/Cat.avsc new file mode 100644 index 00000000000..cb5aa8be4f3 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/examples/separate/Cat.avsc @@ -0,0 +1,8 @@ +{ + "name": "Cat", + "namespace": "example", + "type": "record", + "fields" : [ + {"name": "breed", "type": "Breed"} + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/SimpleEnum.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/SimpleEnum.avsc new file mode 100644 index 00000000000..e4f869dee90 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/SimpleEnum.avsc @@ -0,0 +1,6 @@ +{ + "name": "SimpleEnum", + "namespace": "resolver", + "type": "enum", + "symbols" : ["val1", "val2", "val3"] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/SimpleFixed.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/SimpleFixed.avsc new file mode 100644 index 00000000000..b2b3c56137d --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/SimpleFixed.avsc @@ -0,0 +1,6 @@ +{ + "name": "SimpleFixed", + "type": "fixed", + "namespace": "resolver", + "size": 16 +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/SimpleRecord.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/SimpleRecord.avsc new file mode 100644 index 00000000000..2db062f3303 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/SimpleRecord.avsc @@ -0,0 +1,8 @@ +{ + "name": "SimpleRecord", + "type": "record", + "namespace": "resolver", + "fields": [ + { "name": "field1", "type": "string" } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseArray.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseArray.avsc new file mode 100644 index 00000000000..1b39cd9a055 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseArray.avsc @@ -0,0 +1,10 @@ +{ + "name": "UseRecord", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": {"type": "array", "items": "SimpleRecord"} }, + {"name": "field2", "type": {"type": "array", "items": "SimpleEnum"} }, + {"name": "field3", "type": {"type": "array", "items": "SimpleFixed"} } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseArrayWithType.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseArrayWithType.avsc new file mode 100644 index 00000000000..58407ee1fd1 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseArrayWithType.avsc @@ -0,0 +1,10 @@ +{ + "name": "UseRecord", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": {"type": "array", "items": {"type": "SimpleRecord"} } }, + {"name": "field2", "type": {"type": "array", "items": {"type": "SimpleEnum"} } }, + {"name": "field3", "type": {"type": "array", "items": {"type": "SimpleFixed"} } } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseEnum.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseEnum.avsc new file mode 100644 index 00000000000..d8c97fd2e90 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseEnum.avsc @@ -0,0 +1,8 @@ +{ + "name": "UseEnum", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": "SimpleEnum"} + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseEnumWithType.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseEnumWithType.avsc new file mode 100644 index 00000000000..fc2893f7ef4 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseEnumWithType.avsc @@ -0,0 +1,8 @@ +{ + "name": "UseEnum", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": {"type": "SimpleEnum"} } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseFixed.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseFixed.avsc new file mode 100644 index 00000000000..22017e7ddc5 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseFixed.avsc @@ -0,0 +1,8 @@ +{ + "name": "UseEnum", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": "SimpleFixed"} + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseFixedWithType.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseFixedWithType.avsc new file mode 100644 index 00000000000..f5253e23316 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseFixedWithType.avsc @@ -0,0 +1,8 @@ +{ + "name": "UseEnum", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": {"type": "SimpleFixed"} } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseMap.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseMap.avsc new file mode 100644 index 00000000000..5f0689414ef --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseMap.avsc @@ -0,0 +1,10 @@ +{ + "name": "UseRecord", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": {"type": "map", "values": "SimpleRecord"} }, + {"name": "field2", "type": {"type": "map", "values": "SimpleEnum"} }, + {"name": "field3", "type": {"type": "map", "values": "SimpleFixed"} } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseMapWithType.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseMapWithType.avsc new file mode 100644 index 00000000000..b927f9ec301 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseMapWithType.avsc @@ -0,0 +1,10 @@ +{ + "name": "UseRecord", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": {"type": "map", "values": {"type": "SimpleRecord"} } }, + {"name": "field2", "type": {"type": "map", "values": {"type": "SimpleEnum"} } }, + {"name": "field3", "type": {"type": "map", "values": {"type": "SimpleFixed"} } } + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseRecord.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseRecord.avsc new file mode 100644 index 00000000000..6d9fb0431fb --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseRecord.avsc @@ -0,0 +1,8 @@ +{ + "name": "UseRecord", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": "SimpleRecord"} + ] +} diff --git a/lang/java/gradle-plugin/src/test/resources/resolver/UseRecordWithType.avsc b/lang/java/gradle-plugin/src/test/resources/resolver/UseRecordWithType.avsc new file mode 100644 index 00000000000..522448c22bd --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/resolver/UseRecordWithType.avsc @@ -0,0 +1,8 @@ +{ + "name": "UseRecord", + "namespace": "resolver", + "type": "record", + "fields" : [ + {"name": "field1", "type": {"type": "SimpleRecord"} } + ] +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/build.gradle.kts b/lang/java/gradle-plugin/test-project-kotlin/build.gradle.kts new file mode 100644 index 00000000000..8ef313a8127 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("idea") + id("com.github.davidmc24.gradle.plugin.avro") version ("1.2.0") +// id "com.github.davidmc24.gradle.plugin.avro" version "1.2.1-SNAPSHOT" +} + +repositories { + mavenCentral() +} + +project.ext.set("avroVersion", "1.11.0") +dependencies { + implementation("org.apache.avro:avro:${project.ext.get("avroVersion")}") + implementation("org.apache.avro:avro-tools:${project.ext.get("avroVersion")}") + testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } +} + +avro { + stringType.set("CharSequence") + fieldVisibility.set("private") + customConversion(org.apache.avro.Conversions.UUIDConversion::class.java) +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/gradle/wrapper/gradle-wrapper.jar b/lang/java/gradle-plugin/test-project-kotlin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..7454180f2ae Binary files /dev/null and b/lang/java/gradle-plugin/test-project-kotlin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lang/java/gradle-plugin/test-project-kotlin/gradle/wrapper/gradle-wrapper.properties b/lang/java/gradle-plugin/test-project-kotlin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..05679dc3c18 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lang/java/gradle-plugin/test-project-kotlin/gradlew b/lang/java/gradle-plugin/test-project-kotlin/gradlew new file mode 100755 index 00000000000..744e882ed57 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/lang/java/gradle-plugin/test-project-kotlin/gradlew.bat b/lang/java/gradle-plugin/test-project-kotlin/gradlew.bat new file mode 100644 index 00000000000..107acd32c4e --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lang/java/gradle-plugin/test-project-kotlin/settings.gradle.kts b/lang/java/gradle-plugin/test-project-kotlin/settings.gradle.kts new file mode 100644 index 00000000000..1b5d1a8cdea --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + mavenLocal() + } +} + +rootProject.name = "test-project" diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/BuggyRecord.avsc b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/BuggyRecord.avsc new file mode 100644 index 00000000000..57f422504aa --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/BuggyRecord.avsc @@ -0,0 +1,26 @@ +{ + "namespace":"com.example", + "type":"record", + "name":"BuggyRecord", + "fields":[ + { + "name":"my_mandatory_date", + "type":{ + "type":"long", + "logicalType":"timestamp-millis" + }, + "default":1502250227187 + }, + { + "name":"my_optional_date", + "type":[ + { + "type":"long", + "logicalType":"timestamp-millis" + }, + "null" + ], + "default":1502250227187 + } + ] +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/BuggyRecordWorkaround.avsc b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/BuggyRecordWorkaround.avsc new file mode 100644 index 00000000000..e397d55a025 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/BuggyRecordWorkaround.avsc @@ -0,0 +1,26 @@ +{ + "namespace":"com.example", + "type":"record", + "name":"BuggyRecordWorkaround", + "fields":[ + { + "name":"my_mandatory_date", + "type":{ + "type":"long", + "logicalType":"timestamp-millis" + }, + "default":1502250227187 + }, + { + "name":"my_optional_date", + "type":[ + "null", + { + "type":"long", + "logicalType":"timestamp-millis" + } + ], + "default":null + } + ] +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/Messages.avsc b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/Messages.avsc new file mode 100644 index 00000000000..1dc457940ff --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/Messages.avsc @@ -0,0 +1,26 @@ +{ + "type": "record", + "name": "Messages", + "namespace": "com.somedomain", + "fields": [ + { + "name": "start", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "end", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ], + "default": null + } + ] + } +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/UUIDTestRecord.avsc b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/UUIDTestRecord.avsc new file mode 100644 index 00000000000..a835110902a --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/main/avro/UUIDTestRecord.avsc @@ -0,0 +1,13 @@ +{ + "name": "UUIDTestRecord", + "type": "record", + "fields": [ + { + "name": "id", + "type": { + "type": "string", + "logicalType": "uuid" + } + } + ] +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/main/java/project/SystemUtil.java b/lang/java/gradle-plugin/test-project-kotlin/src/main/java/project/SystemUtil.java new file mode 100644 index 00000000000..5a04d2b5c66 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/main/java/project/SystemUtil.java @@ -0,0 +1,38 @@ +package project; + +import java.security.Permission; + +class SystemUtil { + private static final String PERMISSION_PREFIX = "exitVM."; + + static class ExitTrappedException extends SecurityException { + private final int status; + + ExitTrappedException(int status) { + super("Trapped System.exit(" + status + ")"); + this.status = status; + } + + int getStatus() { + return status; + } + } + + static void forbidSystemExitCall() { + final SecurityManager securityManager = new SecurityManager() { + public void checkPermission(Permission permission) { + String permissionName = permission.getName(); + if (permissionName.startsWith(PERMISSION_PREFIX)) { + String suffix = permissionName.substring(PERMISSION_PREFIX.length()); + int status = Integer.parseInt(suffix); + throw new ExitTrappedException(status); + } + } + }; + System.setSecurityManager(securityManager); + } + + static void allowSystemExitCall() { + System.setSecurityManager(null); + } +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/CLIComparisonTest.java b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/CLIComparisonTest.java new file mode 100644 index 00000000000..9990da98e10 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/CLIComparisonTest.java @@ -0,0 +1,56 @@ +package project; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static project.CLIUtil.runCLITool; + +public class CLIComparisonTest { + @TempDir + Path cliGeneratedDir; + Path schemaDir = Paths.get("src/main/avro"); + Path pluginGeneratedDir = Paths.get("build/generated-main-avro-java"); + + @SuppressWarnings("unused") + private static Stream compareSpecificCompilerOutput() { + return Stream.of( + // From https://stackoverflow.com/questions/45581437/how-to-specify-converter-for-default-value-in-avro-union-logical-type-fields + Arguments.of("BuggyRecord.avsc", "com/example/BuggyRecord.java", "compile schema".split(" ")), + // From https://github.com/davidmc24/gradle-avro-plugin/issues/120 + Arguments.of("Messages.avsc", "com/somedomain/Messages.java", "compile schema".split(" ")) + ); + } + + @ParameterizedTest + @MethodSource + void compareSpecificCompilerOutput(String schemaPath, String generatedPath, String... toolArgs) throws Exception { + Path schemaFile = schemaDir.resolve(schemaPath); + Path pluginGeneratedFile = pluginGeneratedDir.resolve(generatedPath); + Path cliGeneratedFile = cliGeneratedDir.resolve(generatedPath); + + List args = new ArrayList<>(Arrays.asList(toolArgs)); + args.add(schemaFile.toString()); + args.add(cliGeneratedDir.toString()); + runCLITool(args.toArray(new String[0])); + + String pluginGeneratedContent = readFile(pluginGeneratedFile); + String cliGeneratedContent = readFile(cliGeneratedFile); + Assertions.assertEquals(cliGeneratedContent, pluginGeneratedContent); + } + + private static String readFile(Path file) throws Exception { + return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + } +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/CLIUtil.java b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/CLIUtil.java new file mode 100644 index 00000000000..326e33d2586 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/CLIUtil.java @@ -0,0 +1,19 @@ +package project; + +import org.apache.avro.tool.Main; +import org.junit.jupiter.api.Assertions; + +class CLIUtil { + private static final int STATUS_SUCCESS = 0; + + static void runCLITool(String... args) throws Exception { + SystemUtil.forbidSystemExitCall(); + try { + Main.main(args); + } catch (SystemUtil.ExitTrappedException ex) { + Assertions.assertEquals(STATUS_SUCCESS, ex.getStatus(), "CLI tool failed"); + } finally { + SystemUtil.allowSystemExitCall(); + } + } +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/RandomRecordTest.java b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/RandomRecordTest.java new file mode 100644 index 00000000000..47b4d306393 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/RandomRecordTest.java @@ -0,0 +1,46 @@ +package project; + +import org.apache.avro.specific.SpecificRecord; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static project.CLIUtil.runCLITool; + +public class RandomRecordTest { + @TempDir + Path cliGeneratedDir; + Path schemaDir = Paths.get("src/main/avro"); + + @SuppressWarnings("unused") + private static Stream generateRandomRecords() { + return Stream.of( + // From https://stackoverflow.com/questions/45581437/how-to-specify-converter-for-default-value-in-avro-union-logical-type-fields + Arguments.of("BuggyRecord.avsc"), + // From https://github.com/davidmc24/gradle-avro-plugin/issues/120 + Arguments.of("Messages.avsc") + ); + } + + @ParameterizedTest + @MethodSource + void generateRandomRecords(String schemaPath) throws Exception { + Path schemaFile = schemaDir.resolve(schemaPath); + Path outputFile = cliGeneratedDir.resolve("random.avro"); + List args = new ArrayList<>(); + args.add("random"); + args.add("--count"); + args.add("1"); + args.add("--schema-file"); + args.add(schemaFile.toString()); + args.add(outputFile.toString()); + runCLITool(args.toArray(new String[0])); + } +} diff --git a/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/RecordTest.java b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/RecordTest.java new file mode 100644 index 00000000000..8f979a6a7a5 --- /dev/null +++ b/lang/java/gradle-plugin/test-project-kotlin/src/test/java/project/RecordTest.java @@ -0,0 +1,50 @@ +package project; + +import com.example.BuggyRecord; +import com.example.BuggyRecordWorkaround; +import com.somedomain.Messages; +import org.apache.avro.Schema; +import org.apache.avro.io.Encoder; +import org.apache.avro.io.EncoderFactory; +import org.apache.avro.specific.SpecificDatumWriter; +import org.apache.avro.specific.SpecificRecord; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.time.Instant; +import java.util.stream.Stream; + +public class RecordTest { + private static final EncoderFactory encoderFactory = EncoderFactory.get(); + + @SuppressWarnings("unused") + private static Stream buildAndWriteRecord() { + return Stream.of( + // From https://stackoverflow.com/questions/45581437/how-to-specify-converter-for-default-value-in-avro-union-logical-type-fields + // Broken due to an Avro bug + Arguments.of(BuggyRecord.newBuilder().setMyMandatoryDate(Instant.now()).build()), + // Broken due to an Avro bug + Arguments.of(BuggyRecordWorkaround.newBuilder().setMyMandatoryDate(Instant.now()).build()), + // From https://github.com/davidmc24/gradle-avro-plugin/issues/120 + // Broken due to an Avro bug + Arguments.of(Messages.newBuilder().setStart(Instant.now()).build()) + ); + } + + // Broken due to an Avro bug + @Disabled + @ParameterizedTest + @MethodSource + void buildAndWriteRecord(T record) throws Exception { + Schema schema = record.getSchema(); + SpecificDatumWriter writer = new SpecificDatumWriter<>(); + OutputStream outputStream = new ByteArrayOutputStream(); + Encoder jsonEncoder = encoderFactory.jsonEncoder(schema, outputStream, true); + Encoder validatingEncoder = encoderFactory.validatingEncoder(schema, jsonEncoder); + writer.write(record, validatingEncoder); + } +} diff --git a/lang/java/gradle-plugin/test-project/build.gradle b/lang/java/gradle-plugin/test-project/build.gradle new file mode 100644 index 00000000000..e93d555e84f --- /dev/null +++ b/lang/java/gradle-plugin/test-project/build.gradle @@ -0,0 +1,37 @@ +plugins { + id "idea" + id "com.github.davidmc24.gradle.plugin.avro" version "1.2.0" +// id "com.github.davidmc24.gradle.plugin.avro" version "1.2.1-SNAPSHOT" +} + +repositories { + mavenCentral() +} + +ext { + avroVersion = "1.11.0" +} + +dependencies { + implementation "org.apache.avro:avro:${avroVersion}" + implementation "org.apache.avro:avro-tools:${avroVersion}" + testImplementation "org.junit.jupiter:junit-jupiter:5.6.2" +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +avro { + stringType = "CharSequence" + fieldVisibility = "private" + customConversion org.apache.avro.Conversions.UUIDConversion +} + +java { +// withJavadocJar() + withSourcesJar() +} diff --git a/lang/java/gradle-plugin/test-project/gradle/wrapper/gradle-wrapper.jar b/lang/java/gradle-plugin/test-project/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..7454180f2ae Binary files /dev/null and b/lang/java/gradle-plugin/test-project/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lang/java/gradle-plugin/test-project/gradle/wrapper/gradle-wrapper.properties b/lang/java/gradle-plugin/test-project/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..05679dc3c18 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lang/java/gradle-plugin/test-project/gradlew b/lang/java/gradle-plugin/test-project/gradlew new file mode 100755 index 00000000000..744e882ed57 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/lang/java/gradle-plugin/test-project/gradlew.bat b/lang/java/gradle-plugin/test-project/gradlew.bat new file mode 100644 index 00000000000..107acd32c4e --- /dev/null +++ b/lang/java/gradle-plugin/test-project/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lang/java/gradle-plugin/test-project/settings.gradle b/lang/java/gradle-plugin/test-project/settings.gradle new file mode 100644 index 00000000000..1b5d1a8cdea --- /dev/null +++ b/lang/java/gradle-plugin/test-project/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + mavenLocal() + } +} + +rootProject.name = "test-project" diff --git a/lang/java/gradle-plugin/test-project/src/main/avro/BuggyRecord.avsc b/lang/java/gradle-plugin/test-project/src/main/avro/BuggyRecord.avsc new file mode 100644 index 00000000000..57f422504aa --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/main/avro/BuggyRecord.avsc @@ -0,0 +1,26 @@ +{ + "namespace":"com.example", + "type":"record", + "name":"BuggyRecord", + "fields":[ + { + "name":"my_mandatory_date", + "type":{ + "type":"long", + "logicalType":"timestamp-millis" + }, + "default":1502250227187 + }, + { + "name":"my_optional_date", + "type":[ + { + "type":"long", + "logicalType":"timestamp-millis" + }, + "null" + ], + "default":1502250227187 + } + ] +} diff --git a/lang/java/gradle-plugin/test-project/src/main/avro/BuggyRecordWorkaround.avsc b/lang/java/gradle-plugin/test-project/src/main/avro/BuggyRecordWorkaround.avsc new file mode 100644 index 00000000000..e397d55a025 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/main/avro/BuggyRecordWorkaround.avsc @@ -0,0 +1,26 @@ +{ + "namespace":"com.example", + "type":"record", + "name":"BuggyRecordWorkaround", + "fields":[ + { + "name":"my_mandatory_date", + "type":{ + "type":"long", + "logicalType":"timestamp-millis" + }, + "default":1502250227187 + }, + { + "name":"my_optional_date", + "type":[ + "null", + { + "type":"long", + "logicalType":"timestamp-millis" + } + ], + "default":null + } + ] +} diff --git a/lang/java/gradle-plugin/test-project/src/main/avro/Messages.avsc b/lang/java/gradle-plugin/test-project/src/main/avro/Messages.avsc new file mode 100644 index 00000000000..1dc457940ff --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/main/avro/Messages.avsc @@ -0,0 +1,26 @@ +{ + "type": "record", + "name": "Messages", + "namespace": "com.somedomain", + "fields": [ + { + "name": "start", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "end", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ], + "default": null + } + ] + } +} diff --git a/lang/java/gradle-plugin/test-project/src/main/avro/UUIDTestRecord.avsc b/lang/java/gradle-plugin/test-project/src/main/avro/UUIDTestRecord.avsc new file mode 100644 index 00000000000..a835110902a --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/main/avro/UUIDTestRecord.avsc @@ -0,0 +1,13 @@ +{ + "name": "UUIDTestRecord", + "type": "record", + "fields": [ + { + "name": "id", + "type": { + "type": "string", + "logicalType": "uuid" + } + } + ] +} diff --git a/lang/java/gradle-plugin/test-project/src/main/java/project/SystemUtil.java b/lang/java/gradle-plugin/test-project/src/main/java/project/SystemUtil.java new file mode 100644 index 00000000000..5a04d2b5c66 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/main/java/project/SystemUtil.java @@ -0,0 +1,38 @@ +package project; + +import java.security.Permission; + +class SystemUtil { + private static final String PERMISSION_PREFIX = "exitVM."; + + static class ExitTrappedException extends SecurityException { + private final int status; + + ExitTrappedException(int status) { + super("Trapped System.exit(" + status + ")"); + this.status = status; + } + + int getStatus() { + return status; + } + } + + static void forbidSystemExitCall() { + final SecurityManager securityManager = new SecurityManager() { + public void checkPermission(Permission permission) { + String permissionName = permission.getName(); + if (permissionName.startsWith(PERMISSION_PREFIX)) { + String suffix = permissionName.substring(PERMISSION_PREFIX.length()); + int status = Integer.parseInt(suffix); + throw new ExitTrappedException(status); + } + } + }; + System.setSecurityManager(securityManager); + } + + static void allowSystemExitCall() { + System.setSecurityManager(null); + } +} diff --git a/lang/java/gradle-plugin/test-project/src/test/java/project/CLIComparisonTest.java b/lang/java/gradle-plugin/test-project/src/test/java/project/CLIComparisonTest.java new file mode 100644 index 00000000000..9990da98e10 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/test/java/project/CLIComparisonTest.java @@ -0,0 +1,56 @@ +package project; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static project.CLIUtil.runCLITool; + +public class CLIComparisonTest { + @TempDir + Path cliGeneratedDir; + Path schemaDir = Paths.get("src/main/avro"); + Path pluginGeneratedDir = Paths.get("build/generated-main-avro-java"); + + @SuppressWarnings("unused") + private static Stream compareSpecificCompilerOutput() { + return Stream.of( + // From https://stackoverflow.com/questions/45581437/how-to-specify-converter-for-default-value-in-avro-union-logical-type-fields + Arguments.of("BuggyRecord.avsc", "com/example/BuggyRecord.java", "compile schema".split(" ")), + // From https://github.com/davidmc24/gradle-avro-plugin/issues/120 + Arguments.of("Messages.avsc", "com/somedomain/Messages.java", "compile schema".split(" ")) + ); + } + + @ParameterizedTest + @MethodSource + void compareSpecificCompilerOutput(String schemaPath, String generatedPath, String... toolArgs) throws Exception { + Path schemaFile = schemaDir.resolve(schemaPath); + Path pluginGeneratedFile = pluginGeneratedDir.resolve(generatedPath); + Path cliGeneratedFile = cliGeneratedDir.resolve(generatedPath); + + List args = new ArrayList<>(Arrays.asList(toolArgs)); + args.add(schemaFile.toString()); + args.add(cliGeneratedDir.toString()); + runCLITool(args.toArray(new String[0])); + + String pluginGeneratedContent = readFile(pluginGeneratedFile); + String cliGeneratedContent = readFile(cliGeneratedFile); + Assertions.assertEquals(cliGeneratedContent, pluginGeneratedContent); + } + + private static String readFile(Path file) throws Exception { + return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + } +} diff --git a/lang/java/gradle-plugin/test-project/src/test/java/project/CLIUtil.java b/lang/java/gradle-plugin/test-project/src/test/java/project/CLIUtil.java new file mode 100644 index 00000000000..326e33d2586 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/test/java/project/CLIUtil.java @@ -0,0 +1,19 @@ +package project; + +import org.apache.avro.tool.Main; +import org.junit.jupiter.api.Assertions; + +class CLIUtil { + private static final int STATUS_SUCCESS = 0; + + static void runCLITool(String... args) throws Exception { + SystemUtil.forbidSystemExitCall(); + try { + Main.main(args); + } catch (SystemUtil.ExitTrappedException ex) { + Assertions.assertEquals(STATUS_SUCCESS, ex.getStatus(), "CLI tool failed"); + } finally { + SystemUtil.allowSystemExitCall(); + } + } +} diff --git a/lang/java/gradle-plugin/test-project/src/test/java/project/RandomRecordTest.java b/lang/java/gradle-plugin/test-project/src/test/java/project/RandomRecordTest.java new file mode 100644 index 00000000000..47b4d306393 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/test/java/project/RandomRecordTest.java @@ -0,0 +1,46 @@ +package project; + +import org.apache.avro.specific.SpecificRecord; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static project.CLIUtil.runCLITool; + +public class RandomRecordTest { + @TempDir + Path cliGeneratedDir; + Path schemaDir = Paths.get("src/main/avro"); + + @SuppressWarnings("unused") + private static Stream generateRandomRecords() { + return Stream.of( + // From https://stackoverflow.com/questions/45581437/how-to-specify-converter-for-default-value-in-avro-union-logical-type-fields + Arguments.of("BuggyRecord.avsc"), + // From https://github.com/davidmc24/gradle-avro-plugin/issues/120 + Arguments.of("Messages.avsc") + ); + } + + @ParameterizedTest + @MethodSource + void generateRandomRecords(String schemaPath) throws Exception { + Path schemaFile = schemaDir.resolve(schemaPath); + Path outputFile = cliGeneratedDir.resolve("random.avro"); + List args = new ArrayList<>(); + args.add("random"); + args.add("--count"); + args.add("1"); + args.add("--schema-file"); + args.add(schemaFile.toString()); + args.add(outputFile.toString()); + runCLITool(args.toArray(new String[0])); + } +} diff --git a/lang/java/gradle-plugin/test-project/src/test/java/project/RecordTest.java b/lang/java/gradle-plugin/test-project/src/test/java/project/RecordTest.java new file mode 100644 index 00000000000..8f979a6a7a5 --- /dev/null +++ b/lang/java/gradle-plugin/test-project/src/test/java/project/RecordTest.java @@ -0,0 +1,50 @@ +package project; + +import com.example.BuggyRecord; +import com.example.BuggyRecordWorkaround; +import com.somedomain.Messages; +import org.apache.avro.Schema; +import org.apache.avro.io.Encoder; +import org.apache.avro.io.EncoderFactory; +import org.apache.avro.specific.SpecificDatumWriter; +import org.apache.avro.specific.SpecificRecord; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.time.Instant; +import java.util.stream.Stream; + +public class RecordTest { + private static final EncoderFactory encoderFactory = EncoderFactory.get(); + + @SuppressWarnings("unused") + private static Stream buildAndWriteRecord() { + return Stream.of( + // From https://stackoverflow.com/questions/45581437/how-to-specify-converter-for-default-value-in-avro-union-logical-type-fields + // Broken due to an Avro bug + Arguments.of(BuggyRecord.newBuilder().setMyMandatoryDate(Instant.now()).build()), + // Broken due to an Avro bug + Arguments.of(BuggyRecordWorkaround.newBuilder().setMyMandatoryDate(Instant.now()).build()), + // From https://github.com/davidmc24/gradle-avro-plugin/issues/120 + // Broken due to an Avro bug + Arguments.of(Messages.newBuilder().setStart(Instant.now()).build()) + ); + } + + // Broken due to an Avro bug + @Disabled + @ParameterizedTest + @MethodSource + void buildAndWriteRecord(T record) throws Exception { + Schema schema = record.getSchema(); + SpecificDatumWriter writer = new SpecificDatumWriter<>(); + OutputStream outputStream = new ByteArrayOutputStream(); + Encoder jsonEncoder = encoderFactory.jsonEncoder(schema, outputStream, true); + Encoder validatingEncoder = encoderFactory.validatingEncoder(schema, jsonEncoder); + writer.write(record, validatingEncoder); + } +}