diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 51af9c563..8571e5738 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: jdk: [3-openjdk-17-slim, 3-jdk-14, 3-jdk-8-slim] - influxdb: ['1.1', '1.6', '1.8', '2.1', '2.2', '2.3'] + influxdb: ['1.1', '1.6', '1.8', '2.3', '2.4', '2.5'] steps: - name: Checkout @@ -36,7 +36,7 @@ jobs: shasum -a 256 -c codecov.SHA256SUM chmod +x ./codecov ./codecov - if: matrix.influxdb != '2.1' && matrix.influxdb != '2.2' && matrix.influxdb != '2.3' + if: matrix.influxdb != '2.3' && matrix.influxdb != '2.4' && matrix.influxdb != '2.5' # deploy: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cf74ce6b3..2a7b043e7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -7,12 +7,12 @@ on: jobs: build: - runs-on: ubuntu-20.04 + 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.1', '2.2', '2.3'] + influxdb: ['1.1', '1.6', '1.8', '2.3', '2.4', '2.5'] steps: - name: Checkout @@ -40,4 +40,4 @@ jobs: shasum -a 256 -c codecov.SHA256SUM chmod +x ./codecov ./codecov - if: matrix.influxdb != '2.1' && matrix.influxdb != '2.2' && matrix.influxdb != '2.3' + if: matrix.influxdb != '2.3' && matrix.influxdb != '2.4' && matrix.influxdb != '2.5' diff --git a/CHANGELOG.md b/CHANGELOG.md index 847bd95ad..0cdc03965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog -## 2.23 [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) diff --git a/MANUAL.md b/MANUAL.md index 62d45d67a..92b245b21 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -323,21 +323,40 @@ 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 + private Double idle; + @Column + private Boolean happydevop; + @Column(name = "uptimesecs") + private Long uptimeSecs; + // getters (and setters if you need) +} +``` + +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; - @Column(name = "idle") private Double idle; - @Column(name = "happydevop") private Boolean happydevop; @Column(name = "uptimesecs") private Long uptimeSecs; @@ -383,6 +402,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 ab8e1fdde..74f6a5ba1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ 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. +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 diff --git a/pom.xml b/pom.xml index cd2e4b65c..0f322990e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.influxdb influxdb-java jar - 2.23 + 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 - influxdb-java-2.23 + 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,12 +75,12 @@ org.codehaus.mojo versions-maven-plugin - 2.11.0 + 2.16.2 org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.12.1 1.8 1.8 @@ -89,32 +89,32 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M7 + 3.2.5 org.apache.maven.plugins maven-site-plugin - 3.12.0 + 3.12.1 org.apache.maven.plugins maven-clean-plugin - 3.2.0 + 3.3.2 org.apache.maven.plugins maven-deploy-plugin - 3.0.0-M2 + 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.2 + 3.3.0 @@ -126,12 +126,12 @@ org.apache.maven.plugins maven-resources-plugin - 3.2.0 + 3.3.1 org.apache.maven.plugins maven-release-plugin - 3.0.0-M6 + 3.0.1 @@ -139,7 +139,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.1.0 + 3.4.1 enforce-maven @@ -163,8 +163,9 @@ true ossrh - https://oss.sonatype.org/ + https://ossrh-staging-api.central.sonatype.com/ true + 15 @@ -183,7 +184,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.4.0 + 3.6.3 8 @@ -199,7 +200,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.11 @@ -218,7 +219,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.1.2 + 3.3.1 com.puppycrawl.tools @@ -256,13 +257,13 @@ org.junit.jupiter junit-jupiter-engine - 5.8.2 + 5.9.3 test org.junit.platform junit-platform-runner - 1.8.2 + 1.9.3 test @@ -274,13 +275,13 @@ org.assertj assertj-core - 3.23.1 + 3.25.2 test org.mockito mockito-core - 4.6.1 + 4.10.0 test @@ -308,19 +309,19 @@ org.msgpack msgpack-core - 0.9.3 + 0.9.8 com.squareup.okhttp3 okhttp - 4.10.0 + 4.12.0 com.squareup.okhttp3 logging-interceptor - 4.10.0 + 4.12.0 @@ -333,7 +334,7 @@ maven-resources-plugin - 3.2.0 + 3.3.1 copy-resources @@ -390,7 +391,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.1.0 sign-artifacts 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..01e6f52e6 --- /dev/null +++ b/src/main/java/org/influxdb/annotation/Exclude.java @@ -0,0 +1,38 @@ +/* + * 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. + * @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..fa9d19fd5 100644 --- a/src/main/java/org/influxdb/annotation/Measurement.java +++ b/src/main/java/org/influxdb/annotation/Measurement.java @@ -40,4 +40,11 @@ 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. + * @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..277ce4c99 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. @@ -276,22 +283,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 +332,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 +395,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); + } + } } /** 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/impl/InfluxDBImpl.java b/src/main/java/org/influxdb/impl/InfluxDBImpl.java index 825e0708a..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,13 +636,17 @@ 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 { if (query.requiresPost()) { - call = this.influxDBService.query(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize, null); + call = this.influxDBService.postQuery(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize); } else { call = this.influxDBService.query(getDatabase(query), query.getCommandWithUrlEncoded(), chunkSize); } @@ -716,18 +719,21 @@ public void onFailure(final Call call, final Throwable t) { @Override public QueryResult query(final Query query, final TimeUnit timeUnit) { Call call; - if (query instanceof BoundParameterQuery) { - BoundParameterQuery boundParameterQuery = (BoundParameterQuery) query; - call = this.influxDBService.query(getDatabase(query), - TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded(), - boundParameterQuery.getParameterJsonWithUrlEncoded()); + 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 { if (query.requiresPost()) { - call = this.influxDBService.query(getDatabase(query), - TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded(), null); + call = this.influxDBService.postQuery(getDatabase(query), + TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded()); } else { call = this.influxDBService.query(getDatabase(query), - TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded()); + TimeUtil.toTimePrecision(timeUnit), query.getCommandWithUrlEncoded(), null); } } return executeQuery(call); @@ -788,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..5a4d3af85 100644 --- a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java +++ b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java @@ -20,7 +20,16 @@ */ 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.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -33,11 +42,6 @@ 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; - /** * Main class responsible for mapping a QueryResult to a POJO. * @@ -48,8 +52,12 @@ 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<>(); private static final int FRACTION_MIN_WIDTH = 0; private static final int FRACTION_MAX_WIDTH = 9; @@ -202,29 +210,55 @@ 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; } - CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), influxColumnAndFieldMap); + + ClassInfo classInfo = new ClassInfo(); + classInfo.fieldMap = fieldMap; + classInfo.typeMappers = typeMappers; + CLASS_INFO_CACHE.putIfAbsent(clazz.getName(), classInfo); + } + } + + 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 +273,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,17 +280,19 @@ 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()); + + 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 = colNameAndFieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/); + Field correspondingField = classInfo.fieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/); if (correspondingField != null) { if (object == null) { object = clazz.newInstance(); } - setFieldValue(object, correspondingField, row.get(i), precision); + setFieldValue(object, correspondingField, row.get(i), precision, + classInfo.typeMappers.get(correspondingField)); } } // When the "GROUP BY" clause is used, "tags" are returned as Map and @@ -269,10 +301,11 @@ List parseSeriesAs(final QueryResult.Series series, final Class clazz, // "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*/); + 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); + setFieldValue(object, correspondingField, entry.getValue(), precision, + classInfo.typeMappers.get(correspondingField)); } } } @@ -289,112 +322,72 @@ List parseSeriesAs(final QueryResult.Series series, final Class clazz, /** * 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 (fieldValueModified(fieldType, field, object, value, precision) - || fieldValueForPrimitivesModified(fieldType, field, object, value) - || fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) { - return; + 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, 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 (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)); - } - } - - 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; + String.format(msg, className, fieldName, value.getClass().getName(), value)); } - 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; - } - - 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; + throw new InfluxDBMapperException( + String.format("Class '%s' field '%s' is from an unsupported type '%s'.", className, fieldName, fieldType)); } - 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 ce7a811b4..061a76615 100644 --- a/src/main/java/org/influxdb/impl/InfluxDBService.java +++ b/src/main/java/org/influxdb/impl/InfluxDBService.java @@ -47,12 +47,7 @@ public Call writePoints(@Query(DB) String database, @GET("query") public Call query(@Query(DB) String db, - @Query(EPOCH) String epoch, @Query(value = Q, encoded = true) String query); - - @POST("query") - @FormUrlEncoded - public Call query(@Query(DB) String db, - @Query(EPOCH) String epoch, @Field(value = Q, encoded = true) String query, + @Query(EPOCH) String epoch, @Query(value = Q, encoded = true) String query, @Query(value = PARAMS, encoded = true) String params); @GET("query") @@ -66,9 +61,26 @@ public Call postQuery(@Query(DB) String db, @POST("query") @FormUrlEncoded - public Call postQuery(@Query(DB) String db, + public Call postQuery(@Query(DB) String db, @Query(EPOCH) String epoch, + @Field(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") @FormUrlEncoded public Call postQuery(@Field(value = Q, encoded = true) String query); @@ -79,8 +91,7 @@ public Call query(@Query(DB) String db, @Query(value = Q, encoded @Query(CHUNK_SIZE) int chunkSize); @Streaming - @POST("query?chunked=true") - @FormUrlEncoded - public Call query(@Query(DB) String db, @Field(value = Q, encoded = true) String query, + @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/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/InfluxDBMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBMapperTest.java index cd2251ac2..3d5bd86be 100644 --- a/src/test/java/org/influxdb/impl/InfluxDBMapperTest.java +++ b/src/test/java/org/influxdb/impl/InfluxDBMapperTest.java @@ -11,6 +11,7 @@ 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; @@ -129,6 +130,7 @@ static class ServerMeasure { /** Check the instant conversions */ @Column(name = "time") + @TimeColumn private Instant time; @Column(name = "name", tag = true) @@ -322,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()); + } }