diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 000000000..8571e5738 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,49 @@ +name: master + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-20.04 + + strategy: + matrix: + jdk: [3-openjdk-17-slim, 3-jdk-14, 3-jdk-8-slim] + influxdb: ['1.1', '1.6', '1.8', '2.3', '2.4', '2.5'] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Build project + env: + MAVEN_JAVA_VERSION: "${{ matrix.jdk }}" + INFLUXDB_VERSION: "${{ matrix.influxdb }}" + run: ./compile-and-test.sh + + - name: codecov + run: | + sudo apt-get update + sudo apt-get install gpg libdigest-sha-perl -y + curl -Os https://uploader.codecov.io/latest/linux/codecov + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig + curl -s https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import + gpgv codecov.SHA256SUM.sig codecov.SHA256SUM + shasum -a 256 -c codecov.SHA256SUM + chmod +x ./codecov + ./codecov + if: matrix.influxdb != '2.3' && matrix.influxdb != '2.4' && matrix.influxdb != '2.5' + + + # deploy: + # runs-on: ubuntu-20.04 + + # steps: + # - name: deploy snapshot + # env: + # secure: dAJK41xM2dN3q3xJMqAOP6uvrOvpjjUzmHr8mYNyepER8Lpms9/GqVUxqJv12wzCBqv1XZk/CXxrv3iBc2XjlxlrzIJGQChTinwDEigv0BMl/Gh0821ja7gwzMEUmg9f79m5tJxIFQ306cWz1gyRDqM3fLzskvM2ayzvynsNc/w= + # run: ./deploy-snapshot.sh diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 000000000..2a7b043e7 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,43 @@ +name: Build from pull request + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + jdk: [3-openjdk-17-slim, 3-jdk-14, 3-jdk-8-slim] + influxdb: ['1.1', '1.6', '1.8', '2.3', '2.4', '2.5'] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Figure out if running fork PR + id: fork + run: '["${{ secrets.DOCKER_REGISTRY_TOKEN }}" == ""] && echo "::set-output name=is_fork_pr::true" || echo "::set-output name=is_fork_pr::false"' + + - name: Build project + env: + MAVEN_JAVA_VERSION: "${{ matrix.jdk }}" + INFLUXDB_VERSION: "${{ matrix.influxdb }}" + run: ./compile-and-test.sh + + - name: codecov + run: | + sudo apt-get update + sudo apt-get install gpg libdigest-sha-perl -y + curl -Os https://uploader.codecov.io/latest/linux/codecov + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig + curl -s https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import + gpgv codecov.SHA256SUM.sig codecov.SHA256SUM + shasum -a 256 -c codecov.SHA256SUM + chmod +x ./codecov + ./codecov + if: matrix.influxdb != '2.3' && matrix.influxdb != '2.4' && matrix.influxdb != '2.5' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ee46082b1..000000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -sudo: required - -services: - - docker - -before_install: - - sudo apt-get update -qq - - sudo apt-get install -qq libxml2-utils - -# We test against all influxdb versions with the most actual JDK. -# Test only the most recent influxdb version with JDK8 which will be legacy soon. -env: - global: - - secure: aCFuNNixYILZZfcOp0jI66om4w8Cbr+qtz1FUOVgbxeipFA93y7KLoXtFajkE1qmbHOdz8SvPcloY5q6HKIOYuqv4/6at+OaTogXELfrR+oKfxf2t5PfB1D+1RxM90+wjsEPGQh+HmFOEnGCnJ8ZUfcx76Afs+mPzbpjzKeNz7w= - - secure: dAJK41xM2dN3q3xJMqAOP6uvrOvpjjUzmHr8mYNyepER8Lpms9/GqVUxqJv12wzCBqv1XZk/CXxrv3iBc2XjlxlrzIJGQChTinwDEigv0BMl/Gh0821ja7gwzMEUmg9f79m5tJxIFQ306cWz1gyRDqM3fLzskvM2ayzvynsNc/w= - - matrix: - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.8 - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.7 - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.6 - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.5 - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.4 - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.3 - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.2 - - MAVEN_JAVA_VERSION=3-jdk-14 INFLUXDB_VERSION=1.1 - - MAVEN_JAVA_VERSION=3-jdk-8-slim INFLUXDB_VERSION=1.8 SNAPSHOT=Y - -script: "./compile-and-test.sh" - -deploy: - provider: script - script: "./deploy-snapshot.sh" - skip_cleanup: true - on: - repo: influxdata/influxdb-java - condition: $SNAPSHOT == "Y" && $DEPLOY_SNAPSHOT == "Y" - branch: master - -after_success: - - bash <(curl -s https://codecov.io/bash) - -after_failure: - - cat target/surefire-reports/*.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f46a7d82..0cdc03965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Changelog -## 2.22 [unreleased] +## 2.25 [2025-03-26] + +### Improvements +- Add support for parameter binding to built queries [PR #1010](https://github.com/influxdata/influxdb-java/pull/1010) + +## 2.24 [2023-12-14] + +### Improvements +- `allFields` mode to Measurement annotation [PR #972](https://github.com/influxdata/influxdb-java/pull/972) +- Support generic POJO super classes [PR #980](https://github.com/influxdata/influxdb-java/pull/980) + +## 2.23 [2022-07-07] + +### Improvements +- Add implementation information to `Jar` manifest [PR #847](https://github.com/influxdata/influxdb-java/pull/847) + +### Fixes +- Only the request to /write endpoint should be compressed by GZIP [PR #851](https://github.com/influxdata/influxdb-java/pull/851) + +## 2.22 [2021-09-17] + +### Improvements + +- `POST` query variants serializes `'q'` parameter into HTTP body [PR #765](https://github.com/influxdata/influxdb-java/pull/765) ## 2.21 [2020-12-04] diff --git a/MANUAL.md b/MANUAL.md index 254eca360..99e1248b5 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -17,24 +17,33 @@ influxDB.query(new Query("CREATE RETENTION POLICY " + retentionPolicyName + " ON " + databaseName + " DURATION 1d REPLICATION 1 DEFAULT")); influxDB.setRetentionPolicy(retentionPolicyName); // (3) -influxDB.enableBatch(BatchOptions.DEFAULTS); // (4) - -influxDB.write(Point.measurement("h2o_feet") // (5) +influxDB.enableBatch( + BatchOptions.DEFAULTS + .threadFactory(runnable -> { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + return thread; + }) +); // (4) + +Runtime.getRuntime().addShutdownHook(new Thread(influxDB::close)); // (5) + +influxDB.write(Point.measurement("h2o_feet") // (6) .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS) .tag("location", "santa_monica") .addField("level description", "below 3 feet") .addField("water_level", 2.064d) .build()); -influxDB.write(Point.measurement("h2o_feet") // (5) +influxDB.write(Point.measurement("h2o_feet") // (6) .tag("location", "coyote_creek") .addField("level description", "between 6 and 9 feet") .addField("water_level", 8.12d) - .build()); // (6) + .build()); -Thread.sleep(5_000L); // (7) +Thread.sleep(5_000L); -QueryResult queryResult = influxDB.query(new Query("SELECT * FROM h2o_feet")); +QueryResult queryResult = influxDB.query(new Query("SELECT * FROM h2o_feet")); // (7) System.out.println(queryResult); // It will print something like: @@ -44,8 +53,6 @@ System.out.println(queryResult); // [2020-03-22T20:50:12.929Z, below 3 feet, santa_monica, 2.064], // [2020-03-22T20:50:12.929Z, between 6 and 9 feet, coyote_creek, 8.12] // ]]], error=null]], error=null] - -influxDB.close(); // (8) ``` ### Connecting to InfluxDB @@ -137,13 +144,26 @@ With batching enabled the client provides two strategies how to deal with errors When new data points are written before the previous (failed) points are successfully written, those are queued inside the client and wait until older data points are successfully written. Size of this queue is limited and configured by `BatchOptions.bufferLimit` property. When the limit is reached, the oldest points in the queue are dropped. 'Retry on error' strategy is used when individual write batch size defined by `BatchOptions.actions` is lower than `BatchOptions.bufferLimit`. -Note: +#### Ensure application exit when batching is enabled +`BatchOptions.DEFAULTS` creates a non-daemon thread pool which prevents the JVM from initiating shutdown in the case of +exceptions or successful completion of the main thread. This will prevent shutdown hooks (many frameworks and plain JVM +applications use these to close/ cleanup resources) from running, preventing graceful termination of the application. + +Thus, configuring batch options with a daemon thread pool will solve this issue and will for example ensure that the registered +(5) shutdown hook is run to close the `InfluxDB` client properly (flushing and closing of resources will happen). + +### Close InfluxDB Client on JVM Termination +(5) In order to ensure that in-flight points are flushed and resources are released properly, it is essential to call +`influxDB.close()` the client when it is no longer required. + +Registering a shutdown hook is a good way to ensure that this is done on application termination regardless of exceptions +that are thrown in the main thread of the code. Note that if you are using a framework, do check the documentation for its +way of configuring shutdown lifecycle hooks or if it might already be calling `close` automatically. -* Batching functionality creates an internal thread pool that needs to be shutdown explicitly as part of a graceful application shutdown or the application will terminate properly. To do so, call `influxDB.close()`. ### Writing to InfluxDB -(5) ... +(6) ... `----8<----BEGIN DRAFT----8<----` @@ -303,21 +323,21 @@ public class Cpu { } ``` -2. Add @Measurement,@TimeColumn and @Column annotations: +2. Add @Measurement, @TimeColumn and @Column annotations (column names default to field names unless otherwise specified): ```Java @Measurement(name = "cpu") public class Cpu { @TimeColumn - @Column(name = "time") + @Column private Instant time; @Column(name = "host", tag = true) private String hostname; - @Column(name = "region", tag = true) + @Column(tag = true) private String region; - @Column(name = "idle") + @Column private Double idle; - @Column(name = "happydevop") + @Column private Boolean happydevop; @Column(name = "uptimesecs") private Long uptimeSecs; @@ -325,6 +345,43 @@ public class Cpu { } ``` +Alternatively, you can use: + +```Java +@Measurement(name = "cpu", allFields = true) +public class Cpu { + @TimeColumn + private Instant time; + @Column(name = "host", tag = true) + private String hostname; + @Column(tag = true) + private String region; + private Double idle; + private Boolean happydevop; + @Column(name = "uptimesecs") + private Long uptimeSecs; + // getters (and setters if you need) +} +``` + +Or (if you're on JDK14+ and/or [Android SDK34+](https://android-developers.googleblog.com/2023/06/records-in-android-studio-flamingo.html)): + +```Java +@Measurement(name = "cpu", allFields = true) +public record Cpu( + @TimeColumn + Instant time, + @Column(name = "host", tag = true) + String hostname, + @Column(tag = true) + String region, + Double idle, + Boolean happydevop, + @Column(name = "uptimesecs") + Long uptimeSecs +) {} +``` + 3. Call _InfluxDBResultMapper.toPOJO(...)_ to map the QueryResult to your POJO: ```java @@ -363,6 +420,25 @@ influxDB.write(dbName, rpName, point); An alternative way to create InfluxDB queries is available. By using the [QueryBuilder](QUERY_BUILDER.md) you can create queries using java instead of providing the influxdb queries as strings. +#### Generic POJO super classes + +POJO classes can have generic super classes, for cases where multiple measurements have a similar structure, and differ by type(s), as in: + +```java +public class SuperMeasurement { + @Column + @TimeColumn + private Instant time; + @Column + T value; + // Other common columns and tags +} + +public class SubMeasurement extends SuperMeasurement { + // Any specific columns and tags +} +``` + ### InfluxDBMapper In case you want to save and load data using models you can use the [InfluxDBMapper](INFLUXDB_MAPPER.md). diff --git a/QUERY_BUILDER.md b/QUERY_BUILDER.md index 5f500b8e4..d84e6d255 100644 --- a/QUERY_BUILDER.md +++ b/QUERY_BUILDER.md @@ -588,3 +588,19 @@ Query select = select().raw("an expression on select").from(dbName, "cpu").where ```sqlite-psql SELECT an expression on select FROM h2o_feet WHERE an expression as condition; ``` + +Binding parameters + +If your Query is based on user input, it is good practice to use parameter binding to avoid [injection attacks](https://en.wikipedia.org/wiki/SQL_injection). +You can create queries with parameter binding: + +```java +Query query = select().from(DATABASE,"h2o_feet").where(gt("water_level", FunctionFactory.placeholder("level"))) + .bindParameter("level", 8); +``` + +```sqlite-psql +SELECT * FROM h2o_feet WHERE water_level > $level; +``` + +The values of bindParameter() calls are bound to the placeholders in the query (`level`). diff --git a/README.md b/README.md index 81bb748cc..74f6a5ba1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # influxdb-java -[![Build Status](https://travis-ci.org/influxdata/influxdb-java.svg?branch=master)](https://travis-ci.org/influxdata/influxdb-java) +[![Build Status](https://github.com/influxdata/influxdb-java/workflows/master/badge.svg)](https://github.com/influxdata/influxdb-java/actions) [![codecov.io](http://codecov.io/github/influxdata/influxdb-java/coverage.svg?branch=master)](http://codecov.io/github/influxdata/influxdb-java?branch=master) [![Issue Count](https://codeclimate.com/github/influxdata/influxdb-java/badges/issue_count.svg)](https://codeclimate.com/github/influxdata/influxdb-java) This is the official (and community-maintained) Java client library for [InfluxDB](https://www.influxdata.com/products/influxdb-overview/) (1.x), the open source time series database that is part of the TICK (Telegraf, InfluxDB, Chronograf, Kapacitor) stack. -_Note: This library is for use with InfluxDB 1.x. For connecting to InfluxDB 2.x instances, please use the [influxdb-client-java](https://github.com/influxdata/influxdb-client-java) client._ +For InfluxDB 3.0 users, this library is succeeded by the lightweight [v3 client library](https://github.com/InfluxCommunity/influxdb3-java). + +_Note: This library is for use with InfluxDB 1.x and [2.x compatibility API](https://docs.influxdata.com/influxdb/v2.0/reference/api/influxdb-1x/). For full supports of InfluxDB 2.x features, please use the [influxdb-client-java](https://github.com/influxdata/influxdb-client-java) client._ ## Adding the library to your project @@ -66,7 +68,17 @@ influxDB.query(new Query("CREATE RETENTION POLICY " + retentionPolicyName influxDB.setRetentionPolicy(retentionPolicyName); // Enable batch writes to get better performance. -influxDB.enableBatch(BatchOptions.DEFAULTS); +influxDB.enableBatch( + BatchOptions.DEFAULTS + .threadFactory(runnable -> { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + return thread; + }) +); + +// Close it if your application is terminating or you are not using it anymore. +Runtime.getRuntime().addShutdownHook(new Thread(influxDB::close)); // Write points to InfluxDB. influxDB.write(Point.measurement("h2o_feet") @@ -100,9 +112,6 @@ System.out.println(queryResult); // [2020-03-22T20:50:12.929Z, below 3 feet, santa_monica, 2.064], // [2020-03-22T20:50:12.929Z, between 6 and 9 feet, coyote_creek, 8.12] // ]]], error=null]], error=null] - -// Close it if your application is terminating or you are not using it anymore. -influxDB.close(); ``` ## Contribute diff --git a/compile-and-test.sh b/compile-and-test.sh index 3ab581954..633ff51e2 100755 --- a/compile-and-test.sh +++ b/compile-and-test.sh @@ -5,7 +5,7 @@ set -e DEFAULT_INFLUXDB_VERSION="1.8" -DEFAULT_MAVEN_JAVA_VERSION="3-jdk-14" +DEFAULT_MAVEN_JAVA_VERSION="3-openjdk-17-slim" INFLUXDB_VERSION="${INFLUXDB_VERSION:-$DEFAULT_INFLUXDB_VERSION}" MAVEN_JAVA_VERSION="${MAVEN_JAVA_VERSION:-$DEFAULT_MAVEN_JAVA_VERSION}" @@ -20,6 +20,12 @@ docker run \ --publish 8086:8086 \ --publish 8089:8089/udp \ --volume ${PWD}/influxdb.conf:/etc/influxdb/influxdb.conf \ + --env DOCKER_INFLUXDB_INIT_MODE=setup \ + --env DOCKER_INFLUXDB_INIT_USERNAME=my-user \ + --env DOCKER_INFLUXDB_INIT_PASSWORD=my-password \ + --env DOCKER_INFLUXDB_INIT_ORG=my-org \ + --env DOCKER_INFLUXDB_INIT_BUCKET=my-bucket \ + --env DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-token \ influxdb:${INFLUXDB_VERSION}-alpine echo "Starting Nginx" @@ -38,8 +44,22 @@ docker run \ echo "Running tests" PROXY_API_URL=http://nginx:8080/influx-api/ PROXY_UDP_PORT=8080 +if [[ "$INFLUXDB_VERSION" == "2."* ]] +then + TEST_EXPRESSION="InfluxDB2Test" + # Wait to start InfluxDB + docker run --link influxdb:influxdb ubuntu:20.04 bash -c "apt-get update \ + && apt-get install wget --yes \ + && wget -S --spider --tries=20 --retry-connrefused --waitretry=5 http://influxdb:8086/ping" + # Create DBRP Mapping + BUCKET_ID=$(docker exec influxdb bash -c "influx bucket list -o my-org -n my-bucket | grep my-bucket | xargs | cut -d ' ' -f 0") + docker exec influxdb bash -c "influx v1 dbrp create -o my-org --db mydb --rp autogen --default --bucket-id ${BUCKET_ID}" + docker exec influxdb bash -c "influx v1 auth create -o my-org --username my-user --password my-password --read-bucket ${BUCKET_ID} --write-bucket ${BUCKET_ID}" +else + TEST_EXPRESSION="*" +fi -docker run -it --rm \ +docker run --rm \ --volume ${PWD}:/usr/src/mymaven \ --volume ${PWD}/.m2:/root/.m2 \ --workdir /usr/src/mymaven \ @@ -49,7 +69,7 @@ docker run -it --rm \ --env INFLUXDB_IP=influxdb \ --env PROXY_API_URL=${PROXY_API_URL} \ --env PROXY_UDP_PORT=${PROXY_UDP_PORT} \ - maven:${MAVEN_JAVA_VERSION} mvn clean install + maven:${MAVEN_JAVA_VERSION} mvn clean install -Dtest="${TEST_EXPRESSION}" docker kill influxdb || true docker kill nginx || true diff --git a/deploy-snapshot.sh b/deploy-snapshot.sh index f413424d8..60a454ec2 100755 --- a/deploy-snapshot.sh +++ b/deploy-snapshot.sh @@ -2,11 +2,12 @@ set -e -#Parse project version from pom.xml -export PROJECT_VERSION=`xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml` +# Parse project version from pom.xml +PROJECT_VERSION=$(xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml) +export PROJECT_VERSION echo "Project version: $PROJECT_VERSION" -#Skip if not *SNAPSHOT +# Skip if not *SNAPSHOT if [[ $PROJECT_VERSION != *SNAPSHOT ]]; then echo "$PROJECT_VERSION is not SNAPSHOT - skip deploy."; exit; @@ -17,7 +18,7 @@ DEFAULT_MAVEN_JAVA_VERSION="3-jdk-8-slim" MAVEN_JAVA_VERSION="${MAVEN_JAVA_VERSION:-$DEFAULT_MAVEN_JAVA_VERSION}" echo "Deploy snapshot with maven:${MAVEN_JAVA_VERSION}" -docker run -it --rm \ +docker run --rm \ --volume ${PWD}:/usr/src/mymaven \ --volume ${PWD}/.m2:/root/.m2 \ --workdir /usr/src/mymaven \ diff --git a/mvn.sh b/mvn.sh index 01413b449..cd99af3e6 100755 --- a/mvn.sh +++ b/mvn.sh @@ -8,4 +8,4 @@ docker run -it --rm \ -v $PWD:/usr/src/mymaven \ -v $PWD/.m2:/root/.m2 \ -w /usr/src/mymaven \ - maven:3-jdk-14 mvn clean "$@" + maven:3-openjdk-17-slim mvn clean "$@" diff --git a/pom.xml b/pom.xml index 06bd41bfb..21ec65cf4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.influxdb influxdb-java jar - 2.22-SNAPSHOT + 2.26-SNAPSHOT influxdb java bindings Java API to access the InfluxDB REST API http://www.influxdb.org @@ -24,7 +24,7 @@ scm:git:git@github.com:influxdata/influxdb-java.git scm:git:git@github.com:influxdata/influxdb-java.git git@github.com:influxdata/influxdb-java.git - HEAD + influxdb-java-2.25 @@ -52,11 +52,11 @@ ossrh - https://oss.sonatype.org/content/repositories/snapshots + https://central.sonatype.com/repository/maven-snapshots ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/ @@ -75,56 +75,67 @@ org.codehaus.mojo versions-maven-plugin - 2.8.1 + 2.16.2 org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.12.1 1.8 1.8 + + + -parameters + org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.2.5 org.apache.maven.plugins maven-site-plugin - 3.9.1 + 3.12.1 org.apache.maven.plugins maven-clean-plugin - 3.1.0 + 3.3.2 org.apache.maven.plugins maven-deploy-plugin - 3.0.0-M1 + 3.1.1 org.apache.maven.plugins maven-install-plugin - 3.0.0-M1 + 3.1.1 org.apache.maven.plugins maven-jar-plugin - 3.2.0 + 3.3.0 + + + + true + + + org.apache.maven.plugins maven-resources-plugin - 3.2.0 + 3.3.1 org.apache.maven.plugins maven-release-plugin - 3.0.0-M1 + 3.0.1 @@ -132,7 +143,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0-M3 + 3.4.1 enforce-maven @@ -152,12 +163,13 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.8 + 1.6.13 true ossrh - https://oss.sonatype.org/ + https://ossrh-staging-api.central.sonatype.com/ true + 15 @@ -176,7 +188,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.2.0 + 3.6.3 8 @@ -192,7 +204,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.11 @@ -211,12 +223,12 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.1.1 + 3.3.1 com.puppycrawl.tools checkstyle - 8.39 + 9.3 @@ -249,13 +261,13 @@ org.junit.jupiter junit-jupiter-engine - 5.7.0 + 5.9.3 test org.junit.platform junit-platform-runner - 1.7.0 + 1.9.3 test @@ -267,13 +279,13 @@ org.assertj assertj-core - 3.18.0 + 3.27.7 test org.mockito mockito-core - 3.7.0 + 4.10.0 test @@ -301,19 +313,19 @@ org.msgpack msgpack-core - 0.8.22 + 0.9.11 com.squareup.okhttp3 okhttp - 4.9.0 + 4.12.0 com.squareup.okhttp3 logging-interceptor - 4.9.0 + 4.12.0 @@ -326,7 +338,7 @@ maven-resources-plugin - 3.2.0 + 3.3.1 copy-resources @@ -383,7 +395,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.1.0 sign-artifacts @@ -397,5 +409,51 @@ + + + java17 + + 17 + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test-jdk17/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + true + + + 17 + 17 + + + -parameters + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + + + + + + diff --git a/src/main/java/org/influxdb/annotation/Column.java b/src/main/java/org/influxdb/annotation/Column.java index cde2fbe50..6edb256f8 100644 --- a/src/main/java/org/influxdb/annotation/Column.java +++ b/src/main/java/org/influxdb/annotation/Column.java @@ -32,7 +32,10 @@ @Target(ElementType.FIELD) public @interface Column { - String name(); + /** + * If unset, the annotated field's name will be used as the column name. + */ + String name() default ""; boolean tag() default false; } diff --git a/src/main/java/org/influxdb/annotation/Exclude.java b/src/main/java/org/influxdb/annotation/Exclude.java new file mode 100644 index 000000000..23e076797 --- /dev/null +++ b/src/main/java/org/influxdb/annotation/Exclude.java @@ -0,0 +1,41 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2017 azeti Networks AG () + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.influxdb.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * When a POJO annotated with {@code @Measurement(allFields = true)} is loaded or saved, + * this annotation can be used to exclude some of its fields. + *

