Skip to content

Commit b4348e6

Browse files
dafrizmarkpollack
authored andcommitted
Use thread safe DateTimeFormatter instead of SimpleDateFormat
Improve type safety in FilterExpressionConverter date parsing - Moved concurrent test into SimpleVectorStoreFilterExpressionConverterTests - Replace var with explicit Instant typing in date parsing logic - Import java.time.Instant for better type safety across all converters Signed-off-by: David Frizelle <[email protected]> Auto-cherry-pick to 1.0.x Fixes #4172
1 parent 13c39f1 commit b4348e6

File tree

6 files changed

+63
-37
lines changed

6 files changed

+63
-37
lines changed

spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverter.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
package org.springframework.ai.vectorstore.filter.converter;
1818

19-
import java.text.ParseException;
20-
import java.text.SimpleDateFormat;
19+
import java.time.Instant;
20+
import java.time.ZoneOffset;
21+
import java.time.format.DateTimeFormatter;
22+
import java.time.format.DateTimeParseException;
2123
import java.util.Date;
2224
import java.util.List;
23-
import java.util.TimeZone;
2425
import java.util.regex.Pattern;
2526

2627
import org.springframework.ai.vectorstore.filter.Filter;
@@ -36,11 +37,10 @@ public class SimpleVectorStoreFilterExpressionConverter extends AbstractFilterEx
3637

3738
private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z");
3839

39-
private final SimpleDateFormat dateFormat;
40+
private final DateTimeFormatter dateFormat;
4041

4142
public SimpleVectorStoreFilterExpressionConverter() {
42-
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
43-
this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
43+
this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
4444
}
4545

