diff --git a/.dockerignore b/.dockerignore index c046475..a3740ce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,4 @@ /out /.git /.gradle +/gradle/wrapper diff --git a/.editorconfig b/.editorconfig index 58257a9..6deb38d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,5 +12,9 @@ indent_style = space indent_size = 4 continuation_indent_size = 8 +[*.kt] +continuation_indent_size = 4 +disabled_rules=no-wildcard-imports + [*.gradle] indent_size = 4 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 809d51f..8834029 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,7 +43,3 @@ jobs: # Gradle check - name: Check run: ./gradlew check - - # Gradle integration test. Includes internal docker-compose management - - name: Integration test - run: ./gradlew integrationTest diff --git a/Dockerfile b/Dockerfile index f36bd9d..76d2e45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,20 +26,20 @@ COPY ./src/ /code/src RUN gradle distTar \ && cd build/distributions \ && tar xzf *.tar.gz \ - && rm *.tar.gz radar-gateway-*/lib/radar-gateway-*.jar + && rm *.tar.gz radar-push-endpoint-*/lib/radar-push-endpoint-*.jar FROM openjdk:11-jre-slim -MAINTAINER @blootsvoets +MAINTAINER @yatharthranjan -LABEL description="RADAR-base Gateway docker container" +LABEL description="RADAR-base Push Api Gateway docker container" -COPY --from=builder /code/build/distributions/radar-gateway-*/bin/* /usr/bin/ -COPY --from=builder /code/build/distributions/radar-gateway-*/lib/* /usr/lib/ -COPY --from=builder /code/build/libs/radar-gateway-*.jar /usr/lib/ +COPY --from=builder /code/build/distributions/radar-push-endpoint-*/bin/* /usr/bin/ +COPY --from=builder /code/build/distributions/radar-push-endpoint-*/lib/* /usr/lib/ +COPY --from=builder /code/build/libs/radar-push-endpoint-*.jar /usr/lib/ USER 101 EXPOSE 8090 -CMD ["radar-gateway"] +CMD ["radar-push-endpoint"] diff --git a/README.md b/README.md index 813be32..b3e590a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,31 @@ -# RADAR-Gateway +# RADAR-PushEndpoint -[![Build Status](https://travis-ci.org/RADAR-base/RADAR-Gateway.svg?branch=master)](https://travis-ci.org/RADAR-base/RADAR-Gateway) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/79b2380112c5451181367ae16e112025)](https://www.codacy.com/app/RADAR-base/RADAR-Gateway?utm_source=github.com&utm_medium=referral&utm_content=RADAR-base/RADAR-Gateway&utm_campaign=Badge_Grade) -[![Docker Build](https://img.shields.io/docker/build/radarbase/radar-gateway.svg)](https://cloud.docker.com/swarm/radarbase/repository/docker/radarbase/radar-gateway/general) +[![Build Status](https://github.com/RADAR-base/RADAR-PushEndpoint/workflows/CI/badge.svg)](https://github.com/RADAR-base/RADAR-PushEndpoint/actions?query=workflow%3ACI+branch%3Adev+) +[![Docker Build](https://img.shields.io/docker/cloud/build/radarbase/radar-push-endpoint)](https://hub.docker.com/repository/docker/radarbase/radar-push-endpoint) -REST Gateway to the Apache Kafka, similar to the REST Proxy provided by Confluent. In addition, it does authentication and authorization, content validation and decompression if needed. It is available as a [docker image](https://hub.docker.com/r/radarbase/radar-gateway). +RADAR Push Endpoint that exposes REST interface for push subscription based APIs to the Apache + Kafka. ## Configuration -The [RADAR-Auth] library is used for authentication and authorization of users. Refer to the documentation there for a full description of the configuration options. +Currently, Garmin is integrated. For adding more services, see the [Extending section](#extending). + +```yaml + pushIntegration: + # Push service specific config + garmin: + enabled: true + userRepositoryClass: "org.radarbase.push.integration.garmin.user.ServiceUserRepository" + service-n: + enabled: true + property-xyz: "value" +``` + +For Garmin, you will need to configure the endpoints in the [Garmin Developer Portal](https://healthapi.garmin.com/tools/updateEndpoints) ## Usage -Start the REST Proxy with +Start the Service with ```shell docker-compose up -d --build @@ -25,22 +38,127 @@ TOPIC=test docker-compose exec kafka-1 kafka-topics --create --topic $TOPIC --bootstrap-server kafka-1:9092 ``` -Now the gateway is accessible through and the [ManagementPortal] is available through +Now the service is accessible through . +Garmin endpoints are available at - +- +- +- +- +- +- +- +- +- +- +- +- +- + +## Extending +This section walks through add a new push service integration. These should be implemented in a + new package `org.radarbase.push.integration.`. + +### Resource +Create a new Resource and configure the endpoints required by the push service integration. For + reference take a look at [GarminPushEndpoint](src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt) + +### User Repository +Create a new UserRepository to provide user specific info and authorization info. This should + implement the interface [UserRepository](src/main/kotlin/org/radarbase/push/integration/common/user/UserRepository.kt). +For reference, take a look at [ServiceUserRepository](src/main/kotlin/org/radarbase/push/integration/garmin/user/ServiceUserRepository.kt) + +### Auth Validator +Create a new AuthValidator to check the requests and authorise with users provided by + the User Repository. This can be done by implementing the [AuthValidator](https://github.com/RADAR-base/radar-jersey/blob/master/src/main/kotlin/org/radarbase/jersey/auth/AuthValidator.kt) + interface provided by `radar-jersey` library. +For reference, take a look at [GarminAuthValidator](src/main/kotlin/org/radarbase/push/integration/garmin/auth/GarminAuthValidator.kt) + +### Converter +This is optional but will help keep the code consistent. +Create Converters for converting data posted by the push service to Kafka records. This can be + done by implementing the [AvroConverter](src/main/kotlin/org/radarbase/push/integration/common/converter/AvroConverter.kt) interface. +For reference, take a look at converter implementations in [garmin converter](src/main/kotlin/org/radarbase/push/integration/garmin/converter) package. + +### Configuration + +Firstly, create a Resource Enhancer to register all your required classes to Jersey Context. + Remember to use `named` to distinguish your service implementation. + +```kotlin +class ServiceXIntegrationResourceEnhancer(private val config: Config) : + JerseyResourceEnhancer { + + override fun ResourceConfig.enhance() { + packages( + "org.radarbase.push.integration.servicex.resource", + "org.radarbase.push.integration.servicex.filter" + ) + } + + override fun AbstractBinder.enhance() { + + bind(config.pushIntegration.servicex.userRepository) + .to(UserRepository::class.java) + .named("servicex") + .`in`(Singleton::class.java) + + bind(ServiceXAuthValidator::class.java) + .to(AuthValidator::class.java) + .named("servicex") + .`in`(Singleton::class.java) + } +} +``` -The access token should be generated by the aforementioned Management portal. The access token is a JWT (JSON Web Token) that should contain the `MEASUREMENT.CREATE` scope for resource `res_gateway`, and list all applicable sources to submit data for. The gateway does content validation for posted data. It requires to use the Avro format with JSON serialization, using the `application/vnd.kafka.avro.v1+json` or `application/vnd.kafka.avro.v2+json` media types, as described in the [REST Proxy documentation]. It also requires messages to have both a key and a value with schemas. The key should have a `userId` and `sourceId` field. The `userId` should match the `sub` field in the OAuth2 JWT access token. That JWT should also contain a `sources` array claim which should contain the given `sourceId`. Sources can be added in the ManagementPortal or be generated by the app dynamically and then registered with the ManagementPortal. +Next, add your `AuthValidator` to the [DelegatedAuthValidator](src/main/kotlin/org/radarbase/push/integration/common/auth/DelegatedAuthValidator.kt) so service specific Auth can be performed. +Make sure the path to your service's resources contain the matching string (`servicex` in this + case). + +```kotlin +... + + fun delegate(): AuthValidator { + return when { + uriInfo.matches(GARMIN_QUALIFIER) -> namedValidators.named(GARMIN_QUALIFIER).get() + uriInfo.matches("servicex") -> namedValidators.named("servicex").get() + // Add support for more as integrations are added + else -> throw IllegalStateException() + } + } + +... +``` -Now you can access the gateway: -```shell -TOKEN= -curl -H "Authorization: Bearer $TOKEN" http://localhost:8090/radar-gateway/topics +Next, add the configuration to the [Config](src/main/kotlin/org/radarbase/gateway/Config.kt) class. +```kotlin +... +data class PushIntegrationConfig( + val garmin: GarminConfig = GarminConfig(), + val servicex: ServiceXConfig +) + +data class ServiceXConfig( + val enabled: Boolean = false, + val userRepositoryClass: String, + val property1: String, + val property2: List +) + +... ``` -Data compressed with GZIP is decompressed if the `Content-Encoding: gzip` header is present. With `curl`, use the `-H "Content-Encoding: gzip" --data-binary @data.json.gz` flags. It can be activated in `radar-commons` Java `RestClient` by setting `RestClient.Builder.gzipCompression(true)`. Likewise it accepts Apple LZFSE encoded data by adding the header `Content-Encoding: lzfse`. +Finally, add your newly created Resource Enhancer to [PushIntegrationEnhancerFactory](src/main/kotlin/org/radarbase/gateway/inject/PushIntegrationEnhancerFactory.kt) +```kotlin +... + // Push Service specific enhancers + if (config.pushIntegration.garmin.enabled) { + enhancersList.add(GarminPushIntegrationResourceEnhancer(config)) + } + if(config.pushIntegration.servicex.enabled) { + enhancersList.add(ServiceXIntegrationResourceEnhancer(config)) + } -Otherwise, it accepts all the same Avro messages and headers as specified in the Kafka [REST Proxy documentation]. -Finally, the gateway accepts a custom binary format for data ingestion. The data must follow the binary Avro serialization of the [RecordSet schema](https://github.com/RADAR-base/RADAR-Schemas/blob/master/commons/kafka/record_set.avsc). Data in this format can be posted by using the content type `application/vnd.radarbase.avro.v1+binary`. It will construct an `ObservationKey` based on the user data in the `RecordSet`, and read the binary data values using the schema version provided in the `RecordSet`. This data sending mode can be activated in Java by using radar-commons `RestSender.Builder.useBinaryContent(true)`. Using binary mode has the added benefit of having a much more efficient GZIP encoding for many datasets. +... +``` -[REST Proxy documentation]: https://docs.confluent.io/current/kafka-rest/api.html -[RADAR-Auth]: https://github.com/RADAR-base/ManagementPortal/tree/master/radar-auth -[ManagementPortal]: https://github.com/RADAR-base/ManagementPortal diff --git a/build.gradle.kts b/build.gradle.kts index 7227368..fcb2422 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,11 +11,16 @@ plugins { } group = "org.radarbase" -version = "0.5.3" -description = "RADAR Gateway to handle secured data flow to backend." +version = "0.1.0" +description = "RADAR Push API Gateway to handle secured data flow to backend." + +dependencyLocking { + lockAllConfigurations() +} repositories { jcenter() + mavenCentral() // Non-jcenter radar releases maven(url = "https://dl.bintray.com/radar-cns/org.radarcns") maven(url = "https://dl.bintray.com/radar-base/org.radarbase") @@ -39,8 +44,15 @@ dependencies { implementation("org.apache.kafka:kafka-clients:${project.property("kafkaVersion")}") implementation("io.confluent:kafka-avro-serializer:${project.property("confluentVersion")}") + implementation("org.radarcns:oauth-client-util:${project.property("radarOauthClientVersion")}") + implementation("org.slf4j:slf4j-api:${project.property("slf4jVersion")}") - implementation("com.fasterxml.jackson.core:jackson-databind:${project.property("jacksonVersion")}") + + val jacksonVersion: String by project + implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion") val grizzlyVersion: String by project runtimeOnly("org.glassfish.grizzly:grizzly-framework-monitoring:$grizzlyVersion") @@ -48,11 +60,16 @@ dependencies { runtimeOnly("org.glassfish.grizzly:grizzly-http-server-monitoring:$grizzlyVersion") runtimeOnly("ch.qos.logback:logback-classic:${project.property("logbackVersion")}") + val jedisVersion: String by project + implementation("redis.clients:jedis:$jedisVersion") + val junitVersion: String by project val okhttp3Version: String by project val radarSchemasVersion: String by project + implementation("org.radarcns:radar-schemas-commons:$radarSchemasVersion") + testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") - testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:[2.2,3.0)") testImplementation("com.squareup.okhttp3:mockwebserver:$okhttp3Version") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") @@ -67,8 +84,8 @@ val kotlinApiVersion: String by project tasks.withType { kotlinOptions { jvmTarget = "11" - apiVersion = kotlinApiVersion - languageVersion = kotlinApiVersion + apiVersion = "1.4" + languageVersion = "1.4" } } diff --git a/docker-compose.yml b/docker-compose.yml index df68b0a..ad89f43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,10 +19,14 @@ services: image: confluentinc/cp-kafka:${KAFKA_CONFLUENT_VERSION} depends_on: - zookeeper-1 + ports: + - 9093:9093 environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-1:9092 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka-1:9092, EXTERNAL://localhost:9093 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT, EXTERNAL:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" KAFKA_COMPRESSION_TYPE: lz4 KAFKA_CONFLUENT_SUPPORT_METRICS_ENABLE: "false" @@ -45,32 +49,20 @@ services: SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081 SCHEMA_REGISTRY_KAFKASTORE_TOPIC_REPLICATION_FACTOR: 1 - #---------------------------------------------------------------------------# - # Management Portal # - #---------------------------------------------------------------------------# - managementportal-app: - image: radarbase/management-portal:0.6.0 - ports: - - "127.0.0.1:8080:8080" - environment: - SPRING_PROFILES_ACTIVE: dev - MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET: "" - MANAGEMENTPORTAL_COMMON_BASE_URL: http://localhost:8080 - MANAGEMENTPORTAL_COMMON_MANAGEMENT_PORTAL_BASE_URL: http://localhost:8080 - MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE: /mp-includes/config/oauth_client_details.csv - MANAGEMENTPORTAL_CATALOGUE_SERVER_ENABLE_AUTO_IMPORT: 'false' - JAVA_OPTS: -Xmx256m # maximum heap size for the JVM running ManagementPortal, increase this as necessary - volumes: - - ./src/integrationTest/docker/etc/:/mp-includes/ - - gateway: + push-endpoint: build: . - image: radarbase/radar-gateway:SNAPSHOT + image: radarbase/radar-push-endpoint:SNAPSHOT depends_on: - kafka-1 - schema-registry-1 - - managementportal-app ports: - "127.0.0.1:8090:8090" volumes: - ./gateway.yml:/etc/radar-gateway/gateway.yml + + redis: + image: bitnami/redis + ports: + - "6379:6379" + environment: + ALLOW_EMPTY_PASSWORD: "yes" diff --git a/gateway.yml b/gateway.yml index be9ab81..30f0ce5 100644 --- a/gateway.yml +++ b/gateway.yml @@ -3,7 +3,7 @@ server: # URI to serve data to - baseUri: http://0.0.0.0:8090/radar-gateway/ + baseUri: http://0.0.0.0:8090/push/integrations/ # Maximum number of simultaneous requests to Kafka. #maxRequests: 200 # Maximum request content length, also when decompressed. @@ -52,3 +52,16 @@ auth: #ecdsa: [] # RSA public keys #rsa: [] + +# Push Service specific configuration +pushIntegration: + garmin: + enabled: true + backfill: + defaultEndDate: "1590844126" + # Redis configuration + redis: + # Redis URI + uri: redis://localhost:6379 + # Key prefix for locks + lockPrefix: radar-push-garmin/lock/ diff --git a/gradle.properties b/gradle.properties index 3977aa2..bf3a60e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,16 +2,17 @@ org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF dockerComposeStopContainers=true kotlinVersion=1.4.10 -kotlinApiVersion=1.4 -okhttp3Version=4.9.0 -radarJerseyVersion=0.4.1.1 -radarCommonsVersion=0.13.0 -radarSchemasVersion=0.5.14 -jacksonVersion=2.11.3 -slf4jVersion=1.7.30 -logbackVersion=1.2.3 -grizzlyVersion=2.4.4 -lzfseVersion=0.1.0 -kafkaVersion=2.5.1 -confluentVersion=5.5.2 -junitVersion=5.6.2 +okhttp3Version=4.9.+ +radarJerseyVersion=0.4.3 +radarCommonsVersion=0.13.+ +radarSchemasVersion=0.5.15 +radarOauthClientVersion=0.5.8 +jacksonVersion=2.11.+ +slf4jVersion=1.7.+ +logbackVersion=1.2.+ +grizzlyVersion=2.4.+ +lzfseVersion=0.1.+ +kafkaVersion=2.5.+ +confluentVersion=5.5.+ +junitVersion=5.+ +jedisVersion=3.3.+ diff --git a/gradle/dependency-locks/annotationProcessor.lockfile b/gradle/dependency-locks/annotationProcessor.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/annotationProcessor.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/apiDependenciesMetadata.lockfile b/gradle/dependency-locks/apiDependenciesMetadata.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/apiDependenciesMetadata.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/compileClasspath.lockfile b/gradle/dependency-locks/compileClasspath.lockfile new file mode 100644 index 0000000..584c258 --- /dev/null +++ b/gradle/dependency-locks/compileClasspath.lockfile @@ -0,0 +1,55 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.squareup.okhttp3:okhttp:4.7.2 +com.squareup.okio:okio:2.6.0 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.swagger:swagger-annotations:1.6.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.lz4:lz4-java:1.7.1 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/compileOnlyDependenciesMetadata.lockfile b/gradle/dependency-locks/compileOnlyDependenciesMetadata.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/compileOnlyDependenciesMetadata.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/implementationDependenciesMetadata.lockfile b/gradle/dependency-locks/implementationDependenciesMetadata.lockfile new file mode 100644 index 0000000..c964424 --- /dev/null +++ b/gradle/dependency-locks/implementationDependenciesMetadata.lockfile @@ -0,0 +1,56 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.squareup.okhttp3:okhttp:4.7.2 +com.squareup.okio:okio-metadata:2.6.0 +com.squareup.okio:okio:2.6.0 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.swagger:swagger-annotations:1.6.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.lz4:lz4-java:1.7.1 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/integrationTestAnnotationProcessor.lockfile b/gradle/dependency-locks/integrationTestAnnotationProcessor.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/integrationTestAnnotationProcessor.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/integrationTestApiDependenciesMetadata.lockfile b/gradle/dependency-locks/integrationTestApiDependenciesMetadata.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/integrationTestApiDependenciesMetadata.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/integrationTestCompileClasspath.lockfile b/gradle/dependency-locks/integrationTestCompileClasspath.lockfile new file mode 100644 index 0000000..82ba7a3 --- /dev/null +++ b/gradle/dependency-locks/integrationTestCompileClasspath.lockfile @@ -0,0 +1,70 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0 +com.squareup.okhttp3:mockwebserver:4.9.1 +com.squareup.okhttp3:okhttp:4.9.1 +com.squareup.okio:okio:2.8.0 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.swagger:swagger-annotations:1.6.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +junit:junit:4.13 +net.bytebuddy:byte-buddy-agent:1.9.0 +net.bytebuddy:byte-buddy:1.9.0 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.hamcrest:hamcrest-core:1.3 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit:junit-bom:5.7.0 +org.lz4:lz4-java:1.7.1 +org.mockito:mockito-core:2.23.0 +org.objenesis:objenesis:2.6 +org.opentest4j:opentest4j:1.2.0 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons-server:0.13.0 +org.radarbase:radar-commons-testing:0.13.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/integrationTestCompileOnlyDependenciesMetadata.lockfile b/gradle/dependency-locks/integrationTestCompileOnlyDependenciesMetadata.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/integrationTestCompileOnlyDependenciesMetadata.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/integrationTestImplementationDependenciesMetadata.lockfile b/gradle/dependency-locks/integrationTestImplementationDependenciesMetadata.lockfile new file mode 100644 index 0000000..d864292 --- /dev/null +++ b/gradle/dependency-locks/integrationTestImplementationDependenciesMetadata.lockfile @@ -0,0 +1,71 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0 +com.squareup.okhttp3:mockwebserver:4.9.1 +com.squareup.okhttp3:okhttp:4.9.1 +com.squareup.okio:okio-metadata:2.8.0 +com.squareup.okio:okio:2.8.0 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.swagger:swagger-annotations:1.6.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +junit:junit:4.13 +net.bytebuddy:byte-buddy-agent:1.9.0 +net.bytebuddy:byte-buddy:1.9.0 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.hamcrest:hamcrest-core:1.3 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit:junit-bom:5.7.0 +org.lz4:lz4-java:1.7.1 +org.mockito:mockito-core:2.23.0 +org.objenesis:objenesis:2.6 +org.opentest4j:opentest4j:1.2.0 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons-server:0.13.0 +org.radarbase:radar-commons-testing:0.13.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/integrationTestKotlinScriptDef.lockfile b/gradle/dependency-locks/integrationTestKotlinScriptDef.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/integrationTestKotlinScriptDef.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/integrationTestKotlinScriptDefExtensions.lockfile b/gradle/dependency-locks/integrationTestKotlinScriptDefExtensions.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/integrationTestKotlinScriptDefExtensions.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/integrationTestRuntimeClasspath.lockfile b/gradle/dependency-locks/integrationTestRuntimeClasspath.lockfile new file mode 100644 index 0000000..ba33be2 --- /dev/null +++ b/gradle/dependency-locks/integrationTestRuntimeClasspath.lockfile @@ -0,0 +1,120 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.2.3 +ch.qos.logback:logback-core:1.2.3 +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.11.1 +com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.11.1 +com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.11.3 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.github.spullara.mustache.java:compiler:0.9.6 +com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0 +com.opencsv:opencsv:5.2 +com.squareup.okhttp3:mockwebserver:4.9.1 +com.squareup.okhttp3:okhttp:4.10.0-RC1 +com.squareup.okio:okio:2.9.0 +com.sun.activation:jakarta.activation:1.2.2 +com.sun.istack:istack-commons-runtime:3.0.11 +commons-beanutils:commons-beanutils:1.9.4 +commons-codec:commons-codec:1.15 +commons-collections:commons-collections:3.2.2 +commons-logging:commons-logging:1.2 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.github.classgraph:classgraph:4.8.65 +io.swagger.core.v3:swagger-annotations:2.1.6 +io.swagger.core.v3:swagger-core:2.1.6 +io.swagger.core.v3:swagger-integration:2.1.6 +io.swagger.core.v3:swagger-jaxrs2:2.1.6 +io.swagger.core.v3:swagger-models:2.1.6 +io.swagger:swagger-annotations:1.6.2 +jakarta.activation:jakarta.activation-api:1.2.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 +javax.activation:activation:1.1.1 +javax.activation:javax.activation-api:1.2.0 +javax.xml.bind:jaxb-api:2.3.1 +junit:junit:4.13 +net.bytebuddy:byte-buddy-agent:1.9.0 +net.bytebuddy:byte-buddy:1.9.0 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-collections4:4.4 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-lang3:3.10 +org.apache.commons:commons-pool2:2.6.2 +org.apache.commons:commons-text:1.8 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.external:management-api:3.2.1 +org.glassfish.gmbal:gmbal:4.0.0 +org.glassfish.grizzly:grizzly-framework-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-framework:2.4.4 +org.glassfish.grizzly:grizzly-http-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server:2.4.4 +org.glassfish.grizzly:grizzly-http:2.4.4 +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jaxb:jaxb-core:2.3.0.1 +org.glassfish.jaxb:jaxb-runtime:2.3.3 +org.glassfish.jaxb:txw2:2.3.3 +org.glassfish.jersey.containers:jersey-container-grizzly2-http:2.33 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.ext:jersey-entity-filtering:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.glassfish.jersey.media:jersey-media-json-jackson:2.33 +org.glassfish.pfl:pfl-asm:4.0.1 +org.glassfish.pfl:pfl-basic-tools:4.0.1 +org.glassfish.pfl:pfl-basic:4.0.1 +org.glassfish.pfl:pfl-dynamic:4.0.1 +org.glassfish.pfl:pfl-tf-tools:4.0.1 +org.glassfish.pfl:pfl-tf:4.0.1 +org.hamcrest:hamcrest-core:1.3 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.jupiter:junit-jupiter-engine:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit.platform:junit-platform-engine:1.7.0 +org.junit:junit-bom:5.7.0 +org.lz4:lz4-java:1.7.1 +org.mockito:mockito-core:2.23.0 +org.objenesis:objenesis:2.6 +org.opentest4j:opentest4j:1.2.0 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons-server:0.13.0 +org.radarbase:radar-commons-testing:0.13.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/integrationTestRuntimeOnlyDependenciesMetadata.lockfile b/gradle/dependency-locks/integrationTestRuntimeOnlyDependenciesMetadata.lockfile new file mode 100644 index 0000000..2a28d2b --- /dev/null +++ b/gradle/dependency-locks/integrationTestRuntimeOnlyDependenciesMetadata.lockfile @@ -0,0 +1,27 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.2.3 +ch.qos.logback:logback-core:1.2.3 +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.external:management-api:3.2.1 +org.glassfish.gmbal:gmbal:4.0.0 +org.glassfish.grizzly:grizzly-framework-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-framework:2.4.4 +org.glassfish.grizzly:grizzly-http-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server:2.4.4 +org.glassfish.grizzly:grizzly-http:2.4.4 +org.glassfish.pfl:pfl-asm:4.0.1 +org.glassfish.pfl:pfl-basic-tools:4.0.1 +org.glassfish.pfl:pfl-basic:4.0.1 +org.glassfish.pfl:pfl-dynamic:4.0.1 +org.glassfish.pfl:pfl-tf-tools:4.0.1 +org.glassfish.pfl:pfl-tf:4.0.1 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.jupiter:junit-jupiter-engine:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit.platform:junit-platform-engine:1.7.0 +org.junit:junit-bom:5.7.0 +org.opentest4j:opentest4j:1.2.0 +org.slf4j:slf4j-api:1.7.25 diff --git a/gradle/dependency-locks/kotlinCompilerClasspath.lockfile b/gradle/dependency-locks/kotlinCompilerClasspath.lockfile new file mode 100644 index 0000000..20a87c9 --- /dev/null +++ b/gradle/dependency-locks/kotlinCompilerClasspath.lockfile @@ -0,0 +1,11 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +org.jetbrains.intellij.deps:trove4j:1.0.20181211 +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.10 +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.10 +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-script-runtime:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib:1.4.10 +org.jetbrains:annotations:13.0 diff --git a/gradle/dependency-locks/kotlinCompilerPluginClasspath.lockfile b/gradle/dependency-locks/kotlinCompilerPluginClasspath.lockfile new file mode 100644 index 0000000..7bc8ec6 --- /dev/null +++ b/gradle/dependency-locks/kotlinCompilerPluginClasspath.lockfile @@ -0,0 +1,12 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +org.jetbrains.kotlin:kotlin-script-runtime:1.4.10 +org.jetbrains.kotlin:kotlin-scripting-common:1.4.10 +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.4.10 +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.4.10 +org.jetbrains.kotlin:kotlin-scripting-jvm:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib:1.4.10 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7 +org.jetbrains:annotations:13.0 diff --git a/gradle/dependency-locks/kotlinKlibCommonizerClasspath.lockfile b/gradle/dependency-locks/kotlinKlibCommonizerClasspath.lockfile new file mode 100644 index 0000000..e444639 --- /dev/null +++ b/gradle/dependency-locks/kotlinKlibCommonizerClasspath.lockfile @@ -0,0 +1,12 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +org.jetbrains.intellij.deps:trove4j:1.0.20181211 +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.10 +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.10 +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.4.10 +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-script-runtime:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib:1.4.10 +org.jetbrains:annotations:13.0 diff --git a/gradle/dependency-locks/kotlinNativeCompilerPluginClasspath.lockfile b/gradle/dependency-locks/kotlinNativeCompilerPluginClasspath.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/kotlinNativeCompilerPluginClasspath.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/kotlinScriptDef.lockfile b/gradle/dependency-locks/kotlinScriptDef.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/kotlinScriptDef.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/kotlinScriptDefExtensions.lockfile b/gradle/dependency-locks/kotlinScriptDefExtensions.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/kotlinScriptDefExtensions.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/runtimeClasspath.lockfile b/gradle/dependency-locks/runtimeClasspath.lockfile new file mode 100644 index 0000000..5e243a9 --- /dev/null +++ b/gradle/dependency-locks/runtimeClasspath.lockfile @@ -0,0 +1,97 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.2.3 +ch.qos.logback:logback-core:1.2.3 +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.11.1 +com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.11.1 +com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.11.3 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.github.spullara.mustache.java:compiler:0.9.6 +com.squareup.okhttp3:okhttp:4.10.0-RC1 +com.squareup.okio:okio:2.9.0 +com.sun.activation:jakarta.activation:1.2.2 +com.sun.istack:istack-commons-runtime:3.0.11 +commons-codec:commons-codec:1.15 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.github.classgraph:classgraph:4.8.65 +io.swagger.core.v3:swagger-annotations:2.1.6 +io.swagger.core.v3:swagger-core:2.1.6 +io.swagger.core.v3:swagger-integration:2.1.6 +io.swagger.core.v3:swagger-jaxrs2:2.1.6 +io.swagger.core.v3:swagger-models:2.1.6 +io.swagger:swagger-annotations:1.6.2 +jakarta.activation:jakarta.activation-api:1.2.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 +javax.activation:activation:1.1.1 +javax.activation:javax.activation-api:1.2.0 +javax.xml.bind:jaxb-api:2.3.1 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-lang3:3.7 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.glassfish.external:management-api:3.2.1 +org.glassfish.gmbal:gmbal:4.0.0 +org.glassfish.grizzly:grizzly-framework-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-framework:2.4.4 +org.glassfish.grizzly:grizzly-http-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server:2.4.4 +org.glassfish.grizzly:grizzly-http:2.4.4 +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jaxb:jaxb-core:2.3.0.1 +org.glassfish.jaxb:jaxb-runtime:2.3.3 +org.glassfish.jaxb:txw2:2.3.3 +org.glassfish.jersey.containers:jersey-container-grizzly2-http:2.33 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.ext:jersey-entity-filtering:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.glassfish.jersey.media:jersey-media-json-jackson:2.33 +org.glassfish.pfl:pfl-asm:4.0.1 +org.glassfish.pfl:pfl-basic-tools:4.0.1 +org.glassfish.pfl:pfl-basic:4.0.1 +org.glassfish.pfl:pfl-dynamic:4.0.1 +org.glassfish.pfl:pfl-tf-tools:4.0.1 +org.glassfish.pfl:pfl-tf:4.0.1 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.lz4:lz4-java:1.7.1 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/runtimeOnlyDependenciesMetadata.lockfile b/gradle/dependency-locks/runtimeOnlyDependenciesMetadata.lockfile new file mode 100644 index 0000000..2e68e82 --- /dev/null +++ b/gradle/dependency-locks/runtimeOnlyDependenciesMetadata.lockfile @@ -0,0 +1,20 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.2.3 +ch.qos.logback:logback-core:1.2.3 +org.glassfish.external:management-api:3.2.1 +org.glassfish.gmbal:gmbal:4.0.0 +org.glassfish.grizzly:grizzly-framework-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-framework:2.4.4 +org.glassfish.grizzly:grizzly-http-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server:2.4.4 +org.glassfish.grizzly:grizzly-http:2.4.4 +org.glassfish.pfl:pfl-asm:4.0.1 +org.glassfish.pfl:pfl-basic-tools:4.0.1 +org.glassfish.pfl:pfl-basic:4.0.1 +org.glassfish.pfl:pfl-dynamic:4.0.1 +org.glassfish.pfl:pfl-tf-tools:4.0.1 +org.glassfish.pfl:pfl-tf:4.0.1 +org.slf4j:slf4j-api:1.7.25 diff --git a/gradle/dependency-locks/testAnnotationProcessor.lockfile b/gradle/dependency-locks/testAnnotationProcessor.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/testAnnotationProcessor.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/testApiDependenciesMetadata.lockfile b/gradle/dependency-locks/testApiDependenciesMetadata.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/testApiDependenciesMetadata.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/testCompileClasspath.lockfile b/gradle/dependency-locks/testCompileClasspath.lockfile new file mode 100644 index 0000000..78a895e --- /dev/null +++ b/gradle/dependency-locks/testCompileClasspath.lockfile @@ -0,0 +1,68 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0 +com.squareup.okhttp3:mockwebserver:4.9.1 +com.squareup.okhttp3:okhttp:4.9.1 +com.squareup.okio:okio:2.8.0 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.swagger:swagger-annotations:1.6.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +junit:junit:4.13 +net.bytebuddy:byte-buddy-agent:1.9.0 +net.bytebuddy:byte-buddy:1.9.0 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.hamcrest:hamcrest-core:1.3 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit:junit-bom:5.7.0 +org.lz4:lz4-java:1.7.1 +org.mockito:mockito-core:2.23.0 +org.objenesis:objenesis:2.6 +org.opentest4j:opentest4j:1.2.0 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/testCompileOnlyDependenciesMetadata.lockfile b/gradle/dependency-locks/testCompileOnlyDependenciesMetadata.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/testCompileOnlyDependenciesMetadata.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile b/gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile new file mode 100644 index 0000000..f437169 --- /dev/null +++ b/gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile @@ -0,0 +1,69 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0 +com.squareup.okhttp3:mockwebserver:4.9.1 +com.squareup.okhttp3:okhttp:4.9.1 +com.squareup.okio:okio-metadata:2.8.0 +com.squareup.okio:okio:2.8.0 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.swagger:swagger-annotations:1.6.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +junit:junit:4.13 +net.bytebuddy:byte-buddy-agent:1.9.0 +net.bytebuddy:byte-buddy:1.9.0 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.hamcrest:hamcrest-core:1.3 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit:junit-bom:5.7.0 +org.lz4:lz4-java:1.7.1 +org.mockito:mockito-core:2.23.0 +org.objenesis:objenesis:2.6 +org.opentest4j:opentest4j:1.2.0 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/testKotlinScriptDef.lockfile b/gradle/dependency-locks/testKotlinScriptDef.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/testKotlinScriptDef.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/testKotlinScriptDefExtensions.lockfile b/gradle/dependency-locks/testKotlinScriptDefExtensions.lockfile new file mode 100644 index 0000000..656c5db --- /dev/null +++ b/gradle/dependency-locks/testKotlinScriptDefExtensions.lockfile @@ -0,0 +1,3 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. diff --git a/gradle/dependency-locks/testRuntimeClasspath.lockfile b/gradle/dependency-locks/testRuntimeClasspath.lockfile new file mode 100644 index 0000000..413c448 --- /dev/null +++ b/gradle/dependency-locks/testRuntimeClasspath.lockfile @@ -0,0 +1,112 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.2.3 +ch.qos.logback:logback-core:1.2.3 +com.auth0:java-jwt:3.12.0 +com.fasterxml.jackson.core:jackson-annotations:2.11.4 +com.fasterxml.jackson.core:jackson-core:2.11.4 +com.fasterxml.jackson.core:jackson-databind:2.11.4 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.4 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.4 +com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.11.1 +com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.11.1 +com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.11.3 +com.fasterxml.jackson.module:jackson-module-kotlin:2.11.4 +com.github.luben:zstd-jni:1.4.4-7 +com.github.spullara.mustache.java:compiler:0.9.6 +com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0 +com.squareup.okhttp3:mockwebserver:4.9.1 +com.squareup.okhttp3:okhttp:4.10.0-RC1 +com.squareup.okio:okio:2.9.0 +com.sun.activation:jakarta.activation:1.2.2 +com.sun.istack:istack-commons-runtime:3.0.11 +commons-codec:commons-codec:1.15 +io.confluent:common-config:5.5.3 +io.confluent:common-utils:5.5.3 +io.confluent:kafka-avro-serializer:5.5.3 +io.confluent:kafka-schema-registry-client:5.5.3 +io.confluent:kafka-schema-serializer:5.5.3 +io.github.classgraph:classgraph:4.8.65 +io.swagger.core.v3:swagger-annotations:2.1.6 +io.swagger.core.v3:swagger-core:2.1.6 +io.swagger.core.v3:swagger-integration:2.1.6 +io.swagger.core.v3:swagger-jaxrs2:2.1.6 +io.swagger.core.v3:swagger-models:2.1.6 +io.swagger:swagger-annotations:1.6.2 +jakarta.activation:jakarta.activation-api:1.2.2 +jakarta.annotation:jakarta.annotation-api:1.3.5 +jakarta.validation:jakarta.validation-api:2.0.2 +jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 +jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 +javax.activation:activation:1.1.1 +javax.activation:javax.activation-api:1.2.0 +javax.xml.bind:jaxb-api:2.3.1 +junit:junit:4.13 +net.bytebuddy:byte-buddy-agent:1.9.0 +net.bytebuddy:byte-buddy:1.9.0 +org.apache.avro:avro:1.9.2 +org.apache.commons:commons-compress:1.19 +org.apache.commons:commons-lang3:3.7 +org.apache.commons:commons-pool2:2.6.2 +org.apache.kafka:kafka-clients:5.5.3-ccs +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.external:management-api:3.2.1 +org.glassfish.gmbal:gmbal:4.0.0 +org.glassfish.grizzly:grizzly-framework-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-framework:2.4.4 +org.glassfish.grizzly:grizzly-http-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server:2.4.4 +org.glassfish.grizzly:grizzly-http:2.4.4 +org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 +org.glassfish.hk2.external:jakarta.inject:2.6.1 +org.glassfish.hk2:hk2-api:2.6.1 +org.glassfish.hk2:hk2-locator:2.6.1 +org.glassfish.hk2:hk2-utils:2.6.1 +org.glassfish.hk2:osgi-resource-locator:1.0.3 +org.glassfish.jaxb:jaxb-core:2.3.0.1 +org.glassfish.jaxb:jaxb-runtime:2.3.3 +org.glassfish.jaxb:txw2:2.3.3 +org.glassfish.jersey.containers:jersey-container-grizzly2-http:2.33 +org.glassfish.jersey.core:jersey-client:2.33 +org.glassfish.jersey.core:jersey-common:2.33 +org.glassfish.jersey.core:jersey-server:2.33 +org.glassfish.jersey.ext:jersey-entity-filtering:2.33 +org.glassfish.jersey.inject:jersey-hk2:2.33 +org.glassfish.jersey.media:jersey-media-json-jackson:2.33 +org.glassfish.pfl:pfl-asm:4.0.1 +org.glassfish.pfl:pfl-basic-tools:4.0.1 +org.glassfish.pfl:pfl-basic:4.0.1 +org.glassfish.pfl:pfl-dynamic:4.0.1 +org.glassfish.pfl:pfl-tf-tools:4.0.1 +org.glassfish.pfl:pfl-tf:4.0.1 +org.hamcrest:hamcrest-core:1.3 +org.javassist:javassist:3.25.0-GA +org.jetbrains.kotlin:kotlin-reflect:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21 +org.jetbrains.kotlin:kotlin-stdlib:1.4.21 +org.jetbrains:annotations:13.0 +org.json:json:20200518 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.jupiter:junit-jupiter-engine:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit.platform:junit-platform-engine:1.7.0 +org.junit:junit-bom:5.7.0 +org.lz4:lz4-java:1.7.1 +org.mockito:mockito-core:2.23.0 +org.objenesis:objenesis:2.6 +org.opentest4j:opentest4j:1.2.0 +org.radarbase:lzfse-decode:0.1.0 +org.radarbase:radar-commons:0.13.0 +org.radarbase:radar-jersey:0.4.3 +org.radarcns:oauth-client-util:0.5.8 +org.radarcns:radar-auth:0.6.3 +org.radarcns:radar-schemas-commons:0.5.15 +org.slf4j:slf4j-api:1.7.30 +org.xerial.snappy:snappy-java:1.1.7.3 +org.yaml:snakeyaml:1.26 +redis.clients:jedis:3.3.0 diff --git a/gradle/dependency-locks/testRuntimeOnlyDependenciesMetadata.lockfile b/gradle/dependency-locks/testRuntimeOnlyDependenciesMetadata.lockfile new file mode 100644 index 0000000..2a28d2b --- /dev/null +++ b/gradle/dependency-locks/testRuntimeOnlyDependenciesMetadata.lockfile @@ -0,0 +1,27 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.2.3 +ch.qos.logback:logback-core:1.2.3 +org.apiguardian:apiguardian-api:1.1.0 +org.glassfish.external:management-api:3.2.1 +org.glassfish.gmbal:gmbal:4.0.0 +org.glassfish.grizzly:grizzly-framework-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-framework:2.4.4 +org.glassfish.grizzly:grizzly-http-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server-monitoring:2.4.4 +org.glassfish.grizzly:grizzly-http-server:2.4.4 +org.glassfish.grizzly:grizzly-http:2.4.4 +org.glassfish.pfl:pfl-asm:4.0.1 +org.glassfish.pfl:pfl-basic-tools:4.0.1 +org.glassfish.pfl:pfl-basic:4.0.1 +org.glassfish.pfl:pfl-dynamic:4.0.1 +org.glassfish.pfl:pfl-tf-tools:4.0.1 +org.glassfish.pfl:pfl-tf:4.0.1 +org.junit.jupiter:junit-jupiter-api:5.7.0 +org.junit.jupiter:junit-jupiter-engine:5.7.0 +org.junit.platform:junit-platform-commons:1.7.0 +org.junit.platform:junit-platform-engine:1.7.0 +org.junit:junit-bom:5.7.0 +org.opentest4j:opentest4j:1.2.0 +org.slf4j:slf4j-api:1.7.25 diff --git a/settings.gradle.kts b/settings.gradle.kts index 1931a54..d32145b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "radar-gateway" +rootProject.name = "radar-push-endpoint" pluginManagement { val kotlinVersion: String by settings diff --git a/src/main/kotlin/org/radarbase/gateway/Config.kt b/src/main/kotlin/org/radarbase/gateway/Config.kt index b62d47a..8f05f9a 100644 --- a/src/main/kotlin/org/radarbase/gateway/Config.kt +++ b/src/main/kotlin/org/radarbase/gateway/Config.kt @@ -3,21 +3,22 @@ package org.radarbase.gateway import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.MAX_SCHEMAS_PER_SUBJECT_CONFIG import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG import org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG -import org.radarbase.gateway.inject.ManagementPortalEnhancerFactory +import org.radarbase.gateway.inject.PushIntegrationEnhancerFactory import org.radarbase.jersey.config.EnhancerFactory +import org.radarbase.push.integration.garmin.user.GarminUserRepository import java.net.URI -import java.nio.file.Files -import java.nio.file.Path +import java.time.Instant data class Config( - /** Radar-jersey resource configuration class. */ - val resourceConfig: Class = ManagementPortalEnhancerFactory::class.java, - /** Authorization configurations. */ - val auth: AuthConfig = AuthConfig(), - /** Kafka configurations. */ - val kafka: KafkaConfig = KafkaConfig(), - /** Server configurations. */ - val server: GatewayServerConfig = GatewayServerConfig()) { + /** Radar-jersey resource configuration class. */ + val resourceConfig: Class = PushIntegrationEnhancerFactory::class.java, + /** Kafka configurations. */ + val kafka: KafkaConfig = KafkaConfig(), + /** Server configurations. */ + val server: GatewayServerConfig = GatewayServerConfig(), + /** Push integration configs **/ + val pushIntegration: PushIntegrationConfig = PushIntegrationConfig() +) { /** Fill in some default values for the configuration. */ fun withDefaults(): Config = copy(kafka = kafka.withDefaults()) @@ -27,44 +28,115 @@ data class Config( */ fun validate() { kafka.validate() - auth.validate() + pushIntegration.validate() + } +} + +data class PushIntegrationConfig( + val garmin: GarminConfig = GarminConfig() +) { + fun validate() { + garmin.validate() + // Add more validations as services are added + } +} + +data class GarminConfig( + val enabled: Boolean = false, + val consumerKey: String = "", + val consumerSecret: String = "", + val backfill: BackfillConfig = BackfillConfig(), + val userRepositoryClass: String = + "org.radarbase.push.integration.garmin.user.GarminServiceUserRepository", + val userRepositoryUrl: String = "http://localhost:8080/", + val userRepositoryClientId: String = "radar_pushendpoint", + val userRepositoryClientSecret: String = "", + val userRepositoryTokenUrl: String = "http://localhost:8080/token/", + val dailiesTopicName: String = "push_garmin_daily_summary", + val activitiesTopicName: String = "push_garmin_activity_summary", + val activityDetailsTopicName: String = "push_garmin_activity_detail", + val epochSummariesTopicName: String = "push_garmin_epoch_summary", + val sleepsTopicName: String = "push_garmin_sleep_summary", + val bodyCompositionsTopicName: String = "push_garmin_body_composition", + val stressTopicName: String = "push_garmin_stress_detail_summary", + val userMetricsTopicName: String = "push_garmin_user_metrics", + val moveIQTopicName: String = "push_garmin_move_iq_summary", + val pulseOXTopicName: String = "push_garmin_pulse_ox", + val respirationTopicName: String = "push_garmin_respiration", + val activityDetailsSampleTopicName: String = "push_garmin_activity_detail_sample", + val bodyBatterySampleTopicName: String = "push_garmin_body_battery_sample", + val heartRateSampleConverter: String = "push_garmin_heart_rate_sample", + val sleepLevelTopicName: String = "push_garmin_sleep_level", + val stressLevelTopicName: String = "push_garmin_stress_level" +) { + val userRepository: Class<*> = Class.forName(userRepositoryClass) + + fun validate() { + if (enabled) { + check(GarminUserRepository::class.java.isAssignableFrom(userRepository)) { + "$userRepositoryClass is not valid. Please specify a class that is a subclass of" + + " `org.radarbase.push.integration.garmin.user.GarminUserRepository`" + } + } } } +data class BackfillConfig( + val enabled: Boolean = false, + val redis: RedisConfig = RedisConfig(), + val maxThreads: Int = 4, + val defaultEndDate: Instant = Instant.MAX, + val userBackfill: List = emptyList() +) + +data class RedisConfig( + val uri: String = "redis://localhost:6379", + val lockPrefix: String = "radar-push-garmin/lock" +) + +data class UserBackfillConfig( + val userId: String, + val startDate: Instant, + val endDate: Instant +) + data class GatewayServerConfig( - /** Base URL to serve data with. This will determine the base path and the port. */ - val baseUri: URI = URI.create("http://0.0.0.0:8090/radar-gateway/"), - /** Maximum number of simultaneous requests. */ - val maxRequests: Int = 200, - /** - * Maximum request content length, also when decompressed. - * This protects against memory overflows. - */ - val maxRequestSize: Long = 24*1024*1024, - /** - * Whether JMX should be enabled. Disable if not needed, for higher performance. - */ - val isJmxEnabled: Boolean = true) + /** Base URL to serve data with. This will determine the base path and the port. */ + val baseUri: URI = URI.create("http://0.0.0.0:8090/push/integrations/"), + /** Maximum number of simultaneous requests. */ + val maxRequests: Int = 200, + /** + * Maximum request content length, also when decompressed. + * This protects against memory overflows. + */ + val maxRequestSize: Long = 24 * 1024 * 1024, + /** + * Whether JMX should be enabled. Disable if not needed, for higher performance. + */ + val isJmxEnabled: Boolean = true +) data class KafkaConfig( - /** Number of Kafka brokers to keep in a pool for reuse in multiple requests. */ - val poolSize: Int = 20, - /** Kafka producer settings. Read from https://kafka.apache.org/documentation/#producerconfigs. */ - val producer: Map = mapOf(), - /** Kafka Admin Client settings. Read from https://kafka.apache.org/documentation/#adminclientconfigs. */ - val admin: Map = mapOf(), - /** Kafka serialization settings, used in KafkaAvroSerializer. Read from [io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig]. */ - val serialization: Map = mapOf()) { + /** Number of Kafka brokers to keep in a pool for reuse in multiple requests. */ + val poolSize: Int = 20, + /** Kafka producer settings. Read from https://kafka.apache.org/documentation/#producerconfigs. */ + val producer: Map = mapOf(), + /** Kafka Admin Client settings. Read from https://kafka.apache.org/documentation/#adminclientconfigs. */ + val admin: Map = mapOf(), + /** Kafka serialization settings, used in KafkaAvroSerializer. Read from [io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig]. */ + val serialization: Map = mapOf() +) { fun withDefaults(): KafkaConfig = copy( - producer = producerDefaults + producer, - admin = mutableMapOf().apply { - producer[BOOTSTRAP_SERVERS_CONFIG]?.let { - this[BOOTSTRAP_SERVERS_CONFIG] = it - } - this += adminDefaults - this += admin - }, - serialization = serializationDefaults + serialization) + producer = producerDefaults + producer, + admin = mutableMapOf().apply { + producer[BOOTSTRAP_SERVERS_CONFIG]?.let { + this[BOOTSTRAP_SERVERS_CONFIG] = it + } + this += adminDefaults + this += admin + }, + serialization = serializationDefaults + serialization + ) fun validate() { check(producer[BOOTSTRAP_SERVERS_CONFIG] is String) { "$BOOTSTRAP_SERVERS_CONFIG missing in kafka: producer: {} configuration" } @@ -77,72 +149,21 @@ data class KafkaConfig( companion object { private val producerDefaults = mapOf( - "request.timeout.ms" to 3000, - "max.block.ms" to 6000, - "linger.ms" to 10, - "retries" to 5, - "acks" to "all", - "delivery.timeout.ms" to 6000) + "request.timeout.ms" to 3000, + "max.block.ms" to 6000, + "linger.ms" to 10, + "retries" to 5, + "acks" to "all", + "delivery.timeout.ms" to 6000 + ) private val adminDefaults = mapOf( - "default.api.timeout.ms" to 6000, - "request.timeout.ms" to 3000, - "retries" to 5) + "default.api.timeout.ms" to 6000, + "request.timeout.ms" to 3000, + "retries" to 5 + ) private val serializationDefaults = mapOf( - MAX_SCHEMAS_PER_SUBJECT_CONFIG to 10_000) - } -} - -data class AuthConfig( - /** OAuth 2.0 resource name. */ - val resourceName: String = "res_gateway", - /** - * Whether to check that the user that submits data has the reported source ID registered - * in the ManagementPortal. - */ - val checkSourceId: Boolean = true, - /** OAuth 2.0 token issuer. If null, this is not checked. */ - val issuer: String? = null, - /** - * ManagementPortal URL. If available, this is used to read the public key from - * ManagementPortal directly. This is the recommended method of getting public key. - */ - val managementPortalUrl: String? = null, - /** Key store for checking the digital signature of OAuth 2.0 JWTs. */ - val keyStore: KeyStoreConfig = KeyStoreConfig(), - /** Public keys for checking the digital signature of OAuth 2.0 JWTs. */ - val publicKeys: KeyConfig = KeyConfig() -) { - fun validate() { - keyStore.validate() - check(managementPortalUrl != null || keyStore.isConfigured || publicKeys.isConfigured) { - "At least one of auth.keyStore, auth.publicKeys or auth.managementPortalUrl must be configured" - } + MAX_SCHEMAS_PER_SUBJECT_CONFIG to 10_000 + ) } } - -data class KeyStoreConfig( - /** Path to the p12 key store. */ - val path: Path? = null, - /** Key alias in the key store. */ - val alias: String? = null, - /** Password of the key store. */ - val password: String? = null) { - fun validate() { - if (path != null) { - check(Files.exists(path)) { "KeyStore configured in auth.keyStore.path does not exist" } - checkNotNull(alias) { "KeyStore is configured without auth.keyStore.alias" } - checkNotNull(password) { "KeyStore is configured without auth.keyStore.password" } - } - } - - val isConfigured: Boolean = path != null -} - -data class KeyConfig( - /** List of ECDSA public key signatures in PEM format. */ - val ecdsa: List? = null, - /** List of RSA public key signatures in PEM format. */ - val rsa: List? = null) { - val isConfigured: Boolean = !ecdsa.isNullOrEmpty() || !rsa.isNullOrEmpty() -} diff --git a/src/main/kotlin/org/radarbase/gateway/filter/KafkaTopicFilter.kt b/src/main/kotlin/org/radarbase/gateway/filter/KafkaTopicFilter.kt deleted file mode 100644 index 2499341..0000000 --- a/src/main/kotlin/org/radarbase/gateway/filter/KafkaTopicFilter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.radarbase.gateway.filter - -import org.radarbase.gateway.inject.ProcessAvro -import org.radarbase.gateway.kafka.KafkaAdminService -import org.radarbase.jersey.exception.HttpApplicationException -import org.radarbase.jersey.exception.HttpNotFoundException -import org.slf4j.LoggerFactory -import java.util.concurrent.ExecutionException -import javax.annotation.Priority -import javax.inject.Singleton -import javax.ws.rs.Priorities -import javax.ws.rs.container.ContainerRequestContext -import javax.ws.rs.container.ContainerRequestFilter -import javax.ws.rs.core.Context -import javax.ws.rs.core.Response -import javax.ws.rs.ext.Provider - -/** - * Asserts that data is only submitted to Kafka topics that already exist. - */ -@Provider -@ProcessAvro -@Priority(Priorities.USER) -@Singleton -class KafkaTopicFilter constructor( - @Context private val kafkaAdmin: KafkaAdminService -) : ContainerRequestFilter { - override fun filter(requestContext: ContainerRequestContext) { - val topic = requestContext.uriInfo.pathParameters.getFirst("topic_name") - - try { - // topic exists or exists after an update - if (!kafkaAdmin.containsTopic(topic)) { - throw HttpNotFoundException("not_found", "Topic $topic not present in Kafka.") - } - } catch (ex: ExecutionException) { - logger.error("Failed to list topics", ex) - throw HttpApplicationException(Response.Status.SERVICE_UNAVAILABLE, "Cannot complete topic list operation") - } - } - - companion object { - private val logger = LoggerFactory.getLogger(KafkaTopicFilter::class.java) - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/inject/EcdsaJwtEnhancerFactory.kt b/src/main/kotlin/org/radarbase/gateway/inject/EcdsaJwtEnhancerFactory.kt deleted file mode 100644 index 8ab1f6b..0000000 --- a/src/main/kotlin/org/radarbase/gateway/inject/EcdsaJwtEnhancerFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.radarbase.gateway.inject - -import org.radarbase.gateway.Config -import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.auth.MPConfig -import org.radarbase.jersey.config.* - - -/** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ -class EcdsaJwtEnhancerFactory(private val config: Config) : EnhancerFactory { - override fun createEnhancers(): List { - val authConfig = AuthConfig( - managementPortal = MPConfig(url = config.auth.managementPortalUrl), - jwtResourceName = config.auth.resourceName, - jwtIssuer = config.auth.issuer, - jwtECPublicKeys = config.auth.publicKeys.ecdsa, - jwtRSAPublicKeys = config.auth.publicKeys.rsa, - jwtKeystoreAlias = config.auth.keyStore.alias, - jwtKeystorePassword = config.auth.keyStore.password, - jwtKeystorePath = config.auth.keyStore.path.toString()) - return listOf( - GatewayResourceEnhancer(config), - ConfigLoader.Enhancers.radar(authConfig), - ConfigLoader.Enhancers.ecdsa, - ConfigLoader.Enhancers.httpException, - ConfigLoader.Enhancers.generalException) - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/inject/GatewayResourceEnhancer.kt b/src/main/kotlin/org/radarbase/gateway/inject/GatewayResourceEnhancer.kt index 6cd1c1b..b70724f 100644 --- a/src/main/kotlin/org/radarbase/gateway/inject/GatewayResourceEnhancer.kt +++ b/src/main/kotlin/org/radarbase/gateway/inject/GatewayResourceEnhancer.kt @@ -1,59 +1,34 @@ package org.radarbase.gateway.inject import org.glassfish.jersey.internal.inject.AbstractBinder -import org.glassfish.jersey.internal.inject.PerThread import org.glassfish.jersey.message.DeflateEncoder import org.glassfish.jersey.message.GZipEncoder import org.glassfish.jersey.server.filter.EncodingFilter import org.radarbase.gateway.Config -import org.radarbase.gateway.io.AvroProcessor -import org.radarbase.gateway.io.AvroProcessorFactory -import org.radarbase.gateway.io.BinaryToAvroConverter -import org.radarbase.gateway.io.LzfseEncoder import org.radarbase.gateway.kafka.* -import org.radarbase.gateway.service.SchedulingService -import org.radarbase.gateway.service.SchedulingServiceFactory import org.radarbase.jersey.config.ConfigLoader import org.radarbase.jersey.config.JerseyResourceEnhancer import org.radarbase.jersey.service.HealthService -import org.radarbase.jersey.service.ProjectService import org.radarbase.producer.rest.SchemaRetriever import javax.inject.Singleton class GatewayResourceEnhancer(private val config: Config): JerseyResourceEnhancer { - override val packages: Array = arrayOf( - "org.radarbase.gateway.filter", - "org.radarbase.gateway.io", - "org.radarbase.gateway.resource") override val classes: Array> = arrayOf( EncodingFilter::class.java, GZipEncoder::class.java, DeflateEncoder::class.java, - LzfseEncoder::class.java, ConfigLoader.Filters.logResponse) override fun AbstractBinder.enhance() { bind(config) .to(Config::class.java) - bindFactory(AvroProcessorFactory::class.java) - .to(AvroProcessor::class.java) - .`in`(Singleton::class.java) - - bind(BinaryToAvroConverter::class.java) - .to(BinaryToAvroConverter::class.java) - .`in`(PerThread::class.java) - // Bind factories. bindFactory(SchemaRetrieverFactory::class.java) .to(SchemaRetriever::class.java) .`in`(Singleton::class.java) - bindFactory(SchedulingServiceFactory::class.java) - .to(SchedulingService::class.java) - .`in`(Singleton::class.java) - bindFactory(ProducerPoolFactory::class.java) .to(ProducerPool::class.java) .`in`(Singleton::class.java) @@ -62,18 +37,9 @@ class GatewayResourceEnhancer(private val config: Config): JerseyResourceEnhance .to(KafkaAdminService::class.java) .`in`(Singleton::class.java) - bind(UnverifiedProjectService::class.java) - .to(ProjectService::class.java) - .`in`(Singleton::class.java) - bind(KafkaHealthMetric::class.java) .named("kafka") .to(HealthService.Metric::class.java) .`in`(Singleton::class.java) } - - /** Project service without validation of the project's existence. */ - class UnverifiedProjectService: ProjectService { - override fun ensureProject(projectId: String) = Unit - } } diff --git a/src/main/kotlin/org/radarbase/gateway/inject/ManagementPortalEnhancerFactory.kt b/src/main/kotlin/org/radarbase/gateway/inject/ManagementPortalEnhancerFactory.kt deleted file mode 100644 index e525b99..0000000 --- a/src/main/kotlin/org/radarbase/gateway/inject/ManagementPortalEnhancerFactory.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.radarbase.gateway.inject - -import org.radarbase.gateway.Config -import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.auth.MPConfig -import org.radarbase.jersey.config.* - -/** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ -class ManagementPortalEnhancerFactory(private val config: Config) : EnhancerFactory { - override fun createEnhancers(): List { - val authConfig = AuthConfig( - managementPortal = MPConfig( - url = config.auth.managementPortalUrl), - jwtResourceName = config.auth.resourceName, - jwtIssuer = config.auth.issuer) - return listOf( - GatewayResourceEnhancer(config), - ConfigLoader.Enhancers.radar(authConfig), - ConfigLoader.Enhancers.managementPortal(authConfig), - ConfigLoader.Enhancers.health, - ConfigLoader.Enhancers.httpException, - ConfigLoader.Enhancers.generalException) - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/inject/ProcessAvro.kt b/src/main/kotlin/org/radarbase/gateway/inject/ProcessAvro.kt deleted file mode 100644 index 6fc02a2..0000000 --- a/src/main/kotlin/org/radarbase/gateway/inject/ProcessAvro.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.radarbase.gateway.inject - -import javax.ws.rs.NameBinding - -/** Tag for additional processing required for incoming Avro data. */ -@NameBinding -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) -@Retention(AnnotationRetention.RUNTIME) -annotation class ProcessAvro diff --git a/src/main/kotlin/org/radarbase/gateway/inject/PushIntegrationEnhancerFactory.kt b/src/main/kotlin/org/radarbase/gateway/inject/PushIntegrationEnhancerFactory.kt new file mode 100644 index 0000000..f44bd3e --- /dev/null +++ b/src/main/kotlin/org/radarbase/gateway/inject/PushIntegrationEnhancerFactory.kt @@ -0,0 +1,31 @@ +package org.radarbase.gateway.inject + +import okhttp3.internal.toImmutableList +import org.radarbase.gateway.Config +import org.radarbase.jersey.config.ConfigLoader +import org.radarbase.jersey.config.EnhancerFactory +import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.push.integration.GarminPushIntegrationResourceEnhancer +import org.radarbase.push.integration.common.inject.PushIntegrationResourceEnhancer + +class PushIntegrationEnhancerFactory(private val config: Config) : EnhancerFactory { + + override fun createEnhancers(): List { + + val enhancersList = mutableListOf( + GatewayResourceEnhancer(config), + ConfigLoader.Enhancers.health, + ConfigLoader.Enhancers.httpException, + ConfigLoader.Enhancers.generalException, + RadarResourceEnhancer(), + PushIntegrationResourceEnhancer() + ) + + if (config.pushIntegration.garmin.enabled) { + enhancersList.add(GarminPushIntegrationResourceEnhancer(config)) + } + // Add more enhancers as services are added + + return enhancersList.toImmutableList() + } +} diff --git a/src/main/kotlin/org/radarbase/gateway/inject/RadarResourceEnhancer.kt b/src/main/kotlin/org/radarbase/gateway/inject/RadarResourceEnhancer.kt new file mode 100644 index 0000000..140943f --- /dev/null +++ b/src/main/kotlin/org/radarbase/gateway/inject/RadarResourceEnhancer.kt @@ -0,0 +1,49 @@ +package org.radarbase.gateway.inject + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule +import okhttp3.OkHttpClient +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.glassfish.jersey.server.ResourceConfig +import org.radarbase.jersey.auth.filter.AuthenticationFilter +import org.radarbase.jersey.auth.filter.AuthorizationFeature +import org.radarbase.jersey.config.JerseyResourceEnhancer +import java.util.concurrent.TimeUnit +import javax.ws.rs.ext.ContextResolver + +class RadarResourceEnhancer: JerseyResourceEnhancer { + + var mapper: ObjectMapper = ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(JavaTimeModule()) + .registerModule(KotlinModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + var client: OkHttpClient = OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + override val classes = arrayOf( + AuthenticationFilter::class.java, + AuthorizationFeature::class.java) + + override fun ResourceConfig.enhance() { + register(ContextResolver { mapper }) + } + + override fun AbstractBinder.enhance() { + + bind(client) + .to(OkHttpClient::class.java) + + bind(mapper) + .to(ObjectMapper::class.java) + } +} diff --git a/src/main/kotlin/org/radarbase/gateway/io/AvroJsonReader.kt b/src/main/kotlin/org/radarbase/gateway/io/AvroJsonReader.kt deleted file mode 100644 index 19af4c1..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/AvroJsonReader.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.radarbase.gateway.io - -import com.fasterxml.jackson.core.JsonParseException -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import org.radarbase.jersey.exception.HttpBadRequestException -import java.io.InputStream -import java.lang.reflect.Type -import javax.ws.rs.Consumes -import javax.ws.rs.core.Context -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.MultivaluedMap -import javax.ws.rs.ext.MessageBodyReader -import javax.ws.rs.ext.Provider - -/** Reads in JSON Avro. */ -@Provider -@Consumes("application/vnd.kafka.avro.v1+json", "application/vnd.kafka.avro.v2+json") -class AvroJsonReader( - @Context objectMapper: ObjectMapper, -) : MessageBodyReader { - private val objectFactory = objectMapper.factory - - override fun readFrom(type: Class?, genericType: Type?, - annotations: Array?, mediaType: MediaType?, - httpHeaders: MultivaluedMap?, entityStream: InputStream?): JsonNode { - - val parser = objectFactory.createParser(entityStream) - return try { - parser.readValueAsTree() - } catch (ex: JsonParseException) { - throw HttpBadRequestException("malformed_json", ex.message ?: ex.toString()) - } ?: throw HttpBadRequestException("malformed_json", "No content given") - } - - override fun isReadable(type: Class<*>?, genericType: Type?, - annotations: Array?, mediaType: MediaType?) = true -} diff --git a/src/main/kotlin/org/radarbase/gateway/io/AvroProcessingResult.kt b/src/main/kotlin/org/radarbase/gateway/io/AvroProcessingResult.kt deleted file mode 100644 index ee768a2..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/AvroProcessingResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.radarbase.gateway.io - -import org.apache.avro.generic.GenericRecord - -data class AvroProcessingResult( - val keySchemaId: Int, - val valueSchemaId: Int, - val records: List>) diff --git a/src/main/kotlin/org/radarbase/gateway/io/AvroProcessor.kt b/src/main/kotlin/org/radarbase/gateway/io/AvroProcessor.kt deleted file mode 100644 index 753780b..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/AvroProcessor.kt +++ /dev/null @@ -1,386 +0,0 @@ -package org.radarbase.gateway.io - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.ObjectNode -import org.apache.avro.Schema -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericFixed -import org.apache.avro.generic.GenericRecord -import org.apache.avro.generic.GenericRecordBuilder -import org.apache.avro.jsonDefaultValue -import org.radarbase.gateway.Config -import org.radarbase.gateway.service.SchedulingService -import org.radarbase.jersey.auth.Auth -import org.radarbase.jersey.exception.HttpApplicationException -import org.radarbase.jersey.exception.HttpBadGatewayException -import org.radarbase.jersey.exception.HttpInvalidContentException -import org.radarbase.jersey.util.CacheConfig -import org.radarbase.jersey.util.CachedValue -import org.radarbase.producer.rest.AvroDataMapper -import org.radarbase.producer.rest.AvroDataMapperFactory -import org.radarbase.producer.rest.RestException -import org.radarbase.producer.rest.SchemaRetriever -import org.radarcns.auth.authorization.Permission -import java.io.Closeable -import java.io.IOException -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets -import java.text.ParseException -import java.time.Duration -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap - -/** - * Reads messages as semantically valid and authenticated Avro for the RADAR platform. Amends - * unfilled security metadata as necessary. - */ -class AvroProcessor( - private val config: Config, - private val auth: Auth, - private val schemaRetriever: SchemaRetriever, - private val objectMapper: ObjectMapper, - schedulingService: SchedulingService -): Closeable { - private val idMapping: ConcurrentMap, CachedValue> = ConcurrentHashMap() - private val schemaMapping: ConcurrentMap, CachedValue> = ConcurrentHashMap() - private val cleanReference: SchedulingService.RepeatReference - - init { - // Clean stale caches regularly. This reduces memory use for caches that aren't being used. - cleanReference = schedulingService.repeat(SCHEMA_CLEAN, SCHEMA_CLEAN) { - idMapping.values.removeIf { it.isStale } - schemaMapping.values.removeIf { it.isStale } - } - } - - /** - * Validates given data with given access token and returns a modified output array. - * The Avro content validation consists of testing whether both keys and values are being sent, - * both with Avro schema. The authentication validation checks that all records contain a key - * with a project ID, user ID and source ID that is also listed in the access token. If no - * project ID is given in the key, it will be set to the first project ID where the user has - * role {@code ROLE_PARTICIPANT}. - * - * @throws ParseException if the data does not contain valid JSON - * @throws HttpInvalidContentException if the data does not contain semantically correct Kafka Avro data. - * @throws IOException if the data cannot be read - */ - fun process(topic: String, root: JsonNode): AvroProcessingResult { - if (!root.isObject) { - throw HttpInvalidContentException("Expecting JSON object in payload") - } - if (root["key_schema_id"].isMissing && root["key_schema"].isMissing) { - throw HttpInvalidContentException("Missing key schema") - } - if (root["value_schema_id"].isMissing && root["value_schema"].isMissing) { - throw HttpInvalidContentException("Missing value schema") - } - - val keyMapping = schemaMapping(topic, false, root["key_schema_id"], root["key_schema"]) - val valueMapping = schemaMapping(topic, true, root["value_schema_id"], root["value_schema"]) - - val records = root["records"] ?: throw HttpInvalidContentException("Missing records") - return AvroProcessingResult( - keyMapping.targetSchemaId, - valueMapping.targetSchemaId, - processRecords(records, keyMapping, valueMapping)) - } - - - @Throws(IOException::class) - private fun processRecords(records: JsonNode, keyMapping: JsonToObjectMapping, valueMapping: JsonToObjectMapping): List> { - if (!records.isArray) { - throw HttpInvalidContentException("Records should be an array") - } - - return records.map { record -> - val key = record["key"] ?: throw HttpInvalidContentException("Missing key field in record") - val value = record["value"] ?: throw HttpInvalidContentException("Missing value field in record") - Pair(processKey(key, keyMapping), processValue(value, valueMapping)) - } - } - - /** Parse single record key. */ - @Throws(IOException::class) - private fun processKey(key: JsonNode, keyMapping: JsonToObjectMapping): GenericRecord { - if (!key.isObject) { - throw HttpInvalidContentException("Field key must be a JSON object") - } - - val projectId = key["projectId"]?.let { project -> - if (project.isNull) { - // no project ID was provided, fill it in for the sender - val newProject = objectMapper.createObjectNode() - newProject.put("string", auth.defaultProject) - (key as ObjectNode).set("projectId", newProject) as JsonNode? - auth.defaultProject - } else { - // project ID was provided, it should match one of the validated project IDs. - project["string"]?.asText() ?: throw HttpInvalidContentException( - "Project ID should be wrapped in string union type") - } - } ?: auth.defaultProject - - if (config.auth.checkSourceId) { - auth.checkPermissionOnSource(Permission.MEASUREMENT_CREATE, - projectId, key["userId"]?.asText(), key["sourceId"]?.asText()) - } else { - auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, - projectId, key["userId"]?.asText()) - } - - return keyMapping.jsonToAvro(key) - } - - - /** Parse single record key. */ - @Throws(IOException::class) - private fun processValue(value: JsonNode, valueMapping: JsonToObjectMapping): GenericRecord { - if (!value.isObject) { - throw HttpInvalidContentException("Field value must be a JSON object") - } - - return valueMapping.jsonToAvro(value) - } - - private fun createMapping(topic: String, ofValue: Boolean, sourceSchema: Schema): JsonToObjectMapping { - val latestSchema = schemaRetriever.getBySubjectAndVersion(topic, ofValue, -1) - val schemaMapper = AvroDataMapperFactory.get().createMapper(sourceSchema, latestSchema.schema, null) - return JsonToObjectMapping(sourceSchema, latestSchema.schema, latestSchema.id, schemaMapper) - } - - private fun schemaMapping(topic: String, ofValue: Boolean, id: JsonNode?, schema: JsonNode?): JsonToObjectMapping { - val subject = "$topic-${if (ofValue) "value" else "key"}" - return when { - id?.isNumber == true -> { - idMapping.computeIfAbsent(Pair(subject, id.asInt())) { - CachedValue(cacheConfig, { - val parsedSchema = try { - schemaRetriever.getBySubjectAndId(topic, ofValue, id.asInt()) - } catch (ex: RestException) { - if (ex.statusCode == 404) { - throw HttpApplicationException(422, "schema_not_found", "Schema ID not found in subject") - } else { - throw HttpBadGatewayException("cannot get data from schema registry: ${ex.javaClass.simpleName}") - } - } - createMapping(topic, ofValue, parsedSchema.schema) - }) - }.get() - } - schema?.isTextual == true -> { - schemaMapping.computeIfAbsent(Pair(subject, schema.asText())) { - CachedValue(cacheConfig, { - try { - val parsedSchema = Schema.Parser().parse(schema.textValue()) - createMapping(topic, ofValue, parsedSchema) - } catch (ex: Exception) { - throw throw HttpApplicationException(422, "schema_not_found", "Schema ID not found in subject") - } - }) - }.get() - } - else -> throw HttpInvalidContentException("No schema provided") - } - } - - private fun JsonNode.toAvro(to: Schema, defaultVal: JsonNode? = null): Any? { - val useNode = if (isNull) { - if (to.type == Schema.Type.NULL) return null - if (defaultVal.isMissing) throw HttpInvalidContentException("No value given to field without default.") - return defaultVal!!.toAvro(to) - } else this - - return when (to.type!!) { - Schema.Type.RECORD -> useNode.toAvroObject(to) - Schema.Type.LONG, Schema.Type.FLOAT, Schema.Type.DOUBLE, Schema.Type.INT -> useNode.toAvroNumber(to.type) - Schema.Type.BOOLEAN -> useNode.toAvroBoolean() - Schema.Type.ARRAY -> useNode.toAvroArray(to.elementType) - Schema.Type.NULL -> null - Schema.Type.BYTES -> useNode.toAvroBytes() - Schema.Type.FIXED -> useNode.toAvroFixed(to) - Schema.Type.ENUM -> useNode.toAvroEnum(to, defaultVal) - Schema.Type.MAP -> useNode.toAvroMap(to.valueType) - Schema.Type.STRING -> useNode.toAvroString() - Schema.Type.UNION -> useNode.toAvroUnion(to, defaultVal) - } - } - - private fun JsonNode.toAvroString(): String { - return if (isTextual || isNumber || isBoolean) asText() - else throw HttpInvalidContentException("Cannot map non-simple types to string: $this") - } - - private fun JsonNode.toAvroUnion(to: Schema, defaultVal: JsonNode?): Any? { - return when { - this is ObjectNode -> { - val fieldName = fieldNames().asSequence().firstOrNull() - ?: throw HttpInvalidContentException("Cannot union without a value") - val type = to.types.firstOrNull { unionType -> - fieldName == unionType.name || unionType.fullName == fieldName - } ?: throw HttpInvalidContentException("Cannot find any matching union types") - - this[fieldName].toAvro(type) - } - isNumber -> { - val type = to.types.firstOrNull { unionType -> - unionType.type == Schema.Type.LONG - || unionType.type == Schema.Type.INT - || unionType.type == Schema.Type.FLOAT - || unionType.type == Schema.Type.DOUBLE - } ?: throw HttpInvalidContentException("Cannot map number to non-number union") - toAvroNumber(type.type) - } - isTextual -> { - val type = to.types.firstOrNull { unionType -> - unionType.type == Schema.Type.STRING - || unionType.type == Schema.Type.FIXED - || unionType.type == Schema.Type.BYTES - || unionType.type == Schema.Type.ENUM - } ?: throw HttpInvalidContentException("Cannot map number to non-number union") - toAvro(type) - } - isBoolean -> { - if (to.types.none { unionType -> unionType.type == Schema.Type.BOOLEAN }) { - throw HttpInvalidContentException("Cannot map boolean to non-boolean union") - } - toAvroBoolean() - } - isArray -> { - val type = to.types.firstOrNull { unionType -> - unionType.type == Schema.Type.ARRAY - } ?: throw HttpInvalidContentException("Cannot map array to non-array union") - return toAvroArray(type.elementType) - } - isObject -> { - val type = to.types.firstOrNull { unionType -> - unionType.type == Schema.Type.MAP - || unionType.type == Schema.Type.RECORD - } ?: throw HttpInvalidContentException("Cannot map object to non-object union") - return toAvro(type, defaultVal) - } - else -> throw HttpInvalidContentException("Cannot map unknown JSON node type") - } - } - - private fun JsonNode.toAvroEnum(schema: Schema, defaultVal: JsonNode?): GenericData.EnumSymbol { - return if (isTextual) { - val textValue = asText()!! - if (schema.hasEnumSymbol(textValue)) { - GenericData.EnumSymbol(schema, textValue) - } else if (defaultVal != null && defaultVal.isTextual) { - val defaultText = defaultVal.asText() - if (schema.hasEnumSymbol(defaultText)) { - GenericData.EnumSymbol(schema, defaultText) - } else throw HttpInvalidContentException("Enum symbol default cannot be found") - } else throw HttpInvalidContentException("Enum symbol without default cannot be found") - } else throw HttpInvalidContentException("Can only convert strings to enum") - } - - private fun JsonNode.toAvroMap(schema: Schema): Map { - return if (this is ObjectNode) { - fieldNames().asSequence() - .associateWithTo(LinkedHashMap()) { key -> get(key).toAvro(schema) } - } else throw HttpInvalidContentException("Can only convert objects to map") - } - - private fun JsonNode.toAvroBytes(): ByteBuffer { - return if (isTextual) { - val fromArray = textValue()!!.toByteArray(StandardCharsets.ISO_8859_1) - ByteBuffer.wrap(fromArray) - } else throw HttpInvalidContentException("Can only convert strings to byte arrays") - } - - private fun JsonNode.toAvroFixed(schema: Schema): GenericFixed { - return if (isTextual) { - val bytes = textValue()!!.toByteArray(StandardCharsets.ISO_8859_1) - if (bytes.size != schema.fixedSize) { - throw HttpInvalidContentException("Cannot use a different Fixed size") - } - GenericData.Fixed(schema, bytes) - } else throw HttpInvalidContentException("Can only convert strings to byte arrays") - } - - private fun JsonNode.toAvroObject(schema: Schema): GenericRecord { - this as? ObjectNode ?: throw HttpInvalidContentException("Cannot map non-object to object") - val builder = GenericRecordBuilder(schema) - for (field in schema.fields) { - get(field.name())?.let { node -> - builder[field] = node.toAvro(field.schema(), field.jsonDefaultValue) - } - } - return builder.build() - } - - private fun JsonNode.toAvroArray(schema: Schema): Any { - return when { - isArray -> GenericData.Array(schema, (this as ArrayNode).toList()) - else -> throw HttpInvalidContentException("Cannot map non-array to array") - } - } - - private fun JsonNode.toAvroNumber(schemaType: Schema.Type): Number { - return when { - isNumber -> when (schemaType) { - Schema.Type.LONG -> asLong() - Schema.Type.FLOAT -> asDouble().toFloat() - Schema.Type.DOUBLE -> asDouble() - Schema.Type.INT -> asInt() - else -> throw HttpInvalidContentException("Non-number type used for numbers") - } - isTextual -> when (schemaType) { - Schema.Type.LONG -> asText().toLong() - Schema.Type.FLOAT -> asText().toFloat() - Schema.Type.DOUBLE -> asText().toDouble() - Schema.Type.INT -> asText().toInt() - else -> throw HttpInvalidContentException("Non-number type used for numbers") - } - else -> throw HttpInvalidContentException("Cannot map non-number to number") - } - } - - private fun JsonNode.toAvroBoolean(): Boolean { - return when { - isBoolean -> asBoolean() - isTextual -> when (asText()!!) { - "true" -> true - "false" -> false - else -> throw HttpInvalidContentException("Cannot map non-boolean string to boolean") - } - isNumber -> asDouble() != 0.0 - else -> throw HttpInvalidContentException("Cannot map non-boolean to boolean") - } - } - - override fun close() { - cleanReference.close() - } - - companion object { - private val SCHEMA_CLEAN = Duration.ofHours(2) - - private val cacheConfig = CacheConfig( - refreshDuration = Duration.ofHours(1), - retryDuration = Duration.ofMinutes(2), - staleThresholdDuration = Duration.ofMinutes(30), - maxSimultaneousCompute = 3, - ) - - val JsonNode?.isMissing: Boolean - get() = this == null || this.isNull - } - - - data class JsonToObjectMapping( - val sourceSchema: Schema, - val targetSchema: Schema, - val targetSchemaId: Int, - val mapper: AvroDataMapper) - - private fun JsonToObjectMapping.jsonToAvro(node: JsonNode): GenericRecord { - val originalRecord = node.toAvroObject(sourceSchema) - return mapper.convertAvro(originalRecord) as GenericRecord - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/io/AvroProcessorFactory.kt b/src/main/kotlin/org/radarbase/gateway/io/AvroProcessorFactory.kt deleted file mode 100644 index 0183b46..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/AvroProcessorFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.radarbase.gateway.io - -import com.fasterxml.jackson.databind.ObjectMapper -import org.glassfish.jersey.internal.inject.DisposableSupplier -import org.radarbase.gateway.Config -import org.radarbase.gateway.service.SchedulingService -import org.radarbase.jersey.auth.Auth -import org.radarbase.producer.rest.SchemaRetriever -import javax.ws.rs.core.Context - -class AvroProcessorFactory( - @Context private val config: Config, - @Context private val auth: Auth, - @Context private val objectMapper: ObjectMapper, - @Context private val schedulingService: SchedulingService, - @Context private val schemaRetriever: SchemaRetriever -): DisposableSupplier { - override fun get() = AvroProcessor(config, auth, schemaRetriever, objectMapper, schedulingService) - - override fun dispose(instance: AvroProcessor?) { - instance?.close() - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/io/BinaryToAvroConverter.kt b/src/main/kotlin/org/radarbase/gateway/io/BinaryToAvroConverter.kt deleted file mode 100644 index 4f4b274..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/BinaryToAvroConverter.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.radarbase.gateway.io - -import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream -import org.apache.avro.Schema -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericDatumReader -import org.apache.avro.generic.GenericRecord -import org.apache.avro.io.BinaryDecoder -import org.apache.avro.io.Decoder -import org.apache.avro.io.DecoderFactory -import org.radarbase.gateway.Config -import org.radarbase.jersey.auth.Auth -import org.radarbase.jersey.exception.HttpInvalidContentException -import org.radarbase.producer.rest.SchemaRetriever -import java.io.IOException -import java.io.InputStream -import java.nio.ByteBuffer -import javax.ws.rs.core.Context - -/** Converts binary input from a RecordSet to Kafka JSON. */ -class BinaryToAvroConverter( - @Context private val schemaRetriever: SchemaRetriever, - @Context private val auth: Auth, - @Context private val config: Config) { - - private var binaryDecoder: BinaryDecoder? = null - private val readContext = ReadContext() - - fun process(topic: String, input: InputStream): AvroProcessingResult { - val decoder = DecoderFactory.get().binaryDecoder(input, binaryDecoder) - - binaryDecoder = decoder - - val recordData = DecodedRecordData(topic, decoder, schemaRetriever, auth, readContext, config.auth.checkSourceId) - - return AvroProcessingResult( - recordData.keySchemaMetadata.id, - recordData.valueSchemaMetadata.id, - recordData.map { value -> - Pair(recordData.key, value) - }) - } - - class ReadContext { - private val genericData = GenericData().apply { - isFastReaderEnabled = true - } - private var buffer: ByteBuffer? = null - private var valueDecoder : BinaryDecoder? = null - private var valueReader : GenericDatumReader? = null - - fun init(schema: Schema) { - if (valueReader?.schema != schema) { - @Suppress("UNCHECKED_CAST") - valueReader = genericData.createDatumReader(schema) as GenericDatumReader - } - } - - fun decodeValue(decoder: Decoder): GenericRecord { - return try { - buffer = decoder.readBytes(buffer) - valueDecoder = DecoderFactory.get().binaryDecoder(ByteBufferBackedInputStream(buffer), valueDecoder) - val reader = valueReader ?: throw IllegalStateException("Value reader is not yet set") - reader.read(null, valueDecoder) - ?: throw HttpInvalidContentException("No record in data") - } catch (ex: IOException) { - throw HttpInvalidContentException("Malformed record contents: ${ex.message}") - } - } - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/io/DecodedRecordData.kt b/src/main/kotlin/org/radarbase/gateway/io/DecodedRecordData.kt deleted file mode 100644 index bbe70f6..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/DecodedRecordData.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.radarbase.gateway.io - -import org.apache.avro.Schema -import org.apache.avro.generic.GenericRecord -import org.apache.avro.generic.GenericRecordBuilder -import org.apache.avro.io.BinaryDecoder -import org.radarbase.data.RecordData -import org.radarbase.jersey.auth.Auth -import org.radarbase.producer.rest.ParsedSchemaMetadata -import org.radarbase.producer.rest.SchemaRetriever -import org.radarbase.topic.AvroTopic -import org.radarcns.auth.authorization.Permission - -class DecodedRecordData( - topicName: String, - private val decoder: BinaryDecoder, - schemaRetriever: SchemaRetriever, - auth: Auth, - private val readContext: BinaryToAvroConverter.ReadContext, - checkSources: Boolean) : RecordData { - - private val key: GenericRecord - private var size: Int - private var remaining: Int - private val topic: AvroTopic - - val keySchemaMetadata: ParsedSchemaMetadata - val valueSchemaMetadata: ParsedSchemaMetadata - - init { - val keyVersion = decoder.readInt() - val valueVersion = decoder.readInt() - val projectId = if (decoder.readIndex() == 1) decoder.readString() else auth.defaultProject - val userId = if (decoder.readIndex() == 1) decoder.readString() else auth.userId - val sourceId = decoder.readString() - - if (checkSources) { - auth.checkPermissionOnSource(Permission.MEASUREMENT_CREATE, projectId, userId, sourceId) - } else { - auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, projectId, userId) - } - - remaining = decoder.readArrayStart().toInt() - size = remaining - - keySchemaMetadata = schemaRetriever.getBySubjectAndVersion(topicName, false, keyVersion) - valueSchemaMetadata = schemaRetriever.getBySubjectAndVersion(topicName, true, valueVersion) - - topic = AvroTopic(topicName, keySchemaMetadata.schema, valueSchemaMetadata.schema, - GenericRecord::class.java, GenericRecord::class.java) - - key = createKey(keySchemaMetadata.schema, projectId!!, userId!!, sourceId) - readContext.init(valueSchemaMetadata.schema) - } - - private fun createKey(schema: Schema, projectId: String, userId: String, sourceId: String): - GenericRecord { - val keyBuilder = GenericRecordBuilder(schema) - schema.getField("projectId")?.let { keyBuilder.set(it, projectId) } - schema.getField("userId")?.let { keyBuilder.set(it, userId) } - schema.getField("sourceId")?.let { keyBuilder.set(it, sourceId) } - return keyBuilder.build() - } - - override fun getKey() = key - - override fun isEmpty() = size == 0 - - override fun iterator(): MutableIterator { - check(remaining != 0) { "Cannot read decoded record data twice." } - - return object : MutableIterator { - override fun hasNext() = remaining > 0 - - override fun next(): GenericRecord { - if (!hasNext()) throw NoSuchElementException() - - val result = readContext.decodeValue(decoder) - remaining-- - if (remaining == 0) { - remaining = decoder.arrayNext().toInt() - size += remaining - } - return result - } - - override fun remove() { - throw NotImplementedError() - } - } - } - - override fun size() = size - - override fun getTopic() = topic -} diff --git a/src/main/kotlin/org/radarbase/gateway/io/LimitedInputStream.kt b/src/main/kotlin/org/radarbase/gateway/io/LimitedInputStream.kt deleted file mode 100644 index ec13d7f..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/LimitedInputStream.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.radarbase.gateway.io - -import org.radarbase.jersey.exception.HttpRequestEntityTooLarge -import java.io.InputStream -import kotlin.math.max -import kotlin.math.min - -internal class LimitedInputStream(private val subStream: InputStream, private val limit: Long): InputStream() { - private var count: Long = 0 - - override fun available() = min(subStream.available(), (limit - count).toInt()) - - override fun read(): Int { - return subStream.read().also { - if (it != -1) count++ - if (count > limit) throw HttpRequestEntityTooLarge("Stream size exceeds limit $limit") - } - } - - override fun read(b: ByteArray, off: Int, len: Int): Int { - if (len == 0) return 0 - return subStream.read(b, off, min(max(limit - count, 1L), len.toLong()).toInt()).also { - if (it != -1) count += it - if (count > limit) throw HttpRequestEntityTooLarge("Stream size exceeds limit $limit") - } - } - - override fun close() = subStream.close() -} diff --git a/src/main/kotlin/org/radarbase/gateway/io/LzfseEncoder.kt b/src/main/kotlin/org/radarbase/gateway/io/LzfseEncoder.kt deleted file mode 100644 index e28621f..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/LzfseEncoder.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.radarbase.gateway.io - -import org.glassfish.jersey.spi.ContentEncoder -import org.radarbase.io.lzfse.LZFSEInputStream -import org.radarbase.jersey.exception.HttpApplicationException -import java.io.InputStream -import java.io.OutputStream -import javax.ws.rs.core.Response - -class LzfseEncoder : ContentEncoder("lzfse", "x-lzfse") { - override fun encode( - contentEncoding: String, - entityStream: OutputStream - ): OutputStream = throw HttpApplicationException(Response.Status.NOT_ACCEPTABLE, "LZFSE encoding not implemented") - - override fun decode( - contentEncoding: String, - encodedStream: InputStream - ): InputStream = LZFSEInputStream(encodedStream) -} diff --git a/src/main/kotlin/org/radarbase/gateway/io/SizeLimitInterceptor.kt b/src/main/kotlin/org/radarbase/gateway/io/SizeLimitInterceptor.kt deleted file mode 100644 index 726d448..0000000 --- a/src/main/kotlin/org/radarbase/gateway/io/SizeLimitInterceptor.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.radarbase.gateway.io - -import org.radarbase.gateway.Config -import org.radarbase.gateway.inject.ProcessAvro -import javax.annotation.Priority -import javax.inject.Singleton -import javax.ws.rs.Priorities -import javax.ws.rs.core.Context -import javax.ws.rs.ext.Provider -import javax.ws.rs.ext.ReaderInterceptor -import javax.ws.rs.ext.ReaderInterceptorContext - -/** - * Limits data stream to a maximum request size. - */ -@Provider -@ProcessAvro -@Singleton -@Priority(Priorities.ENTITY_CODER + 100) -class SizeLimitInterceptor(@Context private val config: Config) : ReaderInterceptor { - override fun aroundReadFrom(context: ReaderInterceptorContext): Any { - context.inputStream = LimitedInputStream(context.inputStream, config.server.maxRequestSize) - return context.proceed() - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/kafka/KafkaAvroProducer.kt b/src/main/kotlin/org/radarbase/gateway/kafka/KafkaAvroProducer.kt index a00b27d..388d439 100644 --- a/src/main/kotlin/org/radarbase/gateway/kafka/KafkaAvroProducer.kt +++ b/src/main/kotlin/org/radarbase/gateway/kafka/KafkaAvroProducer.kt @@ -2,7 +2,7 @@ package org.radarbase.gateway.kafka import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient import io.confluent.kafka.serializers.KafkaAvroSerializer -import org.apache.avro.generic.GenericRecord +import org.apache.avro.generic.IndexedRecord import org.apache.kafka.clients.producer.KafkaProducer import org.apache.kafka.clients.producer.Producer import org.apache.kafka.clients.producer.ProducerRecord @@ -13,9 +13,9 @@ import java.io.Closeable import java.util.concurrent.ExecutionException class KafkaAvroProducer( - config: Config, - schemaRegistryClient: SchemaRegistryClient -): Closeable { + config: Config, + schemaRegistryClient: SchemaRegistryClient +) : Closeable { private val producer: Producer init { @@ -32,16 +32,16 @@ class KafkaAvroProducer( } @Throws(KafkaException::class) - fun produce(topic: String, records: List>) { + fun produce(topic: String, records: List>) { records - .map { (key, value) -> producer.send(ProducerRecord(topic, key, value)) } - .forEach { - try { - it.get() // asserts that the send completed and was successful - } catch (ex: ExecutionException) { - throw ex.cause!! - } + .map { (key, value) -> producer.send(ProducerRecord(topic, key, value)) } + .forEach { + try { + it.get() // asserts that the send completed and was successful + } catch (ex: ExecutionException) { + throw ex.cause!! } + } } override fun close() { diff --git a/src/main/kotlin/org/radarbase/gateway/kafka/ProducerPool.kt b/src/main/kotlin/org/radarbase/gateway/kafka/ProducerPool.kt index 501cd99..c8d8c01 100644 --- a/src/main/kotlin/org/radarbase/gateway/kafka/ProducerPool.kt +++ b/src/main/kotlin/org/radarbase/gateway/kafka/ProducerPool.kt @@ -6,6 +6,7 @@ import io.confluent.kafka.schemaregistry.client.rest.RestService import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.MAX_SCHEMAS_PER_SUBJECT_CONFIG import org.apache.avro.generic.GenericRecord +import org.apache.avro.generic.IndexedRecord import org.apache.kafka.common.KafkaException import org.apache.kafka.common.errors.* import org.radarbase.gateway.Config @@ -39,7 +40,7 @@ class ProducerPool( schemaRegistryClient = CachedSchemaRegistryClient(restService, config.kafka.serialization[MAX_SCHEMAS_PER_SUBJECT_CONFIG] as Int, null, config.kafka.serialization, null) } - fun produce(topic: String, records: List>) { + fun produce(topic: String, records: List>) { if (!semaphore.tryAcquire()) throw HttpApplicationException(Response.Status.SERVICE_UNAVAILABLE, "Too many open Kafka requests") try { val producer = pool.poll() ?: KafkaAvroProducer(config, schemaRegistryClient) diff --git a/src/main/kotlin/org/radarbase/gateway/main.kt b/src/main/kotlin/org/radarbase/gateway/main.kt index c3dcc5e..6baf837 100644 --- a/src/main/kotlin/org/radarbase/gateway/main.kt +++ b/src/main/kotlin/org/radarbase/gateway/main.kt @@ -1,17 +1,27 @@ package org.radarbase.gateway +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule import org.radarbase.jersey.GrizzlyServer import org.radarbase.jersey.config.ConfigLoader import org.slf4j.LoggerFactory -import java.lang.IllegalStateException import kotlin.system.exitProcess fun main(args: Array) { val config = try { - ConfigLoader.loadConfig(listOf( + ConfigLoader.loadConfig( + listOf( "gateway.yml", - "/etc/radar-gateway/gateway.yml"), args) - .withDefaults() + "/etc/radar-gateway/gateway.yml" + ), + args, + ObjectMapper(YAMLFactory()) + .registerModule(KotlinModule()) + .registerModule(JavaTimeModule()) + ) + .withDefaults() } catch (ex: IllegalArgumentException) { logger.error("No configuration file was found.") logger.error("Usage: radar-gateway ") diff --git a/src/main/kotlin/org/radarbase/gateway/resource/KafkaRoot.kt b/src/main/kotlin/org/radarbase/gateway/resource/KafkaRoot.kt deleted file mode 100644 index 05e1821..0000000 --- a/src/main/kotlin/org/radarbase/gateway/resource/KafkaRoot.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.radarbase.gateway.resource - -import org.radarbase.gateway.resource.KafkaTopics.Companion.PRODUCE_AVRO_V1_JSON -import org.radarbase.gateway.resource.KafkaTopics.Companion.PRODUCE_JSON -import javax.inject.Singleton -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces - -/** Root path, just forward requests without authentication. */ -@Path("/") -@Singleton -class KafkaRoot { - @GET - @Produces(PRODUCE_AVRO_V1_JSON, PRODUCE_JSON) - fun root() = mapOf() -} diff --git a/src/main/kotlin/org/radarbase/gateway/resource/KafkaTopics.kt b/src/main/kotlin/org/radarbase/gateway/resource/KafkaTopics.kt deleted file mode 100644 index 7f4e92d..0000000 --- a/src/main/kotlin/org/radarbase/gateway/resource/KafkaTopics.kt +++ /dev/null @@ -1,103 +0,0 @@ -package org.radarbase.gateway.resource - -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.JsonNode -import org.radarbase.gateway.inject.ProcessAvro -import org.radarbase.gateway.io.AvroProcessor -import org.radarbase.gateway.io.BinaryToAvroConverter -import org.radarbase.gateway.kafka.KafkaAdminService -import org.radarbase.gateway.kafka.ProducerPool -import org.radarbase.jersey.auth.Authenticated -import org.radarbase.jersey.auth.NeedsPermission -import org.radarbase.jersey.exception.HttpBadRequestException -import org.radarcns.auth.authorization.Permission.Entity.MEASUREMENT -import org.radarcns.auth.authorization.Permission.Operation.CREATE -import org.slf4j.LoggerFactory -import java.io.IOException -import java.io.InputStream -import javax.inject.Singleton -import javax.ws.rs.* -import javax.ws.rs.core.Context -import javax.ws.rs.core.Response - -/** Topics submission and listing. Requests need authentication. */ -@Path("/topics") -@Singleton -@Authenticated -class KafkaTopics( - @Context private val kafkaAdminService: KafkaAdminService, - @Context private val producerPool: ProducerPool -) { - @GET - @Produces(PRODUCE_AVRO_V1_JSON) - fun topics() = kafkaAdminService.listTopics() - - @Path("/{topic_name}") - @GET - fun topic( - @PathParam("topic_name") topic: String - ) = kafkaAdminService.topicInfo(topic) - - @OPTIONS - @Path("/{topic_name}") - fun topicOptions(): Response = Response.noContent() - .header("Accept", "$ACCEPT_BINARY_V1,$ACCEPT_AVRO_V2_JSON,$ACCEPT_AVRO_V1_JSON") - .header("Accept-Encoding", "gzip,lzfse") - .header("Accept-Charset", "utf-8") - .header("Allow", "HEAD,GET,POST,OPTIONS") - .build() - - @Path("/{topic_name}") - @POST - @Consumes(ACCEPT_AVRO_V1_JSON, ACCEPT_AVRO_V2_JSON) - @Produces(PRODUCE_AVRO_V1_JSON, PRODUCE_JSON) - @NeedsPermission(MEASUREMENT, CREATE) - @ProcessAvro - fun postToTopic( - tree: JsonNode, - @PathParam("topic_name") topic: String, - @Context avroProcessor: AvroProcessor): TopicPostResponse { - - val processingResult = avroProcessor.process(topic, tree) - producerPool.produce(topic, processingResult.records) - return TopicPostResponse(processingResult.keySchemaId, processingResult.valueSchemaId) - } - - @Path("/{topic_name}") - @POST - @ProcessAvro - @Consumes(ACCEPT_BINARY_V1) - @Produces(PRODUCE_AVRO_V1_JSON, PRODUCE_JSON) - @NeedsPermission(MEASUREMENT, CREATE) - fun postToTopicBinary( - input: InputStream, - @Context binaryToAvroConverter: BinaryToAvroConverter, - @PathParam("topic_name") topic: String): TopicPostResponse { - - val processingResult = try { - binaryToAvroConverter.process(topic, input) - } catch (ex: IOException) { - logger.error("Invalid RecordSet content: {}", ex.toString()) - throw HttpBadRequestException("bad_content", "Content is not a valid binary RecordSet") - } - - producerPool.produce(topic, processingResult.records) - - return TopicPostResponse(processingResult.keySchemaId, processingResult.valueSchemaId) - } - - data class TopicPostResponse( - @JsonProperty("key_schema_id") - val keySchemaId: Int, - @JsonProperty("value_schema_id") - val valueSchemaId: Int) - - companion object { - private val logger = LoggerFactory.getLogger(KafkaTopics::class.java) - const val ACCEPT_AVRO_V1_JSON = "application/vnd.kafka.avro.v1+json" - const val ACCEPT_AVRO_V2_JSON = "application/vnd.kafka.avro.v2+json" - const val ACCEPT_BINARY_V1 = "application/vnd.radarbase.avro.v1+binary" - const val PRODUCE_AVRO_V1_JSON = "application/vnd.kafka.v1+json" - const val PRODUCE_JSON = "application/json" - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/service/SchedulingService.kt b/src/main/kotlin/org/radarbase/gateway/service/SchedulingService.kt deleted file mode 100644 index 42de6e3..0000000 --- a/src/main/kotlin/org/radarbase/gateway/service/SchedulingService.kt +++ /dev/null @@ -1,62 +0,0 @@ -package org.radarbase.gateway.service - -import org.slf4j.LoggerFactory -import java.io.Closeable -import java.time.Duration -import java.util.concurrent.CancellationException -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit - -class SchedulingService: Closeable { - private val scheduler = Executors.newSingleThreadScheduledExecutor() - - fun execute(method: () -> Unit) = scheduler.execute(method) - - fun repeat(rate: Duration, initialDelay: Duration, method: () -> Unit): RepeatReference { - val ref = scheduler.scheduleAtFixedRate(method, initialDelay.toMillis(), rate.toMillis(), TimeUnit.MILLISECONDS) - return FutureRepeatReference(ref) - } - - interface RepeatReference: Closeable - - private inner class FutureRepeatReference(private val ref: ScheduledFuture<*>): RepeatReference { - override fun close() { - if (ref.cancel(false)) { - try { - ref.get() - } catch (ex: CancellationException) { - // this is expected - } catch (ex: Exception) { - logger.warn("Failed to get repeating job result", ex) - } - } - } - } - - override fun close() { - scheduler.shutdown() // Disable new tasks from being submitted - - try { - // Wait a while for existing tasks to terminate - if (!scheduler.awaitTermination(TERMINATION_INTERVAL_SECONDS, TimeUnit.SECONDS)) { - scheduler.shutdownNow() // Cancel currently executing tasks - // Wait a while for tasks to respond to being cancelled - if (!scheduler.awaitTermination(TERMINATION_INTERVAL_SECONDS, TimeUnit.SECONDS)) { - logger.error("SchedulingService did not terminate") - } - } - } catch (ie: InterruptedException) { - // (Re-)Cancel if current thread also interrupted - scheduler.shutdownNow() - // Preserve interrupt status - Thread.currentThread().interrupt() - } - } - - companion object { - private val logger = LoggerFactory.getLogger(SchedulingService::class.java) - - private const val TERMINATION_INTERVAL_SECONDS = 60L - } -} diff --git a/src/main/kotlin/org/radarbase/gateway/service/SchedulingServiceFactory.kt b/src/main/kotlin/org/radarbase/gateway/service/SchedulingServiceFactory.kt deleted file mode 100644 index e74bbe1..0000000 --- a/src/main/kotlin/org/radarbase/gateway/service/SchedulingServiceFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.radarbase.gateway.service - -import org.glassfish.jersey.internal.inject.DisposableSupplier - -class SchedulingServiceFactory: DisposableSupplier { - override fun get() = SchedulingService() - - override fun dispose(instance: SchedulingService?) { - instance?.close() - } -} diff --git a/src/main/kotlin/org/radarbase/push/integration/GarminPushIntegrationResourceEnhancer.kt b/src/main/kotlin/org/radarbase/push/integration/GarminPushIntegrationResourceEnhancer.kt new file mode 100644 index 0000000..0a5cf0d --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/GarminPushIntegrationResourceEnhancer.kt @@ -0,0 +1,66 @@ +package org.radarbase.push.integration + +import com.fasterxml.jackson.databind.JsonNode +import org.glassfish.hk2.api.TypeLiteral +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.glassfish.jersey.process.internal.RequestScoped +import org.glassfish.jersey.server.ResourceConfig +import org.radarbase.gateway.Config +import org.radarbase.jersey.auth.AuthValidator +import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.push.integration.common.auth.DelegatedAuthValidator.Companion.GARMIN_QUALIFIER +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.garmin.auth.GarminAuthValidator +import org.radarbase.push.integration.garmin.factory.GarminAuthMetadataFactory +import org.radarbase.push.integration.garmin.factory.GarminUserTreeMapFactory +import org.radarbase.push.integration.garmin.service.BackfillService +import org.radarbase.push.integration.garmin.service.GarminHealthApiService +import org.radarbase.push.integration.garmin.user.GarminUserRepository +import javax.inject.Singleton + +class GarminPushIntegrationResourceEnhancer(private val config: Config) : + JerseyResourceEnhancer { + + override fun ResourceConfig.enhance() { + packages( + "org.radarbase.push.integration.garmin.resource", + "org.radarbase.push.integration.common.filter" + ) + } + + override val classes: Array> + get() = if (config.pushIntegration.garmin.backfill.enabled) { + arrayOf(BackfillService::class.java) + } else { + emptyArray() + } + + override fun AbstractBinder.enhance() { + + bind(config.pushIntegration.garmin.userRepository) + .to(GarminUserRepository::class.java) + .named(GARMIN_QUALIFIER) + .`in`(Singleton::class.java) + + bind(GarminHealthApiService::class.java) + .to(GarminHealthApiService::class.java) + .`in`(Singleton::class.java) + + bind(GarminAuthValidator::class.java) + .to(AuthValidator::class.java) + .named(GARMIN_QUALIFIER) + .`in`(Singleton::class.java) + + bindFactory(GarminUserTreeMapFactory::class.java) + .to(object : TypeLiteral>() {}.type) + .proxy(true) + .named(GARMIN_QUALIFIER) + .`in`(RequestScoped::class.java) + + bindFactory(GarminAuthMetadataFactory::class.java) + .to(object : TypeLiteral>() {}.type) + .proxy(true) + .named(GARMIN_QUALIFIER) + .`in`(RequestScoped::class.java) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/auth/DelegatedAuthValidator.kt b/src/main/kotlin/org/radarbase/push/integration/common/auth/DelegatedAuthValidator.kt new file mode 100644 index 0000000..f3283ec --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/auth/DelegatedAuthValidator.kt @@ -0,0 +1,36 @@ +package org.radarbase.push.integration.common.auth + +import org.glassfish.hk2.api.IterableProvider +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.AuthValidator +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.core.Context +import javax.ws.rs.core.UriInfo + +class DelegatedAuthValidator( + @Context private val uriInfo: UriInfo, + @Context private val namedValidators: IterableProvider +) : AuthValidator { + + private val basePath: String = "push" + + fun delegate(): AuthValidator { + return when { + uriInfo.matches(GARMIN_QUALIFIER) -> namedValidators.named(GARMIN_QUALIFIER).get() + // Add support for more as integrations are added + else -> throw IllegalStateException() + } + } + + private fun UriInfo.matches(name: String): Boolean = + this.absolutePath.path.contains("^/$basePath/integrations/$name/.*".toRegex()) + + companion object { + const val GARMIN_QUALIFIER = "garmin" + } + + override fun verify(token: String, request: ContainerRequestContext): Auth? = + delegate().verify(token, request) + + override fun getToken(request: ContainerRequestContext): String? = delegate().getToken(request) +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/auth/Oauth1Signing.kt b/src/main/kotlin/org/radarbase/push/integration/common/auth/Oauth1Signing.kt new file mode 100644 index 0000000..855d70d --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/auth/Oauth1Signing.kt @@ -0,0 +1,45 @@ +package org.radarbase.push.integration.common.auth + +import okhttp3.Request +import java.io.IOException + +class Oauth1Signing( + val parameters: Map +) { + + @Throws(IOException::class) + fun signRequest(request: Request): Request { + //Create auth header + val authHeader = "OAuth ${parameters.toHeaderFormat()}" + + return request.newBuilder().addHeader("Authorization", authHeader).build() + } + + private fun Map.toHeaderFormat() = + filterKeys { it in baseKeys } + .entries + .sortedBy { (key, _) -> key } + .joinToString(", ") { (key, value) -> "$key=\"$value\"" } + + companion object { + const val OAUTH_CONSUMER_KEY = "oauth_consumer_key" + const val OAUTH_NONCE = "oauth_nonce" + const val OAUTH_SIGNATURE = "oauth_signature" + const val OAUTH_SIGNATURE_METHOD = "oauth_signature_method" + const val OAUTH_SIGNATURE_METHOD_VALUE = "HMAC-SHA1" + const val OAUTH_TIMESTAMP = "oauth_timestamp" + const val OAUTH_TOKEN = "oauth_token" + const val OAUTH_VERSION = "oauth_version" + const val OAUTH_VERSION_VALUE = "1.0" + + private val baseKeys = arrayListOf( + OAUTH_CONSUMER_KEY, + OAUTH_NONCE, + OAUTH_SIGNATURE, + OAUTH_SIGNATURE_METHOD, + OAUTH_TIMESTAMP, + OAUTH_TOKEN, + OAUTH_VERSION + ) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/auth/OauthKeys.kt b/src/main/kotlin/org/radarbase/push/integration/common/auth/OauthKeys.kt new file mode 100644 index 0000000..1dd6363 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/auth/OauthKeys.kt @@ -0,0 +1,6 @@ +package org.radarbase.push.integration.common.auth + +data class OauthKeys( + val consumerKey: String, + val accessToken: String? = null, +) diff --git a/src/main/kotlin/org/radarbase/push/integration/common/auth/SignRequestParams.kt b/src/main/kotlin/org/radarbase/push/integration/common/auth/SignRequestParams.kt new file mode 100644 index 0000000..f7200eb --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/auth/SignRequestParams.kt @@ -0,0 +1,9 @@ +package org.radarbase.push.integration.common.auth + +import com.fasterxml.jackson.annotation.JsonProperty + +data class SignRequestParams( + @JsonProperty("url") var url: String, + @JsonProperty("method") var method: String, + @JsonProperty("parameters") val parameters: Map, +) diff --git a/src/main/kotlin/org/radarbase/push/integration/common/converter/AvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/common/converter/AvroConverter.kt new file mode 100644 index 0000000..6191326 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/converter/AvroConverter.kt @@ -0,0 +1,17 @@ +package org.radarbase.push.integration.common.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import java.io.IOException + +interface AvroConverter { + + val topic: String + + @Throws(IOException::class) + fun convert( + tree: JsonNode, + user: User + ): List> +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/filter/CorsFilter.kt b/src/main/kotlin/org/radarbase/push/integration/common/filter/CorsFilter.kt new file mode 100644 index 0000000..9a12a6f --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/filter/CorsFilter.kt @@ -0,0 +1,22 @@ +package org.radarbase.push.integration.common.filter + +import java.io.IOException +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.container.ContainerResponseContext +import javax.ws.rs.container.ContainerResponseFilter +import javax.ws.rs.ext.Provider + +@Provider +class CorsFilter : ContainerResponseFilter { + @Throws(IOException::class) + override fun filter( + requestContext: ContainerRequestContext, cres: ContainerResponseContext + ) { + cres.headers.add("Access-Control-Allow-Origin", "*") + cres.headers + .add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization") + cres.headers.add("Access-Control-Allow-Credentials", "true") + cres.headers.add("Access-Control-Allow-Methods", "POST, OPTIONS") + cres.headers.add("Access-Control-Max-Age", "1209600") + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/inject/PushIntegrationResourceEnhancer.kt b/src/main/kotlin/org/radarbase/push/integration/common/inject/PushIntegrationResourceEnhancer.kt new file mode 100644 index 0000000..0969cce --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/inject/PushIntegrationResourceEnhancer.kt @@ -0,0 +1,14 @@ +package org.radarbase.push.integration.common.inject + +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.radarbase.jersey.auth.AuthValidator +import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.push.integration.common.auth.DelegatedAuthValidator + +class PushIntegrationResourceEnhancer : JerseyResourceEnhancer { + + override fun AbstractBinder.enhance() { + bind(DelegatedAuthValidator::class.java) + .to(AuthValidator::class.java) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/user/User.kt b/src/main/kotlin/org/radarbase/push/integration/common/user/User.kt new file mode 100644 index 0000000..caa9f83 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/user/User.kt @@ -0,0 +1,21 @@ +package org.radarbase.push.integration.common.user + +import org.radarcns.kafka.ObservationKey +import java.time.Instant + +interface User { + val id: String + val projectId: String + val userId: String + val sourceId: String + val externalId: String? + val startDate: Instant + val endDate: Instant + val createdAt: Instant + val humanReadableUserId: String? + val serviceUserId: String + val version: String? + val isAuthorized: Boolean + val observationKey: ObservationKey + val versionedId: String +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/user/UserRepository.kt b/src/main/kotlin/org/radarbase/push/integration/common/user/UserRepository.kt new file mode 100644 index 0000000..9cf7185 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/user/UserRepository.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2018 The Hyve + * + * 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 org.radarbase.push.integration.common.user + +import java.io.IOException +import java.util.stream.Stream +import javax.ws.rs.NotAuthorizedException + +/** + * User repository for Garmin users. + */ +interface UserRepository { + /** + * Get specified Garmin user. + * + * @throws IOException if the user cannot be retrieved from the repository. + */ + @Throws(IOException::class) + operator fun get(key: String): User? + + /** + * Get all relevant Garmin users. + * + * @throws IOException if the list cannot be retrieved from the repository. + */ + @Throws(IOException::class) + fun stream(): Stream + + /** + * Get the current access token of given user. + * + * @throws IOException if the new access token cannot be retrieved from the repository. + * @throws NotAuthorizedException if the refresh token is no longer valid. Manual action + * should be taken to get a new refresh token. + * @throws NoSuchElementException if the user does not exists in this repository. + */ + @Throws(IOException::class, NotAuthorizedException::class) + fun getAccessToken(user: User): String + + /** + * Get the current refresh token of given user. + * + * @throws IOException if the new access token secret cannot be retrieved from the repository. + * @throws NotAuthorizedException if the token is no longer valid. Manual action + * should be taken to get a new token. + * @throws NoSuchElementException if the user does not exists in this repository. + */ + @Throws(IOException::class, NotAuthorizedException::class) + fun getRefreshToken(user: User): String + + /** + * Finds [User] using [User.externalUserId] + * + * @throws IOException if there was an error when finding the user. + * @throws NoSuchElementException if the user does not exists in this repository. + */ + @Throws(NoSuchElementException::class, IOException::class) + fun findByExternalId(externalId: String): User { + return stream() + .filter { user -> user.serviceUserId == externalId } + .findFirst() + .orElseGet { throw NoSuchElementException("User not found in the User repository") } + } + + /** + * The functions allows the repository to supply when there are pending updates. + * This gives more control to the user repository in updating and caching users. + * @return `true` if there are new updates available, `false` otherwise. + */ + fun hasPendingUpdates(): Boolean + + /** + * Apply any pending updates to users. This could include, for instance, refreshing a cache + * of users with latest information. + * This is called when [.hasPendingUpdates] is `true`. + * @throws IOException if there was an error when applying updates. + */ + @Throws(IOException::class) + fun applyPendingUpdates() + +} diff --git a/src/main/kotlin/org/radarbase/push/integration/common/user/Users.kt b/src/main/kotlin/org/radarbase/push/integration/common/user/Users.kt new file mode 100644 index 0000000..465d74c --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/common/user/Users.kt @@ -0,0 +1,9 @@ +package org.radarbase.push.integration.common.user + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import org.radarbase.push.integration.garmin.user.GarminUser + + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Users(@JsonProperty("users") val users: List) diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/auth/GarminAuthValidator.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/auth/GarminAuthValidator.kt new file mode 100644 index 0000000..39051f1 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/auth/GarminAuthValidator.kt @@ -0,0 +1,111 @@ +package org.radarbase.push.integration.garmin.auth + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.AuthValidator +import org.radarbase.jersey.auth.disabled.DisabledAuth +import org.radarbase.jersey.exception.HttpUnauthorizedException +import org.radarbase.push.integration.common.auth.DelegatedAuthValidator.Companion.GARMIN_QUALIFIER +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.garmin.user.GarminUserRepository +import org.slf4j.LoggerFactory +import javax.inject.Named +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.core.Context + + +class GarminAuthValidator( + @Context private val objectMapper: ObjectMapper, + @Named(GARMIN_QUALIFIER) private val userRepository: GarminUserRepository +) : + AuthValidator { + + override fun verify(token: String, request: ContainerRequestContext): Auth { + return if (token.isBlank()) { + throw HttpUnauthorizedException("invalid_token", "The token was empty") + } else { + var isAnyUnauthorised = false + // Enrich the request by adding the User + // the data format in Garmin's post is { : [ {}, {} ] } + val tree = request.getProperty("tree") as JsonNode + + val userTreeMap: Map = + // group by user ID since request can contain data from multiple users + tree[tree.fieldNames().next()].groupBy { node -> + node[USER_ID_KEY].asText() + }.filter { (userId, userData) -> + val accessToken = userData[0][USER_ACCESS_TOKEN_KEY].asText() + if (checkIsAuthorised(userId, accessToken)) true else { + isAnyUnauthorised = true + userRepository.deregisterUser(userId, accessToken) + false + } + }.entries.associate { (userId, userData) -> + userRepository.findByExternalId(userId) to + // Map the List back to : [ {}, {} ] + // so it can be processed in the services without much refactoring + objectMapper.createObjectNode() + .set(tree.fieldNames().next(), objectMapper.valueToTree(userData)) + } + + request.setProperty("user_tree_map", userTreeMap) + request.setProperty( + "auth_metadata", + mapOf("isAnyUnauthorised" to isAnyUnauthorised.toString()) + ) + request.removeProperty("tree") + + // Disable auth since we don't have proper auth support + DisabledAuth("res_gateway") + } + } + + override fun getToken(request: ContainerRequestContext): String? { + return if (request.hasEntity()) { + // We put the json tree in the request because the entity stream will be closed here + val tree = objectMapper.readTree(request.entityStream) + request.setProperty("tree", tree) + val userAccessToken = tree[tree.fieldNames().next()][0][USER_ACCESS_TOKEN_KEY] + ?: throw HttpUnauthorizedException("invalid_token", "No user access token provided") + userAccessToken.asText().also { + request.setProperty(USER_ACCESS_TOKEN_KEY, it) + } + } else { + null + } + } + + private fun checkIsAuthorised(userId: String, accessToken: String): Boolean { + val user = try { + userRepository.findByExternalId(userId) + } catch (exc: NoSuchElementException) { + logger.warn( + "no_user: The user {} could not be found in the " + + "user repository.", userId + ) + return false + } + if (!user.isAuthorized) { + logger.warn( + "invalid_user: The user {} does not seem to be authorized.", userId + ) + return false + } + if (userRepository.getAccessToken(user) != accessToken) { + logger.warn( + "invalid_token: The token for user {} does not" + + " match with the records on the system.", userId + ) + return false + } + return true + } + + companion object { + const val USER_ID_KEY = "userId" + const val USER_ACCESS_TOKEN_KEY = "userAccessToken" + + private val logger = LoggerFactory.getLogger(GarminAuthValidator::class.java) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/GarminRequestGenerator.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/GarminRequestGenerator.kt new file mode 100644 index 0000000..0c48630 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/GarminRequestGenerator.kt @@ -0,0 +1,115 @@ +package org.radarbase.push.integration.garmin.backfill + +import okhttp3.Response +import org.radarbase.gateway.Config +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.garmin.backfill.route.* +import org.radarbase.push.integration.garmin.user.GarminUserRepository +import org.radarbase.push.integration.garmin.util.RedisHolder +import org.radarbase.push.integration.garmin.util.offset.* +import org.slf4j.LoggerFactory +import redis.clients.jedis.JedisPool +import java.nio.file.Path +import java.time.Duration +import java.time.Instant + +class GarminRequestGenerator( + val config: Config, + private val userRepository: GarminUserRepository, + private val redisHolder: RedisHolder = + RedisHolder(JedisPool(config.pushIntegration.garmin.backfill.redis.uri)), + private val offsetPersistenceFactory: OffsetPersistenceFactory = + OffsetRedisPersistence(redisHolder), + private val defaultQueryRange: Duration = Duration.ofDays(15), +) : + RequestGenerator { + + private val routes: List = listOf( + GarminActivitiesRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminDailiesRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminActivityDetailsRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminBodyCompsRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminEpochsRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminMoveIQRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminPulseOxRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminRespirationRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminSleepsRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminStressDetailsRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ), + GarminUserMetricsRoute( + config.pushIntegration.garmin.consumerKey, + userRepository + ) + ) + + override fun requests(user: User, max: Int): List { + return routes.map { route -> + val offsets: Offsets? = offsetPersistenceFactory.read(user.versionedId) + val startDate = userRepository.getBackfillStartDate(user) + val startOffset: Instant = if (offsets == null) { + logger.debug("No offsets found for $user, using the start date.") + startDate + } else { + logger.debug("Offsets found in persistence.") + offsets.offsetsMap.getOrDefault( + UserRoute(user.versionedId, route.toString()), startDate + ).takeIf { it >= startDate } ?: startDate + } + val endDate = userRepository.getBackfillEndDate(user) + val endTime = when { + endDate <= startOffset -> return@map emptyList() // Already at end. No further requests + endDate < startOffset.plus(defaultQueryRange) -> endDate + else -> startOffset.plus(defaultQueryRange) + } + route.generateRequests(user, startOffset, endTime, max / routes.size) + }.flatten() + } + + override fun requestSuccessful(request: RestRequest, response: Response) { + logger.debug("Request successful: {}. Writing to offsets...", request.request) + offsetPersistenceFactory.add( + Path.of(request.user.versionedId), UserRouteOffset( + request.user.versionedId, + request.route.toString(), + request.endDate + ) + ) + } + + override fun requestFailed(request: RestRequest, response: Response) { + logger.warn("Request Failed: {}, {}", request, response) + } + + companion object { + private val logger = LoggerFactory.getLogger(GarminRequestGenerator::class.java) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/RequestGenerator.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/RequestGenerator.kt new file mode 100644 index 0000000..faf035d --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/RequestGenerator.kt @@ -0,0 +1,14 @@ +package org.radarbase.push.integration.garmin.backfill + +import okhttp3.Request +import okhttp3.Response +import org.radarbase.push.integration.common.user.User + +interface RequestGenerator { + + fun requests(user: User, max: Int): List + + fun requestSuccessful(request: RestRequest, response: Response) + + fun requestFailed(request: RestRequest, response: Response) +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/RestRequest.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/RestRequest.kt new file mode 100644 index 0000000..4c1e9ab --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/RestRequest.kt @@ -0,0 +1,11 @@ +package org.radarbase.push.integration.garmin.backfill + +import okhttp3.Request +import org.radarbase.push.integration.common.user.User +import java.time.Instant + +data class RestRequest(val request: Request, + val user: User, + val route: Route, + val startDate: Instant, + val endDate: Instant) diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/Route.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/Route.kt new file mode 100644 index 0000000..7f84b81 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/Route.kt @@ -0,0 +1,19 @@ +package org.radarbase.push.integration.garmin.backfill + +import org.radarbase.push.integration.common.user.User +import java.time.Instant + +interface Route { + + /** + * The number of days to request in a single request of this route. + */ + val maxDaysPerRequest: Int + + fun generateRequests(user: User, start: Instant, end: Instant, max: Int): List + + /** + * This is how it would appear in the offsets + */ + override fun toString(): String +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminActivitiesRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminActivitiesRoute.kt new file mode 100644 index 0000000..fb9cae2 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminActivitiesRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminActivitiesRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "activities" + + override fun toString(): String = "garmin_activities" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminActivityDetailsRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminActivityDetailsRoute.kt new file mode 100644 index 0000000..5f5e8db --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminActivityDetailsRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminActivityDetailsRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "activityDetails" + + override fun toString(): String = "garmin_activity_details" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminBodyCompsRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminBodyCompsRoute.kt new file mode 100644 index 0000000..0c75110 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminBodyCompsRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminBodyCompsRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "bodyComps" + + override fun toString(): String = "garmin_body_comps" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminDailiesRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminDailiesRoute.kt new file mode 100644 index 0000000..2c3fcaa --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminDailiesRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminDailiesRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "dailies" + + override fun toString(): String = "garmin_dailies" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminEpochsRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminEpochsRoute.kt new file mode 100644 index 0000000..8010a2e --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminEpochsRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminEpochsRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "epochs" + + override fun toString(): String = "garmin_epochs" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminMoveIQRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminMoveIQRoute.kt new file mode 100644 index 0000000..2dd89cb --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminMoveIQRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminMoveIQRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "moveiq" + + override fun toString(): String = "garmin_move_iq" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminPulseOxRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminPulseOxRoute.kt new file mode 100644 index 0000000..c839632 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminPulseOxRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminPulseOxRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "pulseOx" + + override fun toString(): String = "garmin_pulse_ox" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminRespirationRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminRespirationRoute.kt new file mode 100644 index 0000000..09f053f --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminRespirationRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminRespirationRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "respiration" + + override fun toString(): String = "garmin_respiration" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminRoute.kt new file mode 100644 index 0000000..ca9093e --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminRoute.kt @@ -0,0 +1,82 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import okhttp3.HttpUrl +import okhttp3.Request +import org.radarbase.push.integration.common.auth.Oauth1Signing +import org.radarbase.push.integration.common.auth.Oauth1Signing.Companion.OAUTH_CONSUMER_KEY +import org.radarbase.push.integration.common.auth.Oauth1Signing.Companion.OAUTH_VERSION +import org.radarbase.push.integration.common.auth.Oauth1Signing.Companion.OAUTH_VERSION_VALUE +import org.radarbase.push.integration.common.auth.Oauth1Signing.Companion.OAUTH_NONCE +import org.radarbase.push.integration.common.auth.Oauth1Signing.Companion.OAUTH_TIMESTAMP +import org.radarbase.push.integration.common.auth.SignRequestParams +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.garmin.backfill.RestRequest +import org.radarbase.push.integration.garmin.backfill.Route +import org.radarbase.push.integration.garmin.user.GarminUserRepository +import java.time.Duration +import java.time.Instant + +abstract class GarminRoute( + private val consumerKey: String, + private val userRepository: GarminUserRepository +) : Route { + override val maxDaysPerRequest: Int + get() = 5 + + fun createRequest(user: User, baseUrl: String, queryParams: String): Request { + val request = Request.Builder() + .url(baseUrl + queryParams) + .get() + .build() + + val parameters = getParams(request.url) + val requestParams = SignRequestParams(baseUrl, ROUTE_METHOD, parameters) + val signedRequest = userRepository.getSignedRequest(user, requestParams) + + return Oauth1Signing(signedRequest.parameters).signRequest(request) + } + + fun getParams(url: HttpUrl): Map { + return ( + mapOf( + OAUTH_CONSUMER_KEY to consumerKey, + OAUTH_NONCE to java.util.UUID.randomUUID().toString(), + OAUTH_TIMESTAMP to (System.currentTimeMillis() / 1000L).toString(), + OAUTH_VERSION to OAUTH_VERSION_VALUE, + ) + + (0 until url.querySize).associate { Pair(url.queryParameterName(it), url.queryParameterValue(it) ?: "") } + ) + } + + + override fun generateRequests( + user: User, + start: Instant, + end: Instant, + max: Int + ): List { + var startRange = Instant.from(start) + val requests = mutableListOf() + + while (startRange < end && requests.size < max) { + val endRange = startRange.plus(Duration.ofDays(maxDaysPerRequest.toLong())) + val request = createRequest( + user, "$GARMIN_BACKFILL_BASE_URL/${subPath()}", + "?summaryStartTimeInSeconds=${startRange.epochSecond}" + + "&summaryEndTimeInSeconds=${endRange.epochSecond}" + ) + requests.add(RestRequest(request, user, this, startRange, endRange)) + startRange = endRange + } + return requests.toMutableList() + } + + abstract fun subPath(): String + + companion object { + + const val GARMIN_BACKFILL_BASE_URL = + "https://healthapi.garmin.com/wellness-api/rest/backfill" + const val ROUTE_METHOD = "GET" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminSleepsRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminSleepsRoute.kt new file mode 100644 index 0000000..8d6c6a1 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminSleepsRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminSleepsRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "sleeps" + + override fun toString(): String = "garmin_sleeps" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminStressDetailsRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminStressDetailsRoute.kt new file mode 100644 index 0000000..305f6ee --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminStressDetailsRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminStressDetailsRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "stressDetails" + + override fun toString(): String = "garmin_stress_details" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminUserMetricsRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminUserMetricsRoute.kt new file mode 100644 index 0000000..b3a0f9f --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/backfill/route/GarminUserMetricsRoute.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.backfill.route + +import org.radarbase.push.integration.garmin.user.GarminUserRepository + +class GarminUserMetricsRoute( + consumerKey: String, + userRepository: GarminUserRepository +) : GarminRoute(consumerKey, userRepository) { + + override fun subPath(): String = "userMetrics" + + override fun toString(): String = "garmin_user_metrics" +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivitiesGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivitiesGarminAvroConverter.kt new file mode 100644 index 0000000..58d73a3 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivitiesGarminAvroConverter.kt @@ -0,0 +1,62 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminActivitySummary +import java.time.Instant +import javax.ws.rs.BadRequestException + +class ActivitiesGarminAvroConverter(topic: String = "push_integration_garmin_activity") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val activities = tree[ROOT] + if (activities == null || !activities.isArray) { + throw BadRequestException("The activities data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): GarminActivitySummary { + return GarminActivitySummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + activityType = node["activityType"]?.asText() + duration = node["durationInSeconds"]?.asInt() + averageBikeCadence = node["averageBikeCadenceInRoundsPerMinute"]?.floatValue() + averageHeartRate = node["averageHeartRateInBeatsPerMinute"]?.asInt() + averageRunCadence = node["averageRunCadenceInStepsPerMinute"]?.floatValue() + averageSpeed = node["averageSpeedInMetersPerSecond"]?.floatValue() + averageSwimCadence = node["averageSwimCadenceInStrokesPerMinute"]?.floatValue() + averagePace = node["averagePaceInMinutesPerKilometer"]?.floatValue() + activeKilocalories = node["activeKilocalories"]?.asInt() + deviceName = node["deviceName"]?.asText() + distance = node["distanceInMeters"]?.floatValue() + maxBikeCadence = node["maxBikeCadenceInRoundsPerMinute"]?.floatValue() + maxHeartRate = node["maxHeartRateInBeatsPerMinute"]?.asInt() + maxPace = node["maxPaceInMinutesPerKilometer"]?.floatValue() + maxRunCadence = node["maxRunCadenceInStepsPerMinute"]?.floatValue() + maxSpeed = node["maxSpeedInMetersPerSecond"]?.floatValue() + numberOfActiveLengths = node["numberOfActiveLengths"]?.asInt() + startingLatitude = node["startingLatitudeInDegree"]?.floatValue() + startingLongitude = node["startingLongitudeInDegree"]?.floatValue() + steps = node.get("steps")?.asInt() + totalElevationGain = node["totalElevationGainInMeters"]?.floatValue() + totalElevationLoss = node["totalElevationLossInMeters"]?.floatValue() + isParent = node["isParent"]?.asBoolean() + parentSummaryId = node["parentSummaryId"]?.asText() + manual = node["manual"]?.asBoolean() + }.build() + } + + companion object { + const val ROOT = "activities" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsGarminAvroConverter.kt new file mode 100644 index 0000000..88528b4 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsGarminAvroConverter.kt @@ -0,0 +1,63 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminActivityDetails +import java.time.Instant +import javax.ws.rs.BadRequestException + +class ActivityDetailsGarminAvroConverter(topic: String = "push_integration_garmin_activity_detail") : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) { + val activityDetails = tree[ROOT] + if (activityDetails == null || !activityDetails.isArray) { + throw BadRequestException("The Activity Details Data was invalid") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): SpecificRecord { + val summary = node[SUB_NODE] + return GarminActivityDetails.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = summary["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + startTimeOffset = summary["startTimeOffsetInSeconds"]?.asInt() + activityType = summary["activityType"]?.asText() + duration = summary["durationInSeconds"]?.asInt() + averageBikeCadence = summary["averageBikeCadenceInRoundsPerMinute"]?.floatValue() + averageHeartRate = summary["averageHeartRateInBeatsPerMinute"]?.asInt() + averageRunCadence = summary["averageRunCadenceInStepsPerMinute"]?.floatValue() + averageSpeed = summary["averageSpeedInMetersPerSecond"]?.floatValue() + averageSwimCadence = summary["averageSwimCadenceInStrokesPerMinute"]?.floatValue() + averagePace = summary["averagePaceInMinutesPerKilometer"]?.floatValue() + activeKilocalories = summary["activeKilocalories"]?.asInt() + deviceName = summary["deviceName"]?.asText() + distance = summary["distanceInMeters"]?.floatValue() + maxBikeCadence = summary["maxBikeCadenceInRoundsPerMinute"]?.floatValue() + maxHeartRate = summary["maxHeartRateInBeatsPerMinute"]?.asInt() + maxPace = summary["maxPaceInMinutesPerKilometer"]?.floatValue() + maxRunCadence = summary["maxRunCadenceInStepsPerMinute"]?.floatValue() + maxSpeed = summary["maxSpeedInMetersPerSecond"]?.floatValue() + numberOfActiveLengths = summary["numberOfActiveLengths"]?.asInt() + startingLatitude = summary["startingLatitudeInDegree"]?.floatValue() + startingLongitude = summary["startingLongitudeInDegree"]?.floatValue() + steps = summary["steps"]?.asInt() + totalElevationGain = summary["totalElevationGainInMeters"]?.floatValue() + totalElevationLoss = summary["totalElevationLossInMeters"]?.floatValue() + isParent = summary["isParent"]?.asBoolean() + parentSummaryId = summary["parentSummaryId"]?.asText() + manual = summary["manual"]?.asBoolean() + }.build() + } + + companion object { + const val ROOT = "activityDetails" + const val SUB_NODE = "summary" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsSampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsSampleGarminAvroConverter.kt new file mode 100644 index 0000000..7a310f7 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsSampleGarminAvroConverter.kt @@ -0,0 +1,63 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminActivityDetailsSample +import java.time.Instant + +class ActivityDetailsSampleGarminAvroConverter( + topic: String = "push_integration_garmin_activity_detail_sample" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> + createSamples( + node[SUB_NODE], node["summaryId"].asText(), user.observationKey + ) + }.flatten() + } + + private fun createSamples( + samples: JsonNode?, summaryId: String, observationKey: + ObservationKey + ): List> { + if (samples == null) { + return emptyList() + } + return samples.map { sample -> + Pair( + observationKey, + GarminActivityDetailsSample.newBuilder().apply { + this.summaryId = summaryId + time = sample["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + airTemperature = sample["airTemperatureCelcius"]?.floatValue() + heartRate = sample["heartRate"]?.asInt() + speed = sample["speedMetersPerSecond"]?.floatValue() + stepsPerMinute = sample["stepsPerMinute"]?.floatValue() + totalDistance = sample["totalDistanceInMeters"]?.floatValue() + timerDuration = sample["timerDurationInSeconds"]?.asInt() + clockDuration = sample["clockDurationInSeconds"]?.asInt() + movingDuration = sample["movingDurationInSeconds"]?.asInt() + power = sample["powerInWatts"]?.floatValue() + bikeCadence = sample["bikeCadenceInRPM"]?.asInt() + swimCadence = sample["swimCadenceInStrokesPerMinute"]?.asInt() + latitude = sample["latitudeInDegree"]?.floatValue() + longitude = sample["longitudeInDegree"]?.floatValue() + elevation = sample["elevationInMeters"]?.floatValue() + }.build() + ) + } + + } + + companion object { + const val ROOT = "activityDetails" + const val SUB_NODE = "samples" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/BodyCompGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/BodyCompGarminAvroConverter.kt new file mode 100644 index 0000000..0d7583d --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/BodyCompGarminAvroConverter.kt @@ -0,0 +1,44 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminBodyComposition +import java.time.Instant +import javax.ws.rs.BadRequestException + +class BodyCompGarminAvroConverter(topic: String = "push_integration_garmin_body_composition") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val bodyComps = tree[ROOT] + if (bodyComps == null || !bodyComps.isArray) { + throw BadRequestException("The Body Composition data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): GarminBodyComposition { + return GarminBodyComposition.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["measurementTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + measurementTimeOffset = node["measurementTimeOffsetInSeconds"]?.asInt() + muscleMass = node["muscleMassInGrams"]?.asInt() + boneMass = node["boneMassInGrams"]?.asInt() + bodyWater = node["bodyWaterInPercent"]?.floatValue() + bodyFat = node["bodyFatInPercent"]?.floatValue() + bodyMassIndex = node["bodyMassIndex"]?.floatValue() + weight = node["weightInGrams"]?.asInt() + }.build() + } + + companion object { + const val ROOT = "bodyComps" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/DailiesGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/DailiesGarminAvroConverter.kt new file mode 100644 index 0000000..d35c68c --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/DailiesGarminAvroConverter.kt @@ -0,0 +1,70 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminDailySummary +import java.io.IOException +import java.time.Instant +import javax.ws.rs.BadRequestException + +class DailiesGarminAvroConverter(topic: String = "push_integration_garmin_daily") : + GarminAvroConverter(topic) { + + @Throws(IOException::class) + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): SpecificRecord { + return GarminDailySummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + date = node["calendarDate"]?.asText() + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + activityType = node["activityType"]?.asText() + duration = node["durationInSeconds"]?.asInt() + steps = node["steps"]?.asInt() + distance = node["distanceInMeters"]?.floatValue() + activeTime = node["activeTimeInSeconds"]?.asInt() + activeKilocalories = node["activeKilocalories"]?.asInt() + bmrKilocalories = node["bmrKilocalories"]?.asInt() + consumedCalories = node["consumedCalories"]?.asInt() + moderateIntensityDuration = node["moderateIntensityDurationInSeconds"]?.asInt() + vigorousIntensityDuration = node["vigorousIntensityDurationInSeconds"]?.asInt() + floorsClimbed = node["floorsClimbed"]?.asInt() + minHeartRate = node["minHeartRateInBeatsPerMinute"]?.asInt() + averageHeartRate = node["averageHeartRateInBeatsPerMinute"]?.asInt() + maxHeartRate = node["maxHeartRateInBeatsPerMinute"]?.asInt() + restingHeartRate = node["restingHeartRateInBeatsPerMinute"]?.asInt() + averageStressLevel = node["averageStressLevel"]?.asInt() + maxStressLevel = node["maxStressLevel"]?.asInt() + stressDuration = node["stressDurationInSeconds"]?.asInt() + restStressDuration = node["restStressDurationInSeconds"]?.asInt() + activityStressDuration = node["activityStressDurationInSeconds"]?.asInt() + lowStressDuration = node["lowStressDurationInSeconds"]?.asInt() + mediumStressDuration = node["mediumStressDurationInSeconds"]?.asInt() + highStressDuration = node["highStressDurationInSeconds"]?.asInt() + stressQualifier = node["stressQualifier"]?.asText() + stepsGoal = node["stepsGoal"]?.asInt() + netKilocaloriesGoal = node["netKilocaloriesGoal"]?.asInt() + intensityDurationGoal = node["intensityDurationGoalInSeconds"]?.asInt() + floorsClimbedGoal = node["floorsClimbedGoal"]?.asInt() + source = node["source"]?.asText() + }.build() + } + + @Throws(BadRequestException::class) + override fun validate(tree: JsonNode) { + val dailies = tree[ROOT] + if (dailies == null || !dailies.isArray) { + throw BadRequestException("The Dailies Data was invalid") + } + } + + companion object { + const val ROOT = "dailies" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/EpochsGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/EpochsGarminAvroConverter.kt new file mode 100644 index 0000000..37b7cd0 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/EpochsGarminAvroConverter.kt @@ -0,0 +1,51 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminEpochSummary +import java.time.Instant +import javax.ws.rs.BadRequestException + +class EpochsGarminAvroConverter(topic: String = "push_integration_garmin_epoch") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val epochs = tree[ROOT] + if (epochs == null || !epochs.isArray) { + throw BadRequestException("The epochs data was invalid.") + } + } + + override fun convert( + tree: JsonNode, + user: User + ): List> { + + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): GarminEpochSummary { + return GarminEpochSummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + activityType = node["activityType"]?.asText() + duration = node["durationInSeconds"]?.asInt() + steps = node["steps"]?.asInt() + distance = node["distanceInMeters"]?.floatValue() + activeTime = node["activeTimeInSeconds"]?.asInt() + activeKilocalories = node["activeKilocalories"]?.asInt() + metabolicEquivalentOfTask = node["met"]?.floatValue() + intensity = node["intensity"]?.asText() + meanMotionIntensity = node["meanMotionIntensity"]?.floatValue() + maxMotionIntensity = node["maxMotionIntensity"]?.floatValue() + }.build() + } + + companion object { + const val ROOT = "epochs" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/GarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/GarminAvroConverter.kt new file mode 100644 index 0000000..510b808 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/GarminAvroConverter.kt @@ -0,0 +1,19 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.converter.AvroConverter +import org.radarbase.push.integration.common.user.User +import javax.ws.rs.BadRequestException + +abstract class GarminAvroConverter(override val topic: String) : AvroConverter { + + @Throws(BadRequestException::class) + abstract fun validate(tree: JsonNode) + + fun validateAndConvert(tree: JsonNode, user: User): + List> { + validate(tree) + return convert(tree, user) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateSampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateSampleGarminAvroConverter.kt new file mode 100644 index 0000000..854e490 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateSampleGarminAvroConverter.kt @@ -0,0 +1,52 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminHeartRateSample +import java.time.Instant + +class HeartRateSampleGarminAvroConverter( + topic: String = "push_integration_garmin_heart_rate_sample" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double + ): List> { + if (node == null) { + return emptyList() + } + + return node.fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminHeartRateSample.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.heartRate = value?.floatValue() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = "dailies" + const val SUB_NODE = "timeOffsetHeartRateSamples" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/MoveIQGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/MoveIQGarminAvroConverter.kt new file mode 100644 index 0000000..8c07acb --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/MoveIQGarminAvroConverter.kt @@ -0,0 +1,42 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminMoveIQSummary +import java.time.Instant +import javax.ws.rs.BadRequestException + +class MoveIQGarminAvroConverter(topic: String = "push_integration_garmin_move_iq") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val moveIQs = tree[ROOT] + if (moveIQs == null || !moveIQs.isArray) { + throw BadRequestException("The Move IQ data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): GarminMoveIQSummary { + return GarminMoveIQSummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + offset = node["offsetInSeconds"]?.asInt() + activityType = node["activityType"]?.asText() + duration = node["durationInSeconds"]?.asInt() + date = node["calendarDate"]?.asText() + activitySubType = node["activitySubType"]?.asText() + }.build() + } + + companion object { + const val ROOT = "moveIQActivities" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/PulseOxGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/PulseOxGarminAvroConverter.kt new file mode 100644 index 0000000..e63f2da --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/PulseOxGarminAvroConverter.kt @@ -0,0 +1,54 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminPulseOx +import java.time.Instant +import javax.ws.rs.BadRequestException + +class PulseOxGarminAvroConverter(topic: String = "push_integration_garmin_pulse_ox") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val pulseOx = tree[ROOT] + if (pulseOx == null || !pulseOx.isArray) { + throw BadRequestException("The Pulse Ox data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + + return tree[ROOT] + .map { node -> + getRecords(node, user.observationKey) + }.flatten() + } + + private fun getRecords(node: JsonNode, observationKey: ObservationKey): + List> { + val startTime = node["startTimeInSeconds"].asDouble() + return node[SUB_NODE].fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminPulseOx.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = startTime + key.toDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + duration = node["durationInSeconds"]?.asInt() + date = node["calendarDate"]?.asText() + spo2Value = value?.floatValue() + onDemand = node["onDemand"]?.asBoolean() + }.build() + ) + }.toList() + + } + + companion object { + const val ROOT = "pulseox" + const val SUB_NODE = "timeOffsetSpo2Values" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/RespirationGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/RespirationGarminAvroConverter.kt new file mode 100644 index 0000000..d02b348 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/RespirationGarminAvroConverter.kt @@ -0,0 +1,51 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminRespiration +import java.time.Instant +import javax.ws.rs.BadRequestException + +class RespirationGarminAvroConverter(topic: String = "push_integration_garmin_respiration") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val respiration = tree[ROOT] + if (respiration == null || !respiration.isArray) { + throw BadRequestException("The Respiration data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> getRecord(node, user.observationKey) } + .flatten() + } + + private fun getRecord( + node: JsonNode, + observationKey: ObservationKey + ): List> { + val startTime = node["startTimeInSeconds"].asDouble() + return node[SUB_NODE].fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminRespiration.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = startTime + key.toDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + respiration = value?.floatValue() + duration = node["durationInSeconds"]?.asInt() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = "allDayRespiration" + const val SUB_NODE = "timeOffsetEpochToBreaths" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepLevelGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepLevelGarminAvroConverter.kt new file mode 100644 index 0000000..bff02e4 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepLevelGarminAvroConverter.kt @@ -0,0 +1,52 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminSleepLevel +import java.time.Instant + +class SleepLevelGarminAvroConverter( + topic: String = "push_integration_garmin_sleep_level" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples(node[SUB_NODE], node["summaryId"].asText(), user.observationKey) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey + ): List> { + if (node == null) { + return emptyList() + } + + return node.fields().asSequence().map { (key, value) -> + value.map { + Pair( + observationKey, + GarminSleepLevel.newBuilder().apply { + time = it["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.summaryId = summaryId + sleepLevel = key + startTime = it["startTimeInSeconds"].asDouble() + endTime = it["endTimeInSeconds"].asDouble() + }.build() + ) + } + }.flatten().toList() + } + + companion object { + const val ROOT = "sleeps" + const val SUB_NODE = "sleepLevelsMap" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepPulseOxGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepPulseOxGarminAvroConverter.kt new file mode 100644 index 0000000..741546f --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepPulseOxGarminAvroConverter.kt @@ -0,0 +1,56 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminPulseOx +import java.time.Instant + +class SleepPulseOxGarminAvroConverter( + topic: String = "push_integration_garmin_pulse_ox" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble(), + node["calendarDate"]?.asText(), node["startTimeOffsetInSeconds"]?.intValue() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double, + calendarDate: String?, + offset: Int? + ): List> { + if (node == null) { + return emptyList() + } + return node.fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminPulseOx.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.spo2Value = value?.floatValue() + this.date = calendarDate + this.startTimeOffset = offset + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = "sleeps" + const val SUB_NODE = "timeOffsetSleepSpo2" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepRespirationGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepRespirationGarminAvroConverter.kt new file mode 100644 index 0000000..1476d4e --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepRespirationGarminAvroConverter.kt @@ -0,0 +1,54 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminRespiration +import java.time.Instant + +class SleepRespirationGarminAvroConverter( + topic: String = "push_integration_garmin_respiration" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble(), + node["startTimeOffsetInSeconds"]?.intValue() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double, + offset: Int? + ): List> { + if (node == null) { + return emptyList() + } + return node.fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminRespiration.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.respiration = value?.floatValue() + this.startTimeOffset = offset + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = "sleeps" + const val SUB_NODE = "timeOffsetSleepRespiration" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepsGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepsGarminAvroConverter.kt new file mode 100644 index 0000000..a30ce36 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepsGarminAvroConverter.kt @@ -0,0 +1,45 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminSleepSummary +import java.time.Instant +import javax.ws.rs.BadRequestException + +class SleepsGarminAvroConverter(topic: String = "push_integration_garmin_sleep") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val sleeps = tree[ROOT] + if (sleeps == null || !sleeps.isArray) { + throw BadRequestException("The sleep data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): GarminSleepSummary { + return GarminSleepSummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + date = node["calendarDate"]?.asText() + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + duration = node["durationInSeconds"]?.asInt() + unmeasurableSleepDuration = node["unmeasurableSleepDurationInSeconds"]?.asInt() + deepSleepDuration = node["deepSleepDurationInSeconds"]?.asInt() + lightSleepDuration = node["lightSleepDurationInSeconds"]?.asInt() + remSleepDuration = node["remSleepInSeconds"]?.asInt() + awakeDuration = node["awakeDurationInSeconds"]?.asInt() + validation = node["validation"]?.asText() + }.build() + } + + companion object { + const val ROOT = "sleeps" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressBodyBatteryGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressBodyBatteryGarminAvroConverter.kt new file mode 100644 index 0000000..960cce3 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressBodyBatteryGarminAvroConverter.kt @@ -0,0 +1,53 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminBodyBatterySample +import java.time.Instant + +class StressBodyBatteryGarminAvroConverter( + topic: String = "push_integration_garmin_body_battery_sample", + val root: String = ROOT +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + + return tree[root].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double + ): List> { + if (node == null) { + return emptyList() + } + return node.fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminBodyBatterySample.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.bodyBattery = value?.floatValue() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = "stressDetails" + const val SUB_NODE = "timeOffsetBodyBatteryDetails" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressDetailsGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressDetailsGarminAvroConverter.kt new file mode 100644 index 0000000..268d393 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressDetailsGarminAvroConverter.kt @@ -0,0 +1,44 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminStressDetailSummary +import java.time.Instant +import javax.ws.rs.BadRequestException +import javax.ws.rs.container.ContainerRequestContext + +class StressDetailsGarminAvroConverter(topic: String = "push_integration_garmin_stress") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val stress = tree[ROOT] + if (stress == null || !stress.isArray) { + throw BadRequestException("The Stress data was invalid.") + } + } + + override fun convert( + tree: JsonNode, + user: User + ): List> { + + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): GarminStressDetailSummary { + return GarminStressDetailSummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + duration = node["durationInSeconds"]?.asInt() + date = node["calendarDate"]?.asText() + }.build() + } + + companion object { + const val ROOT = "stressDetails" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressLevelGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressLevelGarminAvroConverter.kt new file mode 100644 index 0000000..c09c1d1 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/StressLevelGarminAvroConverter.kt @@ -0,0 +1,57 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminStressLevelSample +import java.time.Instant +import javax.ws.rs.container.ContainerRequestContext + +class StressLevelGarminAvroConverter( + topic: String = "push_integration_garmin_stress_level" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert( + tree: JsonNode, + user: User + ): List> { + + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double + ): List> { + if (node == null) { + return emptyList() + } + + return node.fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminStressLevelSample.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.stressLevel = value?.floatValue() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = "stressDetails" + const val SUB_NODE = "timeOffsetStressLevelValues" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/UserMetricsGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/UserMetricsGarminAvroConverter.kt new file mode 100644 index 0000000..dc6f1b3 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/UserMetricsGarminAvroConverter.kt @@ -0,0 +1,43 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminUserMetrics +import java.time.Instant +import javax.ws.rs.BadRequestException + +class UserMetricsGarminAvroConverter(topic: String = "push_integration_garmin_user_metrics") : + GarminAvroConverter(topic) { + + override fun validate(tree: JsonNode) { + val userMetrics = tree[ROOT] + if (userMetrics == null || !userMetrics.isArray) { + throw BadRequestException("The User Metrics data was invalid.") + } + } + + override fun convert( + tree: JsonNode, + user: User + ): List> { + + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): GarminUserMetrics { + return GarminUserMetrics.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = Instant.now().toEpochMilli() / 1000.0 + timeReceived = time + date = node["calendarDate"]?.asText() + vo2Max = node["vo2Max"]?.floatValue() + fitnessAge = node["fitnessAge"]?.asInt() + }.build() + } + + companion object { + const val ROOT = "userMetrics" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/factory/GarminAuthMetadataFactory.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/factory/GarminAuthMetadataFactory.kt new file mode 100644 index 0000000..8aa1647 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/factory/GarminAuthMetadataFactory.kt @@ -0,0 +1,13 @@ +package org.radarbase.push.integration.garmin.factory + +import java.util.function.Supplier +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.core.Context + +@Suppress("UNCHECKED_CAST") +class GarminAuthMetadataFactory( + @Context private val requestContext: ContainerRequestContext +) : Supplier> { + override fun get(): Map = + requestContext.getProperty("auth_metadata") as Map +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/factory/GarminUserTreeMapFactory.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/factory/GarminUserTreeMapFactory.kt new file mode 100644 index 0000000..cadfb0f --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/factory/GarminUserTreeMapFactory.kt @@ -0,0 +1,15 @@ +package org.radarbase.push.integration.garmin.factory + +import com.fasterxml.jackson.databind.JsonNode +import org.radarbase.push.integration.common.user.User +import java.util.function.Supplier +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.core.Context + +@Suppress("UNCHECKED_CAST") +class GarminUserTreeMapFactory( + @Context private val requestContext: ContainerRequestContext +) : Supplier> { + override fun get(): Map = + requestContext.getProperty("user_tree_map") as Map +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt new file mode 100644 index 0000000..b73f4d2 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt @@ -0,0 +1,149 @@ +package org.radarbase.push.integration.garmin.resource + +import com.fasterxml.jackson.databind.JsonNode +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.exception.HttpInternalServerException +import org.radarbase.jersey.exception.HttpUnauthorizedException +import org.radarbase.push.integration.common.auth.DelegatedAuthValidator.Companion.GARMIN_QUALIFIER +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.garmin.service.GarminHealthApiService +import javax.inject.Named +import javax.inject.Singleton +import javax.ws.rs.Consumes +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +@Consumes(MediaType.APPLICATION_JSON) +@Singleton +@Path("garmin") +@Authenticated +class GarminPushEndpoint( + @Context private val healthApiService: GarminHealthApiService, + // Using MutableMap due to https://discuss.kotlinlang.org/t/warning-from-jersey-due-to-signature-change/1328 + @Context @Named(GARMIN_QUALIFIER) private val userTreeMap: MutableMap, + @Context @Named(GARMIN_QUALIFIER) private var authMetadata: MutableMap +) { + + @POST + @Path("dailies") + fun addDalies(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processDailies(tree, user) + } + } + + @POST + @Path("activities") + fun addActivities(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processActivities(tree, user) + } + } + + @POST + @Path("activityDetails") + fun addActivityDetails(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processActivityDetails(tree, user) + } + } + + @POST + @Path("manualActivities") + fun addManualActivities(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processManualActivities(tree, user) + } + } + + @POST + @Path("epochs") + fun addEpochSummaries(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processEpochs(tree, user) + } + } + + @POST + @Path("sleeps") + fun addSleeps(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processSleeps(tree, user) + } + } + + @POST + @Path("bodyCompositions") + fun addBodyCompositions(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processBodyCompositions(tree, user) + } + } + + @POST + @Path("stress") + fun addStress(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processStress(tree, user) + } + } + + @POST + @Path("userMetrics") + fun addUserMetrics(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processUserMetrics(tree, user) + } + } + + @POST + @Path("moveIQ") + fun addMoveIQ(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processMoveIQ(tree, user) + } + } + + @POST + @Path("pulseOx") + fun addPluseOX(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processPulseOx(tree, user) + } + } + + @POST + @Path("respiration") + fun addRespiration(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processRespiration(tree, user) + } + } + + /** + * Processes responses for all users + * @param function: The function to use to process data + */ + private fun processResponses(function: (JsonNode, User) -> Response): Response { + val responses = userTreeMap.map { (user, tree) -> + function(tree, user) + } + if ( + authMetadata.getOrDefault("isAnyUnauthorised", "false").toBoolean() + ) { + throw HttpUnauthorizedException( + "invalid_auth", "One of the users did not have " + + "correct authorisation information." + ) + } + if (responses.any { it.status !in 200..299 }) { + throw HttpInternalServerException( + "exception", "There was an exception while processing the data." + ) + } + return Response.ok().build() + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/service/BackfillService.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/service/BackfillService.kt new file mode 100644 index 0000000..d40a0f5 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/service/BackfillService.kt @@ -0,0 +1,122 @@ +package org.radarbase.push.integration.garmin.service + +import okhttp3.OkHttpClient +import org.glassfish.jersey.server.monitoring.ApplicationEvent +import org.glassfish.jersey.server.monitoring.ApplicationEvent.Type.DESTROY_FINISHED +import org.glassfish.jersey.server.monitoring.ApplicationEvent.Type.INITIALIZATION_FINISHED +import org.glassfish.jersey.server.monitoring.ApplicationEventListener +import org.glassfish.jersey.server.monitoring.RequestEvent +import org.glassfish.jersey.server.monitoring.RequestEventListener +import org.radarbase.gateway.Config +import org.radarbase.push.integration.common.auth.DelegatedAuthValidator.Companion.GARMIN_QUALIFIER +import org.radarbase.push.integration.garmin.backfill.GarminRequestGenerator +import org.radarbase.push.integration.garmin.backfill.RestRequest +import org.radarbase.push.integration.garmin.user.GarminUserRepository +import org.radarbase.push.integration.garmin.util.RedisHolder +import org.radarbase.push.integration.garmin.util.RedisRemoteLockManager +import org.slf4j.LoggerFactory +import redis.clients.jedis.JedisPool +import java.io.IOException +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.ws.rs.core.Context + +/** + * The backfill service should be used to collect historic data. This will send requests to garmin's + * service to create POST requests for historic data to our server. + */ +class BackfillService( + @Context private val config: Config, + @Named(GARMIN_QUALIFIER) private val userRepository: GarminUserRepository +) : ApplicationEventListener { + + private val redisHolder = + RedisHolder(JedisPool(config.pushIntegration.garmin.backfill.redis.uri)) + private val executorService = Executors.newSingleThreadScheduledExecutor() + private val requestExecutorService = Executors.newFixedThreadPool( + config.pushIntegration.garmin.backfill.maxThreads + ) + private val requestGenerator = GarminRequestGenerator(config, userRepository) + private val remoteLockManager = RedisRemoteLockManager( + redisHolder, + config.pushIntegration.garmin.backfill.redis.lockPrefix + ) + private val httpClient = OkHttpClient() + private val requestsPerUserPerIteration: Int + get() = 40 + + private val futures: MutableList> = mutableListOf() + + override fun onEvent(event: ApplicationEvent?) { + when (event?.type) { + INITIALIZATION_FINISHED -> start() + DESTROY_FINISHED -> stop() + else -> logger.info("Application event received: ${event?.type}") + } + } + + override fun onRequest(requestEvent: RequestEvent?): RequestEventListener? = null + + private fun start() { + logger.info("Application Initialisation completed. Starting Backfill service...") + + executorService.scheduleAtFixedRate(::iterateUsers, 1, 5, TimeUnit.MINUTES) + } + + private fun stop() { + logger.info("Application Destroy completed. Stopping Backfill service...") + try { + requestExecutorService.awaitTermination(30, TimeUnit.SECONDS) + executorService.awaitTermination(30, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + logger.error("Failed to complete execution: interrupted") + } + } + + private fun iterateUsers() { + try { + while (futures.any { !it.isDone }) { + logger.info("The previous task is already running. Waiting ${WAIT_TIME_MS / 1000}s...") + Thread.sleep(WAIT_TIME_MS) + } + futures.clear() + logger.info("Making Garmin Backfill requests...") + try { + userRepository.stream().forEach { user -> + futures.add(requestExecutorService.submit { + remoteLockManager.tryRunLocked(user.versionedId) { + requestGenerator.requests(user, requestsPerUserPerIteration) + .forEach { req -> makeRequest(req) } + } + }) + } + } catch (exc: IOException) { + logger.warn("I/O Exception while making Backfill requests.", exc) + } + } catch (ex: Throwable) { + logger.warn("Error Making Garmin Backfill requests.", ex) + } + } + + private fun makeRequest(req: RestRequest) { + logger.debug("Making Request: {}", req.request) + try { + httpClient.newCall(req.request).execute().use { response -> + if (response.isSuccessful) { + requestGenerator.requestSuccessful(req, response) + } else { + requestGenerator.requestFailed(req, response) + } + } + } catch (ex: Throwable) { + logger.warn("Error making request ${req.request.url}.", ex) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(BackfillService::class.java) + private const val WAIT_TIME_MS = 10000L + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/service/GarminHealthApiService.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/service/GarminHealthApiService.kt new file mode 100644 index 0000000..17083bb --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/service/GarminHealthApiService.kt @@ -0,0 +1,185 @@ +package org.radarbase.push.integration.garmin.service + +import com.fasterxml.jackson.databind.JsonNode +import org.radarbase.gateway.Config +import org.radarbase.gateway.GarminConfig +import org.radarbase.gateway.kafka.ProducerPool +import org.radarbase.push.integration.common.auth.DelegatedAuthValidator.Companion.GARMIN_QUALIFIER +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.garmin.converter.* +import org.radarbase.push.integration.garmin.user.GarminUserRepository +import java.io.IOException +import javax.inject.Named +import javax.ws.rs.BadRequestException +import javax.ws.rs.core.Context +import javax.ws.rs.core.Response +import javax.ws.rs.core.Response.Status.OK + +class GarminHealthApiService( + @Named(GARMIN_QUALIFIER) private val userRepository: GarminUserRepository, + @Context private val producerPool: ProducerPool, + @Context private val config: Config +) { + private val garminConfig: GarminConfig = config.pushIntegration.garmin + + private val dailiesConverter = + DailiesGarminAvroConverter(garminConfig.dailiesTopicName) + + private val activitiesConverter = + ActivitiesGarminAvroConverter(garminConfig.activitiesTopicName) + + private val activityDetailsConverter = + ActivityDetailsGarminAvroConverter(garminConfig.activityDetailsTopicName) + + private val epochsConverter = EpochsGarminAvroConverter(garminConfig.epochSummariesTopicName) + + private val sleepSummaryConverter = SleepsGarminAvroConverter(garminConfig.sleepsTopicName) + + private val bodyCompsConverter = + BodyCompGarminAvroConverter(garminConfig.bodyCompositionsTopicName) + + private val stressConverter = StressDetailsGarminAvroConverter(garminConfig.stressTopicName) + + private val userMetricsConverter = + UserMetricsGarminAvroConverter(garminConfig.userMetricsTopicName) + + private val moveIQConverter = MoveIQGarminAvroConverter(garminConfig.moveIQTopicName) + + private val pulseOxConverter = PulseOxGarminAvroConverter(garminConfig.pulseOXTopicName) + + private val respirationConverter = + RespirationGarminAvroConverter(garminConfig.respirationTopicName) + + private val activityDetailsSampleConverter = ActivityDetailsSampleGarminAvroConverter( + garminConfig.activityDetailsSampleTopicName + ) + + private val stressBodyBatteryConverter = StressBodyBatteryGarminAvroConverter( + garminConfig.bodyBatterySampleTopicName + ) + + private val heartRateSampleConverter = HeartRateSampleGarminAvroConverter( + garminConfig.heartRateSampleConverter + ) + + private val sleepLevelConverter = SleepLevelGarminAvroConverter( + garminConfig.sleepLevelTopicName + ) + + private val sleepPulseOxConverter = + SleepPulseOxGarminAvroConverter(garminConfig.pulseOXTopicName) + + private val sleepRespirationConverter = + SleepRespirationGarminAvroConverter(garminConfig.respirationTopicName) + + private val stressLevelConverter = StressLevelGarminAvroConverter( + garminConfig.stressLevelTopicName + ) + + @Throws(IOException::class, BadRequestException::class) + fun processDailies(tree: JsonNode, user: User): Response { + val records = dailiesConverter.validateAndConvert(tree, user) + producerPool.produce(dailiesConverter.topic, records) + + val samples = heartRateSampleConverter.validateAndConvert(tree, user) + producerPool.produce(heartRateSampleConverter.topic, samples) + + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processActivities(tree: JsonNode, user: User): Response { + val records = activitiesConverter.validateAndConvert(tree, user) + producerPool.produce(activitiesConverter.topic, records) + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processActivityDetails(tree: JsonNode, user: User): Response { + val records = activityDetailsConverter.validateAndConvert(tree, user) + producerPool.produce(activityDetailsConverter.topic, records) + + val samples = activityDetailsSampleConverter.validateAndConvert(tree, user) + producerPool.produce(activityDetailsSampleConverter.topic, samples) + + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processManualActivities(tree: JsonNode, user: User): Response { + return this.processActivities(tree, user) + } + + @Throws(IOException::class, BadRequestException::class) + fun processEpochs(tree: JsonNode, user: User): Response { + val records = epochsConverter.validateAndConvert(tree, user) + producerPool.produce(epochsConverter.topic, records) + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processSleeps(tree: JsonNode, user: User): Response { + val records = sleepSummaryConverter.validateAndConvert(tree, user) + producerPool.produce(sleepSummaryConverter.topic, records) + + val levels = sleepLevelConverter.validateAndConvert(tree, user) + producerPool.produce(sleepLevelConverter.topic, levels) + + val pulseOx = sleepPulseOxConverter.validateAndConvert(tree, user) + producerPool.produce(sleepPulseOxConverter.topic, pulseOx) + + val respiration = sleepRespirationConverter.validateAndConvert(tree, user) + producerPool.produce(sleepRespirationConverter.topic, respiration) + + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processBodyCompositions(tree: JsonNode, user: User): Response { + val records = bodyCompsConverter.validateAndConvert(tree, user) + producerPool.produce(bodyCompsConverter.topic, records) + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processStress(tree: JsonNode, user: User): Response { + val records = stressConverter.validateAndConvert(tree, user) + producerPool.produce(stressConverter.topic, records) + + val levels = stressLevelConverter.validateAndConvert(tree, user) + producerPool.produce(stressLevelConverter.topic, levels) + + val bodyBattery = stressBodyBatteryConverter.validateAndConvert(tree, user) + producerPool.produce(stressBodyBatteryConverter.topic, bodyBattery) + + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processUserMetrics(tree: JsonNode, user: User): Response { + val records = userMetricsConverter.validateAndConvert(tree, user) + producerPool.produce(userMetricsConverter.topic, records) + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processMoveIQ(tree: JsonNode, user: User): Response { + val records = moveIQConverter.validateAndConvert(tree, user) + producerPool.produce(moveIQConverter.topic, records) + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processPulseOx(tree: JsonNode, user: User): Response { + val records = pulseOxConverter.validateAndConvert(tree, user) + producerPool.produce(pulseOxConverter.topic, records) + return Response.status(OK).build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processRespiration(tree: JsonNode, user: User): Response { + val records = respirationConverter.validateAndConvert(tree, user) + producerPool.produce(respirationConverter.topic, records) + return Response.status(OK).build() + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminServiceUserRepository.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminServiceUserRepository.kt new file mode 100644 index 0000000..deb403a --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminServiceUserRepository.kt @@ -0,0 +1,206 @@ +package org.radarbase.push.integration.garmin.user + +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectReader +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import io.confluent.common.config.ConfigException +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import org.radarbase.gateway.Config +import org.radarbase.gateway.GarminConfig +import org.radarbase.jersey.exception.HttpBadRequestException +import org.radarbase.push.integration.common.auth.SignRequestParams +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.common.user.Users +import org.radarcns.exception.TokenException +import org.radarcns.oauth.OAuth2Client +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URL +import java.time.Duration +import java.time.Instant +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.stream.Stream +import javax.ws.rs.NotAuthorizedException +import javax.ws.rs.core.Context + +@Suppress("UNCHECKED_CAST") +class GarminServiceUserRepository( + @Context private val config: Config +) : GarminUserRepository(config) { + private val garminConfig: GarminConfig = config.pushIntegration.garmin + private val client: OkHttpClient = OkHttpClient() + private val cachedCredentials: ConcurrentHashMap = + ConcurrentHashMap() + private var nextFetch = MIN_INSTANT + + private val baseUrl: HttpUrl + + private var timedCachedUsers: List = ArrayList() + + private val repositoryClient: OAuth2Client + private val tokenUrl: URL + private val clientId: String + private val clientSecret: String + + init { + baseUrl = garminConfig.userRepositoryUrl.toHttpUrl() + tokenUrl = URL(garminConfig.userRepositoryTokenUrl) + clientId = garminConfig.userRepositoryClientId + clientSecret = garminConfig.userRepositoryClientSecret + + if (clientId.isEmpty()) + throw ConfigException("Client ID for user repository is not set.") + + repositoryClient = OAuth2Client.Builder() + .credentials(clientId, clientSecret) + .endpoint(tokenUrl) + .scopes("SUBJECT.READ", "MEASUREMENT.READ", "SUBJECT.UPDATE", "MEASUREMENT.CREATE") + .httpClient(client) + .build() + } + + @Throws(IOException::class) + override fun get(key: String): User? { + val request: Request = requestFor("users/$key").build() + return makeRequest(request, USER_READER) + } + + @Throws(IOException::class) + override fun stream(): Stream { + if (hasPendingUpdates()) { + applyPendingUpdates() + } + return timedCachedUsers.stream() + } + + fun requestUserCredentials(user: User): OAuth1UserCredentials { + val request = requestFor("users/" + user.id + "/token").build() + val credentials = makeRequest(request, OAUTH_READER) as OAuth1UserCredentials + cachedCredentials[user.id] = credentials + return credentials + } + + @Throws(IOException::class, NotAuthorizedException::class) + override fun getAccessToken(user: User): String { + val credentials: OAuth1UserCredentials = cachedCredentials[user.id] ?: requestUserCredentials(user) + return credentials.accessToken + } + + @Throws(IOException::class, NotAuthorizedException::class) + override fun getUserAccessTokenSecret(user: User): String { + throw HttpBadRequestException("", "Not available for source type") + } + + override fun getSignedRequest(user: User, payload: SignRequestParams): SignRequestParams { + val body = JSONObject(payload).toString().toRequestBody(JSON_MEDIA_TYPE) + val request = requestFor("users/" + user.id + "/token/sign").method("POST", body).build() + + return makeRequest(request, SIGNED_REQUEST_READER) + } + + override fun deregisterUser(serviceUserId: String, userAccessToken: String) { + val request = + requestFor("source-clients/garmin/authorization/$serviceUserId?accessToken=$userAccessToken") + .method("DELETE", EMPTY_BODY).build() + return makeRequest(request, null) + } + + override fun findByExternalId(externalId: String): User { + return super.findByExternalId(externalId) + } + + override fun hasPendingUpdates(): Boolean { + val now = Instant.now() + return now.isAfter(nextFetch) + } + + @Throws(IOException::class) + override fun applyPendingUpdates() { + logger.info("Requesting user information from webservice") + val request = requestFor("users?source-type=Garmin").build() + timedCachedUsers = makeRequest(request, USER_LIST_READER).users + + nextFetch = Instant.now().plus(FETCH_THRESHOLD) + } + + @Throws(IOException::class) + private fun requestFor(relativeUrl: String): Request.Builder { + val url: HttpUrl = baseUrl.resolve(relativeUrl) + ?: throw IllegalArgumentException("Relative URL is invalid") + val builder: Request.Builder = Request.Builder().url(url) + val authorization = requestAuthorization() + builder.addHeader("Authorization", authorization) + + return builder + } + + @Throws(IOException::class) + private fun requestAuthorization(): String { + return try { + "Bearer " + repositoryClient.validToken.accessToken + } catch (ex: TokenException) { + throw IOException(ex) + } + + } + + @Throws(IOException::class) + private fun makeRequest(request: Request, reader: ObjectReader?): T { + logger.info("Requesting info from {}", request.url) + client.newCall(request).execute().use { response -> + val body: ResponseBody? = response.body + if (response.code == 404) { + throw NoSuchElementException("URL " + request.url + " does not exist") + } else if (!response.isSuccessful || body == null) { + var message = "Failed to make request (HTTP status code " + response.code + ')' + if (body != null) { + message += body.string() + } + throw IOException(message) + } + val bodyString = body.string() + return try { + if (reader == null) "" as T + else reader.readValue(bodyString) + } catch (ex: JsonProcessingException) { + logger.error("Failed to parse JSON: {}\n{}", ex.toString(), bodyString) + throw ex + } + } + } + + private fun String.toHttpUrl(): HttpUrl { + var urlString: String = this.trim() + if (urlString[urlString.length - 1] != '/') urlString += '/' + + return urlString.toHttpUrlOrNull() + ?: throw NoSuchElementException("User repository URL $urlString cannot be parsed as URL.") + } + + companion object { + private val JSON_FACTORY = JsonFactory() + private val JSON_READER: ObjectReader = + ObjectMapper(JSON_FACTORY).registerModule(JavaTimeModule()).reader() + private val USER_LIST_READER: ObjectReader = JSON_READER.forType(Users::class.java) + private val USER_READER: ObjectReader = JSON_READER.forType(GarminUser::class.java) + + private val OAUTH_READER: ObjectReader = + JSON_READER.forType(OAuth1UserCredentials::class.java) + private val SIGNED_REQUEST_READER: ObjectReader = + JSON_READER.forType(SignRequestParams::class.java) + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + private val EMPTY_BODY: RequestBody = "".toRequestBody(JSON_MEDIA_TYPE) + + private val FETCH_THRESHOLD: Duration = Duration.ofMinutes(1L) + val MIN_INSTANT = Instant.EPOCH + + private val logger = LoggerFactory.getLogger(GarminServiceUserRepository::class.java) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminUser.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminUser.kt new file mode 100644 index 0000000..bfc65ed --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminUser.kt @@ -0,0 +1,28 @@ +package org.radarbase.push.integration.garmin.user + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import java.time.Instant + +@JsonIgnoreProperties(ignoreUnknown = true) +data class GarminUser( + @JsonProperty("id") override val id: String, + @JsonProperty("createdAt") override val createdAt: Instant, + @JsonProperty("projectId") override val projectId: String, + @JsonProperty("userId") override val userId: String, + @JsonProperty("humanReadableUserId") override val humanReadableUserId: String?, + @JsonProperty("sourceId") override val sourceId: String, + @JsonProperty("externalId") override val externalId: String?, + @JsonProperty("isAuthorized") override val isAuthorized: Boolean, + @JsonProperty("startDate") override val startDate: Instant, + @JsonProperty("endDate") override val endDate: Instant, + @JsonProperty("version") override val version: String? = null, + @JsonProperty("serviceUserId") override val serviceUserId: String +) : User { + + override val observationKey: ObservationKey = ObservationKey(projectId, userId, sourceId) + override val versionedId: String = "$id${version?.let { "#$it" } ?: ""}" + +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminUserRepository.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminUserRepository.kt new file mode 100644 index 0000000..839247a --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/user/GarminUserRepository.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2018 The Hyve + * + * 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 org.radarbase.push.integration.garmin.user + +import org.radarbase.gateway.Config +import org.radarbase.push.integration.common.auth.SignRequestParams +import org.radarbase.push.integration.common.user.User +import org.radarbase.push.integration.common.user.UserRepository +import java.io.IOException +import java.time.Instant +import javax.ws.rs.NotAuthorizedException + +/** + * User repository for Garmin users. + */ +abstract class GarminUserRepository(private val config: Config) : UserRepository { + + /** + * Garmin uses Oauth 1.0 and hence has a user access + * token secret instead of a refresh token. This should + * not be required in most cases anyways since only the access token + * is required. + */ + @Throws(IOException::class, NotAuthorizedException::class) + override fun getRefreshToken(user: User): String { + return getUserAccessTokenSecret(user) + } + + @Throws(IOException::class, NotAuthorizedException::class) + abstract fun getUserAccessTokenSecret(user: User): String + + fun getBackfillStartDate(user: User): Instant { + return config.pushIntegration.garmin.backfill.userBackfill.find { + it.userId == user.versionedId + }?.startDate ?: user.startDate + } + + fun getBackfillEndDate(user: User): Instant { + return config.pushIntegration.garmin.backfill.userBackfill.find { + it.userId == user.versionedId + }?.endDate?.takeIf { it <= user.endDate } + ?: config.pushIntegration.garmin.backfill.defaultEndDate.takeIf { it < user.endDate } + ?: user.createdAt + } + + abstract fun getSignedRequest(user: User, payload: SignRequestParams): SignRequestParams + + + /** + * This is to deregister the users from garmin. It requires serviceUserId and userAccessToken. + * + * */ + abstract fun deregisterUser(serviceUserId: String, userAccessToken: String) +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/user/OAuth1UserCredentials.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/user/OAuth1UserCredentials.kt new file mode 100644 index 0000000..aae442e --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/user/OAuth1UserCredentials.kt @@ -0,0 +1,11 @@ +package org.radarbase.push.integration.garmin.user + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +data class OAuth1UserCredentials( + @JsonProperty("accessToken") var accessToken: String +) + + diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/RedisHolder.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/RedisHolder.kt new file mode 100644 index 0000000..42d38ce --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/RedisHolder.kt @@ -0,0 +1,24 @@ +package org.radarbase.push.integration.garmin.util + +import redis.clients.jedis.Jedis +import redis.clients.jedis.JedisPool +import redis.clients.jedis.exceptions.JedisException +import java.io.Closeable +import java.io.IOException + +class RedisHolder(private val jedisPool: JedisPool): Closeable { + @Throws(IOException::class) + fun execute(routine: (Jedis) -> T): T { + return try { + jedisPool.resource.use { + routine(it) + } + } catch (ex: JedisException) { + throw IOException(ex) + } + } + + override fun close() { + jedisPool.close() + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/RedisRemoteLockManager.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/RedisRemoteLockManager.kt new file mode 100644 index 0000000..c9ab2a7 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/RedisRemoteLockManager.kt @@ -0,0 +1,45 @@ +package org.radarbase.push.integration.garmin.util + +import org.slf4j.LoggerFactory +import redis.clients.jedis.params.SetParams +import java.time.Duration +import java.util.* + +class RedisRemoteLockManager( + private val redisHolder: RedisHolder, + private val keyPrefix: String +) : RemoteLockManager { + private val uuid: String = UUID.randomUUID().toString() + + init { + logger.info("Managing locks as ID {}", uuid) + } + + override fun acquireLock(name: String): RemoteLockManager.RemoteLock? { + val lockKey = "$keyPrefix/$name.lock" + return redisHolder.execute { redis -> + redis.set(lockKey, uuid, setParams)?.let { + RemoteLock(lockKey) + } + } + } + + private inner class RemoteLock( + private val lockKey: String + ) : RemoteLockManager.RemoteLock { + override fun close() { + return redisHolder.execute { redis -> + if (redis.get(lockKey) == uuid) { + redis.del(lockKey) + } + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(RedisRemoteLockManager::class.java) + private val setParams = SetParams() + .nx() // only set if not already set + .px(Duration.ofDays(1).toMillis()) // limit the duration of a lock to 24 hours + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/RemoteLockManager.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/RemoteLockManager.kt new file mode 100644 index 0000000..dbbee31 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/RemoteLockManager.kt @@ -0,0 +1,12 @@ +package org.radarbase.push.integration.garmin.util + +import java.io.Closeable + +interface RemoteLockManager { + fun acquireLock(name: String): RemoteLock? + fun tryRunLocked(name: String, action: () -> T): T? = acquireLock(name)?.use { + action() + } + + interface RemoteLock: Closeable +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/OffsetPersistenceFactory.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/OffsetPersistenceFactory.kt new file mode 100644 index 0000000..4b196fe --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/OffsetPersistenceFactory.kt @@ -0,0 +1,21 @@ +package org.radarbase.push.integration.garmin.util.offset + +import java.io.Closeable +import java.io.Flushable +import java.nio.file.Path + +/** + * Accesses a OffsetRange file using the CSV format. On construction, this will create the file if + * not present. + */ +interface OffsetPersistenceFactory { + /** + * Read offsets from the persistence store. On error, this will return null. + */ + fun read(path: String): Offsets? + + /** + * Add a specific Offset to the provided path. + */ + fun add(path: Path, offset: UserRouteOffset) +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/OffsetRedisPersistence.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/OffsetRedisPersistence.kt new file mode 100644 index 0000000..5a9d939 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/OffsetRedisPersistence.kt @@ -0,0 +1,81 @@ +package org.radarbase.push.integration.garmin.util.offset + +import com.fasterxml.jackson.databind.ObjectReader +import com.fasterxml.jackson.databind.ObjectWriter +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.radarbase.push.integration.garmin.util.RedisHolder +import org.slf4j.LoggerFactory +import java.io.IOException +import java.nio.file.Path +import java.time.Instant + +/** + * Accesses a OffsetRange json object a Redis entry. + */ +class OffsetRedisPersistence( + private val redisHolder: RedisHolder +) : OffsetPersistenceFactory { + + override fun read(path: String): Offsets? { + return try { + redisHolder.execute { redis -> + redis[path]?.let { value -> + redisOffsetReader.readValue(value) + .offsets + .fold(Offsets(), { set, (userId, route, offset) -> + set.apply { add(UserRouteOffset(userId, route, offset)) } + }) + } + } + } catch (ex: IOException) { + logger.error( + "Error reading offsets from Redis: {}. Processing all offsets.", + ex.toString() + ) + null + } + } + + /** + * Read the specified Path in Redis and adds the given UserRouteOffset to the offsets. + */ + override fun add(path: Path, offset: UserRouteOffset) { + val offsets: Offsets = (read(path.toString()) ?: Offsets()).apply { add(offset) } + val redisOffsets = RedisOffsets(offsets.offsetsMap.map { (userRoute, offset) -> + RedisOffset( + userRoute.userId, + userRoute.route, + offset + ) + }) + try { + redisHolder.execute { redis -> + redis.set(path.toString(), redisOffsetWriter.writeValueAsString(redisOffsets)) + } + } catch (e: IOException) { + logger.error("Failed to write offsets to Redis: {}", e.toString()) + } + } + + companion object { + data class RedisOffsets( + val offsets: List + ) + + data class RedisOffset( + val userId: String, + val route: String, + val offset: Instant + ) + + private val logger = LoggerFactory.getLogger(OffsetRedisPersistence::class.java) + private val mapper = jacksonObjectMapper().apply { + registerModule(JavaTimeModule()) + configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + } + val redisOffsetWriter: ObjectWriter = mapper.writerFor(RedisOffsets::class.java) + val redisOffsetReader: ObjectReader = mapper.readerFor(RedisOffsets::class.java) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/Offsets.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/Offsets.kt new file mode 100644 index 0000000..8f735cf --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/Offsets.kt @@ -0,0 +1,15 @@ +package org.radarbase.push.integration.garmin.util.offset + +import java.time.Instant +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +class Offsets(val offsetsMap: ConcurrentMap = ConcurrentHashMap()) { + fun add(userRouteOffset: UserRouteOffset) { + offsetsMap[userRouteOffset.userRoute] = userRouteOffset.offset + } + + fun addAll(offsets: Offsets) { + offsetsMap.putAll(offsets.offsetsMap) + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/UserRoute.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/UserRoute.kt new file mode 100644 index 0000000..8fbb676 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/UserRoute.kt @@ -0,0 +1,26 @@ +package org.radarbase.push.integration.garmin.util.offset + +import java.util.* + +class UserRoute(val userId: String, val route: String) : Comparable { + private val hash = Objects.hash(userId, route) + + override fun hashCode(): Int = hash + + override fun compareTo(other: UserRoute): Int = compareValuesBy( + this, other, + UserRoute::userId, UserRoute::route + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserRoute + + if (userId != other.userId) return false + if (route != other.route) return false + + return true + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/UserRouteOffset.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/UserRouteOffset.kt new file mode 100644 index 0000000..d80cdca --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/util/offset/UserRouteOffset.kt @@ -0,0 +1,23 @@ +package org.radarbase.push.integration.garmin.util.offset + +import com.fasterxml.jackson.annotation.JsonIgnore +import java.time.Instant + +class UserRouteOffset(val userRoute: UserRoute, val offset: Instant) { + @JsonIgnore + val userId: String = userRoute.userId + + @JsonIgnore + val route: String = userRoute.route + + constructor(userId: String, route: String, offset: Instant): this( + UserRoute(userId, route), + offset + ) + + override fun toString(): String { + return "$userId+$route ($offset)" + } + + +} diff --git a/src/test/kotlin/org/radarbase/gateway/io/BinaryToAvroConverterTest.kt b/src/test/kotlin/org/radarbase/gateway/io/BinaryToAvroConverterTest.kt deleted file mode 100644 index 5dbc244..0000000 --- a/src/test/kotlin/org/radarbase/gateway/io/BinaryToAvroConverterTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.radarbase.gateway.io - -import com.fasterxml.jackson.databind.JsonNode -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import okio.Buffer -import org.apache.avro.generic.GenericRecordBuilder -import org.apache.kafka.clients.producer.ProducerRecord -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.radarbase.data.AvroRecordData -import org.radarbase.gateway.AuthConfig -import org.radarbase.gateway.Config -import org.radarbase.jersey.auth.Auth -import org.radarbase.producer.rest.BinaryRecordRequest -import org.radarbase.producer.rest.ParsedSchemaMetadata -import org.radarbase.producer.rest.SchemaRetriever -import org.radarbase.topic.AvroTopic -import org.radarcns.auth.authorization.Permission -import org.radarcns.auth.token.RadarToken -import org.radarcns.kafka.ObservationKey -import org.radarcns.passive.phone.PhoneAcceleration - -class BinaryToAvroConverterTest { - @Test - fun testConversion() { - - val topic = AvroTopic("test", - ObservationKey.getClassSchema(), PhoneAcceleration.getClassSchema(), - ObservationKey::class.java, PhoneAcceleration::class.java) - - val keySchemaMetadata = ParsedSchemaMetadata(1, 1, topic.keySchema) - val valueSchemaMetadata = ParsedSchemaMetadata(2, 1, topic.valueSchema) - - val requestRecordData = AvroRecordData(topic, - ObservationKey("p", "u", "s"), - listOf( - PhoneAcceleration(1.0, 1.1, 1.2f, 1.3f, 1.4f), - PhoneAcceleration(2.0, 2.1, 2.2f, 2.3f, 2.4f), - )) - val binaryRequest = BinaryRecordRequest(topic) - binaryRequest.prepare(keySchemaMetadata, valueSchemaMetadata, requestRecordData) - val requestBuffer = Buffer() - binaryRequest.writeToSink(requestBuffer) - - val schemaRetriever = mock { - on { getBySubjectAndVersion("test", false, 1) } doReturn keySchemaMetadata - on { getBySubjectAndVersion("test", true, 1) } doReturn valueSchemaMetadata - } - - val token = mock { - on { hasPermissionOnSource(Permission.MEASUREMENT_CREATE, "p", "u", "s") } doReturn true - } - - val auth = object : Auth { - override val token: RadarToken = token - - override fun getClaim(name: String): JsonNode { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override val defaultProject: String = "p" - override val userId: String = "u" - - override fun hasRole(projectId: String, role: String) = true - } - val converter = BinaryToAvroConverter(schemaRetriever, auth, Config()) - - val genericKey = GenericRecordBuilder(ObservationKey.getClassSchema()).apply { - this["projectId"] = "p" - this["userId"] = "u" - this["sourceId"] = "s" - }.build() - val genericValue1 = GenericRecordBuilder(PhoneAcceleration.getClassSchema()).apply { - set("time", 1.0) - set("timeReceived", 1.1) - set("x", 1.2f) - set("y", 1.3f) - set("z", 1.4f) - }.build() - val genericValue2 = GenericRecordBuilder(PhoneAcceleration.getClassSchema()).apply { - set("time", 2.0) - set("timeReceived", 2.1) - set("x", 2.2f) - set("y", 2.3f) - set("z", 2.4f) - }.build() - - assertEquals( - AvroProcessingResult(1, 2, listOf( - Pair(genericKey, genericValue1), - Pair(genericKey, genericValue2), - )), - converter.process("test", requestBuffer.inputStream())) - } -}