From 0264b1a90f3cbfd189ddb4a5a2bd25fbb1221030 Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Wed, 19 Jan 2022 01:20:00 +0900 Subject: [PATCH 1/7] Update dependencies - Brave 5.13.3 -> 5.13.7 - Bouncy Castle 1.69 -> 1.70 - Bucket4J 6.3.0 -> 7.0.0 - Caffeine 2.9.2 -> 2.9.3 - Dropwizard 2.0.25 -> 2.0.28 - Dropwizard Metrics 4.2.4 -> 4.2.7 - gRPC Java 1.41.1 -> 1.41.2 - gRPC Kotlin 1.1.0 -> 1.2.0 - Jackson 2.13.0 -> 2.13.1 - java-jwt 3.18.2 -> 3.18.3 - jcl-over-slf4j 1.7.32 -> 1.7.33 - jboss-logging 3.4.2 -> 3.4.3 - Kotlin 1.5.32 -> 1.6.10 - Logback 1.2.7 -> 1.2.10 - Micrometer 1.7.6 -> 1.8.2 - Netty 4.1.70 -> 4.1.73 - io_uring 0.0.1 -> 0.0.11 - Prometheus 0.12.0 -> 0.14.1 - protobuf-jackson 1.2.0 -> 2.0.0 - protobuf-java 3.17.3 -> 3.19.2 - Reactor 3.4.12 -> 3.4.14 - RESTEasy 4.7.3 -> 5.0.2 - Scala 3.13.7 -> 2.13.8 - scala-collection-compat 2.5.0 -> 2.6.0 - ScalaPB 0.11.6 -> 0.11.8 - Spring 5.3.13 -> 5.3.15 - Spring Boot 2.5.7 -> 2.6.2 - Tomcat 9.0.55 -> 9.0.56 - Build - AssertJ 3.21.0 -> 3.22.0 - Checkstyle 8.45.1 -> 9.2.1 - Dagger 2.37 -> 2.40.5 - Finagle 2.0.25 -> 2.0.28 - graphql-dgs-client 4.9.10 -> 4.9.16 - graphql-kotlin 5.1.1 -> 5.3.2 - gax-grpc 1.67.0 -> 2.9.0 - JMH 1.33 -> 1.34 - ktlint 10.2.0 -> 10.2.1 - Mockito 4.0.0 -> 4.2.0 - TestNG 7.4.0 -> 7.5 --- .../TokenBucketThrottlingStrategy.java | 16 ++-- build.gradle | 2 +- dependencies.yml | 90 +++++++++---------- gradle/scripts/lib/scala.gradle | 5 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- .../common/kotlin/FlowCollectingPublisher.kt | 3 +- 6 files changed, 62 insertions(+), 56 deletions(-) diff --git a/bucket4j/src/main/java/com/linecorp/armeria/server/throttling/bucket4j/TokenBucketThrottlingStrategy.java b/bucket4j/src/main/java/com/linecorp/armeria/server/throttling/bucket4j/TokenBucketThrottlingStrategy.java index d6dc1fd19fb..8efaea157f4 100644 --- a/bucket4j/src/main/java/com/linecorp/armeria/server/throttling/bucket4j/TokenBucketThrottlingStrategy.java +++ b/bucket4j/src/main/java/com/linecorp/armeria/server/throttling/bucket4j/TokenBucketThrottlingStrategy.java @@ -29,10 +29,12 @@ import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.server.throttling.ThrottlingStrategy; -import io.github.bucket4j.AsyncBucket; -import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.BucketConfiguration; import io.github.bucket4j.ConfigurationBuilder; import io.github.bucket4j.TokensInheritanceStrategy; +import io.github.bucket4j.distributed.AsyncBucketProxy; +import io.github.bucket4j.distributed.AsyncBucketProxyAdapter; import io.github.bucket4j.local.LocalBucketBuilder; /** @@ -50,7 +52,7 @@ public static TokenBucketThrottlingStrategyBuilder builde return new TokenBucketThrottlingStrategyBuilder<>(tokenBucket); } - private final AsyncBucket asyncBucket; + private final AsyncBucketProxy asyncBucket; private final long minimumBackoffSeconds; @Nullable private final ThrottlingHeaders headersScheme; @@ -78,12 +80,12 @@ public static TokenBucketThrottlingStrategyBuilder builde @Nullable String name) { super(name); // construct the bucket builder - final LocalBucketBuilder builder = Bucket4j.builder().withNanosecondPrecision(); + final LocalBucketBuilder builder = Bucket.builder().withNanosecondPrecision(); for (BandwidthLimit limit : tokenBucket.limits()) { builder.addLimit(limit.bandwidth()); } // build the bucket - asyncBucket = builder.build().asAsync(); + asyncBucket = AsyncBucketProxyAdapter.fromSync(builder.build()); minimumBackoffSeconds = (minimumBackoff == null) ? 0L : minimumBackoff.getSeconds(); this.headersScheme = headersScheme; this.sendQuota = sendQuota; @@ -98,7 +100,7 @@ public static TokenBucketThrottlingStrategyBuilder builde */ public CompletableFuture reconfigure(TokenBucket tokenBucket) { // construct the configuration builder - final ConfigurationBuilder builder = Bucket4j.configurationBuilder(); + final ConfigurationBuilder builder = BucketConfiguration.builder(); for (BandwidthLimit limit : tokenBucket.limits()) { builder.addLimit(limit.bandwidth()); } @@ -112,7 +114,7 @@ public CompletableFuture reconfigure(TokenBucket tokenBucket) { * Registers a request with the bucket. */ @Override - public CompletionStage accept(final ServiceRequestContext ctx, final T request) { + public CompletionStage accept(ServiceRequestContext ctx, T request) { return asyncBucket.tryConsumeAndReturnRemaining(1L).thenApply(probe -> { final boolean accepted = probe.isConsumed(); final long remainingTokens = probe.getRemainingTokens(); diff --git a/build.gradle b/build.gradle index 9ee478e5e35..e28ec665f48 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' // If you want to change `org.jetbrains.kotlin.jvm` version, // you also need to change `org.jetbrains.kotlin:kotlin-allopen` version in `dependencies.yml`. - id 'org.jetbrains.kotlin.jvm' version '1.5.32' apply false + id 'org.jetbrains.kotlin.jvm' version '1.6.10' apply false id 'org.jlleitschuh.gradle.ktlint' version '10.1.0' apply false id 'net.ltgt.errorprone' version '2.0.2' apply false diff --git a/dependencies.yml b/dependencies.yml index b86c64da7d0..1e123eb81c2 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -3,15 +3,15 @@ # boms: - - com.fasterxml.jackson:jackson-bom:2.13.0 - - io.dropwizard.metrics:metrics-bom:4.2.4 + - com.fasterxml.jackson:jackson-bom:2.13.1 + - io.dropwizard.metrics:metrics-bom:4.2.7 # Ensure that we use the same Protobuf version as what gRPC depends on. # See: https://github.com/grpc/grpc-java/blob/master/build.gradle # (Switch to the right tag and look for 'protobufVersion' and 'protocVersion'.) - io.grpc:grpc-bom:1.43.2 - - io.micrometer:micrometer-bom:1.7.6 - - io.netty:netty-bom:4.1.70.Final - - io.zipkin.brave:brave-bom:5.13.3 + - io.micrometer:micrometer-bom:1.8.2 + - io.netty:netty-bom:4.1.73.Final + - io.zipkin.brave:brave-bom:5.13.7 - org.eclipse.jetty:jetty-bom:9.4.44.v20210927 - org.junit:junit-bom:5.8.2 @@ -24,9 +24,9 @@ ch.megard: ch.qos.logback: logback-classic: - version: '1.2.7' + version: '1.2.10' javadocs: - - https://www.javadoc.io/doc/ch.qos.logback/logback-classic/1.2.7/ + - https://www.javadoc.io/doc/ch.qos.logback/logback-classic/1.2.10/ com.aayushatharva.brotli4j: brotli4j: { version: &BROTLI4J_VERSION '1.6.0' } @@ -36,13 +36,13 @@ com.aayushatharva.brotli4j: com.auth0: java-jwt: - version: '3.18.2' + version: '3.18.3' javadocs: - - https://www.javadoc.io/doc/com.auth0/java-jwt/3.18.2/ + - https://www.javadoc.io/doc/com.auth0/java-jwt/3.18.3/ # This is only used for examples/graphql-kotlin com.expediagroup: - graphql-kotlin-client: { version: &GRAPHQL_KOTLIN_VERSION '5.1.1' } + graphql-kotlin-client: { version: &GRAPHQL_KOTLIN_VERSION '5.3.2' } graphql-kotlin-client-jackson: { version: *GRAPHQL_KOTLIN_VERSION } graphql-kotlin-client-serialization: { version: *GRAPHQL_KOTLIN_VERSION } graphql-kotlin-schema-generator: { version: *GRAPHQL_KOTLIN_VERSION } @@ -61,11 +61,11 @@ com.fasterxml.jackson.core: # Don't upgrade Caffeine to 3.x that requires Java 11. com.github.ben-manes.caffeine: caffeine: - version: '2.9.2' + version: '2.9.3' exclusions: - com.google.errorprone:error_prone_annotations javadocs: - - https://www.javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/2.9.2/ + - https://www.javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/2.9.3/ relocations: - from: com.github.benmanes.caffeine to: com.linecorp.armeria.internal.shaded.caffeine @@ -77,14 +77,14 @@ com.github.node-gradle: gradle-node-plugin: { version: '3.1.1' } com.google.api: - gax-grpc: { version: '1.67.0' } + gax-grpc: { version: '2.9.0' } com.google.code.findbugs: jsr305: { version: '3.0.2' } # This is only used for examples/context-propagation/dagger com.google.dagger: - dagger: { version: &DAGGER_VERSION '2.37' } + dagger: { version: &DAGGER_VERSION '2.40.5' } dagger-compiler: { version: *DAGGER_VERSION } dagger-producers: { version: *DAGGER_VERSION } @@ -153,13 +153,13 @@ com.netflix.eureka: # DGS is used only for testing in it:dgs module. com.netflix.graphql.dgs: - graphql-dgs-client: { version: '4.9.10' } + graphql-dgs-client: { version: '4.9.16' } com.pszymczyk.consul: embedded-consul: { version: '2.2.1' } com.puppycrawl.tools: - checkstyle: { version: '8.45.1' } + checkstyle: { version: '9.2.1' } com.salesforce.servicelibs: reactor-grpc: { version: &REACTVIVE_GRPC_VERSION '1.2.3' } @@ -186,7 +186,7 @@ com.squareup.retrofit2: converter-jackson: { version: *RETROFIT2_VERSION } com.thesamet.scalapb: - scalapb-runtime_2.12: { version: &SCALAPB_VERSION '0.11.6' } + scalapb-runtime_2.12: { version: &SCALAPB_VERSION '0.11.8' } scalapb-runtime_2.13: { version: *SCALAPB_VERSION } scalapb-runtime-grpc_2.12: { version: *SCALAPB_VERSION } scalapb-runtime-grpc_2.13: { version: *SCALAPB_VERSION } @@ -202,7 +202,7 @@ com.typesafe.akka: # Finagle is used only for testing in zookeeper module. com.twitter: finagle-serversets_2.13: - version: '21.9.0' + version: '22.1.0' cz.alenkacz: gradle-scalafmt: { version: '1.16.2' } @@ -212,7 +212,7 @@ gradle.plugin.net.davidecavestro: io.dropwizard: dropwizard-core: - version: &DROPWIZARD_VERSION '2.0.25' + version: &DROPWIZARD_VERSION '2.0.28' javadocs: - https://www.javadoc.io/doc/io.dropwizard/dropwizard-core/2.0.25/ dropwizard-jackson: { version: *DROPWIZARD_VERSION } @@ -249,7 +249,7 @@ io.grpc: - io.netty:netty-handler-proxy - io.netty:netty-transport - io.netty:netty-tcnative-boringssl-static - grpc-kotlin-stub: { version: &GRPC_KOTLIN_VERSION '1.1.0' } + grpc-kotlin-stub: { version: &GRPC_KOTLIN_VERSION '1.2.0' } protoc-gen-grpc-kotlin: { version: *GRPC_KOTLIN_VERSION } io.micrometer: @@ -277,11 +277,11 @@ io.netty: - https://netty.io/4.1/api/ io.netty.incubator: - netty-incubator-transport-native-io_uring: { version: '0.0.10.Final' } + netty-incubator-transport-native-io_uring: { version: '0.0.11.Final' } io.projectreactor: reactor-core: - version: &REACTOR_VERSION '3.4.12' + version: &REACTOR_VERSION '3.4.14' javadocs: - https://projectreactor.io/docs/core/release/api/ reactor-test: { version: *REACTOR_VERSION } @@ -291,7 +291,7 @@ io.projectreactor.kotlin: io.prometheus: simpleclient_common: - version: '0.12.0' + version: '0.14.1' javadocs: - https://prometheus.github.io/client_java/ @@ -408,7 +408,7 @@ org.apache.thrift: org.apache.tomcat.embed: tomcat-embed-core: - version: &TOMCAT_VERSION '9.0.55' + version: &TOMCAT_VERSION '9.0.56' javadocs: - https://tomcat.apache.org/tomcat-9.0-doc/api/ tomcat-embed-jasper: { version: *TOMCAT_VERSION } @@ -426,14 +426,14 @@ org.apache.zookeeper: - org.slf4j:slf4j-log4j12 org.assertj: - assertj-core: { version: '3.21.0' } + assertj-core: { version: '3.22.0' } org.awaitility: awaitility: { version: '4.1.1' } org.bouncycastle: bcpkix-jdk15on: - version: &BOUNCYCASTLE_VERSION '1.69' + version: &BOUNCYCASTLE_VERSION '1.70' relocations: - from: org.bouncycastle to: com.linecorp.armeria.internal.shaded.bouncycastle @@ -453,7 +453,7 @@ org.checkerframework: org.curioswitch.curiostack: protobuf-jackson: - version: '1.2.0' + version: '2.0.0' exclusions: - javax.annotation:javax.annotation-api javadocs: @@ -495,18 +495,18 @@ org.jctools: # If you want to change `org.jetbrains.kotlin:kotlin-allopen` version, # you also need to change `org.jetbrains.kotlin.jvm` version in `build.gradle`. org.jetbrains.kotlin: - kotlin-allopen: { version: &KOTLIN_VERSION '1.5.32' } + kotlin-allopen: { version: &KOTLIN_VERSION '1.6.10' } kotlin-reflect: { version: *KOTLIN_VERSION } org.jetbrains.kotlinx: - kotlinx-coroutines-core: { version: &KOTLIN_COROUTINE_VERSION '1.5.2' } + kotlinx-coroutines-core: { version: &KOTLIN_COROUTINE_VERSION '1.6.0' } kotlinx-coroutines-jdk8: { version: *KOTLIN_COROUTINE_VERSION } # kotlinx-coroutines-reactor is only used for testing :it:spring:boot2-kotlin kotlinx-coroutines-reactor: { version: *KOTLIN_COROUTINE_VERSION } kotlinx-coroutines-rx2: { version: *KOTLIN_COROUTINE_VERSION } org.jlleitschuh.gradle: - ktlint-gradle: { version: '10.2.0' } + ktlint-gradle: { version: '10.2.1' } org.jooq: joor: { version: '0.9.14' } @@ -515,14 +515,14 @@ org.jsoup: jsoup: { version: '1.14.3' } org.mockito: - mockito-core: { version: &MOCKITO_VERSION '4.0.0' } + mockito-core: { version: &MOCKITO_VERSION '4.2.0' } mockito-junit-jupiter: { version: *MOCKITO_VERSION } org.mortbay.jetty.alpn: jetty-alpn-agent: { version: '2.0.10' } org.openjdk.jmh: - jmh-core: { version: &JMH_VERSION '1.33' } + jmh-core: { version: &JMH_VERSION '1.34' } jmh-generator-annprocess: { version: *JMH_VERSION } # Don't upgrade OpenSAML to 4.x that requires Java 11. @@ -555,10 +555,10 @@ org.reflections: to: com.linecorp.armeria.internal.shaded.reflections org.scala-lang: - scala-library: { version: '2.13.7' } + scala-library: { version: '2.13.8' } org.scala-lang.modules: - scala-collection-compat_2.12: { version: '2.5.0' } + scala-collection-compat_2.12: { version: '2.6.0' } scala-java8-compat_2.12: { version: &SCALA_JAVA8_COMPAT_VERSION '1.0.2' } scala-java8-compat_2.13: { version: *SCALA_JAVA8_COMPAT_VERSION } @@ -573,21 +573,21 @@ org.sangria-graphql: sangria-slowlog_2.13: { version: *SANGRIA_SLOWLOG_VERSION } org.slf4j: - jcl-over-slf4j: { version: &SLF4J_VERSION '1.7.32' } + jcl-over-slf4j: { version: &SLF4J_VERSION '1.7.33' } jul-to-slf4j: { version: *SLF4J_VERSION } log4j-over-slf4j: { version: *SLF4J_VERSION } slf4j-api: version: *SLF4J_VERSION javadocs: - - https://www.javadoc.io/doc/org.slf4j/slf4j-api/1.7.31/ + - https://www.javadoc.io/doc/org.slf4j/slf4j-api/1.7.33/ slf4j-simple: { version: *SLF4J_VERSION } org.springframework: - spring-web: { version: '5.3.13' } + spring-web: { version: '5.3.15' } org.springframework.boot: spring-boot-starter: - version: &SPRING_BOOT_VERSION '2.5.7' + version: &SPRING_BOOT_VERSION '2.6.2' javadocs: - https://docs.spring.io/spring/docs/current/javadoc-api/ spring-boot-actuator-autoconfigure: { version: *SPRING_BOOT_VERSION } @@ -604,7 +604,7 @@ org.springframework.boot: spring-boot-gradle-plugin: { version: *SPRING_BOOT_VERSION } org.testng: - testng: { version: '7.4.0' } + testng: { version: '7.5' } org.xerial.snappy: snappy-java: { version: '1.1.8.4' } @@ -618,15 +618,15 @@ xml-apis: com.github.vladimir-bukhtoyarov: bucket4j-core: - version: '6.3.0' + version: '7.0.0' javadocs: - - https://javadoc.io/doc/com.github.vladimir-bukhtoyarov/bucket4j-core/6.3.0/ + - https://javadoc.io/doc/com.github.vladimir-bukhtoyarov/bucket4j-core/7.0.0/ org.jboss.resteasy: resteasy-core-spi: - version: &RESTEASY_VERSION '4.7.3.Final' + version: &RESTEASY_VERSION '5.0.2.Final' javadocs: - - https://docs.jboss.org/resteasy/docs/4.6.0.Final/javadocs/ + - https://docs.jboss.org/resteasy/docs/5.0.2.Final/javadocs/ resteasy-core: { version: *RESTEASY_VERSION } resteasy-client: { version: *RESTEASY_VERSION } resteasy-jackson2-provider: { version: *RESTEASY_VERSION } @@ -634,9 +634,9 @@ org.jboss.resteasy: # "provided" dependency required by RESTEasy 'resteasy-core-spi' org.jboss.logging: jboss-logging: - version: '3.4.2.Final' + version: '3.4.3.Final' javadocs: - - https://javadoc.io/doc/org.jboss.logging/jboss-logging/3.4.2.Final/ + - https://javadoc.io/doc/org.jboss.logging/jboss-logging/3.4.3.Final/ jboss-logging-annotations: version: '2.2.1.Final' javadocs: diff --git a/gradle/scripts/lib/scala.gradle b/gradle/scripts/lib/scala.gradle index f34d5cc9d63..cfaeae8ae6d 100644 --- a/gradle/scripts/lib/scala.gradle +++ b/gradle/scripts/lib/scala.gradle @@ -12,6 +12,11 @@ configure(scala212 + scala213) { scalaCompileOptions.with { // Disable incremental compilation to avoid intermittent compile errors. force = true + // A workaround for 'jvm-1.16' is not a valid choice for '-target'. bad option: '-target:jvm-1.16' + // Toolchain passes a wrong target parameter to the Scala compiler. + // - https://github.com/gradle/gradle/issues/19456 + // - https://github.com/gradle/gradle/pull/18347 + additionalParameters = [ '-target:jvm-1.8'] } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a0f7639f7d3..669386b870a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/kotlin/src/main/kotlin/com/linecorp/armeria/internal/common/kotlin/FlowCollectingPublisher.kt b/kotlin/src/main/kotlin/com/linecorp/armeria/internal/common/kotlin/FlowCollectingPublisher.kt index 61f95d91c9f..8e77ca60789 100644 --- a/kotlin/src/main/kotlin/com/linecorp/armeria/internal/common/kotlin/FlowCollectingPublisher.kt +++ b/kotlin/src/main/kotlin/com/linecorp/armeria/internal/common/kotlin/FlowCollectingPublisher.kt @@ -21,7 +21,6 @@ import io.netty.util.concurrent.EventExecutor import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import org.reactivestreams.Publisher @@ -33,7 +32,7 @@ import kotlin.coroutines.EmptyCoroutineContext * [Publisher] implementation which emits values collected from [Flow]. * Reactive streams back-pressure works on its backing [flow] too. */ -internal class FlowCollectingPublisher( +internal class FlowCollectingPublisher( private val flow: Flow, private val executor: EventExecutor, private val context: CoroutineContext = EmptyCoroutineContext From 3ebf84d28893d52ccd0152531230ec0827be3741 Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Wed, 19 Jan 2022 11:11:37 +0900 Subject: [PATCH 2/7] Revert Spring versions upgraded seperately --- dependencies.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies.yml b/dependencies.yml index 1e123eb81c2..dd6ff689e22 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -583,11 +583,11 @@ org.slf4j: slf4j-simple: { version: *SLF4J_VERSION } org.springframework: - spring-web: { version: '5.3.15' } + spring-web: { version: '5.3.13' } org.springframework.boot: spring-boot-starter: - version: &SPRING_BOOT_VERSION '2.6.2' + version: &SPRING_BOOT_VERSION '2.5.7' javadocs: - https://docs.spring.io/spring/docs/current/javadoc-api/ spring-boot-actuator-autoconfigure: { version: *SPRING_BOOT_VERSION } From e668623cfeaf7d2c7f8c3f81fd6315a55617871c Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Wed, 19 Jan 2022 20:42:58 +0900 Subject: [PATCH 3/7] Support Scala 3 --- dependencies.yml | 7 + gradle/scripts/lib/scala.gradle | 45 ++- .../server/sangria/SangriaGraphqlSuite.scala | 3 +- .../armeria/common/util/FutureSuite.scala | 2 +- .../server/ServerListenerBuilderSuite.scala | 16 +- scala/scala_3/build.gradle | 21 ++ scalapb/scalapb_2.12/build.gradle | 3 + scalapb/scalapb_2.13/build.gradle | 5 + .../server/scalapb/ScalaPbConverterUtil.scala | 10 +- ...laPbRequestConverterFunctionProvider.scala | 2 +- ...aPbResponseConverterFunctionProvider.scala | 6 +- .../scalapb/ScalaPbJsonMarshaller.scala | 0 .../ScalaPbRequestConverterFunction.scala | 12 +- .../ScalaPbResponseConverterFunction.scala | 0 .../ScalaPbRequestConverterFunctionTest.scala | 76 ++-- .../ScalaPbResponseAnnotatedServiceTest.scala | 5 +- scalapb/scalapb_3/build.gradle | 33 ++ .../scalapb/ScalaPbJsonMarshaller.scala | 120 +++++++ .../ScalaPbRequestConverterFunction.scala | 327 ++++++++++++++++++ .../ScalaPbResponseConverterFunction.scala | 213 ++++++++++++ settings.gradle | 5 + 21 files changed, 848 insertions(+), 63 deletions(-) create mode 100644 scala/scala_3/build.gradle rename scalapb/scalapb_2.13/src/main/{scala => scala_2}/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala (100%) rename scalapb/scalapb_2.13/src/main/{scala => scala_2}/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala (97%) rename scalapb/scalapb_2.13/src/main/{scala => scala_2}/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala (100%) create mode 100644 scalapb/scalapb_3/build.gradle create mode 100644 scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala create mode 100644 scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala create mode 100644 scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala diff --git a/dependencies.yml b/dependencies.yml index dd6ff689e22..0252f80359f 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -188,10 +188,13 @@ com.squareup.retrofit2: com.thesamet.scalapb: scalapb-runtime_2.12: { version: &SCALAPB_VERSION '0.11.8' } scalapb-runtime_2.13: { version: *SCALAPB_VERSION } + scalapb-runtime_3: { version: *SCALAPB_VERSION } scalapb-runtime-grpc_2.12: { version: *SCALAPB_VERSION } scalapb-runtime-grpc_2.13: { version: *SCALAPB_VERSION } + scalapb-runtime-grpc_3: { version: *SCALAPB_VERSION } scalapb-json4s_2.12: { version: &SCALAPB_JSON4S_VERSION '0.12.0' } scalapb-json4s_2.13: { version: *SCALAPB_JSON4S_VERSION } + scalapb-json4s_3: { version: *SCALAPB_JSON4S_VERSION } # Akka is used only for testing in it:grpcweb module. com.typesafe.akka: @@ -270,6 +273,7 @@ io.micrometer: io.monix: monix-reactive_2.12: { version: &MONIX_VERSION '3.4.0' } monix-reactive_2.13: { version: *MONIX_VERSION } + monix-reactive_3: { version: *MONIX_VERSION } io.netty: netty-common: @@ -556,15 +560,18 @@ org.reflections: org.scala-lang: scala-library: { version: '2.13.8' } + scala3-library_3: { version: '3.1.0' } org.scala-lang.modules: scala-collection-compat_2.12: { version: '2.6.0' } scala-java8-compat_2.12: { version: &SCALA_JAVA8_COMPAT_VERSION '1.0.2' } scala-java8-compat_2.13: { version: *SCALA_JAVA8_COMPAT_VERSION } + scala-java8-compat_3: { version: *SCALA_JAVA8_COMPAT_VERSION } org.scalameta: munit_2.12: { version: &MUNIT_VERSION '0.7.29' } munit_2.13: { version: *MUNIT_VERSION } + munit_3: { version: *MUNIT_VERSION } org.sangria-graphql: sangria_2.12: { version: &SANGRIA_VERSION '2.1.6' } diff --git a/gradle/scripts/lib/scala.gradle b/gradle/scripts/lib/scala.gradle index cfaeae8ae6d..d35fb103169 100644 --- a/gradle/scripts/lib/scala.gradle +++ b/gradle/scripts/lib/scala.gradle @@ -1,7 +1,8 @@ def scala212 = projectsWithFlags('scala_2.12') def scala213 = projectsWithFlags('scala_2.13') +def scala3 = projectsWithFlags('scala_3') -configure(scala212 + scala213) { +configure(scala212 + scala213 + scala3) { apply plugin: 'scala' @@ -12,14 +13,21 @@ configure(scala212 + scala213) { scalaCompileOptions.with { // Disable incremental compilation to avoid intermittent compile errors. force = true - // A workaround for 'jvm-1.16' is not a valid choice for '-target'. bad option: '-target:jvm-1.16' - // Toolchain passes a wrong target parameter to the Scala compiler. - // - https://github.com/gradle/gradle/issues/19456 - // - https://github.com/gradle/gradle/pull/18347 - additionalParameters = [ '-target:jvm-1.8'] } } + // Delete the generated source directory on clean. + ext { + genSrcDir = "${projectDir}/gen-src" + } + clean { + delete project.ext.genSrcDir + } + // Add the generated source directories to the source sets. + project.sourceSets.all { sourceSet -> + sourceSet.java.srcDir file("${project.ext.genSrcDir}/${sourceSet.name}/scala") + } + tasks.withType(Test) { useJUnitPlatform { // A workaround for 'java.lang.InternalError: Malformed class name' @@ -69,6 +77,15 @@ configure(scala212) { } testImplementation 'org.scalameta:munit_2.12' } + tasks.withType(ScalaCompile) { + scalaCompileOptions.with { + // A workaround for 'jvm-1.16' is not a valid choice for '-target'. bad option: '-target:jvm-1.16' + // Toolchain passes a wrong target parameter to the Scala compiler. + // - https://github.com/gradle/gradle/issues/19456 + // - https://github.com/gradle/gradle/pull/18347 + additionalParameters = [ '-target:jvm-1.8'] + } + } } configure(scala213) { @@ -76,5 +93,21 @@ configure(scala213) { implementation 'org.scala-lang:scala-library' testImplementation 'org.scalameta:munit_2.13' } + tasks.withType(ScalaCompile) { + scalaCompileOptions.with { + additionalParameters = [ '-target:1.8'] + } + } } +configure(scala3) { + dependencies { + implementation 'org.scala-lang:scala3-library_3' + testImplementation 'org.scalameta:munit_3' + } + tasks.withType(ScalaCompile) { + scalaCompileOptions.with { + additionalParameters = [ '-release:8'] + } + } +} diff --git a/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala b/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala index eeeafbc44d4..a62cecfab93 100644 --- a/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala +++ b/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala @@ -83,7 +83,8 @@ class SangriaGraphqlSuite extends FunSuite with ServerSuite { } """ - val response = executeQuery(server.webClient(), method, query = query, variables = Map("humanId" -> "1002")) + val response = + executeQuery(server.webClient(), method, query = query, variables = Map("humanId" -> "1002")) assertEquals(response.headers().status(), HttpStatus.OK) assertThatJson(response.contentUtf8()) .isEqualTo(""" diff --git a/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala b/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala index 1489d33d230..228e6d020f1 100644 --- a/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala +++ b/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala @@ -33,7 +33,7 @@ class FutureSuite extends FunSuite { val scalaFuture: Future[Unit] = javaFuture.toScala javaFuture.complete(null) - assert(Await.result(scalaFuture, 10.seconds).isInstanceOf[Unit]) + assert(Await.result(scalaFuture, 10.seconds).getClass.getName == "void") } test("CompletionStage[HttpResponse].toHttpResponse") { diff --git a/scala/scala_2.13/src/test/scala/com/linecorp/armeria/server/ServerListenerBuilderSuite.scala b/scala/scala_2.13/src/test/scala/com/linecorp/armeria/server/ServerListenerBuilderSuite.scala index d3dbb3ae36a..f8c7e5c2ff1 100644 --- a/scala/scala_2.13/src/test/scala/com/linecorp/armeria/server/ServerListenerBuilderSuite.scala +++ b/scala/scala_2.13/src/test/scala/com/linecorp/armeria/server/ServerListenerBuilderSuite.scala @@ -22,14 +22,14 @@ class ServerListenerBuilderSuite extends FunSuite { test("Should be able to register a callback with a lambda expression") { ServerListener .builder() - .whenStarting { server: Server => println(s"$server is starting.") } - .whenStarting { server: Server => server.activePorts } - .whenStarted { server: Server => println(s"$server is started.") } - .whenStarted { server: Server => server.isClosing } - .whenStopping { server: Server => println(s"$server is stopping.") } - .whenStopping { server: Server => server.activePorts } - .whenStopped { server: Server => println(s"$server is stopped.") } - .whenStopped { server: Server => server.isClosed } + .whenStarting { (server: Server) => println(s"$server is starting.") } + .whenStarting { (server: Server) => server.activePorts } + .whenStarted { (server: Server) => println(s"$server is started.") } + .whenStarted { (server: Server) => server.isClosing } + .whenStopping { (server: Server) => println(s"$server is stopping.") } + .whenStopping { (server: Server) => server.activePorts } + .whenStopped { (server: Server) => println(s"$server is stopped.") } + .whenStopped { (server: Server) => server.isClosed } .build() } } diff --git a/scala/scala_3/build.gradle b/scala/scala_3/build.gradle new file mode 100644 index 00000000000..2c1b89055ab --- /dev/null +++ b/scala/scala_3/build.gradle @@ -0,0 +1,21 @@ +dependencies { + implementation 'org.scala-lang.modules:scala-java8-compat_3' + + // Added for supporting Scala types in Jackson{Request,Response}ConverterFunction + implementation 'com.fasterxml.jackson.module:jackson-module-scala_3' +} + +// Use the sources from ':scala_2.13'. +// NB: We should never add these directories using the 'sourceSets' directive because that will make +// them added to more than one project and having a source directory with more than one output directory +// will confuse IDEs such as IntelliJ IDEA. +def scala213ProjectDir = "${rootProject.projectDir}/scala/scala_2.13" +tasks.compileScala.source "${scala213ProjectDir}/src/main/scala", "${scala213ProjectDir}/src/main/scala_2.13" +tasks.processResources.from "${scala213ProjectDir}/src/main/resources" + +tasks.compileTestScala.source "${scala213ProjectDir}/src/test/scala" +tasks.processTestResources.from "${scala213ProjectDir}/src/test/resources" + +tasks.sourcesJar.from "${scala213ProjectDir}/src/main/scala" +tasks.sourcesJar.from "${scala213ProjectDir}/src/main/resources" +tasks.scaladoc.source "${scala213ProjectDir}/src/main/scala" diff --git a/scalapb/scalapb_2.12/build.gradle b/scalapb/scalapb_2.12/build.gradle index 537c9d46a2c..b8e9e853424 100644 --- a/scalapb/scalapb_2.12/build.gradle +++ b/scalapb/scalapb_2.12/build.gradle @@ -15,6 +15,7 @@ dependencies { // will confuse IDEs such as IntelliJ IDEA. def scalapb213ProjectDir = "${rootProject.projectDir}/scalapb/scalapb_2.13" tasks.compileScala.source "${scalapb213ProjectDir}/src/main/scala" +tasks.compileScala.source "${scalapb213ProjectDir}/src/main/scala_2" tasks.processResources.from "${scalapb213ProjectDir}/src/main/resources" // Uncomment the following three lines once we have src/main/proto in :scalapb_2.13. //// Use the source code generated from :scalapb_2.13/src/main/proto/*.proto. @@ -28,5 +29,7 @@ tasks.compileTestScala.dependsOn(":scalapb_2.13:generateTestProto") tasks.compileTestScala.source "${scalapb213ProjectDir}/gen-src/test/scalapb" tasks.sourcesJar.from "${scalapb213ProjectDir}/src/main/scala" +tasks.sourcesJar.from "${scalapb213ProjectDir}/src/main/scala_2" tasks.sourcesJar.from "${scalapb213ProjectDir}/src/main/resources" tasks.scaladoc.source "${scalapb213ProjectDir}/src/main/scala" +tasks.scaladoc.source "${scalapb213ProjectDir}/src/main/scala_2" diff --git a/scalapb/scalapb_2.13/build.gradle b/scalapb/scalapb_2.13/build.gradle index 1a9bcfa01b2..3bae6d623c3 100644 --- a/scalapb/scalapb_2.13/build.gradle +++ b/scalapb/scalapb_2.13/build.gradle @@ -9,6 +9,11 @@ dependencies { testImplementation 'io.monix:monix-reactive_2.13' } +project.sourceSets.all { sourceSet -> + // Add the source directories for Scala 2.x only. + sourceSet.scala.srcDir file("src/${sourceSet.name}/scala_2") +} + sourceSets { test { scala { diff --git a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbConverterUtil.scala b/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbConverterUtil.scala index 5765a215a13..6fc874bec48 100644 --- a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbConverterUtil.scala +++ b/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbConverterUtil.scala @@ -26,12 +26,12 @@ import scalapb.descriptors.{Descriptor, FieldDescriptor, PValue, Reads} import scalapb.json4s.Printer import scalapb.{GeneratedEnumCompanion, GeneratedMessage, GeneratedMessageCompanion, GeneratedSealedOneof} -private[scalapb] object ScalaPbConverterUtil { +private[scalapb] object ResultType extends Enumeration { + val UNKNOWN, PROTOBUF, LIST_PROTOBUF, SET_PROTOBUF, MAP_PROTOBUF, SCALA_LIST_PROTOBUF, SCALA_VECTOR_PROTOBUF, + SCALA_SET_PROTOBUF, SCALA_MAP_PROTOBUF = Value +} - object ResultType extends Enumeration { - val UNKNOWN, PROTOBUF, LIST_PROTOBUF, SET_PROTOBUF, MAP_PROTOBUF, SCALA_LIST_PROTOBUF, - SCALA_VECTOR_PROTOBUF, SCALA_SET_PROTOBUF, SCALA_MAP_PROTOBUF = Value - } +private[scalapb] object ScalaPbConverterUtil { val X_PROTOBUF: MediaType = MediaType.create("application", "x-protobuf") val defaultJsonPrinter: Printer = new Printer().includingDefaultValueFields diff --git a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionProvider.scala b/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionProvider.scala index 99c70eb5356..786d006c3f0 100644 --- a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionProvider.scala +++ b/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionProvider.scala @@ -18,7 +18,7 @@ package com.linecorp.armeria.server.scalapb import com.linecorp.armeria.common.annotation.{Nullable, UnstableApi} import com.linecorp.armeria.server.annotation.{RequestConverterFunction, RequestConverterFunctionProvider} -import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil.{ResultType, toResultType} +import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil.toResultType import java.lang.reflect.Type /** diff --git a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunctionProvider.scala b/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunctionProvider.scala index ba2fadee587..03b796368fa 100644 --- a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunctionProvider.scala +++ b/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunctionProvider.scala @@ -18,11 +18,7 @@ package com.linecorp.armeria.server.scalapb import com.linecorp.armeria.common.annotation.{Nullable, UnstableApi} import com.linecorp.armeria.server.annotation.{ResponseConverterFunction, ResponseConverterFunctionProvider} -import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil.{ - ResultType, - isSupportedGenericType, - toResultType -} +import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil.{isSupportedGenericType, toResultType} import java.lang.reflect.Type /** diff --git a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala b/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala similarity index 100% rename from scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala rename to scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala diff --git a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala b/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala similarity index 97% rename from scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala rename to scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala index 17d0a04b8c9..61d6be9e5eb 100644 --- a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala +++ b/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020 LINE Corporation + * Copyright 2022 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -27,7 +27,7 @@ import com.linecorp.armeria.common.AggregatedHttpRequest import com.linecorp.armeria.common.annotation.{Nullable, UnstableApi} import com.linecorp.armeria.server.ServiceRequestContext import com.linecorp.armeria.server.annotation.RequestConverterFunction -import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil.ResultType._ +import com.linecorp.armeria.server.scalapb.ResultType._ import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil._ import com.linecorp.armeria.server.scalapb.ScalaPbRequestConverterFunction._ @@ -81,7 +81,7 @@ object ScalaPbRequestConverterFunction { * Creates a new instance with the specified [[scalapb.json4s.Parser]]. */ def apply(jsonParser: Parser = defaultJsonParser): ScalaPbRequestConverterFunction = - new ScalaPbRequestConverterFunction(jsonParser, ResultType.UNKNOWN) + new ScalaPbRequestConverterFunction(jsonParser, UNKNOWN) private[scalapb] def apply(resultType: ResultType.Value): ScalaPbRequestConverterFunction = new ScalaPbRequestConverterFunction(defaultJsonParser, resultType) @@ -226,9 +226,9 @@ final class ScalaPbRequestConverterFunction private (jsonParser: Parser, resultT } var resultType0 = resultType - if (resultType0 == ResultType.UNKNOWN) + if (resultType0 == UNKNOWN) resultType0 = toResultType(expectedParameterizedResultType) - if (resultType0 == ResultType.UNKNOWN || resultType0 == ResultType.PROTOBUF || + if (resultType0 == UNKNOWN || resultType0 == PROTOBUF || contentType == null || isProtobuf(contentType)) return RequestConverterFunction.fallthrough @@ -249,7 +249,7 @@ final class ScalaPbRequestConverterFunction private (jsonParser: Parser, resultT val messageType = typeArguments(0).asInstanceOf[Class[_]] val iter = jsonNode.iterator() resultType match { - case LIST_PROTOBUF | SET_PROTOBUF => + case ResultType.LIST_PROTOBUF | SET_PROTOBUF => val builder = if (resultType == LIST_PROTOBUF) ImmutableList.builderWithExpectedSize[Any with Serializable](size) else ImmutableSet.builderWithExpectedSize[Any with Serializable](size) diff --git a/scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala b/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala similarity index 100% rename from scalapb/scalapb_2.13/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala rename to scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala diff --git a/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionTest.scala b/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionTest.scala index 6fa2581ccbd..5b16634ae3f 100644 --- a/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionTest.scala +++ b/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunctionTest.scala @@ -16,8 +16,8 @@ package com.linecorp.armeria.server.scalapb +import com.fasterxml.jackson.core.`type`.TypeReference import com.google.common.collect.{ImmutableList, ImmutableMap, ImmutableSet} -import com.google.common.reflect.TypeToken import com.linecorp.armeria.common.{AggregatedHttpRequest, HttpData, HttpMethod, HttpRequest, MediaType} import com.linecorp.armeria.scalapb.testing.messages.SimpleRequest import com.linecorp.armeria.server.ServiceRequestContext @@ -45,11 +45,13 @@ class ScalaPbRequestConverterFunctionTest { @Test def failProtobufToCollection(): Unit = { - val typeToken = new TypeToken[List[SimpleRequest]]() {} + val typeToken = new TypeReference[List[SimpleRequest]]() {} val converter = ScalaPbRequestConverterFunction() val req = AggregatedHttpRequest.of(ctx.request.headers, HttpData.wrap(simpleRequest1.toByteArray)) - assertThatThrownBy(() => - converter.convertRequest(ctx, req, classOf[List[_]], typeToken.getType.asInstanceOf[ParameterizedType])) + assertThatThrownBy(() => { + val parameterizedType = typeToken.getType.asInstanceOf[ParameterizedType] + converter.convertRequest(ctx, req, classOf[List[_]], parameterizedType) + }) .isInstanceOf(classOf[FallthroughException]) } @@ -65,49 +67,45 @@ class ScalaPbRequestConverterFunctionTest { @ArgumentsSource(classOf[JsonArrayRequestProvider]) @ParameterizedTest - def jsonArrayToCollection(collection: Any, json: String, typeToken: TypeToken[_]): Unit = { + def jsonArrayToCollection( + collection: Any, + json: String, + rawType: Class[_], + parameterizedType: ParameterizedType): Unit = { val converter = ScalaPbRequestConverterFunction() val req = AggregatedHttpRequest.of( ctx.request.headers .withMutations(builder => builder.contentType(MediaType.JSON)), HttpData.ofUtf8(json)) val requestObject = - converter.convertRequest( - ctx, - req, - typeToken.getRawType, - typeToken.getType.asInstanceOf[ParameterizedType]) + converter.convertRequest(ctx, req, rawType, parameterizedType) assertThat(requestObject).isEqualTo(collection) } @ArgumentsSource(classOf[JsonObjectRequestProvider]) @ParameterizedTest - def jsonObjectToMap(map: Any, json: String, typeToken: TypeToken[_]): Unit = { + def jsonObjectToMap(map: Any, json: String, rawType: Class[_], parameterizedType: ParameterizedType): Unit = { val converter = ScalaPbRequestConverterFunction() val req = AggregatedHttpRequest.of( ctx.request.headers .withMutations(builder => builder.contentType(MediaType.JSON)), HttpData.ofUtf8(json)) val requestObject = - converter.convertRequest( - ctx, - req, - typeToken.getRawType, - typeToken.getType.asInstanceOf[ParameterizedType]) + converter.convertRequest(ctx, req, rawType, parameterizedType) assertThat(requestObject).isEqualTo(map) } @ArgumentsSource(classOf[JsonArrayRequestProvider]) @ParameterizedTest - def jsonArrayWithNoContentType(collection: Any, json: String, typeToken: TypeToken[_]): Unit = { + def jsonArrayWithNoContentType( + collection: Any, + json: String, + rawType: Class[_], + parameterizedType: ParameterizedType): Unit = { val converter = ScalaPbRequestConverterFunction() val req = AggregatedHttpRequest.of(ctx.request.headers, HttpData.ofUtf8(json)) assertThatThrownBy { () => - converter.convertRequest( - ctx, - req, - typeToken.getRawType, - typeToken.getType.asInstanceOf[ParameterizedType]) + converter.convertRequest(ctx, req, rawType, parameterizedType) }.isInstanceOf(classOf[FallthroughException]) } } @@ -129,11 +127,23 @@ private[scalapb] object ScalaPbRequestConverterFunctionTest { val jset = ImmutableSet.of(simpleRequest1, simpleRequest2); java.util.stream.Stream.of( - Arguments.of(list, toJson(list), new TypeToken[List[SimpleRequest]]() {}), - Arguments.of(vector, toJson(vector), new TypeToken[Vector[SimpleRequest]]() {}), - Arguments.of(set, toJson(set), new TypeToken[Set[SimpleRequest]]() {}), - Arguments.of(jlist, toJson(jlist), new TypeToken[java.util.List[SimpleRequest]]() {}), - Arguments.of(jset, toJson(jset), new TypeToken[java.util.Set[SimpleRequest]]() {}) + Arguments.of(list, toJson(list), classOf[List[_]], new TypeReference[List[SimpleRequest]]() {}.getType), + Arguments.of( + vector, + toJson(vector), + classOf[Vector[_]], + new TypeReference[Vector[SimpleRequest]]() {}.getType), + Arguments.of(set, toJson(set), classOf[Set[_]], new TypeReference[Set[SimpleRequest]]() {}.getType), + Arguments.of( + jlist, + toJson(jlist), + classOf[java.util.List[_]], + new TypeReference[java.util.List[SimpleRequest]]() {}.getType), + Arguments.of( + jset, + toJson(jset), + classOf[java.util.Set[_]], + new TypeReference[java.util.Set[SimpleRequest]]() {}.getType) ) } } @@ -148,8 +158,16 @@ private[scalapb] object ScalaPbRequestConverterFunctionTest { .build() java.util.stream.Stream.of( - Arguments.of(map, toJson(map), new TypeToken[Map[String, SimpleRequest]]() {}), - Arguments.of(jmap, toJson(jmap), new TypeToken[java.util.Map[String, SimpleRequest]]() {}) + Arguments.of( + map, + toJson(map), + classOf[Map[_, _]], + new TypeReference[Map[String, SimpleRequest]]() {}.getType), + Arguments.of( + jmap, + toJson(jmap), + classOf[java.util.Map[_, _]], + new TypeReference[java.util.Map[String, SimpleRequest]]() {}.getType) ) } } diff --git a/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseAnnotatedServiceTest.scala b/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseAnnotatedServiceTest.scala index 4a0c6831ba4..1df8ca6b32e 100644 --- a/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseAnnotatedServiceTest.scala +++ b/scalapb/scalapb_2.13/src/test/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseAnnotatedServiceTest.scala @@ -44,6 +44,7 @@ import com.linecorp.armeria.server.annotation.{ ProducesJsonSequences, ProducesProtobuf } +import com.linecorp.armeria.server.logging.LoggingService import com.linecorp.armeria.server.scalapb.ScalaPbResponseAnnotatedServiceTest.server import com.linecorp.armeria.server.{ServerBuilder, ServiceRequestContext} import com.linecorp.armeria.testing.junit5.server.ServerExtension @@ -227,9 +228,11 @@ class ScalaPbResponseAnnotatedServiceTest { object ScalaPbResponseAnnotatedServiceTest { @RegisterExtension val server: ServerExtension = new ServerExtension() { - override protected def configure(sb: ServerBuilder): Unit = + override protected def configure(sb: ServerBuilder): Unit = { // A workaround for 'ambiguous reference to overloaded definition' in Scala 2.12.x sb.annotatedService(new GreetingService(), Array.emptyObjectArray: _*) + sb.decorator(LoggingService.newDecorator()) + } } private var cause: Option[Throwable] = None diff --git a/scalapb/scalapb_3/build.gradle b/scalapb/scalapb_3/build.gradle new file mode 100644 index 00000000000..ea6e4ff899f --- /dev/null +++ b/scalapb/scalapb_3/build.gradle @@ -0,0 +1,33 @@ +dependencies { + implementation project(':grpc') + + // ScalaPB + api "com.thesamet.scalapb:scalapb-json4s_3" + implementation "com.thesamet.scalapb:scalapb-runtime_3" + implementation "com.thesamet.scalapb:scalapb-runtime-grpc_3" + + testImplementation 'io.monix:monix-reactive_3' +} + +// Use the sources from ':scalapb_2.13'. +// NB: We should never add these directories using the 'sourceSets' directive because that will make +// them added to more than one project and having a source directory with more than one output directory +// will confuse IDEs such as IntelliJ IDEA. +def scalapb213ProjectDir = "${rootProject.projectDir}/scalapb/scalapb_2.13" + +tasks.compileScala.source "${scalapb213ProjectDir}/src/main/scala" +tasks.processResources.from "${scalapb213ProjectDir}/src/main/resources" +// Uncomment the following three lines once we have src/main/proto in :scalapb_2.13. +//// Use the source code generated from :scalapb_2.13/src/main/proto/*.proto. +// tasks.compileScala.dependsOn(":scalapb_2.13:generateProto") +// tasks.compileScala.source "${scalapb213ProjectDir}/gen-src/main/scalapb" + +tasks.compileTestScala.source "${scalapb213ProjectDir}/src/test/scala" +tasks.processTestResources.from "${scalapb213ProjectDir}/src/test/resources" +// Use the source code generated from :scalapb_2.13/src/test/proto/*.proto. +tasks.compileTestScala.dependsOn(":scalapb_2.13:generateTestProto") +tasks.compileTestScala.source "${scalapb213ProjectDir}/gen-src/test/scalapb" + +tasks.sourcesJar.from "${scalapb213ProjectDir}/src/main/scala" +tasks.sourcesJar.from "${scalapb213ProjectDir}/src/main/resources" +tasks.scaladoc.source "${scalapb213ProjectDir}/src/main/scala" diff --git a/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala new file mode 100644 index 00000000000..0775c017ae5 --- /dev/null +++ b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/common/scalapb/ScalaPbJsonMarshaller.scala @@ -0,0 +1,120 @@ +/* + * Copyright 2022 LINE Corporation + * + * LINE Corporation 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. + */ + +package com.linecorp.armeria.common.scalapb + +import com.google.common.collect.MapMaker +import com.linecorp.armeria.common.grpc.GrpcJsonMarshaller +import com.linecorp.armeria.common.scalapb.ScalaPbJsonMarshaller.{ + jsonDefaultParser, + jsonDefaultPrinter, + messageCompanionCache, + typeMapperMethodCache +} +import io.grpc.MethodDescriptor.Marshaller +import java.io.{InputStream, OutputStream} +import java.lang.reflect.Field +import java.util.concurrent.ConcurrentMap +import scala.io.{Codec, Source} +import scalapb.grpc.TypeMappedMarshaller +import scalapb.json4s.{Parser, Printer} +import scalapb.{GeneratedMessage, GeneratedMessageCompanion, GeneratedSealedOneof, TypeMapper} + +/** + * A [[com.linecorp.armeria.common.grpc.GrpcJsonMarshaller]] that serializes and deserializes + * a [[scalapb.GeneratedMessage]] to and from JSON. + */ +final class ScalaPbJsonMarshaller private ( + jsonPrinter: Printer = jsonDefaultPrinter, + jsonParser: Parser = jsonDefaultParser +) extends GrpcJsonMarshaller { + + // TODO(ikhoon): Remove this forked file if https://github.com/lampepfl/dotty/issues/11332 is fixed. + + override def serializeMessage[A](marshaller: Marshaller[A], message: A, os: OutputStream): Unit = + message match { + case msg: GeneratedSealedOneof => + os.write(jsonPrinter.print(msg.asMessage).getBytes()) + case msg: GeneratedMessage => + os.write(jsonPrinter.print(msg).getBytes()) + case _ => + throw new IllegalStateException( + s"Unexpected message type: ${message.getClass} (expected: ${classOf[GeneratedMessage]})") + } + + override def deserializeMessage[A](marshaller: Marshaller[A], in: InputStream): A = { + val companion = getMessageCompanion(marshaller) + val jsonString = Source.fromInputStream(in)(Codec.UTF8).mkString + val message = jsonParser.fromJsonString(jsonString)(companion) + marshaller match { + case marshaller: TypeMappedMarshaller[_, _] => + val method = typeMapperMethodCache.computeIfAbsent( + marshaller, + key => { + val field = key.getClass.getDeclaredField("typeMapper") + field.setAccessible(true) + field + }) + val typeMapper = method.get(marshaller).asInstanceOf[TypeMapper[GeneratedMessage, A]] + typeMapper.toCustom(message) + case _ => + message.asInstanceOf[A] + } + } + + private def getMessageCompanion[A](marshaller: Marshaller[A]): GeneratedMessageCompanion[GeneratedMessage] = { + val companion = messageCompanionCache.get(marshaller) + if (companion != null) + companion + else + messageCompanionCache.computeIfAbsent( + marshaller, + key => { + val field = key.getClass.getDeclaredField("companion") + field.setAccessible(true) + field.get(marshaller).asInstanceOf[GeneratedMessageCompanion[GeneratedMessage]] + } + ) + } +} + +/** + * A companion object for [[com.linecorp.armeria.common.scalapb.ScalaPbJsonMarshaller]]. + */ +object ScalaPbJsonMarshaller { + + private val messageCompanionCache: ConcurrentMap[Marshaller[_], GeneratedMessageCompanion[GeneratedMessage]] = + new MapMaker().weakKeys().makeMap() + + private val typeMapperMethodCache: ConcurrentMap[Marshaller[_], Field] = + new MapMaker().weakKeys().makeMap() + + private val jsonDefaultPrinter: Printer = new Printer().includingDefaultValueFields + private val jsonDefaultParser: Parser = new Parser() + + private val defaultInstance: ScalaPbJsonMarshaller = new ScalaPbJsonMarshaller() + + /** + * Returns a newly-created [[com.linecorp.armeria.common.scalapb.ScalaPbJsonMarshaller]]. + */ + def apply( + jsonPrinter: Printer = jsonDefaultPrinter, + jsonParser: Parser = jsonDefaultParser): ScalaPbJsonMarshaller = + if (jsonPrinter == jsonDefaultPrinter && jsonParser == jsonDefaultParser) + defaultInstance + else + new ScalaPbJsonMarshaller(jsonPrinter, jsonParser) +} diff --git a/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala new file mode 100644 index 00000000000..b95e5d26d61 --- /dev/null +++ b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala @@ -0,0 +1,327 @@ +/* + * Copyright 2022 LINE Corporation + * + * LINE Corporation 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. + */ + +package com.linecorp.armeria.server.scalapb + +import java.lang.invoke.{MethodHandle, MethodHandles, MethodType} +import java.lang.reflect.{Method, ParameterizedType, Type} +import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentMap +import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} +import com.google.common.collect.{ImmutableList, ImmutableMap, ImmutableSet, MapMaker} +import com.google.protobuf.CodedInputStream +import com.linecorp.armeria.common.AggregatedHttpRequest +import com.linecorp.armeria.common.annotation.{Nullable, UnstableApi} +import com.linecorp.armeria.server.ServiceRequestContext +import com.linecorp.armeria.server.annotation.RequestConverterFunction +import com.linecorp.armeria.server.scalapb.ResultType._ +import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil._ + +import scalapb.json4s.Parser +import scalapb.{GeneratedMessage, GeneratedMessageCompanion, GeneratedSealedOneof} + +/** + * A [[com.linecorp.armeria.server.annotation.RequestConverterFunction]] which converts + * either a Protocol Buffers or JSON body of the [[com.linecorp.armeria.common.AggregatedHttpRequest]] to + * an [[scalapb.GeneratedMessage]]. + */ +@UnstableApi +final class ScalaPbRequestConverterFunction private (jsonParser: Parser, resultType: ResultType.Value) + extends RequestConverterFunction { + + // TODO(ikhoon): Remove this forked file if https://github.com/lampepfl/dotty/issues/11332 is fixed. + + import com.linecorp.armeria.server.scalapb.ScalaPbRequestConverterFunction._ + + @Nullable + override def convertRequest( + ctx: ServiceRequestContext, + request: AggregatedHttpRequest, + expectedResultType: Class[_], + @Nullable expectedParameterizedResultType: ParameterizedType): Object = { + val contentType = request.contentType() + val charset = + if (contentType == null) StandardCharsets.UTF_8 + else contentType.charset(StandardCharsets.UTF_8) + + if (resultType == PROTOBUF || + (resultType == UNKNOWN && isProtobufMessage(expectedResultType))) { + if (contentType == null || isProtobuf(contentType)) { + val is = request.content.toInputStream + try { + val message = getDefaultInstance(expectedResultType).companion + .asInstanceOf[GeneratedMessageCompanion[GeneratedMessage]] + .merge(getDefaultInstance(expectedResultType), CodedInputStream.newInstance(is)) + .asInstanceOf[GeneratedMessage] + return toGenerateMessageOrOneof(expectedResultType, message).asInstanceOf[Object] + } finally if (is != null) + is.close() + } + if (contentType.isJson) { + val jsonString = request.content(charset) + return jsonToScalaPbMessage(expectedResultType, jsonString).asInstanceOf[Object] + } + + if (!contentType.isJson || expectedParameterizedResultType == null) + return RequestConverterFunction.fallthrough + } + + var resultType0 = resultType + if (resultType0 == UNKNOWN) + resultType0 = toResultType(expectedParameterizedResultType) + if (resultType0 == UNKNOWN || resultType0 == PROTOBUF || + contentType == null || isProtobuf(contentType)) + return RequestConverterFunction.fallthrough + + val typeArguments = expectedParameterizedResultType.getActualTypeArguments + val jsonString = request.content(charset) + convertToCollection(jsonString, typeArguments, resultType0) + } + + private def convertToCollection( + jsonString: String, + typeArguments: Array[Type], + resultType: ResultType.Value): AnyRef = { + val jsonNode = mapper.readTree(jsonString) + val size = jsonNode.size + val numTypes = typeArguments.length + + if (jsonNode.isArray && numTypes == 1) { + val messageType = typeArguments(0).asInstanceOf[Class[_]] + val iter = jsonNode.iterator() + resultType match { + case LIST_PROTOBUF | SET_PROTOBUF => + val builder = + if (resultType == LIST_PROTOBUF) ImmutableList.builderWithExpectedSize[Any with Serializable](size) + else ImmutableSet.builderWithExpectedSize[Any with Serializable](size) + + while (iter.hasNext) + builder.add(jsonToScalaPbMessage(messageType, iter.next())) + builder.build + + case SCALA_LIST_PROTOBUF | SCALA_VECTOR_PROTOBUF | SCALA_SET_PROTOBUF => + val builder = + if (resultType == SCALA_LIST_PROTOBUF) + List.newBuilder[Any] + else if (resultType == SCALA_VECTOR_PROTOBUF) + Vector.newBuilder[Any] + else + Set.newBuilder[Any] + builder.sizeHint(size) + + while (iter.hasNext) + builder += jsonToScalaPbMessage(messageType, iter.next()) + builder.result() + + case _ => RequestConverterFunction.fallthrough + } + } else if (jsonNode.isObject && numTypes == 2) { + val messageType = typeArguments(1).asInstanceOf[Class[_]] + val iter = jsonNode.fields + resultType match { + case MAP_PROTOBUF => + val builder = ImmutableMap.builderWithExpectedSize[String, Any](size) + + while (iter.hasNext) { + val entry = iter.next + builder.put(entry.getKey, jsonToScalaPbMessage(messageType, entry.getValue)) + } + builder.build + case SCALA_MAP_PROTOBUF => + val builder = Map.newBuilder[String, Any] + builder.sizeHint(size) + + while (iter.hasNext) { + val entry = iter.next + builder += entry.getKey -> jsonToScalaPbMessage(messageType, entry.getValue) + } + builder.result() + + case _ => RequestConverterFunction.fallthrough() + } + } else + RequestConverterFunction.fallthrough() + } + + /** + * Returns a deserialized [[scalapb.GeneratedMessage]] or [[scalapb.GeneratedSealedOneof]] from the + * `JsonNode`. + */ + private def jsonToScalaPbMessage(expectedResultType: Class[_], node: JsonNode): Any with Serializable = { + val json = mapper.writeValueAsString(node) + jsonToScalaPbMessage(expectedResultType, json) + } + + /** + * Returns a deserialized [[scalapb.GeneratedMessage]] or [[scalapb.GeneratedSealedOneof]] from the `json`. + */ + private def jsonToScalaPbMessage(expectedResultType: Class[_], json: String): Any with Serializable = { + val messageType = extractGeneratedMessageType(expectedResultType) + val message: GeneratedMessage = jsonParser.fromJsonString(json)(getCompanion(messageType)) + toGenerateMessageOrOneof(expectedResultType, message) + } +} + +/** + * A [[com.linecorp.armeria.server.annotation.RequestConverterFunction]] which converts either + * a Protocol Buffers or JSON body of the [[com.linecorp.armeria.common.AggregatedHttpRequest]] + * to an [[scalapb.GeneratedMessage]]. + * + * The built-in parser of [[scalapb.GeneratedMessage]] for Protocol Buffers is applied only when + * the `content-type` of [[com.linecorp.armeria.common.RequestHeaders]] is either + * one of [[com.linecorp.armeria.common.MediaType.PROTOBUF]] or + * [[com.linecorp.armeria.common.MediaType.OCTET_STREAM]] or + * `application/x-protobuf`. + * The [[scalapb.json4s.Parser]] for JSON is applied only when the `content-type` of + * the [[com.linecorp.armeria.common.RequestHeaders]] is either [[com.linecorp.armeria.common.MediaType.JSON]] + * or ends with `+json`. + * + * ===Conversion of multiple Protobuf messages=== + * A sequence of Protocol Buffer messages can not be handled by this + * [[com.linecorp.armeria.server.annotation.RequestConverterFunction]], + * because Protocol Buffers wire format is not self-delimiting. + * See [[https://developers.google.com/protocol-buffers/docs/techniques#streaming Streaming Multiple Messages]] + * for more information. + * However, [[scala.Iterable]] types such as `List[scalapb.GeneratedMessage]` and + * `Set[scalapb.GeneratedMessage]` are supported only when converted from + * [[https://datatracker.ietf.org/doc/html/rfc7159#section-5 JSON array]]. + * + * Note that this [[com.linecorp.armeria.server.annotation.RequestConverterFunction]] is applied to + * an annotated service by default, so you don't have to specify this converter explicitly unless you want to + * use your own [[scalapb.json4s.Parser]]. + */ +@UnstableApi +object ScalaPbRequestConverterFunction { + + private val toOneofMethodCache: ConcurrentMap[Class[_], Method] = + new MapMaker().weakKeys.makeMap() + + private val defaultInstanceCache: ConcurrentMap[Class[_], GeneratedMessage] = + new MapMaker().weakKeys.makeMap() + + private val companionCache: ConcurrentMap[Class[_], GeneratedMessageCompanion[_]] = + new MapMaker().weakKeys.makeMap() + + private val defaultJsonParser: Parser = new Parser().ignoringUnknownFields + private val mapper = new ObjectMapper + + /** + * Creates a new instance with the specified [[scalapb.json4s.Parser]]. + */ + def apply(jsonParser: Parser = defaultJsonParser): ScalaPbRequestConverterFunction = + new ScalaPbRequestConverterFunction(jsonParser, UNKNOWN) + + private[scalapb] def apply(resultType: ResultType.Value): ScalaPbRequestConverterFunction = + new ScalaPbRequestConverterFunction(defaultJsonParser, resultType) + + /** + * Returns a default instance from the specified [[scalapb.GeneratedMessage]]'s class. + */ + private def getDefaultInstance(clazz: Class[_]): GeneratedMessage = { + val defaultInstance = defaultInstanceCache.computeIfAbsent( + clazz, + key => + try { + val genClass = extractGeneratedMessageType(key) + val method = genClass.getDeclaredMethod("defaultInstance") + method.invoke(null).asInstanceOf[GeneratedMessage] + } catch { + case _: NoSuchMethodError | _: IllegalAccessException => + unknownGeneratedMessage + } + ) + if (defaultInstance == unknownGeneratedMessage) + throw new IllegalStateException(s"Failed to find a static defaultInstance() method from $clazz") + defaultInstance + } + + /** + * Extracts a [[scalapb.GeneratedMessage]] class from the specified [[scalapb.GeneratedSealedOneof]] or + * returns itself. + * + * @param clazz Either `Class[GeneratedMessage]` or `Class[GeneratedSealedOneof]`. + */ + private def extractGeneratedMessageType(clazz: Class[_]): Class[GeneratedMessage] = { + val isOneOf = classOf[GeneratedSealedOneof].isAssignableFrom(clazz) + val messageType = + if (isOneOf) + clazz.getDeclaredMethod("asMessage").getReturnType + else + clazz + messageType.asInstanceOf[Class[GeneratedMessage]] + } + + /** + * Converts a [[scalapb.GeneratedMessage]] into a [[scalapb.GeneratedSealedOneof]] + * if the `expectedResultType` is a subtype of [[scalapb.GeneratedSealedOneof]]. + */ + private def toGenerateMessageOrOneof( + expectedResultType: Class[_], + result: GeneratedMessage): Any with Serializable = + if (classOf[GeneratedSealedOneof].isAssignableFrom(expectedResultType)) { + val method = toOneofMethodCache.computeIfAbsent( + expectedResultType, + key => + try { + result.getClass.getDeclaredMethod(s"to${key.getSimpleName}") + } catch { + case _: NoSuchMethodException | _: SecurityException => + unknownMethod + } + ) + + if (method eq unknownMethod) + throw new IllegalStateException( + s"Failed to find to${expectedResultType.getSimpleName} method from $result") + + method.invoke(result).asInstanceOf[Any with Serializable] + } else + result + + /** + * Returns a companion object used to convert JSON to a [[scalapb.GeneratedMessage]]. + */ + private def getCompanion[A <: GeneratedMessage](clazz: Class[_]): GeneratedMessageCompanion[A] = { + val messageCompanion = + companionCache + .computeIfAbsent( + clazz, + key => { + val companionClass = Class.forName(key.getName + "$") + try companionClass + .getDeclaredField("MODULE$") + .get(null) + .asInstanceOf[GeneratedMessageCompanion[_]] + catch { + case _: NoSuchFieldException | _: ClassNotFoundException => + unknownGeneratedMessageCompanion + } + } + ) + .asInstanceOf[GeneratedMessageCompanion[A]] + + if (messageCompanion eq unknownGeneratedMessageCompanion) + throw new IllegalStateException("Failed to find a companion object from " + clazz) + + messageCompanion + } + + private[scalapb] val unknownMethod: Method = { + val method = classOf[String].getDeclaredMethod("valueOf", classOf[Any]) + assert(method != null) + method + } +} diff --git a/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala new file mode 100644 index 00000000000..3fadc680416 --- /dev/null +++ b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbResponseConverterFunction.scala @@ -0,0 +1,213 @@ +/* + * Copyright 2020 LINE Corporation + * + * LINE Corporation 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. + */ + +package com.linecorp.armeria.server.scalapb + +import _root_.scalapb.json4s.Printer +import _root_.scalapb.{GeneratedMessage, GeneratedSealedOneof} +import com.google.common.base.Preconditions.checkArgument +import com.google.common.collect.Iterables +import com.linecorp.armeria.common.annotation.{Nullable, UnstableApi} +import com.linecorp.armeria.common.{HttpData, HttpHeaders, HttpResponse, MediaType, ResponseHeaders} +import com.linecorp.armeria.internal.server.ResponseConversionUtil.aggregateFrom +import com.linecorp.armeria.server.ServiceRequestContext +import com.linecorp.armeria.server.annotation.ResponseConverterFunction +import com.linecorp.armeria.server.scalapb.ScalaPbConverterUtil._ +import com.linecorp.armeria.server.scalapb.ScalaPbResponseConverterFunction.{ + fromObjectMH, + fromPublisherMH, + fromStreamMH +} +import com.linecorp.armeria.server.streaming.JsonTextSequences +import java.lang.reflect.Method +import java.nio.charset.{Charset, StandardCharsets} +import java.util.concurrent.Executor +import java.util.function.{Function => JFunction} +import java.util.stream.Stream +import org.reactivestreams.Publisher +import scala.collection.mutable.ArrayBuffer + +/** + * A [[com.linecorp.armeria.server.annotation.ResponseConverterFunction]] which creates an + * [[com.linecorp.armeria.common.HttpResponse]] with `content-type: application/protobuf` + * or `content-type: application/json; charset=utf-8`. + * If the returned object is an instance of [[scalapb.GeneratedMessage]], the object can be converted to + * either [[https://developers.google.com/protocol-buffers/docs/encoding Protocol Buffers]] or + * [[https://developers.google.com/protocol-buffers/docs/proto3#json JSON]] format. + * + * ===Conversion of multiple Protobuf messages=== + * A sequence of Protocol Buffer messages can not be handled by this + * [[com.linecorp.armeria.server.annotation.ResponseConverterFunction]], because Protocol Buffers wire format + * is not self-delimiting. + * See [[https://developers.google.com/protocol-buffers/docs/techniques#streaming Streaming Multiple Messages]] + * for more information. + * However, [[org.reactivestreams.Publisher]], [[java.util.stream.Stream]], [[scala.Iterable]] and + * [[java.util.List]] are supported when converting to + * [[https://datatracker.ietf.org/doc/html/rfc7159#section-5 JSON array]]. + * [[https://datatracker.ietf.org/doc/rfc7464/ JavaScript Object Notation (JSON) Text Sequences]] + * is also supported for [[org.reactivestreams.Publisher]] and [[java.util.stream.Stream]]. + * + * Note that this [[com.linecorp.armeria.server.annotation.ResponseConverterFunction]] is applied to + * the annotated service by default, so you don't have to set explicitly unless you want to + * use your own [[scalapb.json4s.Printer]]. + * + * @constructor Creates a new instance with the specified [[scalapb.json4s.Printer]]. + */ +@UnstableApi +final class ScalaPbResponseConverterFunction(jsonPrinter: Printer = defaultJsonPrinter) + extends ResponseConverterFunction { + + // TODO(ikhoon): Remove this forked file if https://github.com/lampepfl/dotty/issues/11332 is fixed. + + override def convertResponse( + ctx: ServiceRequestContext, + headers: ResponseHeaders, + @Nullable result: Any, + trailers: HttpHeaders): HttpResponse = { + val contentType = headers.contentType + val isJsonType = contentType != null && contentType.isJson + val isJsonSeq = contentType != null && MediaType.JSON_SEQ.is(contentType) + + result match { + case _ if isJsonSeq => + val jfunction: JFunction[Object, String] = obj => toJson(obj) + val response = result match { + case publisher: Publisher[_] => + fromPublisherMH.invoke(null, headers, publisher.asInstanceOf[Publisher[Any]], trailers, jfunction) + case stream: Stream[_] => + fromStreamMH.invoke( + null, + headers, + stream.asInstanceOf[Stream[Any]], + trailers, + ctx.blockingTaskExecutor(), + jfunction) + case _ => + fromObjectMH.invoke(null, headers, result, trailers, jfunction) + } + response.asInstanceOf[HttpResponse] + + case msg: GeneratedSealedOneof => + convertMessage(headers, msg.asMessage, trailers, contentType, isJsonType) + case message: GeneratedMessage => + convertMessage(headers, message, trailers, contentType, isJsonType) + + case _ if isJsonType => + checkArgument(result != null, "a null value is not allowed for %s", contentType) + val charset = + if (contentType == null) StandardCharsets.UTF_8 else contentType.charset(StandardCharsets.UTF_8) + result match { + case publisher: Publisher[_] => + aggregateFrom(publisher, headers, trailers, toJsonHttpData(_, charset), ctx) + case stream: Stream[_] => + aggregateFrom(stream, headers, trailers, toJsonHttpData(_, charset), ctx.blockingTaskExecutor) + case _ => + HttpResponse.of(headers, toJsonHttpData(result, charset), trailers) + } + + case _ => + throw new IllegalArgumentException("Cannot convert a " + result + " to Protocol Buffers wire format") + } + } + + private def convertMessage( + headers: ResponseHeaders, + result: GeneratedMessage, + trailers: HttpHeaders, + @Nullable contentType: MediaType, + isJson: Boolean): HttpResponse = { + if (isJson) { + val charset = contentType.charset(StandardCharsets.UTF_8) + return HttpResponse.of(headers, toJsonHttpData(result, charset), trailers) + } + + if (contentType == null) + HttpResponse.of( + headers.toBuilder.contentType(MediaType.PROTOBUF).build, + HttpData.wrap(result.toByteArray), + trailers) + else if (isProtobuf(contentType)) + HttpResponse.of(headers, HttpData.wrap(result.toByteArray), trailers) + else + ResponseConverterFunction.fallthrough() + } + + private def toJsonHttpData(message: Any, charset: Charset): HttpData = + HttpData.of(charset, toJson(message)) + + private def toJson(message: Any): String = + message match { + case map: java.util.Map[_, _] => + val builder = new ArrayBuffer[String](map.size()) + map.forEach((key: Any, value: Any) => builder += s""""$key": ${toJson(value)}""") + builder.mkString("{", ",", "}") + + case map: Map[_, _] => + map.map { case (k, v) => s""""$k": ${toJson(v)}""" }.mkString("{", ",", "}") + + case iterable: java.lang.Iterable[_] => + val builder = new ArrayBuffer[String](Iterables.size(iterable)) + iterable.forEach(value => builder += toJson(value)) + builder.mkString("[", ",", "]") + + case iter: Iterable[_] => + iter.map(this.toJson).mkString("[", ",", "]") + + case message: GeneratedSealedOneof => jsonPrinter.print(message.asMessage) + case message: GeneratedMessage => jsonPrinter.print(message) + case _ => + throw new IllegalStateException( + s"Unexpected message type : ${message.getClass} " + + s"(expected: a subtype of ${classOf[GeneratedMessage].getName})") + } +} + +private object ScalaPbResponseConverterFunction { + + private val fromPublisherMH: Method = { + val method: Method = classOf[JsonTextSequences].getDeclaredMethod( + "fromPublisher", + classOf[ResponseHeaders], + classOf[Publisher[_]], + classOf[HttpHeaders], + classOf[java.util.function.Function[_, _]]) + method.setAccessible(true) + method + } + + private val fromStreamMH: Method = { + val method: Method = classOf[JsonTextSequences].getDeclaredMethod( + "fromStream", + classOf[ResponseHeaders], + classOf[Stream[_]], + classOf[HttpHeaders], + classOf[Executor], + classOf[java.util.function.Function[_, _]]) + method.setAccessible(true) + method + } + + private val fromObjectMH: Method = { + val method: Method = classOf[JsonTextSequences].getDeclaredMethod( + "fromObject", + classOf[ResponseHeaders], + classOf[Object], + classOf[HttpHeaders], + classOf[java.util.function.Function[_, _]]) + method.setAccessible(true) + method + } +} diff --git a/settings.gradle b/settings.gradle index e04cb1adc5b..b216ce433b8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,15 +31,20 @@ includeWithFlags ':rxjava3', 'java', 'publish', 'rel includeWithFlags ':sangria_2.12', 'java', 'publish', 'relocate', 'no_aggregation', 'scala_2.12' project(':sangria_2.12').projectDir = file('sangria/sangria_2.12') includeWithFlags ':sangria_2.13', 'java', 'publish', 'relocate', 'scala_2.13' +// Sangria does not support Scala 3 yet. https://github.com/sangria-graphql/sangria/issues/649 project(':sangria_2.13').projectDir = file('sangria/sangria_2.13') includeWithFlags ':scala_2.12', 'java', 'publish', 'relocate', 'no_aggregation', 'scala_2.12' project(':scala_2.12').projectDir = file('scala/scala_2.12') includeWithFlags ':scala_2.13', 'java', 'publish', 'relocate', 'scala_2.13' project(':scala_2.13').projectDir = file('scala/scala_2.13') +includeWithFlags ':scala_3', 'java', 'publish', 'relocate', 'no_aggregation', 'scala_3' +project(':scala_3').projectDir = file('scala/scala_3') includeWithFlags ':scalapb_2.12', 'java', 'publish', 'relocate', 'no_aggregation', 'scala_2.12' project(':scalapb_2.12').projectDir = file('scalapb/scalapb_2.12') includeWithFlags ':scalapb_2.13', 'java', 'publish', 'relocate', 'scala-grpc_2.13', 'scala_2.13' project(':scalapb_2.13').projectDir = file('scalapb/scalapb_2.13') +includeWithFlags ':scalapb_3', 'java', 'publish', 'relocate', 'no_aggregation', 'scala_3' +project(':scalapb_3').projectDir = file('scalapb/scalapb_3') includeWithFlags ':spring:boot1-autoconfigure', 'java', 'publish', 'relocate', 'no_aggregation' includeWithFlags ':spring:boot1-starter', 'java', 'publish', 'relocate', 'no_aggregation' From 72fd151591a1b7221ed479871be629e7272b8837 Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Wed, 19 Jan 2022 21:07:09 +0900 Subject: [PATCH 4/7] Clean up --- gradle/scripts/lib/scala.gradle | 12 ------------ .../linecorp/armeria/common/util/FutureSuite.scala | 2 ++ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/gradle/scripts/lib/scala.gradle b/gradle/scripts/lib/scala.gradle index d35fb103169..c1013919c51 100644 --- a/gradle/scripts/lib/scala.gradle +++ b/gradle/scripts/lib/scala.gradle @@ -16,18 +16,6 @@ configure(scala212 + scala213 + scala3) { } } - // Delete the generated source directory on clean. - ext { - genSrcDir = "${projectDir}/gen-src" - } - clean { - delete project.ext.genSrcDir - } - // Add the generated source directories to the source sets. - project.sourceSets.all { sourceSet -> - sourceSet.java.srcDir file("${project.ext.genSrcDir}/${sourceSet.name}/scala") - } - tasks.withType(Test) { useJUnitPlatform { // A workaround for 'java.lang.InternalError: Malformed class name' diff --git a/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala b/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala index 228e6d020f1..57437cc39ed 100644 --- a/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala +++ b/scala/scala_2.13/src/test/scala/com/linecorp/armeria/common/util/FutureSuite.scala @@ -33,6 +33,8 @@ class FutureSuite extends FunSuite { val scalaFuture: Future[Unit] = javaFuture.toScala javaFuture.complete(null) + // `()` is a `BoxedUnit` that can not be cast into `Unit` in Scala3. + // https://stackoverflow.com/questions/32289511/boxedunit-vs-unit-in-scala assert(Await.result(scalaFuture, 10.seconds).getClass.getName == "void") } From f8060a740ef372263764da124e364ed8988b7168 Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Fri, 21 Jan 2022 16:11:24 +0900 Subject: [PATCH 5/7] Bump Scalafmt version and revert incorrectly formatted files. --- .scalafmt.conf | 2 +- .../linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 784bb56a119..fd8d207bd4d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.0.0-RC6" +version = "3.3.2" style = default diff --git a/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala b/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala index a62cecfab93..eeeafbc44d4 100644 --- a/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala +++ b/sangria/sangria_2.13/src/test/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlSuite.scala @@ -83,8 +83,7 @@ class SangriaGraphqlSuite extends FunSuite with ServerSuite { } """ - val response = - executeQuery(server.webClient(), method, query = query, variables = Map("humanId" -> "1002")) + val response = executeQuery(server.webClient(), method, query = query, variables = Map("humanId" -> "1002")) assertEquals(response.headers().status(), HttpStatus.OK) assertThatJson(response.contentUtf8()) .isEqualTo(""" From b74d132407e803e9b17fed026eba2363e11ee405 Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Tue, 25 Jan 2022 17:41:27 +0900 Subject: [PATCH 6/7] Catch up new updates --- dependencies.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dependencies.yml b/dependencies.yml index 0252f80359f..7b0310ee36e 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -77,7 +77,7 @@ com.github.node-gradle: gradle-node-plugin: { version: '3.1.1' } com.google.api: - gax-grpc: { version: '2.9.0' } + gax-grpc: { version: '2.10.0' } com.google.code.findbugs: jsr305: { version: '3.0.2' } @@ -252,7 +252,7 @@ io.grpc: - io.netty:netty-handler-proxy - io.netty:netty-transport - io.netty:netty-tcnative-boringssl-static - grpc-kotlin-stub: { version: &GRPC_KOTLIN_VERSION '1.2.0' } + grpc-kotlin-stub: { version: &GRPC_KOTLIN_VERSION '1.2.1' } protoc-gen-grpc-kotlin: { version: *GRPC_KOTLIN_VERSION } io.micrometer: @@ -396,7 +396,7 @@ org.apache.httpcomponents: org.apache.kafka: kafka-clients: - version: '3.0.0' + version: '3.1.0' javadocs: - https://kafka.apache.org/30/javadoc/ @@ -519,7 +519,7 @@ org.jsoup: jsoup: { version: '1.14.3' } org.mockito: - mockito-core: { version: &MOCKITO_VERSION '4.2.0' } + mockito-core: { version: &MOCKITO_VERSION '4.3.0' } mockito-junit-jupiter: { version: *MOCKITO_VERSION } org.mortbay.jetty.alpn: @@ -560,7 +560,7 @@ org.reflections: org.scala-lang: scala-library: { version: '2.13.8' } - scala3-library_3: { version: '3.1.0' } + scala3-library_3: { version: '3.1.1' } org.scala-lang.modules: scala-collection-compat_2.12: { version: '2.6.0' } @@ -580,7 +580,7 @@ org.sangria-graphql: sangria-slowlog_2.13: { version: *SANGRIA_SLOWLOG_VERSION } org.slf4j: - jcl-over-slf4j: { version: &SLF4J_VERSION '1.7.33' } + jcl-over-slf4j: { version: &SLF4J_VERSION '1.7.34' } jul-to-slf4j: { version: *SLF4J_VERSION } log4j-over-slf4j: { version: *SLF4J_VERSION } slf4j-api: From 54208f718ae6532a943d914e6b39900c95bc8cc6 Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Tue, 25 Jan 2022 17:55:50 +0900 Subject: [PATCH 7/7] Specify dialect to scala212source3 in scalafmt --- .scalafmt.conf | 2 ++ .../ScalaPbRequestConverterFunction.scala | 16 ++++++++++------ .../ScalaPbRequestConverterFunction.scala | 18 +++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index fd8d207bd4d..df746a77f3c 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -2,6 +2,8 @@ version = "3.3.2" style = default +runner.dialect = scala212source3 + maxColumn = 112 align.preset = default diff --git a/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala b/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala index 61d6be9e5eb..c20196a805a 100644 --- a/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala +++ b/scalapb/scalapb_2.13/src/main/scala_2/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala @@ -164,10 +164,11 @@ object ScalaPbRequestConverterFunction { clazz, key => { val companionClass = Class.forName(key.getName + "$") - try companionClass - .getDeclaredField("MODULE$") - .get(null) - .asInstanceOf[GeneratedMessageCompanion[_]] + try + companionClass + .getDeclaredField("MODULE$") + .get(null) + .asInstanceOf[GeneratedMessageCompanion[_]] catch { case _: NoSuchFieldException | _: ClassNotFoundException => unknownGeneratedMessageCompanion @@ -213,8 +214,11 @@ final class ScalaPbRequestConverterFunction private (jsonParser: Parser, resultT .merge(getDefaultInstance(expectedResultType), CodedInputStream.newInstance(is)) .asInstanceOf[GeneratedMessage] return toGenerateMessageOrOneof(expectedResultType, message).asInstanceOf[Object] - } finally if (is != null) - is.close() + } finally { + if (is != null) { + is.close() + } + } } if (contentType.isJson) { val jsonString = request.content(charset) diff --git a/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala index b95e5d26d61..2b61f6517db 100644 --- a/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala +++ b/scalapb/scalapb_3/src/main/scala/com/linecorp/armeria/server/scalapb/ScalaPbRequestConverterFunction.scala @@ -67,8 +67,11 @@ final class ScalaPbRequestConverterFunction private (jsonParser: Parser, resultT .merge(getDefaultInstance(expectedResultType), CodedInputStream.newInstance(is)) .asInstanceOf[GeneratedMessage] return toGenerateMessageOrOneof(expectedResultType, message).asInstanceOf[Object] - } finally if (is != null) - is.close() + } finally { + if (is != null) { + is.close() + } + } } if (contentType.isJson) { val jsonString = request.content(charset) @@ -301,11 +304,12 @@ object ScalaPbRequestConverterFunction { clazz, key => { val companionClass = Class.forName(key.getName + "$") - try companionClass - .getDeclaredField("MODULE$") - .get(null) - .asInstanceOf[GeneratedMessageCompanion[_]] - catch { + try { + companionClass + .getDeclaredField("MODULE$") + .get(null) + .asInstanceOf[GeneratedMessageCompanion[_]] + } catch { case _: NoSuchFieldException | _: ClassNotFoundException => unknownGeneratedMessageCompanion }