4646
@Override
@@ -113,17 +113,17 @@ private void appendSpELContains(StringBuilder formattedList, StringBuilder conte
113113
protected void doSingleValue(Object value, StringBuilder context) {
114114
if (value instanceof Date date) {
115115
context.append("'");
116-
context.append(this.dateFormat.format(date));
116+
context.append(this.dateFormat.format(date.toInstant()));
117117
context.append("'");
118118
}
119119
else if (value instanceof String text) {
120120
context.append("'");
121121
if (DATE_FORMAT_PATTERN.matcher(text).matches()) {
122122
try {
123-
Date date = this.dateFormat.parse(text);
123+
Instant date = Instant.from(this.dateFormat.parse(text));
124124
context.append(this.dateFormat.format(date));
125125
}
126-
catch (ParseException e) {
126+
catch (DateTimeParseException e) {
127127
throw new IllegalArgumentException("Invalid date type:" + text, e);
128128
}
129129
}

spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverterTests.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Date;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.stream.IntStream;
2223

2324
import org.junit.jupiter.api.Assertions;
2425
import org.junit.jupiter.api.Test;
@@ -68,6 +69,18 @@ public void testDate() {
6869

6970
}
7071

72+
@Test
73+
public void testDatesConcurrently() {
74+
IntStream.range(0, 10).parallel().forEach(i -> {
75+
String vectorExpr = this.converter.convertExpression(new Filter.Expression(EQ,
76+
new Filter.Key("activationDate"), new Filter.Value(new Date(1704637752148L))));
77+
String vectorExpr2 = this.converter.convertExpression(new Filter.Expression(EQ,
78+
new Filter.Key("activationDate"), new Filter.Value(new Date(1704637753150L))));
79+
assertThat(vectorExpr).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:12Z'");
80+
assertThat(vectorExpr2).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:13Z'");
81+
});
82+
}
83+
7184
@Test
7285
public void testEQ() {
7386
String vectorExpr = this.converter

vector-stores/spring-ai-azure-store/src/main/java/org/springframework/ai/vectorstore/azure/AzureAiSearchFilterExpressionConverter.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
package org.springframework.ai.vectorstore.azure;
1818

19-
import java.text.ParseException;
20-
import java.text.SimpleDateFormat;
19+
import java.time.Instant;
20+
import java.time.ZoneOffset;
21+
import java.time.format.DateTimeFormatter;
22+
import java.time.format.DateTimeParseException;
2123
import java.util.Date;
2224
import java.util.List;
23-
import java.util.TimeZone;
2425
import java.util.regex.Pattern;
2526

2627
import org.springframework.ai.vectorstore.azure.AzureVectorStore.MetadataField;
@@ -40,18 +41,17 @@
4041
*/
4142
public class AzureAiSearchFilterExpressionConverter extends AbstractFilterExpressionConverter {
4243

43-
private static Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z");
44+
private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z");
4445

45-
private final SimpleDateFormat dateFormat;
46+
private final DateTimeFormatter dateFormat;
4647

4748
private List<String> allowedIdentifierNames;
4849

4950
public AzureAiSearchFilterExpressionConverter(List<MetadataField> filterMetadataFields) {
5051
Assert.notNull(filterMetadataFields, "The filterMetadataFields can not null.");
5152

5253
this.allowedIdentifierNames = filterMetadataFields.stream().map(MetadataField::name).toList();
53-
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
54-
this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
54+
this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
5555
}
5656

5757
@Override
@@ -137,15 +137,15 @@ protected void doValue(Filter.Value filterValue, StringBuilder context) {
137137
@Override
138138
protected void doSingleValue(Object value, StringBuilder context) {
139139
if (value instanceof Date date) {
140-
context.append(this.dateFormat.format(date));
140+
context.append(this.dateFormat.format(date.toInstant()));
141141
}
142142
else if (value instanceof String text) {
143143
if (DATE_FORMAT_PATTERN.matcher(text).matches()) {
144144
try {
145-
Date date = this.dateFormat.parse(text);
145+
Instant date = Instant.from(this.dateFormat.parse(text));
146146
context.append(this.dateFormat.format(date));
147147
}
148-
catch (ParseException e) {
148+
catch (DateTimeParseException e) {
149149
throw new IllegalArgumentException("Invalid date type:" + text, e);
150150
}
151151
}

vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverter.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
package org.springframework.ai.vectorstore.elasticsearch;
1818

19-
import java.text.ParseException;
20-
import java.text.SimpleDateFormat;
19+
import java.time.Instant;
20+
import java.time.ZoneOffset;
21+
import java.time.format.DateTimeFormatter;
22+
import java.time.format.DateTimeParseException;
2123
import java.util.Date;
2224
import java.util.List;
23-
import java.util.TimeZone;
2425
import java.util.regex.Pattern;
2526

2627
import org.springframework.ai.vectorstore.filter.Filter;
@@ -40,11 +41,10 @@ public class ElasticsearchAiSearchFilterExpressionConverter extends AbstractFilt
4041

4142
private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z");
4243

43-
private final SimpleDateFormat dateFormat;
44+
private final DateTimeFormatter dateFormat;
4445

4546
public ElasticsearchAiSearchFilterExpressionConverter() {
46-
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
47-
this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
47+
this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
4848
}
4949

5050
@Override
@@ -121,15 +121,15 @@ protected void doValue(Filter.Value filterValue, StringBuilder context) {
121121
@Override
122122
protected void doSingleValue(Object value, StringBuilder context) {
123123
if (value instanceof Date date) {
124-
context.append(this.dateFormat.format(date));
124+
context.append(this.dateFormat.format(date.toInstant()));
125125
}
126126
else if (value instanceof String text) {
127127
if (DATE_FORMAT_PATTERN.matcher(text).matches()) {
128128
try {
129-
Date date = this.dateFormat.parse(text);
129+
Instant date = Instant.from(this.dateFormat.parse(text));
130130
context.append(this.dateFormat.format(date));
131131
}
132-
catch (ParseException e) {
132+
catch (DateTimeParseException e) {
133133
throw new IllegalArgumentException("Invalid date type:" + text, e);
134134
}
135135
}

vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverterTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Date;
2020
import java.util.List;
21+
import java.util.stream.IntStream;
2122

2223
import org.junit.jupiter.api.Test;
2324

@@ -49,6 +50,18 @@ public void testDate() {
4950
assertThat(vectorExpr).isEqualTo("metadata.activationDate:1970-01-01T00:00:02Z");
5051
}
5152

53+
@Test
54+
public void testDatesConcurrently() {
55+
IntStream.range(0, 10).parallel().forEach(i -> {
56+
String vectorExpr = this.converter.convertExpression(new Filter.Expression(EQ,
57+
new Filter.Key("activationDate"), new Filter.Value(new Date(1704637752148L))));
58+
String vectorExpr2 = this.converter.convertExpression(new Filter.Expression(EQ,
59+
new Filter.Key("activationDate"), new Filter.Value(new Date(1704637753150L))));
60+
assertThat(vectorExpr).isEqualTo("metadata.activationDate:2024-01-07T14:29:12Z");
61+
assertThat(vectorExpr2).isEqualTo("metadata.activationDate:2024-01-07T14:29:13Z");
62+
});
63+
}
64+
5265
@Test
5366
public void testEQ() {
5467
String vectorExpr = this.converter

vector-stores/spring-ai-opensearch-store/src/main/java/org/springframework/ai/vectorstore/opensearch/OpenSearchAiSearchFilterExpressionConverter.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
package org.springframework.ai.vectorstore.opensearch;
1818

19-
import java.text.ParseException;
20-
import java.text.SimpleDateFormat;
19+
import java.time.Instant;
20+
import java.time.ZoneOffset;
21+
import java.time.format.DateTimeFormatter;
22+
import java.time.format.DateTimeParseException;
2123
import java.util.Date;
2224
import java.util.List;
23-
import java.util.TimeZone;
2425
import java.util.regex.Pattern;
2526

2627
import org.springframework.ai.vectorstore.filter.Filter;
@@ -38,11 +39,10 @@ public class OpenSearchAiSearchFilterExpressionConverter extends AbstractFilterE
3839

3940
private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z");
4041

41-
private final SimpleDateFormat dateFormat;
42+
private final DateTimeFormatter dateFormat;
4243

4344
public OpenSearchAiSearchFilterExpressionConverter() {
44-
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
45-
this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
45+
this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
4646
}
4747

4848
@Override
@@ -119,15 +119,15 @@ protected void doValue(Filter.Value filterValue, StringBuilder context) {
119119
@Override
120120
protected void doSingleValue(Object value, StringBuilder context) {
121121
if (value instanceof Date date) {
122-
context.append(this.dateFormat.format(date));
122+
context.append(this.dateFormat.format(date.toInstant()));
123123
}
124124
else if (value instanceof String text) {
125125
if (DATE_FORMAT_PATTERN.matcher(text).matches()) {
126126
try {
127-
Date date = this.dateFormat.parse(text);
127+
Instant date = Instant.from(this.dateFormat.parse(text));
128128
context.append(this.dateFormat.format(date));
129129
}
130-
catch (ParseException e) {
130+
catch (DateTimeParseException e) {
131131
throw new IllegalArgumentException("Invalid date type:" + text, e);
132132
}
133133
}

0 commit comments

Comments
 (0)