+ * Note: this is not considered when loading record measurements. + * + * @see Measurement#allFields() + * + * @author Eran Leshem + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Exclude { +} diff --git a/src/main/java/org/influxdb/annotation/Measurement.java b/src/main/java/org/influxdb/annotation/Measurement.java index a834bfbae..6ea8142e0 100644 --- a/src/main/java/org/influxdb/annotation/Measurement.java +++ b/src/main/java/org/influxdb/annotation/Measurement.java @@ -40,4 +40,15 @@ String retentionPolicy() default "autogen"; TimeUnit timeUnit() default TimeUnit.MILLISECONDS; + + /** + * If {@code true}, then all non-static fields of this measurement will be loaded or saved, + * regardless of any {@code @Column} annotations. + *

+ * Note: When loading record measurements, this is always implied to be true, + * since the record's canonical constructor is used to populate the record. + * + * @see Exclude + */ + boolean allFields() default false; } diff --git a/src/main/java/org/influxdb/dto/BoundParameterQuery.java b/src/main/java/org/influxdb/dto/BoundParameterQuery.java index 0c7b08b90..1f197289e 100644 --- a/src/main/java/org/influxdb/dto/BoundParameterQuery.java +++ b/src/main/java/org/influxdb/dto/BoundParameterQuery.java @@ -1,77 +1,9 @@ package org.influxdb.dto; -import com.squareup.moshi.JsonWriter; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -import org.influxdb.InfluxDBIOException; - -import okio.Buffer; - public final class BoundParameterQuery extends Query { - private final Map params = new HashMap<>(); - private BoundParameterQuery(final String command, final String database) { - super(command, database, true); - } - - public String getParameterJsonWithUrlEncoded() { - try { - String jsonParameterObject = createJsonObject(params); - String urlEncodedJsonParameterObject = encode(jsonParameterObject); - return urlEncodedJsonParameterObject; - } catch (IOException e) { - throw new InfluxDBIOException(e); - } - } - - private String createJsonObject(final Map parameterMap) throws IOException { - Buffer b = new Buffer(); - JsonWriter writer = JsonWriter.of(b); - writer.beginObject(); - for (Entry pair : parameterMap.entrySet()) { - String name = pair.getKey(); - Object value = pair.getValue(); - if (value instanceof Number) { - Number number = (Number) value; - writer.name(name).value(number); - } else if (value instanceof String) { - writer.name(name).value((String) value); - } else if (value instanceof Boolean) { - writer.name(name).value((Boolean) value); - } else { - writer.name(name).value(String.valueOf(value)); - } - } - writer.endObject(); - return b.readString(Charset.forName("utf-8")); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + params.hashCode(); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - BoundParameterQuery other = (BoundParameterQuery) obj; - if (!params.equals(other.params)) { - return false; - } - return true; + super(command, database); } public static class QueryBuilder { @@ -93,7 +25,7 @@ public QueryBuilder bind(final String placeholder, final Object value) { if (query == null) { query = new BoundParameterQuery(influxQL, null); } - query.params.put(placeholder, value); + query.bindParameter(placeholder, value); return this; } diff --git a/src/main/java/org/influxdb/dto/Point.java b/src/main/java/org/influxdb/dto/Point.java index 96069026c..1663913dc 100755 --- a/src/main/java/org/influxdb/dto/Point.java +++ b/src/main/java/org/influxdb/dto/Point.java @@ -1,7 +1,19 @@ package org.influxdb.dto; +import org.influxdb.BuilderException; +import org.influxdb.InfluxDBMapperException; +import org.influxdb.annotation.Column; +import org.influxdb.annotation.Exclude; +import org.influxdb.annotation.Measurement; +import org.influxdb.annotation.TimeColumn; +import org.influxdb.impl.Preconditions; +import org.influxdb.impl.TypeMapper; + import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; @@ -11,14 +23,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.TreeMap; import java.util.Optional; +import java.util.TreeMap; import java.util.concurrent.TimeUnit; -import org.influxdb.BuilderException; -import org.influxdb.annotation.Column; -import org.influxdb.annotation.Measurement; -import org.influxdb.annotation.TimeColumn; -import org.influxdb.impl.Preconditions; /** * Representation of a InfluxDB database Point. @@ -43,6 +50,7 @@ public class Point { }); private static final int DEFAULT_STRING_BUILDER_SIZE = 1024; + private static final int MAX_STRING_BUILDER_SIZE = 64 * 1024; private static final ThreadLocal CACHED_STRINGBUILDERS = ThreadLocal.withInitial(() -> new StringBuilder(DEFAULT_STRING_BUILDER_SIZE)); @@ -276,22 +284,45 @@ public boolean hasFields() { */ public Builder addFieldsFromPOJO(final Object pojo) { - Class clazz = pojo.getClass(); + Class clazz = pojo.getClass(); + Measurement measurement = clazz.getAnnotation(Measurement.class); + boolean allFields = measurement != null && measurement.allFields(); + while (clazz != null) { + TypeMapper typeMapper = TypeMapper.empty(); + while (clazz != null) { for (Field field : clazz.getDeclaredFields()) { Column column = field.getAnnotation(Column.class); - if (column == null) { + if (column == null && !(allFields + && !field.isAnnotationPresent(Exclude.class) && !Modifier.isStatic(field.getModifiers()))) { continue; } field.setAccessible(true); - String fieldName = column.name(); - addFieldByAttribute(pojo, field, column, fieldName); + + String fieldName; + if (column != null && !column.name().isEmpty()) { + fieldName = column.name(); + } else { + fieldName = field.getName(); + } + + addFieldByAttribute(pojo, field, column != null && column.tag(), fieldName, typeMapper); + } + + Class superclass = clazz.getSuperclass(); + Type genericSuperclass = clazz.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass); + } else { + typeMapper = TypeMapper.empty(); } - clazz = clazz.getSuperclass(); + + clazz = superclass; + } } if (this.fields.isEmpty()) { @@ -302,36 +333,41 @@ public Builder addFieldsFromPOJO(final Object pojo) { return this; } - private void addFieldByAttribute(final Object pojo, final Field field, final Column column, - final String fieldName) { + private void addFieldByAttribute(final Object pojo, final Field field, final boolean tag, + final String fieldName, final TypeMapper typeMapper) { try { Object fieldValue = field.get(pojo); TimeColumn tc = field.getAnnotation(TimeColumn.class); - if (tc != null && Instant.class.isAssignableFrom(field.getType())) { - Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> { - TimeUnit timeUnit = tc.timeUnit(); - if (timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) { - this.time = BigInteger.valueOf(instant.getEpochSecond()) - .multiply(NANOSECONDS_PER_SECOND) - .add(BigInteger.valueOf(instant.getNano())) - .divide(BigInteger.valueOf(TimeUnit.NANOSECONDS.convert(1, timeUnit))); - } else { - this.time = TimeUnit.MILLISECONDS.convert(instant.toEpochMilli(), timeUnit); + Class fieldType = (Class) typeMapper.resolve(field.getGenericType()); + if (tc != null) { + if (Instant.class.isAssignableFrom(fieldType)) { + Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> { + TimeUnit timeUnit = tc.timeUnit(); + if (timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) { + this.time = BigInteger.valueOf(instant.getEpochSecond()) + .multiply(NANOSECONDS_PER_SECOND) + .add(BigInteger.valueOf(instant.getNano())) + .divide(BigInteger.valueOf(TimeUnit.NANOSECONDS.convert(1, timeUnit))); + } else { + this.time = timeUnit.convert(instant.toEpochMilli(), TimeUnit.MILLISECONDS); + } this.precision = timeUnit; - } - this.precision = timeUnit; - }); - return; + }); + return; + } + + throw new InfluxDBMapperException( + "Unsupported type " + fieldType + " for time: should be of Instant type"); } - if (column.tag()) { + if (tag) { if (fieldValue != null) { this.tags.put(fieldName, (String) fieldValue); } } else { if (fieldValue != null) { - this.fields.put(fieldName, fieldValue); + setField(fieldType, fieldName, fieldValue); } } @@ -360,6 +396,32 @@ public Point build() { point.setTags(this.tags); return point; } + + private void setField( + final Class fieldType, + final String columnName, + final Object value) { + if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) { + addField(columnName, (boolean) value); + } else if (long.class.isAssignableFrom(fieldType) || Long.class.isAssignableFrom(fieldType)) { + addField(columnName, (long) value); + } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) { + addField(columnName, (double) value); + } else if (float.class.isAssignableFrom(fieldType) || Float.class.isAssignableFrom(fieldType)) { + addField(columnName, (float) value); + } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) { + addField(columnName, (int) value); + } else if (short.class.isAssignableFrom(fieldType) || Short.class.isAssignableFrom(fieldType)) { + addField(columnName, (short) value); + } else if (String.class.isAssignableFrom(fieldType)) { + addField(columnName, (String) value); + } else if (Enum.class.isAssignableFrom(fieldType)) { + addField(columnName, ((Enum) value).name()); + } else { + throw new InfluxDBMapperException( + "Unsupported type " + fieldType + " for column " + columnName); + } + } } /** @@ -493,7 +555,12 @@ public String lineProtocol(final TimeUnit precision) { // setLength(0) is used for reusing cached StringBuilder instance per thread // it reduces GC activity and performs better then new StringBuilder() StringBuilder sb = CACHED_STRINGBUILDERS.get(); - sb.setLength(0); + if (sb.capacity() > MAX_STRING_BUILDER_SIZE) { + sb = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE); + CACHED_STRINGBUILDERS.set(sb); + } else { + sb.setLength(0); + } escapeKey(sb, measurement); concatenatedTags(sb); diff --git a/src/main/java/org/influxdb/dto/Query.java b/src/main/java/org/influxdb/dto/Query.java index 5c4921b8c..ebed08e7e 100644 --- a/src/main/java/org/influxdb/dto/Query.java +++ b/src/main/java/org/influxdb/dto/Query.java @@ -1,8 +1,18 @@ package org.influxdb.dto; +import com.squareup.moshi.JsonWriter; +import okio.Buffer; +import org.influxdb.InfluxDBIOException; +import org.influxdb.querybuilder.Appendable; + +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; /** * Represents a Query against Influxdb. @@ -15,6 +25,7 @@ public class Query { private final String command; private final String database; private final boolean requiresPost; + protected final Map params = new HashMap<>(); /** * @param command the query command @@ -68,38 +79,43 @@ public boolean requiresPost() { return requiresPost; } - @SuppressWarnings("checkstyle:avoidinlineconditionals") - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((command == null) ? 0 : command.hashCode()); - result = prime * result - + ((database == null) ? 0 : database.hashCode()); - return result; + public Query bindParameter(final String placeholder, final Object value) { + params.put(placeholder, value); + return this; + } + + public boolean hasBoundParameters() { + return !params.isEmpty(); + } + + public String getParameterJsonWithUrlEncoded() { + try { + String jsonParameterObject = createJsonObject(params); + String urlEncodedJsonParameterObject = encode(jsonParameterObject); + return urlEncodedJsonParameterObject; + } catch (IOException e) { + throw new InfluxDBIOException(e); + } } - @SuppressWarnings("checkstyle:needbraces") @Override - public boolean equals(final Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Query other = (Query) obj; - if (command == null) { - if (other.command != null) - return false; - } else if (!command.equals(other.command)) + public boolean equals(final Object o) { + if (o == null || getClass() != o.getClass()) { return false; - if (database == null) { - if (other.database != null) - return false; - } else if (!database.equals(other.database)) - return false; - return true; + } + + Query query = (Query) o; + return Objects.equals(command, query.command) && Objects.equals(database, query.database) && params.equals( + query.params); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = Objects.hashCode(command); + result = prime * result + Objects.hashCode(database); + result = prime * result + params.hashCode(); + return result; } /** @@ -115,4 +131,30 @@ public static String encode(final String command) { throw new IllegalStateException("Every JRE must support UTF-8", e); } } + + private String createJsonObject(final Map parameterMap) throws IOException { + Buffer b = new Buffer(); + JsonWriter writer = JsonWriter.of(b); + writer.beginObject(); + for (Map.Entry pair : parameterMap.entrySet()) { + String name = pair.getKey(); + Object value = pair.getValue(); + if (value instanceof Number) { + Number number = (Number) value; + writer.name(name).value(number); + } else if (value instanceof String) { + writer.name(name).value((String) value); + } else if (value instanceof Boolean) { + writer.name(name).value((Boolean) value); + } else if (value instanceof Appendable) { + StringBuilder stringBuilder = new StringBuilder(); + ((Appendable) value).appendTo(stringBuilder); + writer.name(name).value(stringBuilder.toString()); + } else { + writer.name(name).value(String.valueOf(value)); + } + } + writer.endObject(); + return b.readString(Charset.forName("utf-8")); + } } diff --git a/src/main/java/org/influxdb/example/Android.java b/src/main/java/org/influxdb/example/Android.java new file mode 100644 index 000000000..f26495cd2 --- /dev/null +++ b/src/main/java/org/influxdb/example/Android.java @@ -0,0 +1,98 @@ +package org.influxdb.example; + +import org.influxdb.InfluxDB; +import org.influxdb.InfluxDBFactory; +import org.influxdb.dto.QueryResult; +import org.influxdb.dto.Query; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author StrakarCe + * @since 07/05/2021 + * @version 1 + */ +public class Android { + // put the address IP of your database + String address = "http://192.168.1.75:8000/"; + String dbName = "myDatabase"; + String table = "SERIES"; + QueryResult actual; + Boolean flag = false; + InfluxDB con; + + public Android() { + super(); + } + public void queryExecute(final Query query) { + Thread thread = new Thread(new Runnable() { + + @Override + public void run() { + try { + //InfluxDB connector = InfluxDBFactory.connect(address); + // if you want to open every time + System.out.println("Send the query to the database ..."); + // FOR A REAL APP CREATE A LOGGER ; + List results = new LinkedList<>(); + actual = con.query(query); + } catch (Exception e) { + e.printStackTrace(); + } + flag = true; // For simplicity, I use a simple flag to know when the thread have finished + } + }); + + thread.start(); + } + + /** + * It's to open the connexion with the database. + * In my case I decide to open once, do many query and close. + */ + public void connexion() { + con = InfluxDBFactory.connect(address); + } + /** + * It's to close after my list of query. + */ + public void close() { + con.close(); + } + /* + * simple example of how you can create a query + */ + private void queryLauncher(final String query) { + queryExecute(new Query(query, dbName)); + while (!flag) { // ugly method to wait the thread + System.out.println("Wait the thread"); + } + flag = false; + } + public String getEtat() { + queryLauncher("select last(value) from PTEC"); + return actual.getResults().get(0).getSeries().get(0).getValues().get(0).get(1).toString(); + } + public String getHC() { + queryLauncher("SELECT last(value) FROM HCHC"); + return actual.getResults().get(0).getSeries().get(0).getValues().get(0).get(1).toString(); + } + // ------------------------- Example when you want to use it ------------ + /* + Android test = new Android(); + refresh.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + test.connexion(); + etat2.setText(test.getEtat()); + hc2.setText(test.getHC()); + hp2.setText(test.getHP()); + prix2.setText(test.getDepense()); + percMens2.setText(test.getPercentageMensuel()); + percTotal2.setText(test.getPercentageTotal()); + test.close(); + } + }); + */ +} diff --git a/src/main/java/org/influxdb/impl/BatchProcessor.java b/src/main/java/org/influxdb/impl/BatchProcessor.java index 4430072a5..28c45b693 100644 --- a/src/main/java/org/influxdb/impl/BatchProcessor.java +++ b/src/main/java/org/influxdb/impl/BatchProcessor.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -46,6 +47,7 @@ public final class BatchProcessor { private final BatchWriter batchWriter; private boolean dropActionsOnQueueExhaustion; Consumer droppedActionHandler; + Supplier randomSupplier; /** * The Builder to create a BatchProcessor instance. @@ -318,20 +320,21 @@ public static Builder builder(final InfluxDB influxDB) { } else { this.queue = new LinkedBlockingQueue<>(); } + this.randomSupplier = Math::random; Runnable flushRunnable = new Runnable() { @Override public void run() { // write doesn't throw any exceptions write(); - int jitterInterval = (int) (Math.random() * BatchProcessor.this.jitterInterval); + int jitterInterval = (int) (randomSupplier.get() * BatchProcessor.this.jitterInterval); BatchProcessor.this.scheduler.schedule(this, BatchProcessor.this.flushInterval + jitterInterval, BatchProcessor.this.flushIntervalUnit); } }; // Flush at specified Rate this.scheduler.schedule(flushRunnable, - this.flushInterval + (int) (Math.random() * BatchProcessor.this.jitterInterval), + this.flushInterval + (int) (randomSupplier.get() * BatchProcessor.this.jitterInterval), this.flushIntervalUnit); } diff --git a/src/main/java/org/influxdb/impl/GzipRequestInterceptor.java b/src/main/java/org/influxdb/impl/GzipRequestInterceptor.java index 8969780d9..adaa3d528 100644 --- a/src/main/java/org/influxdb/impl/GzipRequestInterceptor.java +++ b/src/main/java/org/influxdb/impl/GzipRequestInterceptor.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; import okhttp3.Interceptor; import okhttp3.MediaType; @@ -19,6 +20,8 @@ */ final class GzipRequestInterceptor implements Interceptor { + private static final Pattern WRITE_PATTERN = Pattern.compile(".*/write", Pattern.CASE_INSENSITIVE); + private AtomicBoolean enabled = new AtomicBoolean(false); GzipRequestInterceptor() { @@ -48,6 +51,10 @@ public Response intercept(final Interceptor.Chain chain) throws IOException { return chain.proceed(originalRequest); } + if (!WRITE_PATTERN.matcher(originalRequest.url().encodedPath()).matches()) { + return chain.proceed(originalRequest); + } + Request compressedRequest = originalRequest.newBuilder().header("Content-Encoding", "gzip") .method(originalRequest.method(), gzip(body)).build(); return chain.proceed(compressedRequest); diff --git a/src/main/java/org/influxdb/impl/InfluxDBImpl.java b/src/main/java/org/influxdb/impl/InfluxDBImpl.java index 904744f23..23427a23d 100644 --- a/src/main/java/org/influxdb/impl/InfluxDBImpl.java +++ b/src/main/java/org/influxdb/impl/InfluxDBImpl.java @@ -16,7 +16,6 @@ import org.influxdb.InfluxDBException; import org.influxdb.InfluxDBIOException; import org.influxdb.dto.BatchPoints; -import org.influxdb.dto.BoundParameterQuery; import org.influxdb.dto.Point; import org.influxdb.dto.Pong; import org.influxdb.dto.Query; @@ -637,12 +636,20 @@ public void query(final Query query, final int chunkSize, final BiConsumer onNext, final Runnable onComplete, final Consumer onFailure) { Call call; - if (query instanceof BoundParameterQuery) { - BoundParameterQuery boundParameterQuery = (BoundParameterQuery) query; - call = this.influxDBService.query(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize, - boundParameterQuery.getParameterJsonWithUrlEncoded()); + if (query.hasBoundParameters()) { + if (query.requiresPost()) { + call = this.influxDBService.postQuery(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize, + query.getParameterJsonWithUrlEncoded()); + } else { + call = this.influxDBService.query(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize, + query.getParameterJsonWithUrlEncoded()); + } } else { - call = this.influxDBService.query(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize); + if (query.requiresPost()) { + call = this.influxDBService.postQuery(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize); + } else { + call = this.influxDBService.query(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize); + } } call.enqueue(new Callback() { @@ -711,15 +718,23 @@ public void onFailure(final Call call, final Throwable t) { */ @Override public QueryResult query(final Query query, final TimeUnit timeUnit) { - Call call = null; - if (query instanceof BoundParameterQuery) { - BoundParameterQuery boundParameterQuery = (BoundParameterQuery) query; - call = this.influxDBService.query(getDatabase(query), - TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded(), - boundParameterQuery.getParameterJsonWithUrlEncoded()); + Call call; + if (query.hasBoundParameters()) { + if (query.requiresPost()) { + call = this.influxDBService.postQuery(getDatabase(query), TimeUtil.toTimePrecision(timeUnit), + query.getCommandWithUrlEncoded(), query.getParameterJsonWithUrlEncoded()); + } else { + call = this.influxDBService.query(getDatabase(query), TimeUtil.toTimePrecision(timeUnit), + query.getCommandWithUrlEncoded(), query.getParameterJsonWithUrlEncoded()); + } } else { - call = this.influxDBService.query(getDatabase(query), - TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded()); + if (query.requiresPost()) { + call = this.influxDBService.postQuery(getDatabase(query), + TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded()); + } else { + call = this.influxDBService.query(getDatabase(query), + TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded(), null); + } } return executeQuery(call); } @@ -747,7 +762,7 @@ public void deleteDatabase(final String name) { */ @Override public List describeDatabases() { - QueryResult result = executeQuery(this.influxDBService.query(SHOW_DATABASE_COMMAND_ENCODED)); + QueryResult result = executeQuery(this.influxDBService.postQuery(SHOW_DATABASE_COMMAND_ENCODED)); // {"results":[{"series":[{"name":"databases","columns":["name"],"values":[["mydb"]]}]}]} // Series [name=databases, columns=[name], values=[[mydb], [unittest_1433605300968]]] List> databaseNames = result.getResults().get(0).getSeries().get(0).getValues(); @@ -779,10 +794,14 @@ public boolean databaseExists(final String name) { */ private Call callQuery(final Query query) { Call call; - if (query instanceof BoundParameterQuery) { - BoundParameterQuery boundParameterQuery = (BoundParameterQuery) query; + if (query.hasBoundParameters()) { + if (query.requiresPost()) { call = this.influxDBService.postQuery(getDatabase(query), query.getCommandWithUrlEncoded(), - boundParameterQuery.getParameterJsonWithUrlEncoded()); + query.getParameterJsonWithUrlEncoded()); + } else { + call = this.influxDBService.query(getDatabase(query), null, query.getCommandWithUrlEncoded(), + query.getParameterJsonWithUrlEncoded()); + } } else { if (query.requiresPost()) { call = this.influxDBService.postQuery(getDatabase(query), query.getCommandWithUrlEncoded()); diff --git a/src/main/java/org/influxdb/impl/InfluxDBMapper.java b/src/main/java/org/influxdb/impl/InfluxDBMapper.java index 700a960cf..2a6c0dc4c 100644 --- a/src/main/java/org/influxdb/impl/InfluxDBMapper.java +++ b/src/main/java/org/influxdb/impl/InfluxDBMapper.java @@ -1,18 +1,13 @@ package org.influxdb.impl; -import java.lang.reflect.Field; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; import org.influxdb.InfluxDB; -import org.influxdb.InfluxDBMapperException; -import org.influxdb.annotation.Column; import org.influxdb.annotation.Measurement; import org.influxdb.dto.Point; import org.influxdb.dto.Query; import org.influxdb.dto.QueryResult; +import java.util.List; + public class InfluxDBMapper extends InfluxDBResultMapper { private final InfluxDB influxDB; @@ -52,91 +47,16 @@ public List query(final Class clazz) { public void save(final T model) { throwExceptionIfMissingAnnotation(model.getClass()); - cacheMeasurementClass(model.getClass()); - - ConcurrentMap colNameAndFieldMap = getColNameAndFieldMap(model.getClass()); - - try { - Class modelType = model.getClass(); - String measurement = getMeasurementName(modelType); - String database = getDatabaseName(modelType); - String retentionPolicy = getRetentionPolicy(modelType); - TimeUnit timeUnit = getTimeUnit(modelType); - long time = timeUnit.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); - Point.Builder pointBuilder = Point.measurement(measurement).time(time, timeUnit); - - for (String key : colNameAndFieldMap.keySet()) { - Field field = colNameAndFieldMap.get(key); - Column column = field.getAnnotation(Column.class); - String columnName = column.name(); - Class fieldType = field.getType(); - - if (!field.isAccessible()) { - field.setAccessible(true); - } - - Object value = field.get(model); - - if (column.tag()) { - /** Tags are strings either way. */ - pointBuilder.tag(columnName, value.toString()); - } else if ("time".equals(columnName)) { - if (value != null) { - setTime(pointBuilder, fieldType, timeUnit, value); - } - } else { - setField(pointBuilder, fieldType, columnName, value); - } - } - - Point point = pointBuilder.build(); + Class modelType = model.getClass(); + String database = getDatabaseName(modelType); + String retentionPolicy = getRetentionPolicy(modelType); + Point.Builder pointBuilder = Point.measurementByPOJO(modelType).addFieldsFromPOJO(model); + Point point = pointBuilder.build(); - if ("[unassigned]".equals(database)) { - influxDB.write(point); - } else { - influxDB.write(database, retentionPolicy, point); - } - - } catch (IllegalAccessException e) { - throw new InfluxDBMapperException(e); - } - } - - private void setTime( - final Point.Builder pointBuilder, - final Class fieldType, - final TimeUnit timeUnit, - final Object value) { - if (Instant.class.isAssignableFrom(fieldType)) { - Instant instant = (Instant) value; - long time = timeUnit.convert(instant.toEpochMilli(), TimeUnit.MILLISECONDS); - pointBuilder.time(time, timeUnit); - } else { - throw new InfluxDBMapperException( - "Unsupported type " + fieldType + " for time: should be of Instant type"); - } - } - - private void setField( - final Point.Builder pointBuilder, - final Class fieldType, - final String columnName, - final Object value) { - if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) { - pointBuilder.addField(columnName, (boolean) value); - } else if (long.class.isAssignableFrom(fieldType) || Long.class.isAssignableFrom(fieldType)) { - pointBuilder.addField(columnName, (long) value); - } else if (double.class.isAssignableFrom(fieldType) - || Double.class.isAssignableFrom(fieldType)) { - pointBuilder.addField(columnName, (double) value); - } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) { - pointBuilder.addField(columnName, (int) value); - } else if (String.class.isAssignableFrom(fieldType)) { - pointBuilder.addField(columnName, (String) value); + if ("[unassigned]".equals(database)) { + influxDB.write(point); } else { - throw new InfluxDBMapperException( - "Unsupported type " + fieldType + " for column " + columnName); + influxDB.write(database, retentionPolicy, point); } } - } diff --git a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java index d6edef17c..2cfdeced7 100644 --- a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java +++ b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java @@ -20,23 +20,35 @@ */ package org.influxdb.impl; +import org.influxdb.InfluxDBMapperException; +import org.influxdb.annotation.Column; +import org.influxdb.annotation.Exclude; +import org.influxdb.annotation.Measurement; +import org.influxdb.dto.QueryResult; + +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; - -import org.influxdb.InfluxDBMapperException; -import org.influxdb.annotation.Column; -import org.influxdb.annotation.Measurement; -import org.influxdb.dto.QueryResult; +import java.util.stream.Collectors; /** * Main class responsible for mapping a QueryResult to a POJO. @@ -48,15 +60,33 @@ public class InfluxDBResultMapper { /** * Data structure used to cache classes used as measurements. */ + private static class ClassInfo { + ConcurrentMap fieldMap; + ConcurrentMap typeMappers; + } private static final - ConcurrentMap> CLASS_FIELD_CACHE = new ConcurrentHashMap<>(); + ConcurrentMap CLASS_INFO_CACHE = new ConcurrentHashMap<>(); + + /** + * Data structure used to cache records used as measurements. + */ + private static class RecordInfo { + Constructor constructor; + ConcurrentMap constructorParamIndexes; + } + private static final + ConcurrentMap RECORD_INFO = new ConcurrentHashMap<>(); private static final int FRACTION_MIN_WIDTH = 0; private static final int FRACTION_MAX_WIDTH = 9; private static final boolean ADD_DECIMAL_POINT = true; + // Support both standard and Android desugared records + private static final Collection RECORD_CLASS_NAMES = + new HashSet<>(Arrays.asList("java.lang.Record", "com.android.tools.r8.RecordTag")); + /** - * When a query is executed without {@link TimeUnit}, InfluxDB returns the time + * When a query is executed without {@link TimeUnit}, InfluxDB returns the {@code time} * column as a RFC3339 date. */ private static final DateTimeFormatter RFC3339_FORMATTER = new DateTimeFormatterBuilder() @@ -80,7 +110,7 @@ public class InfluxDBResultMapper { * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, - * clazz parameter is not annotated with @Measurement or it was not + * {@code clazz} parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz) throws InfluxDBMapperException { @@ -103,7 +133,7 @@ public List toPOJO(final QueryResult queryResult, final Class clazz) t * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, - * clazz parameter is not annotated with @Measurement or it was not + * {@code clazz} parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz, @@ -129,7 +159,7 @@ public List toPOJO(final QueryResult queryResult, final Class clazz, * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, - * clazz parameter is not annotated with @Measurement or it was not + * {@code clazz} parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz, final String measurementName) @@ -154,7 +184,7 @@ public List toPOJO(final QueryResult queryResult, final Class clazz, f * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, - * clazz parameter is not annotated with @Measurement or it was not + * {@code clazz} parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz, final String measurementName, @@ -166,19 +196,20 @@ public List toPOJO(final QueryResult queryResult, final Class clazz, f Objects.requireNonNull(clazz, "clazz"); throwExceptionIfResultWithError(queryResult); - cacheMeasurementClass(clazz); - List result = new LinkedList(); + if (isRecordClass(clazz)) { + cacheRecordClass(clazz); + } else { + cacheMeasurementClass(clazz); + } + + List result = new LinkedList<>(); queryResult.getResults().stream() .filter(internalResult -> Objects.nonNull(internalResult) && Objects.nonNull(internalResult.getSeries())) - .forEach(internalResult -> { - internalResult.getSeries().stream() - .filter(series -> series.getName().equals(measurementName)) - .forEachOrdered(series -> { - parseSeriesAs(series, clazz, result, precision); - }); - }); + .forEach(internalResult -> internalResult.getSeries().stream() + .filter(series -> series.getName().equals(measurementName)) + .forEachOrdered(series -> parseSeriesAs(series, clazz, result, precision))); return result; } @@ -202,29 +233,98 @@ void throwExceptionIfResultWithError(final QueryResult queryResult) { }); } - ConcurrentMap getColNameAndFieldMap(final Class clazz) { - return CLASS_FIELD_CACHE.get(clazz.getName()); - } - void cacheMeasurementClass(final Class... classVarAgrs) { for (Class clazz : classVarAgrs) { - if (CLASS_FIELD_CACHE.containsKey(clazz.getName())) { + if (CLASS_INFO_CACHE.containsKey(clazz.getName())) { continue; } - ConcurrentMap influxColumnAndFieldMap = new ConcurrentHashMap<>(); + ConcurrentMap fieldMap = new ConcurrentHashMap<>(); + ConcurrentMap typeMappers = new ConcurrentHashMap<>(); + + Measurement measurement = clazz.getAnnotation(Measurement.class); + boolean allFields = measurement != null && measurement.allFields(); Class c = clazz; + TypeMapper typeMapper = TypeMapper.empty(); while (c != null) { for (Field field : c.getDeclaredFields()) { Column colAnnotation = field.getAnnotation(Column.class); - if (colAnnotation != null) { - influxColumnAndFieldMap.put(colAnnotation.name(), field); + if (colAnnotation == null && !(allFields + && !field.isAnnotationPresent(Exclude.class) && !Modifier.isStatic(field.getModifiers()))) { + continue; } + + fieldMap.put(getFieldName(field, colAnnotation), field); + typeMappers.put(field, typeMapper); } - c = c.getSuperclass(); + + Class superclass = c.getSuperclass(); + Type genericSuperclass = c.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass); + } else { + typeMapper = TypeMapper.empty(); + } + + c = superclass; + } + + ClassInfo classInfo = new ClassInfo(); + classInfo.fieldMap = fieldMap; + classInfo.typeMappers = typeMappers; + CLASS_INFO_CACHE.putIfAbsent(clazz.getName(), classInfo); + } + } + + static void cacheRecordClass(final Class clazz) { + if (RECORD_INFO.containsKey(clazz.getName())) { + return; + } + + Map components = Arrays.stream(clazz.getDeclaredFields()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .collect(Collectors.toMap(Field::getName, Field::getGenericType)); + boolean found = false; + for (Constructor constructor : clazz.getDeclaredConstructors()) { + Parameter[] parameters = constructor.getParameters(); + Map parameterTypes = Arrays.stream(parameters) + .collect(Collectors.toMap(Parameter::getName, Parameter::getParameterizedType)); + if (!parameterTypes.equals(components)) { + continue; } - CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), influxColumnAndFieldMap); + + if (found) { + throw new InfluxDBMapperException(String.format( + "Multiple constructors match set of components for record %s", clazz.getName())); + } + + RecordInfo recordInfo = new RecordInfo(); + recordInfo.constructor = constructor; + + try { + ConcurrentMap constructorParamIndexes = new ConcurrentHashMap<>(parameters.length); + for (int i = 0; i < parameters.length; i++) { + Field field = clazz.getDeclaredField(parameters[i].getName()); + Column colAnnotation = field.getAnnotation(Column.class); + String propertyName = getFieldName(field, colAnnotation); + constructorParamIndexes.put(propertyName, i); + } + recordInfo.constructorParamIndexes = constructorParamIndexes; + } catch (NoSuchFieldException e) { + throw new InfluxDBMapperException(e); + } + + RECORD_INFO.putIfAbsent(clazz.getName(), recordInfo); + found = true; + } + } + + private static String getFieldName(final Field field, final Column colAnnotation) { + if (colAnnotation != null && !colAnnotation.name().isEmpty()) { + return colAnnotation.name(); } + + return field.getName(); } String getMeasurementName(final Class clazz) { @@ -239,10 +339,6 @@ String getRetentionPolicy(final Class clazz) { return ((Measurement) clazz.getAnnotation(Measurement.class)).retentionPolicy(); } - TimeUnit getTimeUnit(final Class clazz) { - return ((Measurement) clazz.getAnnotation(Measurement.class)).timeUnit(); - } - List parseSeriesAs(final QueryResult.Series series, final Class clazz, final List result) { return parseSeriesAs(series, clazz, result, TimeUnit.MILLISECONDS); } @@ -250,151 +346,154 @@ List parseSeriesAs(final QueryResult.Series series, final Class clazz, List parseSeriesAs(final QueryResult.Series series, final Class clazz, final List result, final TimeUnit precision) { int columnSize = series.getColumns().size(); - ConcurrentMap colNameAndFieldMap = CLASS_FIELD_CACHE.get(clazz.getName()); - try { - T object = null; - for (List row : series.getValues()) { - for (int i = 0; i < columnSize; i++) { - Field correspondingField = colNameAndFieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/); - if (correspondingField != null) { - if (object == null) { - object = clazz.newInstance(); + + if (isRecordClass(clazz)) { + RecordInfo recordInfo = RECORD_INFO.get(clazz.getName()); + try { + T object = null; + for (List row : series.getValues()) { + Object[] constructorParams = new Object[recordInfo.constructor.getParameterTypes().length]; + for (int i = 0; i < columnSize; i++) { + String columnName = series.getColumns().get(i); /*InfluxDB columnName*/ + addParam(clazz, precision, recordInfo, constructorParams, columnName, row.get(i)); + } + // When the "GROUP BY" clause is used, "tags" are returned as Map and + // accordingly with InfluxDB documentation + // https://docs.influxdata.com/influxdb/v1.2/concepts/glossary/#tag-value + // "tag" values are always String. + if (series.getTags() != null) { + for (Entry entry : series.getTags().entrySet()) { + addParam(clazz, precision, recordInfo, constructorParams, entry.getKey()/*InfluxDB columnName*/, + entry.getValue()); } - setFieldValue(object, correspondingField, row.get(i), precision); } + + //noinspection unchecked + result.add((T) recordInfo.constructor.newInstance(constructorParams)); } - // When the "GROUP BY" clause is used, "tags" are returned as Map and - // accordingly with InfluxDB documentation - // https://docs.influxdata.com/influxdb/v1.2/concepts/glossary/#tag-value - // "tag" values are always String. - if (series.getTags() != null && !series.getTags().isEmpty()) { - for (Entry entry : series.getTags().entrySet()) { - Field correspondingField = colNameAndFieldMap.get(entry.getKey()/*InfluxDB columnName*/); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new InfluxDBMapperException(e); + } + } else { + ClassInfo classInfo = CLASS_INFO_CACHE.get(clazz.getName()); + try { + T object = null; + for (List row : series.getValues()) { + for (int i = 0; i < columnSize; i++) { + Field correspondingField = classInfo.fieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/); if (correspondingField != null) { - // I don't think it is possible to reach here without a valid "object" - setFieldValue(object, correspondingField, entry.getValue(), precision); + if (object == null) { + object = clazz.newInstance(); + } + setFieldValue(object, correspondingField, row.get(i), precision, + classInfo.typeMappers.get(correspondingField)); } } + // When the "GROUP BY" clause is used, "tags" are returned as Map and + // accordingly with InfluxDB documentation + // https://docs.influxdata.com/influxdb/v1.2/concepts/glossary/#tag-value + // "tag" values are always String. + if (series.getTags() != null && !series.getTags().isEmpty()) { + for (Entry entry : series.getTags().entrySet()) { + Field correspondingField = classInfo.fieldMap.get(entry.getKey()/*InfluxDB columnName*/); + if (correspondingField != null) { + // I don't think it is possible to reach here without a valid "object" + setFieldValue(object, correspondingField, entry.getValue(), precision, + classInfo.typeMappers.get(correspondingField)); + } + } + } + if (object != null) { + result.add(object); + object = null; + } } - if (object != null) { - result.add(object); - object = null; - } + } catch (InstantiationException | IllegalAccessException e) { + throw new InfluxDBMapperException(e); } - } catch (InstantiationException | IllegalAccessException e) { - throw new InfluxDBMapperException(e); } return result; } + private static void addParam(final Class clazz, final TimeUnit precision, final RecordInfo recordInfo, + final Object[] constructorParams, final String columnName, final Object value) { + Parameter parameter = recordInfo.constructor.getParameters() + [recordInfo.constructorParamIndexes.get(columnName).intValue()]; + constructorParams[recordInfo.constructorParamIndexes.get(columnName).intValue()] = + adaptValue(parameter.getType(), value, precision, parameter.getName(), clazz.getName()); + } + + private static boolean isRecordClass(final Class clazz) { + return RECORD_CLASS_NAMES.contains(clazz.getSuperclass().getName()); + } + /** * InfluxDB client returns any number as Double. - * See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987 + * See ... * for more information. * - * @param object - * @param field - * @param value - * @param precision - * @throws IllegalArgumentException - * @throws IllegalAccessException */ - void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision) + private static void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision, + final TypeMapper typeMapper) throws IllegalArgumentException, IllegalAccessException { if (value == null) { return; } - Class fieldType = field.getType(); + Type fieldType = typeMapper.resolve(field.getGenericType()); + if (!field.isAccessible()) { + field.setAccessible(true); + } + field.set(object, adaptValue((Class) fieldType, value, precision, field.getName(), object.getClass().getName())); + } + + private static Object adaptValue(final Class fieldType, final Object value, final TimeUnit precision, + final String fieldName, final String className) { try { - if (!field.isAccessible()) { - field.setAccessible(true); + if (String.class.isAssignableFrom(fieldType)) { + return String.valueOf(value); + } + if (Instant.class.isAssignableFrom(fieldType)) { + if (value instanceof String) { + return Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value))); + } + if (value instanceof Long) { + return Instant.ofEpochMilli(toMillis(((Long) value).longValue(), precision)); + } + if (value instanceof Double) { + return Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision)); + } + if (value instanceof Integer) { + return Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision)); + } + throw new InfluxDBMapperException("Unsupported type " + fieldType + " for field " + fieldName); + } + if (Double.class.isAssignableFrom(fieldType) || double.class.isAssignableFrom(fieldType)) { + return value; + } + if (Long.class.isAssignableFrom(fieldType) || long.class.isAssignableFrom(fieldType)) { + return ((Double) value).longValue(); } - if (fieldValueModified(fieldType, field, object, value, precision) - || fieldValueForPrimitivesModified(fieldType, field, object, value) - || fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) { - return; + if (Integer.class.isAssignableFrom(fieldType) || int.class.isAssignableFrom(fieldType)) { + return ((Double) value).intValue(); + } + if (Boolean.class.isAssignableFrom(fieldType) || boolean.class.isAssignableFrom(fieldType)) { + return Boolean.valueOf(String.valueOf(value)); + } + if (Enum.class.isAssignableFrom(fieldType)) { + //noinspection unchecked + return Enum.valueOf((Class) fieldType, String.valueOf(value)); } - String msg = "Class '%s' field '%s' is from an unsupported type '%s'."; - throw new InfluxDBMapperException( - String.format(msg, object.getClass().getName(), field.getName(), field.getType())); } catch (ClassCastException e) { String msg = "Class '%s' field '%s' was defined with a different field type and caused a ClassCastException. " + "The correct type is '%s' (current field value: '%s')."; - throw new InfluxDBMapperException( - String.format(msg, object.getClass().getName(), field.getName(), value.getClass().getName(), value)); + throw new InfluxDBMapperException(String.format(msg, className, fieldName, value.getClass().getName(), value), e); } - } - boolean fieldValueModified(final Class fieldType, final Field field, final T object, final Object value, - final TimeUnit precision) - throws IllegalArgumentException, IllegalAccessException { - if (String.class.isAssignableFrom(fieldType)) { - field.set(object, String.valueOf(value)); - return true; - } - if (Instant.class.isAssignableFrom(fieldType)) { - Instant instant; - if (value instanceof String) { - instant = Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value))); - } else if (value instanceof Long) { - instant = Instant.ofEpochMilli(toMillis((long) value, precision)); - } else if (value instanceof Double) { - instant = Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision)); - } else if (value instanceof Integer) { - instant = Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision)); - } else { - throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName()); - } - field.set(object, instant); - return true; - } - return false; + throw new InfluxDBMapperException( + String.format("Class '%s' field '%s' is from an unsupported type '%s'.", className, fieldName, fieldType)); } - boolean fieldValueForPrimitivesModified(final Class fieldType, final Field field, final T object, - final Object value) throws IllegalArgumentException, IllegalAccessException { - if (double.class.isAssignableFrom(fieldType)) { - field.setDouble(object, ((Double) value).doubleValue()); - return true; - } - if (long.class.isAssignableFrom(fieldType)) { - field.setLong(object, ((Double) value).longValue()); - return true; - } - if (int.class.isAssignableFrom(fieldType)) { - field.setInt(object, ((Double) value).intValue()); - return true; - } - if (boolean.class.isAssignableFrom(fieldType)) { - field.setBoolean(object, Boolean.valueOf(String.valueOf(value)).booleanValue()); - return true; - } - return false; - } - - boolean fieldValueForPrimitiveWrappersModified(final Class fieldType, final Field field, final T object, - final Object value) throws IllegalArgumentException, IllegalAccessException { - if (Double.class.isAssignableFrom(fieldType)) { - field.set(object, value); - return true; - } - if (Long.class.isAssignableFrom(fieldType)) { - field.set(object, Long.valueOf(((Double) value).longValue())); - return true; - } - if (Integer.class.isAssignableFrom(fieldType)) { - field.set(object, Integer.valueOf(((Double) value).intValue())); - return true; - } - if (Boolean.class.isAssignableFrom(fieldType)) { - field.set(object, Boolean.valueOf(String.valueOf(value))); - return true; - } - return false; - } - - private Long toMillis(final long value, final TimeUnit precision) { - + private static long toMillis(final long value, final TimeUnit precision) { return TimeUnit.MILLISECONDS.convert(value, precision); } } diff --git a/src/main/java/org/influxdb/impl/InfluxDBService.java b/src/main/java/org/influxdb/impl/InfluxDBService.java index dfe897257..061a76615 100644 --- a/src/main/java/org/influxdb/impl/InfluxDBService.java +++ b/src/main/java/org/influxdb/impl/InfluxDBService.java @@ -6,6 +6,8 @@ import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.Body; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Query; @@ -44,10 +46,6 @@ public Call writePoints(@Query(DB) String database, @Query(CONSISTENCY) String consistency, @Body RequestBody batchPoints); @GET("query") - public Call query(@Query(DB) String db, - @Query(EPOCH) String epoch, @Query(value = Q, encoded = true) String query); - - @POST("query") public Call query(@Query(DB) String db, @Query(EPOCH) String epoch, @Query(value = Q, encoded = true) String query, @Query(value = PARAMS, encoded = true) String params); @@ -57,18 +55,35 @@ public Call query(@Query(DB) String db, @Query(value = Q, encoded = true) String query); @POST("query") + @FormUrlEncoded public Call postQuery(@Query(DB) String db, - @Query(value = Q, encoded = true) String query); + @Field(value = Q, encoded = true) String query); @POST("query") - public Call postQuery(@Query(DB) String db, - @Query(value = Q, encoded = true) String query, @Query(value = PARAMS, encoded = true) String params); + @FormUrlEncoded + public Call postQuery(@Query(DB) String db, @Query(EPOCH) String epoch, + @Field(value = Q, encoded = true) String query); - @GET("query") - public Call query(@Query(value = Q, encoded = true) String query); + @POST("query") + @FormUrlEncoded + public Call postQuery(@Query(DB) String db, @Query(EPOCH) String epoch, + @Field(value = Q, encoded = true) String query, @Query(value = PARAMS, encoded = true) String params); + + @Streaming + @POST("query?chunked=true") + @FormUrlEncoded + public Call postQuery(@Query(DB) String db, @Field(value = Q, encoded = true) String query, + @Query(CHUNK_SIZE) int chunkSize); + + @Streaming + @POST("query?chunked=true") + @FormUrlEncoded + public Call postQuery(@Query(DB) String db, @Field(value = Q, encoded = true) String query, + @Query(CHUNK_SIZE) int chunkSize, @Query(value = PARAMS, encoded = true) String params); @POST("query") - public Call postQuery(@Query(value = Q, encoded = true) String query); + @FormUrlEncoded + public Call postQuery(@Field(value = Q, encoded = true) String query); @Streaming @GET("query?chunked=true") @@ -76,7 +91,7 @@ public Call query(@Query(DB) String db, @Query(value = Q, encoded @Query(CHUNK_SIZE) int chunkSize); @Streaming - @POST("query?chunked=true") + @GET("query?chunked=true") public Call query(@Query(DB) String db, @Query(value = Q, encoded = true) String query, @Query(CHUNK_SIZE) int chunkSize, @Query(value = PARAMS, encoded = true) String params); } diff --git a/src/main/java/org/influxdb/impl/TypeMapper.java b/src/main/java/org/influxdb/impl/TypeMapper.java new file mode 100644 index 000000000..98f4ada63 --- /dev/null +++ b/src/main/java/org/influxdb/impl/TypeMapper.java @@ -0,0 +1,70 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2017 azeti Networks AG () + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.influxdb.impl; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashMap; +import java.util.Map; + +/** + * Resolves generic type variables to actual types, based on context. + * + * @author Eran Leshem + */ +@FunctionalInterface +public interface TypeMapper { + TypeMapper EMPTY = typeVariable -> null; + + static TypeMapper of(ParameterizedType type, Class clazz) { + TypeVariable>[] typeVariables = clazz.getTypeParameters(); + Type[] types = type.getActualTypeArguments(); + if (types.length != typeVariables.length) { + throw new IllegalStateException("Mismatched lengths for type variables and actual types"); + } + Map, Type> typeMapping = new HashMap<>(typeVariables.length); + for (int i = 0; i < typeVariables.length; i++) { + typeMapping.put(typeVariables[i], types[i]); + } + + return typeMapping::get; + } + + static TypeMapper empty() { + return EMPTY; + } + + default Type resolve(Type type) { + if (type instanceof TypeVariable) { + Type resolvedType = get((TypeVariable) type); + if (resolvedType == null) { + throw new IllegalStateException("Could not resolve type " + type); + } + + return resolvedType; + } + + return type; + } + + Type get(TypeVariable typeVariable); +} diff --git a/src/main/java/org/influxdb/querybuilder/Appender.java b/src/main/java/org/influxdb/querybuilder/Appender.java index 3dab5c02f..8c7e34bfd 100644 --- a/src/main/java/org/influxdb/querybuilder/Appender.java +++ b/src/main/java/org/influxdb/querybuilder/Appender.java @@ -62,6 +62,8 @@ public static StringBuilder appendValue(final Object value, final StringBuilder stringBuilder.append(')'); } else if (value instanceof Column) { appendName(((Column) value).getName(), stringBuilder); + } else if (value instanceof Placeholder) { + stringBuilder.append('$').append(((Placeholder) value).getName()); } else if (value instanceof String) { stringBuilder.append("'").append(value).append("'"); } else if (value != null) { diff --git a/src/main/java/org/influxdb/querybuilder/FunctionFactory.java b/src/main/java/org/influxdb/querybuilder/FunctionFactory.java index 19541c46a..ba5bfaba3 100644 --- a/src/main/java/org/influxdb/querybuilder/FunctionFactory.java +++ b/src/main/java/org/influxdb/querybuilder/FunctionFactory.java @@ -61,6 +61,10 @@ public static Object column(final String name) { return new Column(name); } + public static Object placeholder(final String name) { + return new Placeholder(name); + } + private static void convertToColumns(final Object... arguments) { for (int i = 0; i < arguments.length; i++) { arguments[i] = convertToColumn(arguments[i]); diff --git a/src/main/java/org/influxdb/querybuilder/Placeholder.java b/src/main/java/org/influxdb/querybuilder/Placeholder.java new file mode 100644 index 000000000..8b21cd880 --- /dev/null +++ b/src/main/java/org/influxdb/querybuilder/Placeholder.java @@ -0,0 +1,14 @@ +package org.influxdb.querybuilder; + +public class Placeholder { + + private final String name; + + Placeholder(final String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/test-jdk17/java/org/influxdb/impl/InfluxDBRecordResultMapperTest.java b/src/test-jdk17/java/org/influxdb/impl/InfluxDBRecordResultMapperTest.java new file mode 100644 index 000000000..c16fa24b0 --- /dev/null +++ b/src/test-jdk17/java/org/influxdb/impl/InfluxDBRecordResultMapperTest.java @@ -0,0 +1,579 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2017 azeti Networks AG () + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.influxdb.impl; + +import org.influxdb.InfluxDBMapperException; +import org.influxdb.annotation.Column; +import org.influxdb.annotation.Measurement; +import org.influxdb.annotation.TimeColumn; +import org.influxdb.dto.QueryResult; +import org.junit.Assert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * @author Eran Leshem + */ +@SuppressWarnings({"removal", "deprecation"}) +@RunWith(JUnitPlatform.class) +public class InfluxDBRecordResultMapperTest { + + private final InfluxDBResultMapper mapper = new InfluxDBResultMapper(); + + @Test + public void testToRecord_HappyPath() { + // Given... + var columnList = Arrays.asList("time", "uuid"); + List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString()); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + series.setName("CustomMeasurement"); + series.setValues(List.of(firstSeriesResult)); + + var internalResult = new QueryResult.Result(); + internalResult.setSeries(List.of(series)); + + var queryResult = new QueryResult(); + queryResult.setResults(List.of(internalResult)); + + //When... + var myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class); + + // Then... + Assertions.assertEquals(1, myList.size(), "there must be one entry in the result list"); + } + + @Test + public void testThrowExceptionIfMissingAnnotation() { + Assertions.assertThrows(IllegalArgumentException.class, () -> mapper.throwExceptionIfMissingAnnotation(String.class)); + } + + @Test + public void testThrowExceptionIfError_InfluxQueryResultHasError() { + var queryResult = new QueryResult(); + queryResult.setError("main queryresult error"); + + Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult)); + } + + @Test + public void testThrowExceptionIfError_InfluxQueryResultSeriesHasError() { + var seriesResult = new QueryResult.Result(); + seriesResult.setError("series error"); + + var queryResult = new QueryResult(); + queryResult.setResults(List.of(seriesResult)); + + Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult)); + } + + @Test + public void testGetMeasurementName_testStateMeasurement() { + Assertions.assertEquals("CustomMeasurement", mapper.getMeasurementName(MyCustomMeasurement.class)); + } + + @Test + public void testParseSeriesAs_testTwoValidSeries() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var columnList = Arrays.asList("time", "uuid"); + + List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString()); + List secondSeriesResult = Arrays.asList(Instant.now().plusSeconds(1).toEpochMilli(), + UUID.randomUUID().toString()); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + series.setValues(Arrays.asList(firstSeriesResult, secondSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + Assertions.assertTrue(result.size() == 2, "there must be two series in the result list"); + + Assertions.assertEquals(firstSeriesResult.get(0), result.get(0).time().toEpochMilli(), + "Field 'time' (1st series) is not valid"); + Assertions.assertEquals(firstSeriesResult.get(1), result.get(0).uuid(), "Field 'uuid' (1st series) is not valid"); + + Assertions.assertEquals(secondSeriesResult.get(0), result.get(1).time().toEpochMilli(), + "Field 'time' (2nd series) is not valid"); + Assertions.assertEquals(secondSeriesResult.get(1), result.get(1).uuid(), "Field 'uuid' (2nd series) is not valid"); + } + + @Test + public void testParseSeriesAs_testNonNullAndValidValues() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurementWithPrimitives.class); + + var columnList = Arrays.asList("time", "uuid", + "doubleObject", "longObject", "integerObject", + "doublePrimitive", "longPrimitive", "integerPrimitive", + "booleanObject", "booleanPrimitive"); + + // InfluxDB client returns the time representation as Double. + Double now = Long.valueOf(System.currentTimeMillis()).doubleValue(); + + // InfluxDB client returns any number as Double. + // See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987 + // for more information. + + var series = new QueryResult.Series(); + series.setColumns(columnList); + var uuidAsString = UUID.randomUUID().toString(); + List seriesResult = Arrays.asList(now, uuidAsString, + new Double("1.01"), new Double("2"), new Double("3"), + new Double("1.01"), new Double("4"), new Double("5"), + "false", "true"); + series.setValues(List.of(seriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurementWithPrimitives.class, result); + + //Then... + var myObject = result.get(0); + Assertions.assertEquals(now.longValue(), myObject.time().toEpochMilli(), "field 'time' does not match"); + Assertions.assertEquals(uuidAsString, myObject.uuid(), "field 'uuid' does not match"); + + Assertions.assertEquals(asDouble(seriesResult.get(2)), myObject.doubleObject(), + "field 'doubleObject' does not match"); + Assertions.assertEquals(Long.valueOf(asDouble(seriesResult.get(3)).longValue()), myObject.longObject(), + "field 'longObject' does not match"); + Assertions.assertEquals(Integer.valueOf(asDouble(seriesResult.get(4)).intValue()), myObject.integerObject(), + "field 'integerObject' does not match"); + + Assertions.assertTrue( + Double.compare(asDouble(seriesResult.get(5)).doubleValue(), myObject.doublePrimitive()) == 0, + "field 'doublePrimitive' does not match"); + + Assertions.assertTrue(asDouble(seriesResult.get(6)).longValue() == myObject.longPrimitive(), + "field 'longPrimitive' does not match"); + + Assertions.assertTrue(asDouble(seriesResult.get(7)).intValue() == myObject.integerPrimitive(), + "field 'integerPrimitive' does not match"); + + Assertions.assertEquals( + Boolean.valueOf(String.valueOf(seriesResult.get(8))), myObject.booleanObject(), + "field 'booleanObject' does not match"); + + Assertions.assertEquals( + Boolean.valueOf(String.valueOf(seriesResult.get(9))).booleanValue(), myObject.booleanPrimitive(), + "field 'booleanPrimitive' does not match"); + } + + private static Double asDouble(Object obj) { + return (Double) obj; + } + + @Test + public void testFieldValueModified_DateAsISO8601() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var columnList = List.of("time"); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = List.of("2017-06-19T09:29:45.655123Z"); + series.setValues(List.of(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + Assertions.assertTrue(result.size() == 1); + } + + @Test + public void testFieldValueModified_DateAsInteger() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var columnList = List.of("time"); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = List.of(1_000); + series.setValues(List.of(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + Assertions.assertTrue(result.size() == 1); + } + + @Test + public void testUnsupportedField() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyRecordWithUnsupportedField.class); + + var columnList = List.of("bar"); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = List.of("content representing a Date"); + series.setValues(List.of(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + Assertions.assertThrows(InfluxDBMapperException.class, + () -> mapper.parseSeriesAs(series, MyRecordWithUnsupportedField.class, result)); + } + + /** + * for more information. + */ + @Test + public void testToRecord_SeriesFromQueryResultIsNull() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var internalResult = new QueryResult.Result(); + internalResult.setSeries(null); + + var queryResult = new QueryResult(); + queryResult.setResults(List.of(internalResult)); + + // When... + var myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class); + + // Then... + Assertions.assertTrue( myList.isEmpty(), "there must NO entry in the result list"); + } + + @Test + public void testToRecord_QueryResultCreatedByGroupByClause() { + // Given... + InfluxDBResultMapper.cacheRecordClass(GroupByCarrierDeviceOS.class); + + // InfluxDB client returns the time representation as Double. + Double now = Long.valueOf(System.currentTimeMillis()).doubleValue(); + + // When the "GROUP BY" clause is used, "tags" are returned as Map + Map firstSeriesTagMap = new HashMap<>(2); + firstSeriesTagMap.put("CARRIER", "000/00"); + firstSeriesTagMap.put("DEVICE_OS_VERSION", "4.4.2"); + + Map secondSeriesTagMap = new HashMap<>(2); + secondSeriesTagMap.put("CARRIER", "000/01"); + secondSeriesTagMap.put("DEVICE_OS_VERSION", "9.3.5"); + + var firstSeries = new QueryResult.Series(); + var columnList = Arrays.asList("time", "median", "min", "max"); + firstSeries.setColumns(columnList); + List firstSeriesResult = Arrays.asList(now, new Double("233.8"), new Double("0.0"), + new Double("3090744.0")); + firstSeries.setValues(List.of(firstSeriesResult)); + firstSeries.setTags(firstSeriesTagMap); + firstSeries.setName("tb_network"); + + var secondSeries = new QueryResult.Series(); + secondSeries.setColumns(columnList); + List secondSeriesResult = Arrays.asList(now, new Double("552.0"), new Double("135.0"), + new Double("267705.0")); + secondSeries.setValues(List.of(secondSeriesResult)); + secondSeries.setTags(secondSeriesTagMap); + secondSeries.setName("tb_network"); + + var internalResult = new QueryResult.Result(); + internalResult.setSeries(Arrays.asList(firstSeries, secondSeries)); + + var queryResult = new QueryResult(); + queryResult.setResults(List.of(internalResult)); + + // When... + var myList = mapper.toPOJO(queryResult, GroupByCarrierDeviceOS.class); + + // Then... + var firstGroupByEntry = myList.get(0); + Assertions.assertEquals("000/00", firstGroupByEntry.carrier(), "field 'carrier' does not match"); + Assertions.assertEquals("4.4.2", firstGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match"); + + var secondGroupByEntry = myList.get(1); + Assertions.assertEquals("000/01", secondGroupByEntry.carrier(), "field 'carrier' does not match"); + Assertions.assertEquals("9.3.5", secondGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match"); + } + + @Test + public void testToRecord_ticket363() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var columnList = List.of("time"); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = List.of("2000-01-01T00:00:00.000000001Z"); + series.setValues(List.of(firstSeriesResult)); + + // When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + // Then... + Assertions.assertEquals(1, result.size(), "incorrect number of elemets"); + Assertions.assertEquals(1, result.get(0).time().getNano(), "incorrect value for the nanoseconds field"); + } + + @Test + void testToRecord_Precision() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var series = new QueryResult.Series(); + series.setName("CustomMeasurement"); + var columnList = List.of("time"); + series.setColumns(columnList); + List firstSeriesResult = List.of(1_500_000L); + series.setValues(List.of(firstSeriesResult)); + + var internalResult = new QueryResult.Result(); + internalResult.setSeries(List.of(series)); + + var queryResult = new QueryResult(); + queryResult.setResults(List.of(internalResult)); + + // When... + var result = mapper.toPOJO(queryResult, MyCustomMeasurement.class, TimeUnit.SECONDS); + + // Then... + Assertions.assertEquals(1, result.size(), "incorrect number of elements"); + Assertions.assertEquals(1_500_000_000L, result.get(0).time().toEpochMilli(), + "incorrect value for the millis field"); + } + + @Test + void testToRecord_SetMeasureName() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var series = new QueryResult.Series(); + series.setName("MySeriesName"); + var columnList = List.of("uuid"); + series.setColumns(columnList); + List firstSeriesResult = Collections.singletonList(UUID.randomUUID().toString()); + series.setValues(List.of(firstSeriesResult)); + + var internalResult = new QueryResult.Result(); + internalResult.setSeries(List.of(series)); + + var queryResult = new QueryResult(); + queryResult.setResults(List.of(internalResult)); + + //When... + var result = + mapper.toPOJO(queryResult, MyCustomMeasurement.class, "MySeriesName"); + + //Then... + Assertions.assertTrue(result.size() == 1); + } + + @Test + public void testToRecord_HasTimeColumn() { + // Given... + InfluxDBResultMapper.cacheRecordClass(HasTimeColumnMeasurement.class); + + var columnList = List.of("time"); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + List> valuesList = Arrays.asList( + List.of("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5) + List.of("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5) + List.of("2000-01-01T00:00:00-00:00"), + List.of("2000-01-02T00:00:00+00:00") + ); + series.setValues(valuesList); + + // When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, HasTimeColumnMeasurement.class, result); + + // Then... + Assertions.assertEquals(4, result.size(), "incorrect number of elemets"); + // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC) + Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z"))); + Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z"))); + // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3 + Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z"))); + Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z"))); + + } + + @Test + public void testToRecord_ticket573() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + var columnList = List.of("time"); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + List> valuesList = Arrays.asList( + List.of("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5) + List.of("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5) + List.of("2000-01-01T00:00:00-00:00"), + List.of("2000-01-02T00:00:00+00:00") + ); + series.setValues(valuesList); + + // When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + // Then... + Assertions.assertEquals(4, result.size(), "incorrect number of elemets"); + // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC) + Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z"))); + Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z"))); + // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3 + Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z"))); + Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z"))); + } + + @Test + public void testMultipleConstructors() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MultipleConstructors.class); + + var columnList = List.of("i", "s"); + + var series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = List.of(9.0, "str"); + series.setValues(List.of(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MultipleConstructors.class, result); + + //Then... + Assertions.assertTrue(result.size() == 1); + + Assert.assertEquals(9, result.get(0).i()); + Assert.assertEquals("str", result.get(0).s()); + } + + @Test + public void testConflictingConstructors() { + Assert.assertThrows(InfluxDBMapperException.class, + () -> InfluxDBResultMapper.cacheRecordClass(ConflictingConstructors.class)); + } + + @Measurement(name = "HasTimeColumnMeasurement") + record HasTimeColumnMeasurement( + @TimeColumn + Instant time, + Integer value) {} + + @Measurement(name = "CustomMeasurement") + record MyCustomMeasurement( + Instant time, + String uuid, + Double doubleObject, + Long longObject, + Integer integerObject, + Boolean booleanObject, + + @SuppressWarnings("unused") + String nonColumn1, + + @SuppressWarnings("unused") + Random rnd) {} + + @Measurement(name = "CustomMeasurement") + record MyCustomMeasurementWithPrimitives( + Instant time, + String uuid, + Double doubleObject, + Long longObject, + Integer integerObject, + double doublePrimitive, + long longPrimitive, + int integerPrimitive, + Boolean booleanObject, + boolean booleanPrimitive, + + @SuppressWarnings("unused") + String nonColumn1, + + @SuppressWarnings("unused") + Random rnd) {} + + @Measurement(name = "foo") + record MyRecordWithUnsupportedField( + @Column(name = "bar") + Date myDate) {} + + /** + * Class created based on example from this issue + */ + @Measurement(name = "tb_network") + record GroupByCarrierDeviceOS( + Instant time, + + @Column(name = "CARRIER", tag = true) + String carrier, + + @Column(name = "DEVICE_OS_VERSION", tag = true) + String deviceOsVersion, + + Double median, + Double min, + Double max) {} + + record MultipleConstructors(int i, String s) { + MultipleConstructors(String i, String s) { + this(Integer.parseInt(i), s); + } + + MultipleConstructors(int i, String s, double d) { + this(i, s); + } + } + + record ConflictingConstructors(int i, String s) { + private ConflictingConstructors(String s, int i) { + this(i, s); + } + } +} diff --git a/src/test/java/com/android/tools/r8/RecordTag.java b/src/test/java/com/android/tools/r8/RecordTag.java new file mode 100644 index 000000000..51aacedbb --- /dev/null +++ b/src/test/java/com/android/tools/r8/RecordTag.java @@ -0,0 +1,9 @@ +package com.android.tools.r8; + +/** + * Simulates the super class of Android-desugared records. + * + * @author Eran Leshem + **/ +public class RecordTag { +} diff --git a/src/test/java/org/influxdb/BatchOptionsTest.java b/src/test/java/org/influxdb/BatchOptionsTest.java index 351321b68..1abef4576 100644 --- a/src/test/java/org/influxdb/BatchOptionsTest.java +++ b/src/test/java/org/influxdb/BatchOptionsTest.java @@ -7,13 +7,13 @@ import org.influxdb.dto.Point; import org.influxdb.dto.Query; import org.influxdb.dto.QueryResult; -import org.jetbrains.annotations.NotNull; +import org.influxdb.impl.BatchProcessor; +import org.influxdb.impl.BatchProcessorTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -21,12 +21,15 @@ import java.io.IOException; import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Supplier; @RunWith(JUnitPlatform.class) @@ -234,15 +237,20 @@ public void testFlushDuration() throws InterruptedException { * @throws InterruptedException */ @Test - public void testJitterDuration() throws InterruptedException { + public void testJitterDuration() throws Exception { String dbName = "write_unittest_" + System.currentTimeMillis(); try { - BatchOptions options = BatchOptions.DEFAULTS.flushDuration(100).jitterDuration(500); + // prepare points before start BatchProcessor + List points = prepareSomePoints(0, 19); + BatchOptions options = BatchOptions.DEFAULTS.flushDuration(100).jitterDuration(1000); influxDB.query(new Query("CREATE DATABASE " + dbName)); influxDB.setDatabase(dbName); influxDB.enableBatch(options); - write20Points(influxDB); + BatchProcessor batchProcessor = BatchProcessorTest.getPrivateField(influxDB, "batchProcessor"); + // random always return 1.0 to be sure that first query is null + BatchProcessorTest.setPrivateField(batchProcessor, "randomSupplier", (Supplier) () -> 1.0); + points.forEach(influxDB::write); Thread.sleep(100); @@ -251,7 +259,7 @@ public void testJitterDuration() throws InterruptedException { Assertions.assertNull(result.getResults().get(0).getError()); //wait for at least one flush - Thread.sleep(1000); + Thread.sleep(1500); result = influxDB.query(new Query("select * from weather", dbName)); Assertions.assertEquals(20, result.getResults().get(0).getSeries().get(0).getValues().size()); } @@ -458,8 +466,7 @@ public void testHandlerOnRetryImpossible() throws InterruptedException { writeSomePoints(influxDB, 1); - Thread.sleep(200); - verify(mockHandler, times(1)).accept(any(), any()); + verify(mockHandler, timeout(500).times(1)).accept(any(), any()); QueryResult result = influxDB.query(new Query("select * from weather", dbName)); Assertions.assertNull(result.getResults().get(0).getSeries()); @@ -596,6 +603,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { verify(spy, times(2)).write(any(BatchPoints.class)); + Thread.sleep(1_500); QueryResult result = influxDB.query(new Query("select * from m0", dbName)); Assertions.assertNotNull(result.getResults().get(0).getSeries()); Assertions.assertEquals(200, result.getResults().get(0).getSeries().get(0).getValues().size()); @@ -668,14 +676,7 @@ void writeSomePoints(InfluxDB influxDB, String measurement, int firstIndex, int } void writeSomePoints(InfluxDB influxDB, int firstIndex, int lastIndex) { - for (int i = firstIndex; i <= lastIndex; i++) { - Point point = Point.measurement("weather") - .time(i,TimeUnit.HOURS) - .addField("temperature", (double) i) - .addField("humidity", (double) (i) * 1.1) - .addField("uv_index", "moderate").build(); - influxDB.write(point); - } + prepareSomePoints(firstIndex, lastIndex).forEach(influxDB::write); } void write20Points(InfluxDB influxDB) { @@ -686,6 +687,19 @@ void writeSomePoints(InfluxDB influxDB, int n) { writeSomePoints(influxDB, 0, n - 1); } + List prepareSomePoints(int firstIndex, int lastIndex) { + List points = new ArrayList<>(); + for (int i = firstIndex; i <= lastIndex; i++) { + Point point = Point.measurement("weather") + .time(i, TimeUnit.HOURS) + .addField("temperature", (double) i) + .addField("humidity", (double) (i) * 1.1) + .addField("uv_index", "moderate").build(); + points.add(point); + } + return points; + } + private BatchPoints createBatchPoints(String dbName, String measurement, int n) { BatchPoints batchPoints = BatchPoints.database(dbName).build(); for (int i = 1; i <= n; i++) { diff --git a/src/test/java/org/influxdb/InfluxDB2Test.java b/src/test/java/org/influxdb/InfluxDB2Test.java new file mode 100644 index 000000000..0118d0318 --- /dev/null +++ b/src/test/java/org/influxdb/InfluxDB2Test.java @@ -0,0 +1,57 @@ +package org.influxdb; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.influxdb.dto.Query; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +/** + * @author Jakub Bednar (30/08/2021 11:31) + */ +@RunWith(JUnitPlatform.class) +@EnabledIfEnvironmentVariable(named = "INFLUXDB_VERSION", matches = "2\\..") +public class InfluxDB2Test { + + private InfluxDB influxDB; + + @BeforeEach + public void setUp() throws NoSuchFieldException, IllegalAccessException { + String url = String.format("http://%s:%s", TestUtils.getInfluxIP(), TestUtils.getInfluxPORT(true)); + influxDB = InfluxDBFactory + .connect(url, "my-user", "my-password") + .setDatabase("mydb") + .setRetentionPolicy("autogen"); + } + + @AfterEach + public void cleanup() { + influxDB.close(); + } + + @Test + public void testQuery() throws InterruptedException { + + String measurement = TestUtils.getRandomMeasurement(); + + // prepare data + List records = new ArrayList<>(); + records.add(measurement + ",test=a value=1 1"); + records.add(measurement + ",test=a value=2 2"); + influxDB.write(records); + + // query data + final CountDownLatch countDownLatch = new CountDownLatch(1); + influxDB.query(new Query("SELECT * FROM " + measurement), 2, queryResult -> countDownLatch.countDown()); + + Assertions.assertTrue(countDownLatch.await(2, TimeUnit.SECONDS)); + } +} \ No newline at end of file diff --git a/src/test/java/org/influxdb/InfluxDBTest.java b/src/test/java/org/influxdb/InfluxDBTest.java index 326f45dce..2598b9f23 100644 --- a/src/test/java/org/influxdb/InfluxDBTest.java +++ b/src/test/java/org/influxdb/InfluxDBTest.java @@ -41,6 +41,8 @@ import java.util.function.Consumer; import java.util.regex.Pattern; +import static org.assertj.core.api.Assertions.assertThat; + /** * Test the InfluxDB API. * @@ -722,6 +724,16 @@ public void testWriteBatchWithoutPrecision() throws Exception { Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().get(1).get(0), timeP2); Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().get(2).get(0), timeP3); + // WHEN I use the post query + queryResult = this.influxDB.query(new Query("SELECT * FROM " + measurement, dbName, true), TimeUnit.NANOSECONDS); + + // THEN result will be same + Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().size(), 3); + Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().size(), 3); + Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().get(0).get(0), timeP1); + Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().get(1).get(0), timeP2); + Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().get(2).get(0), timeP3); + this.influxDB.query(new Query("DROP DATABASE " + dbName)); } @@ -1301,6 +1313,37 @@ public void testChunkingOnFailureConnectionError() throws InterruptedException { } + @Test + public void testChunkingQueryPost() throws InterruptedException { + if (this.influxDB.version().startsWith("0.") || this.influxDB.version().startsWith("1.0")) { + // do not test version 0.13 and 1.0 + return; + } + + String dbName = "write_unittest_" + System.currentTimeMillis(); + this.influxDB.query(new Query("CREATE DATABASE " + dbName)); + String rp = TestUtils.defaultRetentionPolicy(this.influxDB.version()); + BatchPoints batchPoints = BatchPoints.database(dbName).retentionPolicy(rp).build(); + Point point1 = Point.measurement("disk").tag("atag", "a").addField("used", 60L).addField("free", 1L).build(); + Point point2 = Point.measurement("disk").tag("atag", "b").addField("used", 70L).addField("free", 2L).build(); + Point point3 = Point.measurement("disk").tag("atag", "c").addField("used", 80L).addField("free", 3L).build(); + batchPoints.point(point1); + batchPoints.point(point2); + batchPoints.point(point3); + this.influxDB.write(batchPoints); + + CountDownLatch countDownLatch = new CountDownLatch(2); + + Thread.sleep(2000); + Query query = new Query("SELECT * FROM disk", dbName, true); + this.influxDB.query(query, 2, result -> countDownLatch.countDown()); + + boolean await = countDownLatch.await(10, TimeUnit.SECONDS); + Assertions.assertTrue(await, "The QueryResults did not arrive!"); + + this.influxDB.query(new Query("DROP DATABASE " + dbName)); + } + @Test public void testFlushPendingWritesWhenBatchingEnabled() { String dbName = "flush_tests_" + System.currentTimeMillis(); @@ -1416,6 +1459,18 @@ public String call() throws Exception { Assertions.assertTrue(MyInfluxDBBean.OKHTTP_BUILDER.interceptors().isEmpty()); } + @Test + public void testQueryPostWithGZIPCompression() { + this.influxDB.enableGzip(); + String database = "db_gzip_" + System.currentTimeMillis(); + this.influxDB.query(new Query(String.format("CREATE DATABASE %s", database), null, true)); + QueryResult query = this.influxDB.query(new Query("SHOW DATABASES", null, true)); + assertThat(query.getResults()).hasSize(1); + assertThat(query.getResults().get(0).getSeries()).hasSize(1); + assertThat(query.getResults().get(0).getSeries().get(0).getValues()).contains(Collections.singletonList(database)); + this.influxDB.query(new Query(String.format("DROP DATABASE %s", database), null, true)); + } + private static final class MyInfluxDBBean { static final OkHttpClient.Builder OKHTTP_BUILDER = new OkHttpClient.Builder(); diff --git a/src/test/java/org/influxdb/MessagePackInfluxDBTest.java b/src/test/java/org/influxdb/MessagePackInfluxDBTest.java index 9abda1246..f19e2343b 100644 --- a/src/test/java/org/influxdb/MessagePackInfluxDBTest.java +++ b/src/test/java/org/influxdb/MessagePackInfluxDBTest.java @@ -164,6 +164,18 @@ public void testWriteBatchWithoutPrecision() throws Exception { value = Double.valueOf(queryResult.getResults().get(0).getSeries().get(0).getValues().get(2).get(0).toString()); Assertions.assertEquals(value, timeP3); + // WHEN I use the post query + queryResult = this.influxDB.query(new Query("SELECT * FROM " + measurement, dbName, true), TimeUnit.NANOSECONDS); + + // THEN result will be same + Assertions.assertEquals(queryResult.getResults().get(0).getSeries().get(0).getValues().size(), 3); + value = Double.valueOf(queryResult.getResults().get(0).getSeries().get(0).getValues().get(0).get(0).toString()); + Assertions.assertEquals(value, timeP1); + value = Double.valueOf(queryResult.getResults().get(0).getSeries().get(0).getValues().get(1).get(0).toString()); + Assertions.assertEquals(value, timeP2); + value = Double.valueOf(queryResult.getResults().get(0).getSeries().get(0).getValues().get(2).get(0).toString()); + Assertions.assertEquals(value, timeP3); + this.influxDB.query(new Query("DROP DATABASE " + dbName)); } diff --git a/src/test/java/org/influxdb/dto/PointTest.java b/src/test/java/org/influxdb/dto/PointTest.java index a49454177..9148c64ba 100755 --- a/src/test/java/org/influxdb/dto/PointTest.java +++ b/src/test/java/org/influxdb/dto/PointTest.java @@ -1,7 +1,18 @@ package org.influxdb.dto; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import org.influxdb.BuilderException; +import org.influxdb.InfluxDB; +import org.influxdb.InfluxDBMapperException; +import org.influxdb.annotation.Column; +import org.influxdb.annotation.Measurement; +import org.influxdb.annotation.TimeColumn; +import org.influxdb.impl.InfluxDBImpl; +import org.junit.Assert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.lang.reflect.Field; import java.math.BigDecimal; @@ -16,18 +27,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import org.influxdb.BuilderException; -import org.influxdb.InfluxDB; -import org.influxdb.annotation.Column; -import org.influxdb.annotation.Measurement; -import org.influxdb.annotation.TimeColumn; -import org.influxdb.impl.InfluxDBImpl; -import org.junit.Assert; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.platform.runner.JUnitPlatform; -import org.junit.runner.RunWith; -import org.mockito.Mockito; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Test for the Point DTO. @@ -695,6 +696,30 @@ public void testAddFieldsFromPOJOWithTimeColumnNanoseconds() throws NoSuchFieldE pojo.time = null; } + @Test + public void testAddFieldsFromPOJOWithTimeColumnSeconds() throws NoSuchFieldException, IllegalAccessException { + TimeColumnPojoSec pojo = new TimeColumnPojoSec(); + pojo.time = Instant.now().plusSeconds(132L).plus(365L * 12000, ChronoUnit.DAYS); + pojo.booleanPrimitive = true; + + Point p = Point.measurementByPOJO(pojo.getClass()).addFieldsFromPOJO(pojo).build(); + Field timeField = p.getClass().getDeclaredField("time"); + Field precisionField = p.getClass().getDeclaredField("precision"); + timeField.setAccessible(true); + precisionField.setAccessible(true); + + Assertions.assertEquals(pojo.booleanPrimitive, p.getFields().get("booleanPrimitive")); + Assertions.assertEquals(TimeUnit.SECONDS, precisionField.get(p)); + Assertions.assertEquals(pojo.time.getEpochSecond(), timeField.get(p)); + } + + @Test + public void testAddFieldsFromPOJOWithBadTimeColumn() { + BadTimeColumnPojo pojo = new BadTimeColumnPojo(); + Assertions.assertThrows(InfluxDBMapperException.class, + () -> Point.measurementByPOJO(pojo.getClass()).addFieldsFromPOJO(pojo).build()); + } + @Test public void testAddFieldsFromPOJOWithTimeColumnNull() throws NoSuchFieldException, IllegalAccessException { TimeColumnPojo pojo = new TimeColumnPojo(); @@ -712,7 +737,7 @@ public void testAddFieldsFromPOJOWithTimeColumnNull() throws NoSuchFieldExceptio } @Test - public void testAddFieldsFromPOJOWithData() throws NoSuchFieldException, IllegalAccessException { + public void testAddFieldsFromPOJOWithData() { Pojo pojo = new Pojo(); pojo.booleanObject = true; pojo.booleanPrimitive = false; @@ -735,7 +760,6 @@ public void testAddFieldsFromPOJOWithData() throws NoSuchFieldException, Illegal Assertions.assertEquals(pojo.integerPrimitive, p.getFields().get("integerPrimitive")); Assertions.assertEquals(pojo.longObject, p.getFields().get("longObject")); Assertions.assertEquals(pojo.longPrimitive, p.getFields().get("longPrimitive")); - Assertions.assertEquals(pojo.time, p.getFields().get("time")); Assertions.assertEquals(pojo.uuid, p.getTags().get("uuid")); } @@ -790,7 +814,63 @@ public void testAddFieldsFromPOJOWithPublicAttributes() { Assertions.assertEquals(pojo.integerPrimitive, p.getFields().get("integerPrimitive")); Assertions.assertEquals(pojo.longObject, p.getFields().get("longObject")); Assertions.assertEquals(pojo.longPrimitive, p.getFields().get("longPrimitive")); - Assertions.assertEquals(pojo.time, p.getFields().get("time")); + Assertions.assertEquals(pojo.uuid, p.getTags().get("uuid")); + } + + @Test + public void testAddFieldsFromPojoWithAllFieldsAnnotation() { + + PojoWithAllFieldsAnnotation pojo = new PojoWithAllFieldsAnnotation(); + pojo.booleanObject = true; + pojo.booleanPrimitive = false; + pojo.doubleObject = 2.0; + pojo.doublePrimitive = 3.1; + pojo.integerObject = 32; + pojo.integerPrimitive = 64; + pojo.longObject = 1L; + pojo.longPrimitive = 2L; + pojo.time = Instant.now(); + pojo.uuid = "TEST"; + + Point p = Point.measurementByPOJO(PojoWithAllFieldsAnnotation.class).addFieldsFromPOJO(pojo).build(); + + assertThat(p.lineProtocol()).startsWith("mymeasurement"); + assertThat(p.getFields()).doesNotContainKey("staticField"); + Assertions.assertEquals(pojo.booleanObject, p.getFields().get("booleanObject")); + Assertions.assertEquals(pojo.booleanPrimitive, p.getFields().get("booleanPrimitive")); + Assertions.assertEquals(pojo.doubleObject, p.getFields().get("doubleObject")); + Assertions.assertEquals(pojo.doublePrimitive, p.getFields().get("doublePrimitive")); + Assertions.assertEquals(pojo.integerObject, p.getFields().get("integerObject")); + Assertions.assertEquals(pojo.integerPrimitive, p.getFields().get("integerPrimitive")); + Assertions.assertEquals(pojo.longObject, p.getFields().get("longObject")); + Assertions.assertEquals(pojo.longPrimitive, p.getFields().get("longPrimitive")); + Assertions.assertEquals(pojo.uuid, p.getTags().get("uuid")); + } + + @Test + public void testAddFieldsFromPojoWithBlankColumnAnnotations() { + PojoWithBlankColumnAnnotations pojo = new PojoWithBlankColumnAnnotations(); + pojo.booleanObject = true; + pojo.booleanPrimitive = false; + pojo.doubleObject = 2.0; + pojo.doublePrimitive = 3.1; + pojo.integerObject = 32; + pojo.integerPrimitive = 64; + pojo.longObject = 1L; + pojo.longPrimitive = 2L; + pojo.time = Instant.now(); + pojo.uuid = "TEST"; + + Point p = Point.measurementByPOJO(PojoWithBlankColumnAnnotations.class).addFieldsFromPOJO(pojo).build(); + + Assertions.assertEquals(pojo.booleanObject, p.getFields().get("booleanObject")); + Assertions.assertEquals(pojo.booleanPrimitive, p.getFields().get("booleanPrimitive")); + Assertions.assertEquals(pojo.doubleObject, p.getFields().get("doubleObject")); + Assertions.assertEquals(pojo.doublePrimitive, p.getFields().get("doublePrimitive")); + Assertions.assertEquals(pojo.integerObject, p.getFields().get("integerObject")); + Assertions.assertEquals(pojo.integerPrimitive, p.getFields().get("integerPrimitive")); + Assertions.assertEquals(pojo.longObject, p.getFields().get("longObject")); + Assertions.assertEquals(pojo.longPrimitive, p.getFields().get("longPrimitive")); Assertions.assertEquals(pojo.uuid, p.getTags().get("uuid")); } @@ -810,6 +890,22 @@ public void testInheritMeasurement() { Assert.assertEquals(expected, actual); } + @Test + public void testGenericInheritMeasurement() { + Point expected = Point.measurementByPOJO(MyGenericSubMeasurement.class) + .addField("superValue", "super") + .addField("subValue", "sub") + .build(); + MyGenericSubMeasurement scm = new MyGenericSubMeasurement(); + scm.subValue = "sub"; + scm.superValue = "super"; + + Point actual = Point.measurementByPOJO(MyGenericSubMeasurement.class) + .addFieldsFromPOJO(scm) + .build(); + Assert.assertEquals(expected, actual); + } + static class PojoWithoutAnnotation { private String id; @@ -857,6 +953,22 @@ static class TimeColumnPojoNano { private Instant time; } + @Measurement(name = "tcmeasurement", allFields = true) + static class TimeColumnPojoSec { + boolean booleanPrimitive; + + @TimeColumn(timeUnit = TimeUnit.SECONDS) + Instant time; + } + + @Measurement(name = "tcmeasurement", allFields = true) + static class BadTimeColumnPojo { + boolean booleanPrimitive; + + @TimeColumn + String time; + } + @Measurement(name = "mymeasurement") static class Pojo { @@ -864,6 +976,7 @@ static class Pojo { private boolean booleanPrimitive; @Column(name = "time") + @TimeColumn private Instant time; @Column(name = "uuid", tag = true) @@ -980,6 +1093,7 @@ static class PojoWithPublicAttributes { public boolean booleanPrimitive; @Column(name = "time") + @TimeColumn public Instant time; @Column(name = "uuid", tag = true) @@ -1007,6 +1121,62 @@ static class PojoWithPublicAttributes { public Boolean booleanObject; } + @Measurement(name = "mymeasurement", allFields = true) + static class PojoWithAllFieldsAnnotation { + public static final String staticField = "static"; + + public boolean booleanPrimitive; + + @TimeColumn + public Instant time; + + @Column(tag = true) + public String uuid; + + public Double doubleObject; + public Long longObject; + public Integer integerObject; + public double doublePrimitive; + public long longPrimitive; + public int integerPrimitive; + public Boolean booleanObject; + } + + @Measurement(name = "mymeasurement") + static class PojoWithBlankColumnAnnotations { + + @Column + public boolean booleanPrimitive; + + @Column + @TimeColumn + public Instant time; + + @Column(tag = true) + public String uuid; + + @Column + public Double doubleObject; + + @Column + public Long longObject; + + @Column + public Integer integerObject; + + @Column + public double doublePrimitive; + + @Column + public long longPrimitive; + + @Column + public int integerPrimitive; + + @Column + public Boolean booleanObject; + } + @Measurement(name = "SuperMeasuremet") static class SuperMeasurement { @Column(name = "superClassField") @@ -1019,6 +1189,20 @@ static class SubClassMeasurement extends SuperMeasurement { String subValue; } + @Measurement(name = "SuperMeasurement") + static class MyGenericSuperMeasurement { + + @Column(name = "superValue") + protected T superValue; + } + + @Measurement(name = "SubMeasurement") + static class MyGenericSubMeasurement extends MyGenericSuperMeasurement { + + @Column(name = "subValue") + protected String subValue; + } + @Measurement(name = "PojoNumberPrimitiveTypes") static class PojoNumberPrimitiveTypes { diff --git a/src/test/java/org/influxdb/impl/BatchProcessorTest.java b/src/test/java/org/influxdb/impl/BatchProcessorTest.java index 213f6d313..df28da328 100644 --- a/src/test/java/org/influxdb/impl/BatchProcessorTest.java +++ b/src/test/java/org/influxdb/impl/BatchProcessorTest.java @@ -226,14 +226,27 @@ public void precision() throws Exception { } } + @Test + @SuppressWarnings("unchecked") + public void randomSupplier() { + InfluxDB mockInfluxDB = mock(InfluxDBImpl.class); + BatchProcessor batchProcessor = BatchProcessor.builder(mockInfluxDB).actions(Integer.MAX_VALUE) + .interval(1, TimeUnit.NANOSECONDS).build(); + + Double random = batchProcessor.randomSupplier.get(); + assertTrue(random >= 0); + assertTrue(random < 1); + Assertions.assertNotEquals(random, batchProcessor.randomSupplier.get()); + } + @SuppressWarnings("unchecked") - static T getPrivateField(final Object obj, final String name) throws Exception { + public static T getPrivateField(final Object obj, final String name) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); return (T) field.get(obj); } - static void setPrivateField(final Object obj, final String name, final Object value) throws Exception { + public static void setPrivateField(final Object obj, final String name, final Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); diff --git a/src/test/java/org/influxdb/impl/InfluxDBAndroidDesugaredRecordResultMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBAndroidDesugaredRecordResultMapperTest.java new file mode 100644 index 000000000..f1dc21dce --- /dev/null +++ b/src/test/java/org/influxdb/impl/InfluxDBAndroidDesugaredRecordResultMapperTest.java @@ -0,0 +1,819 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2017 azeti Networks AG () + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.influxdb.impl; + +import com.android.tools.r8.RecordTag; +import org.influxdb.InfluxDBMapperException; +import org.influxdb.annotation.Column; +import org.influxdb.annotation.Measurement; +import org.influxdb.annotation.TimeColumn; +import org.influxdb.dto.QueryResult; +import org.junit.Assert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Test measurement classes simulate Android desugared records. + * + * @author Eran Leshem + */ +@SuppressWarnings({"removal", "deprecation"}) +@RunWith(JUnitPlatform.class) +public class InfluxDBAndroidDesugaredRecordResultMapperTest { + + private final InfluxDBResultMapper mapper = new InfluxDBResultMapper(); + + @Test + public void testToRecord_HappyPath() { + // Given... + List columnList = Arrays.asList("time", "uuid"); + List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString()); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + series.setName("CustomMeasurement"); + series.setValues(Arrays.asList(firstSeriesResult)); + + QueryResult.Result internalResult = new QueryResult.Result(); + internalResult.setSeries(Arrays.asList(series)); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(Arrays.asList(internalResult)); + + //When... + List myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class); + + // Then... + Assertions.assertEquals(1, myList.size(), "there must be one entry in the result list"); + } + + @Test + public void testThrowExceptionIfMissingAnnotation() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> mapper.throwExceptionIfMissingAnnotation(String.class)); + } + + @Test + public void testThrowExceptionIfError_InfluxQueryResultHasError() { + QueryResult queryResult = new QueryResult(); + queryResult.setError("main queryresult error"); + + Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult)); + } + + @Test + public void testThrowExceptionIfError_InfluxQueryResultSeriesHasError() { + QueryResult.Result seriesResult = new QueryResult.Result(); + seriesResult.setError("series error"); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(Arrays.asList(seriesResult)); + + Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult)); + } + + @Test + public void testGetMeasurementName_testStateMeasurement() { + Assertions.assertEquals("CustomMeasurement", mapper.getMeasurementName(MyCustomMeasurement.class)); + } + + @Test + public void testParseSeriesAs_testTwoValidSeries() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + List columnList = Arrays.asList("time", "uuid"); + + List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString()); + List secondSeriesResult = Arrays.asList(Instant.now().plusSeconds(1).toEpochMilli(), + UUID.randomUUID().toString()); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + series.setValues(Arrays.asList(firstSeriesResult, secondSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + Assertions.assertTrue(result.size() == 2, "there must be two series in the result list"); + + Assertions.assertEquals(firstSeriesResult.get(0), result.get(0).time().toEpochMilli(), + "Field 'time' (1st series) is not valid"); + Assertions.assertEquals(firstSeriesResult.get(1), result.get(0).uuid(), "Field 'uuid' (1st series) is not valid"); + + Assertions.assertEquals(secondSeriesResult.get(0), result.get(1).time().toEpochMilli(), + "Field 'time' (2nd series) is not valid"); + Assertions.assertEquals(secondSeriesResult.get(1), result.get(1).uuid(), "Field 'uuid' (2nd series) is not valid"); + } + + @Test + public void testParseSeriesAs_testNonNullAndValidValues() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurementWithPrimitives.class); + + List columnList = Arrays.asList("time", "uuid", + "doubleObject", "longObject", "integerObject", + "doublePrimitive", "longPrimitive", "integerPrimitive", + "booleanObject", "booleanPrimitive"); + + // InfluxDB client returns the time representation as Double. + Double now = Long.valueOf(System.currentTimeMillis()).doubleValue(); + + // InfluxDB client returns any number as Double. + // See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987 + // for more information. + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + String uuidAsString = UUID.randomUUID().toString(); + List seriesResult = Arrays.asList(now, uuidAsString, + new Double("1.01"), new Double("2"), new Double("3"), + new Double("1.01"), new Double("4"), new Double("5"), + "false", "true"); + series.setValues(Arrays.asList(seriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurementWithPrimitives.class, result); + + //Then... + MyCustomMeasurementWithPrimitives myObject = result.get(0); + Assertions.assertEquals(now.longValue(), myObject.time().toEpochMilli(), "field 'time' does not match"); + Assertions.assertEquals(uuidAsString, myObject.uuid(), "field 'uuid' does not match"); + + Assertions.assertEquals(asDouble(seriesResult.get(2)), myObject.doubleObject(), + "field 'doubleObject' does not match"); + Assertions.assertEquals(Long.valueOf(asDouble(seriesResult.get(3)).longValue()), myObject.longObject(), + "field 'longObject' does not match"); + Assertions.assertEquals(Integer.valueOf(asDouble(seriesResult.get(4)).intValue()), myObject.integerObject(), + "field 'integerObject' does not match"); + + Assertions.assertTrue( + Double.compare(asDouble(seriesResult.get(5)).doubleValue(), myObject.doublePrimitive()) == 0, + "field 'doublePrimitive' does not match"); + + Assertions.assertTrue(asDouble(seriesResult.get(6)).longValue() == myObject.longPrimitive(), + "field 'longPrimitive' does not match"); + + Assertions.assertTrue(asDouble(seriesResult.get(7)).intValue() == myObject.integerPrimitive(), + "field 'integerPrimitive' does not match"); + + Assertions.assertEquals( + Boolean.valueOf(String.valueOf(seriesResult.get(8))), myObject.booleanObject(), + "field 'booleanObject' does not match"); + + Assertions.assertEquals( + Boolean.valueOf(String.valueOf(seriesResult.get(9))).booleanValue(), myObject.booleanPrimitive(), + "field 'booleanPrimitive' does not match"); + } + + private static Double asDouble(Object obj) { + return (Double) obj; + } + + @Test + public void testFieldValueModified_DateAsISO8601() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + List columnList = Arrays.asList("time"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = Arrays.asList("2017-06-19T09:29:45.655123Z"); + series.setValues(Arrays.asList(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + Assertions.assertTrue(result.size() == 1); + } + + @Test + public void testFieldValueModified_DateAsInteger() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + List columnList = Arrays.asList("time"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = Arrays.asList(1_000); + series.setValues(Arrays.asList(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + Assertions.assertTrue(result.size() == 1); + } + + @Test + public void testUnsupportedField() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyRecordWithUnsupportedField.class); + + List columnList = Arrays.asList("bar"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = Arrays.asList("content representing a Date"); + series.setValues(Arrays.asList(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + Assertions.assertThrows(InfluxDBMapperException.class, + () -> mapper.parseSeriesAs(series, MyRecordWithUnsupportedField.class, result)); + } + + /** + * for more information. + */ + @Test + public void testToRecord_SeriesFromQueryResultIsNull() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + QueryResult.Result internalResult = new QueryResult.Result(); + internalResult.setSeries(null); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(Arrays.asList(internalResult)); + + // When... + List myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class); + + // Then... + Assertions.assertTrue(myList.isEmpty(), "there must NO entry in the result list"); + } + + @Test + public void testToRecord_QueryResultCreatedByGroupByClause() { + // Given... + InfluxDBResultMapper.cacheRecordClass(GroupByCarrierDeviceOS.class); + + // InfluxDB client returns the time representation as Double. + Double now = Long.valueOf(System.currentTimeMillis()).doubleValue(); + + // When the "GROUP BY" clause is used, "tags" are returned as Map + Map firstSeriesTagMap = new HashMap<>(2); + firstSeriesTagMap.put("CARRIER", "000/00"); + firstSeriesTagMap.put("DEVICE_OS_VERSION", "4.4.2"); + + Map secondSeriesTagMap = new HashMap<>(2); + secondSeriesTagMap.put("CARRIER", "000/01"); + secondSeriesTagMap.put("DEVICE_OS_VERSION", "9.3.5"); + + QueryResult.Series firstSeries = new QueryResult.Series(); + List columnList = Arrays.asList("time", "median", "min", "max"); + firstSeries.setColumns(columnList); + List firstSeriesResult = Arrays.asList(now, new Double("233.8"), new Double("0.0"), + new Double("3090744.0")); + firstSeries.setValues(Arrays.asList(firstSeriesResult)); + firstSeries.setTags(firstSeriesTagMap); + firstSeries.setName("tb_network"); + + QueryResult.Series secondSeries = new QueryResult.Series(); + secondSeries.setColumns(columnList); + List secondSeriesResult = Arrays.asList(now, new Double("552.0"), new Double("135.0"), + new Double("267705.0")); + secondSeries.setValues(Arrays.asList(secondSeriesResult)); + secondSeries.setTags(secondSeriesTagMap); + secondSeries.setName("tb_network"); + + QueryResult.Result internalResult = new QueryResult.Result(); + internalResult.setSeries(Arrays.asList(firstSeries, secondSeries)); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(Arrays.asList(internalResult)); + + // When... + List myList = mapper.toPOJO(queryResult, GroupByCarrierDeviceOS.class); + + // Then... + GroupByCarrierDeviceOS firstGroupByEntry = myList.get(0); + Assertions.assertEquals("000/00", firstGroupByEntry.carrier(), "field 'carrier' does not match"); + Assertions.assertEquals("4.4.2", firstGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match"); + + GroupByCarrierDeviceOS secondGroupByEntry = myList.get(1); + Assertions.assertEquals("000/01", secondGroupByEntry.carrier(), "field 'carrier' does not match"); + Assertions.assertEquals("9.3.5", secondGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match"); + } + + @Test + public void testToRecord_ticket363() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + List columnList = Arrays.asList("time"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = Arrays.asList("2000-01-01T00:00:00.000000001Z"); + series.setValues(Arrays.asList(firstSeriesResult)); + + // When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + // Then... + Assertions.assertEquals(1, result.size(), "incorrect number of elemets"); + Assertions.assertEquals(1, result.get(0).time().getNano(), "incorrect value for the nanoseconds field"); + } + + @Test + void testToRecord_Precision() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + QueryResult.Series series = new QueryResult.Series(); + series.setName("CustomMeasurement"); + List columnList = Arrays.asList("time"); + series.setColumns(columnList); + List firstSeriesResult = Arrays.asList(1_500_000L); + series.setValues(Arrays.asList(firstSeriesResult)); + + QueryResult.Result internalResult = new QueryResult.Result(); + internalResult.setSeries(Arrays.asList(series)); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(Arrays.asList(internalResult)); + + // When... + List result = mapper.toPOJO(queryResult, MyCustomMeasurement.class, TimeUnit.SECONDS); + + // Then... + Assertions.assertEquals(1, result.size(), "incorrect number of elements"); + Assertions.assertEquals(1_500_000_000L, result.get(0).time().toEpochMilli(), + "incorrect value for the millis field"); + } + + @Test + void testToRecord_SetMeasureName() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + QueryResult.Series series = new QueryResult.Series(); + series.setName("MySeriesName"); + List columnList = Arrays.asList("uuid"); + series.setColumns(columnList); + List firstSeriesResult = Collections.singletonList(UUID.randomUUID().toString()); + series.setValues(Arrays.asList(firstSeriesResult)); + + QueryResult.Result internalResult = new QueryResult.Result(); + internalResult.setSeries(Arrays.asList(series)); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(Arrays.asList(internalResult)); + + //When... + List result = + mapper.toPOJO(queryResult, MyCustomMeasurement.class, "MySeriesName"); + + //Then... + Assertions.assertTrue(result.size() == 1); + } + + @Test + public void testToRecord_HasTimeColumn() { + // Given... + InfluxDBResultMapper.cacheRecordClass(HasTimeColumnMeasurement.class); + + List columnList = Arrays.asList("time"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + List> valuesList = Arrays.asList( + Arrays.asList("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5) + Arrays.asList("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5) + Arrays.asList("2000-01-01T00:00:00-00:00"), + Arrays.asList("2000-01-02T00:00:00+00:00") + ); + series.setValues(valuesList); + + // When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, HasTimeColumnMeasurement.class, result); + + // Then... + Assertions.assertEquals(4, result.size(), "incorrect number of elemets"); + // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC) + Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z"))); + Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z"))); + // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3 + Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z"))); + Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z"))); + + } + + @Test + public void testToRecord_ticket573() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class); + + List columnList = Arrays.asList("time"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + List> valuesList = Arrays.asList( + Arrays.asList("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5) + Arrays.asList("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5) + Arrays.asList("2000-01-01T00:00:00-00:00"), + Arrays.asList("2000-01-02T00:00:00+00:00") + ); + series.setValues(valuesList); + + // When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + // Then... + Assertions.assertEquals(4, result.size(), "incorrect number of elemets"); + // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC) + Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z"))); + Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z"))); + // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3 + Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z"))); + Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z"))); + } + + @Test + public void testMultipleConstructors() { + // Given... + InfluxDBResultMapper.cacheRecordClass(MultipleConstructors.class); + + List columnList = Arrays.asList("i", "s"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + List firstSeriesResult = Arrays.asList(9.0, "str"); + series.setValues(Arrays.asList(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MultipleConstructors.class, result); + + //Then... + Assertions.assertTrue(result.size() == 1); + + Assert.assertEquals(9, result.get(0).i()); + Assert.assertEquals("str", result.get(0).s()); + } + + @Test + public void testConflictingConstructors() { + Assert.assertThrows(InfluxDBMapperException.class, + () -> InfluxDBResultMapper.cacheRecordClass(ConflictingConstructors.class)); + } + + @Measurement(name = "HasTimeColumnMeasurement") + static final class HasTimeColumnMeasurement extends RecordTag { + @TimeColumn + private final Instant time; + private final Integer value; + + HasTimeColumnMeasurement(Instant time, Integer value) { + this.time = time; + this.value = value; + } + + public Instant time() { + return time; + } + + public Integer value() { + return value; + } + } + + @Measurement(name = "CustomMeasurement") + static final class MyCustomMeasurement extends RecordTag { + private final Instant time; + private final String uuid; + private final Double doubleObject; + private final Long longObject; + private final Integer integerObject; + private final Boolean booleanObject; + + @SuppressWarnings("unused") + private final String nonColumn1; + + @SuppressWarnings("unused") + private final Random rnd; + + MyCustomMeasurement( + Instant time, + String uuid, + Double doubleObject, + Long longObject, + Integer integerObject, + Boolean booleanObject, + + @SuppressWarnings("unused") + String nonColumn1, + + @SuppressWarnings("unused") + Random rnd) { + this.time = time; + this.uuid = uuid; + this.doubleObject = doubleObject; + this.longObject = longObject; + this.integerObject = integerObject; + this.booleanObject = booleanObject; + this.nonColumn1 = nonColumn1; + this.rnd = rnd; + } + + public Instant time() { + return time; + } + + public String uuid() { + return uuid; + } + + public Double doubleObject() { + return doubleObject; + } + + public Long longObject() { + return longObject; + } + + public Integer integerObject() { + return integerObject; + } + + public Boolean booleanObject() { + return booleanObject; + } + + @SuppressWarnings("unused") + public String nonColumn1() { + return nonColumn1; + } + + @SuppressWarnings("unused") + public Random rnd() { + return rnd; + } + } + + @Measurement(name = "CustomMeasurement") + static final class MyCustomMeasurementWithPrimitives extends RecordTag { + private final Instant time; + private final String uuid; + private final Double doubleObject; + private final Long longObject; + private final Integer integerObject; + private final double doublePrimitive; + private final long longPrimitive; + private final int integerPrimitive; + private final Boolean booleanObject; + private final boolean booleanPrimitive; + + @SuppressWarnings("unused") + private final String nonColumn1; + + @SuppressWarnings("unused") + private final Random rnd; + + MyCustomMeasurementWithPrimitives( + Instant time, + String uuid, + Double doubleObject, + Long longObject, + Integer integerObject, + double doublePrimitive, + long longPrimitive, + int integerPrimitive, + Boolean booleanObject, + boolean booleanPrimitive, + + @SuppressWarnings("unused") + String nonColumn1, + + @SuppressWarnings("unused") + Random rnd) { + this.time = time; + this.uuid = uuid; + this.doubleObject = doubleObject; + this.longObject = longObject; + this.integerObject = integerObject; + this.doublePrimitive = doublePrimitive; + this.longPrimitive = longPrimitive; + this.integerPrimitive = integerPrimitive; + this.booleanObject = booleanObject; + this.booleanPrimitive = booleanPrimitive; + this.nonColumn1 = nonColumn1; + this.rnd = rnd; + } + + public Instant time() { + return time; + } + + public String uuid() { + return uuid; + } + + public Double doubleObject() { + return doubleObject; + } + + public Long longObject() { + return longObject; + } + + public Integer integerObject() { + return integerObject; + } + + public double doublePrimitive() { + return doublePrimitive; + } + + public long longPrimitive() { + return longPrimitive; + } + + public int integerPrimitive() { + return integerPrimitive; + } + + public Boolean booleanObject() { + return booleanObject; + } + + public boolean booleanPrimitive() { + return booleanPrimitive; + } + + @SuppressWarnings("unused") + public String nonColumn1() { + return nonColumn1; + } + + @SuppressWarnings("unused") + public Random rnd() { + return rnd; + } + } + + @Measurement(name = "foo") + static final class MyRecordWithUnsupportedField extends RecordTag { + @Column(name = "bar") + private final Date myDate; + + MyRecordWithUnsupportedField(Date myDate) { + this.myDate = myDate; + } + + public Date myDate() { + return myDate; + } + } + + /** + * Class created based on example from this issue + */ + @Measurement(name = "tb_network") + static final class GroupByCarrierDeviceOS extends RecordTag { + private final Instant time; + + @Column(name = "CARRIER", tag = true) + private final String carrier; + + @Column(name = "DEVICE_OS_VERSION", tag = true) + private final String deviceOsVersion; + + private final Double median; + private final Double min; + private final Double max; + + GroupByCarrierDeviceOS( + Instant time, + String carrier, + String deviceOsVersion, + Double median, + Double min, + Double max) { + this.time = time; + this.carrier = carrier; + this.deviceOsVersion = deviceOsVersion; + this.median = median; + this.min = min; + this.max = max; + } + + public Instant time() { + return time; + } + + public String carrier() { + return carrier; + } + + public String deviceOsVersion() { + return deviceOsVersion; + } + + public Double median() { + return median; + } + + public Double min() { + return min; + } + + public Double max() { + return max; + } + } + + static final class MultipleConstructors extends RecordTag { + private final int i; + private final String s; + + MultipleConstructors(int i, String s) { + this.i = i; + this.s = s; + } + + MultipleConstructors(String i, String s) { + this(Integer.parseInt(i), s); + } + + MultipleConstructors(int i, String s, double d) { + this(i, s); + } + + int i() { + return i; + } + + String s() { + return s; + } + } + + + static final class ConflictingConstructors extends RecordTag { + private final int i; + private final String s; + + private ConflictingConstructors(int i, String s) { + this.i = i; + this.s = s; + } + + private ConflictingConstructors(String s, int i) { + this(i, s); + } + + public int i() { + return i; + } + + public String s() { + return s; + } + } +} \ No newline at end of file diff --git a/src/test/java/org/influxdb/impl/InfluxDBImplTest.java b/src/test/java/org/influxdb/impl/InfluxDBImplTest.java index 59b33e9e6..9f3804c9c 100644 --- a/src/test/java/org/influxdb/impl/InfluxDBImplTest.java +++ b/src/test/java/org/influxdb/impl/InfluxDBImplTest.java @@ -10,9 +10,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; import okhttp3.OkHttpClient; +@RunWith(JUnitPlatform.class) public class InfluxDBImplTest { private InfluxDB influxDB; diff --git a/src/test/java/org/influxdb/impl/InfluxDBMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBMapperTest.java index 124605477..3d5bd86be 100644 --- a/src/test/java/org/influxdb/impl/InfluxDBMapperTest.java +++ b/src/test/java/org/influxdb/impl/InfluxDBMapperTest.java @@ -11,12 +11,16 @@ import org.influxdb.TestUtils; import org.influxdb.annotation.Column; import org.influxdb.annotation.Measurement; +import org.influxdb.annotation.TimeColumn; import org.influxdb.dto.Query; import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +@RunWith(JUnitPlatform.class) public class InfluxDBMapperTest { private InfluxDB influxDB; @@ -126,6 +130,7 @@ static class ServerMeasure { /** Check the instant conversions */ @Column(name = "time") + @TimeColumn private Instant time; @Column(name = "name", tag = true) @@ -319,6 +324,7 @@ public void setField(Integer field) { static class NonInstantTime { @Column(name = "time") + @TimeColumn private long time; public long getTime() { diff --git a/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java index f4eee0ba2..959678216 100644 --- a/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java +++ b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java @@ -20,6 +20,17 @@ */ package org.influxdb.impl; +import org.influxdb.InfluxDBMapperException; +import org.influxdb.annotation.Column; +import org.influxdb.annotation.Exclude; +import org.influxdb.annotation.Measurement; +import org.influxdb.annotation.TimeColumn; +import org.influxdb.dto.QueryResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + import java.time.Instant; import java.util.Arrays; import java.util.Date; @@ -31,16 +42,6 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; -import org.influxdb.InfluxDBMapperException; -import org.influxdb.annotation.Column; -import org.influxdb.annotation.Measurement; -import org.influxdb.annotation.TimeColumn; -import org.influxdb.dto.QueryResult; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.platform.runner.JUnitPlatform; -import org.junit.runner.RunWith; - /** * @author fmachado */ @@ -71,6 +72,12 @@ public void testToPOJO_HappyPath() { // Then... Assertions.assertEquals(1, myList.size(), "there must be one entry in the result list"); + + //When... + List myList1 = mapper.toPOJO(queryResult, MyAllFieldsCustomMeasurement.class); + + // Then... + Assertions.assertEquals(1, myList1.size(), "there must be one entry in the result list"); } @Test @@ -442,6 +449,38 @@ void testToPOJOInheritance() { Assertions.assertEquals(subValue, result.get(0).subValue); } + @Test + void testToPOJOGenericInheritance() { + // Given... + mapper.cacheMeasurementClass(MyGenericSubMeasurement.class); + + String superValue = UUID.randomUUID().toString(); + String subValue = "my sub value"; + List columnList = Arrays.asList("superValue", "subValue"); + + List firstSeriesResult = Arrays.asList(superValue, subValue); + + QueryResult.Series series = new QueryResult.Series(); + series.setName("MySeriesName"); + series.setColumns(columnList); + series.setValues(Arrays.asList(firstSeriesResult)); + + QueryResult.Result internalResult = new QueryResult.Result(); + internalResult.setSeries(Arrays.asList(series)); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(Arrays.asList(internalResult)); + + //When... + List result = + mapper.toPOJO(queryResult, MyGenericSubMeasurement.class, "MySeriesName"); + + //Then... + Assertions.assertTrue(result.size() == 1); + Assertions.assertEquals(superValue, result.get(0).superValue); + Assertions.assertEquals(subValue, result.get(0).subValue); + } + @Test public void testToPOJO_HasTimeColumn() { // Given... @@ -578,6 +617,35 @@ public String toString() { } } + @Measurement(name = "CustomMeasurement", allFields = true) + static class MyAllFieldsCustomMeasurement { + private Instant time; + private String uuid; + private Double doubleObject; + private Long longObject; + private Integer integerObject; + private double doublePrimitive; + private long longPrimitive; + private int integerPrimitive; + private Boolean booleanObject; + private boolean booleanPrimitive; + + @SuppressWarnings("unused") + @Exclude + private String nonColumn1; + + @SuppressWarnings("unused") + @Exclude + private Random rnd; + + @Override + public String toString() { + return "MyCustomMeasurement [time=" + time + ", uuid=" + uuid + ", doubleObject=" + doubleObject + ", longObject=" + longObject + + ", integerObject=" + integerObject + ", doublePrimitive=" + doublePrimitive + ", longPrimitive=" + longPrimitive + + ", integerPrimitive=" + integerPrimitive + ", booleanObject=" + booleanObject + ", booleanPrimitive=" + booleanPrimitive + "]"; + } + } + @Measurement(name = "SuperMeasurement") static class MySuperMeasurement { @@ -602,6 +670,30 @@ public String toString() { } } + @Measurement(name = "SuperMeasurement") + static class MyGenericSuperMeasurement { + + @Column(name = "superValue") + protected T superValue; + + @Override + public String toString() { + return "SuperMeasurement [superValue=" + superValue + "]"; + } + } + + @Measurement(name = "SubMeasurement") + static class MyGenericSubMeasurement extends MyGenericSuperMeasurement { + + @Column(name = "subValue") + protected String subValue; + + @Override + public String toString() { + return "MySubMeasurement [subValue=" + subValue + ", superValue=" + superValue + "]"; + } + } + @Measurement(name = "foo") static class MyPojoWithUnsupportedField { diff --git a/src/test/java/org/influxdb/querybuilder/api/BuiltQueryTest.java b/src/test/java/org/influxdb/querybuilder/api/BuiltQueryTest.java index 2f9565add..1fcadfd74 100644 --- a/src/test/java/org/influxdb/querybuilder/api/BuiltQueryTest.java +++ b/src/test/java/org/influxdb/querybuilder/api/BuiltQueryTest.java @@ -10,10 +10,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.influxdb.dto.Query; +import org.influxdb.querybuilder.FunctionFactory; import org.influxdb.querybuilder.RawText; import org.influxdb.querybuilder.Where; import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +@RunWith(JUnitPlatform.class) public class BuiltQueryTest { private static final String DATABASE = "testdb"; @@ -973,4 +977,12 @@ public void multipleDatabaseBackReferenceing() { assertEquals(query.getDatabase(), select.getDatabase()); } + @Test + public void testBoundParameters() { + Query query = select().column("a").from(DATABASE, "b") + .where(eq("c", FunctionFactory.placeholder("d"))).bindParameter("d", 3); + assertEquals("SELECT a FROM b WHERE c = $d;", query.getCommand()); + assertEquals(Query.encode("{\"d\":3}"), query.getParameterJsonWithUrlEncoded()); + assertEquals(DATABASE, query.getDatabase()); + } }