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..4fa5111b3 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 @@ -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 @@ -165,6 +165,7 @@ ossrh https://oss.sonatype.org/ 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()); + } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy