Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- [#850](https://github.com/influxdata/influxdb-client-java/pull/850): Update the Spring health indicator integration to use the Spring Boot 4 health APIs.

### Bug Fixes

1. [#856](https://github.com/influxdata/influxdb-client-java/pull/865): with InfluxQL queries parse escaped special characters in tag keys and values e.g. (`"my_data,model\,\ uin=Droid\,\ C3PO ..."`)

### Dependencies

Update dependencies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,68 @@ static InfluxQLQueryResult readInfluxQLCSVResult(

private static Map<String, String> parseTags(@Nonnull final String value) {
final Map<String, String> tags = new HashMap<>();
if (value.length() > 0) {
for (String entry : value.split(",")) {
final String[] kv = entry.split("=");
tags.put(kv[0], kv[1]);
if (value.isEmpty()) {
return tags;
}

StringBuilder currentKey = new StringBuilder();
StringBuilder currentValue = new StringBuilder();
boolean inValue = false;
boolean escaped = false;

for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);

if (escaped) {
// current character is escaped - treat it as a literal
if (inValue) {
currentValue.append(c);
} else {
currentKey.append(c);
}
escaped = false;
continue;
}

if (c == '\\') {
// start escape sequence
// don't preserve escape character
escaped = true;
continue;
}

if (!inValue && c == '=') {
// unescaped '=' marks copula
inValue = true;
continue;
}

if (inValue && c == ',') {
// unescaped comma separates key value pairs
// finalize
String key = currentKey.toString();
String val = currentValue.toString();
if (!key.isEmpty()) {
tags.put(key, val);
}
currentKey.setLength(0);
currentValue.setLength(0);
inValue = false;
continue;
}

if (inValue) {
currentValue.append(c);
} else {
currentKey.append(c);
}
}

// finalize last key/value pair if any
String key = currentKey.toString();
String val = currentValue.toString();
if (inValue && !key.isEmpty()) {
tags.put(key, val);
}

return tags;
Expand Down
25 changes: 25 additions & 0 deletions client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ void testQueryData() {
});
}

@Test
void testQueryWithTagsWithEscapedChars() {
Bucket bucket = influxDBClient.getBucketsApi().findBucketByName("my-bucket");
influxDBClient.getWriteApiBlocking()
.writePoint(bucket.getId(), bucket.getOrgID(), new Point("specialTags")
.time(1655900000, WritePrecision.S)
.addField("free", 10)
.addTag("host", "A")
.addTag("region", "west")
.addTag("location", "vancouver, BC")
.addTag("model, uid","droid, C3PO")
);

Map<String,String> expectedTags = new HashMap<>();
expectedTags.put("host", "A");
expectedTags.put("region", "west");
expectedTags.put("location", "vancouver, BC");
expectedTags.put("model, uid","droid, C3PO");

InfluxQLQueryResult result = influxQLQueryApi.query(
new InfluxQLQuery("SELECT * FROM \"specialTags\" GROUP BY *", DATABASE_NAME));

Assertions.assertThat(result.getResults().get(0).getSeries().get(0).getTags()).isEqualTo(expectedTags);
}

@Test
void testQueryDataWithConversion() {
InfluxQLQueryResult result = influxQLQueryApi.query(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@
import java.io.IOException;
import java.io.StringReader;
import java.time.Instant;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.influxdb.Cancellable;
import com.influxdb.query.InfluxQLQueryResult;
Expand All @@ -34,6 +39,8 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import javax.annotation.Nonnull;

class InfluxQLQueryApiImplTest {

private static final Cancellable NO_CANCELLING = new Cancellable() {
Expand All @@ -47,6 +54,166 @@ public boolean isCancelled() {
}
};

private static Map<String, String> mapOf(@Nonnull final String... valuePairs) {
Map<String, String> map = new HashMap<>();
if (valuePairs.length % 2 != 0) {
throw new IllegalArgumentException("value pairs must be even");
}
for (int i = 0; i < valuePairs.length; i += 2) {
map.put(valuePairs[i], valuePairs[i + 1]);
}
return map;
}

private static void assertParsedTags(
@Nonnull final String rawTags,
@Nonnull final Map<String, String> expectedTags
) throws IOException {

StringReader reader = new StringReader(
"name,tags,time,value\n" +
"m,\"" + rawTags + "\",1,1\n"
);
InfluxQLQueryResult result = InfluxQLQueryApiImpl.readInfluxQLCSVResult(reader, NO_CANCELLING, null);

Assertions.assertThat(result.getResults()).hasSize(1);
Assertions.assertThat(result.getResults().get(0).getSeries()).hasSize(1);
Assertions.assertThat(result.getResults().get(0).getSeries().get(0).getTags()).isEqualTo(expectedTags);
}

@Test
void readInfluxQLResultWithMalformedAndBoundaryTagCases() throws IOException {
assertParsedTags("", mapOf());
assertParsedTags("host=", mapOf("host", ""));
assertParsedTags("host=a,host=b", mapOf("host", "b"));
assertParsedTags("host=a,broken", mapOf("host", "a"));
assertParsedTags("=a,host=b", mapOf("host", "b"));
assertParsedTags("host=a,", mapOf("host", "a"));
assertParsedTags(",host=a", mapOf(",host", "a"));
assertParsedTags("a=1,,b=2", mapOf("a", "1", ",b", "2"));
assertParsedTags("a=foo\\", mapOf("a", "foo"));
assertParsedTags("k\\==v\\=1", mapOf("k=", "v=1"));
assertParsedTags("k\\,x=v\\,y,b=2", mapOf("k,x", "v,y", "b", "2"));
assertParsedTags("k\\=x", mapOf());
}

@Test
void readInfluxQLResultWithTagCommas() throws IOException {
InfluxQLQueryResult.Series.ValueExtractor extractValue = (columnName, rawValue, resultIndex, seriesName) -> {
if (resultIndex == 0 && seriesName.equals("data1")){
switch (columnName){
case "time": return Instant.ofEpochSecond(Long.parseLong(rawValue));
case "first":
return Double.valueOf(rawValue);
}
}
return rawValue;
};

// Note that escapes in tags returned from server are themselves escaped
List<String> testTags = Arrays.asList(
"location=Cheb_CZ", //simpleTag
"region=us-east-1,host=server1", // standardTags * 2
"location=Cheb\\,\\ CZ", // simpleTag with value comma and space
"location=Cheb_CZ,branch=Munchen_DE", // multiple tags with underscore
"location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE", // multiple tags with comma and space
"model\\,\\ uin=C3PO", // tag with comma space in key
"model\\,\\ uin=Droid\\,\\ C3PO", // tag with comma space in key and value
"model\\,\\ uin=Droid\\,\\ C3PO,location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE", // comma space in key and val
"silly\\,\\=long\\,tag=a\\,b\\\\\\,\\ c\\,\\ d", // multi commas in k and v plus escaped reserved chars
"region=us\\,\\ east-1,host\\,\\ name=ser\\,\\ ver1" // legacy broken tags
);

Map<String,Map<String,String>> expectedTagsMap = Stream.of(
// 1. simpleTag
new AbstractMap.SimpleImmutableEntry<>(testTags.get(0),
mapOf("location", "Cheb_CZ")),
// 2. standardTags * 2
new AbstractMap.SimpleImmutableEntry<>(testTags.get(1),
mapOf(
"region", "us-east-1",
"host", "server1"
)),
// 3. simpleTag with value comma and space
new AbstractMap.SimpleImmutableEntry<>(testTags.get(2),
mapOf("location", "Cheb, CZ")),
// 4. multiple tags with underscore
new AbstractMap.SimpleImmutableEntry<>(testTags.get(3),
mapOf(
"location", "Cheb_CZ",
"branch", "Munchen_DE"
)),
// 5. multiple tags with comma and space
new AbstractMap.SimpleImmutableEntry<>(testTags.get(4),
mapOf(
"location", "Cheb, CZ",
"branch", "Munchen, DE"
)),
// 6. tag with comma and space in key
new AbstractMap.SimpleImmutableEntry<>(testTags.get(5),
mapOf("model, uin", "C3PO")),
// 7. tag with comma and space in key and value
new AbstractMap.SimpleImmutableEntry<>(testTags.get(6),
mapOf("model, uin", "Droid, C3PO")),
// 8. comma space in key and val with multiple tags
new AbstractMap.SimpleImmutableEntry<>(testTags.get(7),
mapOf(
"model, uin", "Droid, C3PO",
"location", "Cheb, CZ",
"branch", "Munchen, DE"
)),
// 9. multiple commas in key and value
new AbstractMap.SimpleImmutableEntry<>(testTags.get(8),
mapOf(
"silly,=long,tag", "a,b\\, c, d"
)),
// legacy broken tags
new AbstractMap.SimpleImmutableEntry<>(testTags.get(9),
mapOf(
"region", "us, east-1",
"host, name", "ser, ver1"
))
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

StringReader reader = new StringReader("name,tags,time,first\n"
+ "data1,\"" + testTags.get(0) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(1) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(2) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(3) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(4) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(5) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(6) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(7) + "\",1483225200,42\n"
+ "data1,\"" + testTags.get(8) + "\",1483225200,42\n"
+ "\n"
+ "name,tags,time,usage_user,usage_system\n"
+ "cpu,\"" + testTags.get(9) + "\",1483225200,13.57,1.4\n"
);

InfluxQLQueryResult result = InfluxQLQueryApiImpl.readInfluxQLCSVResult(reader, NO_CANCELLING, extractValue);
List<InfluxQLQueryResult.Result> results = result.getResults();
int index = 0;
for(InfluxQLQueryResult.Result r : results) {
for(InfluxQLQueryResult.Series s : r.getSeries()){
Assertions.assertThat(s.getTags()).isEqualTo(expectedTagsMap.get(testTags.get(index++)));
if(index < 10) {
Assertions.assertThat(s.getColumns()).containsOnlyKeys("time", "first");
InfluxQLQueryResult.Series.Record valRec = s.getValues().get(0);
Assertions.assertThat(valRec.getValueByKey("first")).isEqualTo(Double.valueOf("42.0"));
Assertions.assertThat(valRec.getValueByKey("time")).isEqualTo(Instant.ofEpochSecond(1483225200L));
} else if (index == 10) {
Assertions.assertThat(s.getColumns()).containsOnlyKeys("time", "usage_user", "usage_system");
InfluxQLQueryResult.Series.Record valRec = s.getValues().get(0);
// No value extractor created for "cpu" series
Assertions.assertThat(valRec.getValueByKey("time")).isEqualTo("1483225200");
Assertions.assertThat(valRec.getValueByKey("usage_user")).isEqualTo("13.57");
Assertions.assertThat(valRec.getValueByKey("usage_system")).isEqualTo("1.4");
}
}
}
Assertions.assertThat(index).isEqualTo(testTags.size());
}

@Test
void readInfluxQLResult() throws IOException {
InfluxQLQueryResult.Series.ValueExtractor extractValues = (columnName, rawValue, resultIndex, seriesName) -> {
Expand Down
Loading