Skip to content

Commit 0b36a26

Browse files
Add Query by Example feature spring-projects#2418
Added Query by Example repositories fragments Added Criteria.regexp
1 parent 2fb9062 commit 0b36a26

16 files changed

+1555
-13
lines changed

src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021-2022 the original author or authors.
2+
* Copyright 2021-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@
3939
* query.
4040
*
4141
* @author Peter-Josef Meisch
42+
* @author Ezequiel Antúnez Camacho
4243
* @since 4.4
4344
*/
4445
class CriteriaQueryProcessor {
@@ -329,6 +330,13 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field,
329330
throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable");
330331
}
331332
break;
333+
case REGEXP:
334+
queryBuilder //
335+
.regexp(rb -> rb //
336+
.field(fieldName) //
337+
.value(searchText) //
338+
.boost(boost)); //
339+
break;
332340
default:
333341
throw new CriteriaQueryException("Could not build query for " + entry);
334342
}

src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2022 the original author or authors.
2+
* Copyright 2013-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,6 +44,7 @@
4444
* @author Rasmus Faber-Espensen
4545
* @author James Bodkin
4646
* @author Peter-Josef Meisch
47+
* @author Ezequiel Antúnez Camacho
4748
* @deprecated since 5.0
4849
*/
4950
@Deprecated
@@ -248,6 +249,9 @@ private QueryBuilder queryFor(Criteria.CriteriaEntry entry, Field field) {
248249
}
249250
}
250251
break;
252+
case REGEXP:
253+
query = regexpQuery(fieldName, searchText);
254+
break;
251255
}
252256
return query;
253257
}

src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2022 the original author or authors.
2+
* Copyright 2013-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,6 +50,7 @@
5050
* @author Mohsin Husen
5151
* @author Franck Marchand
5252
* @author Peter-Josef Meisch
53+
* @author Ezequiel Antúnez Camacho
5354
*/
5455
public class Criteria {
5556

@@ -611,6 +612,20 @@ public Criteria notEmpty() {
611612
return this;
612613
}
613614

615+
/**
616+
* Add a {@link OperationKey#REGEXP} entry to the {@link #queryCriteriaEntries}.
617+
*
618+
* @param value the regexp value to match
619+
* @return this object
620+
*/
621+
public Criteria regexp(Object value) {
622+
623+
Assert.notNull(value, "value must not be null");
624+
625+
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.REGEXP, value));
626+
return this;
627+
}
628+
614629
// endregion
615630

616631
// region criteria entries - filter
@@ -954,7 +969,8 @@ public enum OperationKey { //
954969
/**
955970
* @since 4.3
956971
*/
957-
NOT_EMPTY;
972+
NOT_EMPTY, //
973+
REGEXP;
958974

959975
/**
960976
* @return true if this key does not have an associated value

src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java

+24-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2022 the original author or authors.
2+
* Copyright 2013-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,29 +15,33 @@
1515
*/
1616
package org.springframework.data.elasticsearch.repository.support;
1717

18-
import static org.springframework.data.querydsl.QuerydslUtils.*;
19-
20-
import java.lang.reflect.Method;
21-
import java.util.Optional;
22-
2318
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
2419
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
2520
import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery;
2621
import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod;
2722
import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery;
23+
import org.springframework.data.elasticsearch.repository.support.querybyexample.QueryByExampleElasticsearchExecutor;
2824
import org.springframework.data.projection.ProjectionFactory;
2925
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
3026
import org.springframework.data.repository.core.NamedQueries;
3127
import org.springframework.data.repository.core.RepositoryInformation;
3228
import org.springframework.data.repository.core.RepositoryMetadata;
29+
import org.springframework.data.repository.core.support.RepositoryComposition;
3330
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
31+
import org.springframework.data.repository.core.support.RepositoryFragment;
32+
import org.springframework.data.repository.query.QueryByExampleExecutor;
3433
import org.springframework.data.repository.query.QueryLookupStrategy;
3534
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
3635
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3736
import org.springframework.data.repository.query.RepositoryQuery;
3837
import org.springframework.lang.Nullable;
3938
import org.springframework.util.Assert;
4039

40+
import java.lang.reflect.Method;
41+
import java.util.Optional;
42+
43+
import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT;
44+
4145
/**
4246
* Factory to create {@link ElasticsearchRepository}
4347
*
@@ -49,6 +53,7 @@
4953
* @author Christoph Strobl
5054
* @author Sascha Woo
5155
* @author Peter-Josef Meisch
56+
* @author Ezequiel Antúnez Camacho
5257
*/
5358
public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
5459

@@ -122,4 +127,17 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
122127
protected RepositoryMetadata getRepositoryMetadata(Class<?> repositoryInterface) {
123128
return new ElasticsearchRepositoryMetadata(repositoryInterface);
124129
}
130+
131+
@Override
132+
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
133+
RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty();
134+
135+
if (QueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) {
136+
fragments = fragments.append(RepositoryFragment.implemented(QueryByExampleExecutor.class,
137+
instantiateClass(QueryByExampleElasticsearchExecutor.class, elasticsearchOperations)));
138+
}
139+
140+
return fragments;
141+
}
142+
125143
}

src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2022 the original author or authors.
2+
* Copyright 2019-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,15 +25,19 @@
2525
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryMethod;
2626
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchStringQuery;
2727
import org.springframework.data.elasticsearch.repository.query.ReactivePartTreeElasticsearchQuery;
28+
import org.springframework.data.elasticsearch.repository.support.querybyexample.ReactiveQueryByExampleElasticsearchExecutor;
2829
import org.springframework.data.mapping.context.MappingContext;
2930
import org.springframework.data.projection.ProjectionFactory;
3031
import org.springframework.data.repository.core.NamedQueries;
3132
import org.springframework.data.repository.core.RepositoryInformation;
3233
import org.springframework.data.repository.core.RepositoryMetadata;
3334
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
35+
import org.springframework.data.repository.core.support.RepositoryComposition;
36+
import org.springframework.data.repository.core.support.RepositoryFragment;
3437
import org.springframework.data.repository.query.QueryLookupStrategy;
3538
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
3639
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
40+
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
3741
import org.springframework.data.repository.query.RepositoryQuery;
3842
import org.springframework.expression.spel.standard.SpelExpressionParser;
3943
import org.springframework.lang.Nullable;
@@ -45,6 +49,7 @@
4549
*
4650
* @author Christoph Strobl
4751
* @author Ivan Greene
52+
* @author Ezequiel Antúnez Camacho
4853
* @since 3.2
4954
*/
5055
public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport {
@@ -168,4 +173,16 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
168173
}
169174
}
170175
}
176+
177+
@Override
178+
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
179+
RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty();
180+
181+
if (ReactiveQueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) {
182+
fragments = fragments.append(RepositoryFragment.implemented(ReactiveQueryByExampleExecutor.class,
183+
instantiateClass(ReactiveQueryByExampleElasticsearchExecutor.class, operations)));
184+
}
185+
186+
return fragments;
187+
}
171188
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.repository.support.querybyexample;
17+
18+
import org.springframework.dao.InvalidDataAccessApiUsageException;
19+
import org.springframework.data.domain.Example;
20+
import org.springframework.data.domain.ExampleMatcher;
21+
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
22+
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
23+
import org.springframework.data.elasticsearch.core.query.Criteria;
24+
import org.springframework.data.mapping.PersistentPropertyAccessor;
25+
import org.springframework.data.mapping.context.MappingContext;
26+
import org.springframework.data.support.ExampleMatcherAccessor;
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.util.StringUtils;
29+
30+
import java.util.EnumSet;
31+
import java.util.Map;
32+
import java.util.Optional;
33+
import java.util.Set;
34+
35+
/**
36+
* Maps a {@link Example} to a {@link org.springframework.data.elasticsearch.core.query.Criteria}
37+
*
38+
* @param <T> Class type
39+
* @author Ezequiel Antúnez Camacho
40+
*/
41+
class ExampleCriteriaMapper<T> {
42+
43+
private static final Set<ExampleMatcher.StringMatcher> SUPPORTED_MATCHERS = EnumSet.of(
44+
ExampleMatcher.StringMatcher.DEFAULT, ExampleMatcher.StringMatcher.EXACT, ExampleMatcher.StringMatcher.STARTING,
45+
ExampleMatcher.StringMatcher.CONTAINING, ExampleMatcher.StringMatcher.ENDING);
46+
47+
private final MappingContext<? extends ElasticsearchPersistentEntity<T>, ElasticsearchPersistentProperty> mappingContext;
48+
49+
/**
50+
* Builds a {@link ExampleCriteriaMapper}
51+
*
52+
* @param mappingContext mappingContext to use
53+
*/
54+
ExampleCriteriaMapper(
55+
MappingContext<? extends ElasticsearchPersistentEntity<T>, ElasticsearchPersistentProperty> mappingContext) {
56+
this.mappingContext = mappingContext;
57+
}
58+
59+
<S extends T> Criteria criteria(Example<S> example) {
60+
return buildCriteria(example);
61+
}
62+
63+
private <S extends T> Criteria buildCriteria(Example<S> example) {
64+
final ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher());
65+
66+
return applyPropertySpecs(new Criteria(), "", example.getProbe(),
67+
mappingContext.getRequiredPersistentEntity(example.getProbeType()), matcherAccessor,
68+
example.getMatcher().getMatchMode());
69+
}
70+
71+
private Criteria applyPropertySpecs(Criteria criteria, String path, @Nullable Object probe,
72+
ElasticsearchPersistentEntity<?> persistentEntity, ExampleMatcherAccessor exampleSpecAccessor,
73+
ExampleMatcher.MatchMode matchMode) {
74+
75+
if (probe == null) {
76+
return criteria;
77+
}
78+
79+
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(probe);
80+
81+
for (ElasticsearchPersistentProperty property : persistentEntity) {
82+
final String propertyName = getPropertyName(property);
83+
String propertyPath = StringUtils.hasText(path) ? (path + "." + propertyName) : propertyName;
84+
if (exampleSpecAccessor.isIgnoredPath(propertyPath) || property.isCollectionLike()
85+
|| property.isVersionProperty()) {
86+
continue;
87+
}
88+
89+
Object propertyValue = propertyAccessor.getProperty(property);
90+
if (property.isMap() && propertyValue != null) {
91+
for (Map.Entry<String, Object> entry : ((Map<String, Object>) propertyValue).entrySet()) {
92+
String key = entry.getKey();
93+
Object value = entry.getValue();
94+
criteria = applyPropertySpec(propertyPath + "." + key, value, exampleSpecAccessor, property, matchMode,
95+
criteria);
96+
}
97+
continue;
98+
}
99+
100+
criteria = applyPropertySpec(propertyPath, propertyValue, exampleSpecAccessor, property, matchMode, criteria);
101+
}
102+
return criteria;
103+
}
104+
105+
private String getPropertyName(ElasticsearchPersistentProperty property) {
106+
return property.isIdProperty() ? "_id" : property.getName();
107+
}
108+
109+
private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMatcherAccessor exampleSpecAccessor,
110+
ElasticsearchPersistentProperty property, ExampleMatcher.MatchMode matchMode, Criteria criteria) {
111+
112+
if (exampleSpecAccessor.isIgnoreCaseForPath(path)) {
113+
throw new InvalidDataAccessApiUsageException(
114+
"Current implementation of Query-by-Example supports only case-sensitive matching.");
115+
}
116+
117+
ExampleMatcher.StringMatcher stringMatcher = exampleSpecAccessor.getStringMatcherForPath(path);
118+
if (!SUPPORTED_MATCHERS.contains(exampleSpecAccessor.getStringMatcherForPath(path))) {
119+
throw new InvalidDataAccessApiUsageException(String.format(
120+
"Current implementation of Query-by-Example does not support string matcher %s. Supported matchers are: %s.",
121+
stringMatcher, SUPPORTED_MATCHERS));
122+
}
123+
124+
final Object transformedValue = exampleSpecAccessor.getValueTransformerForPath(path)
125+
.apply(Optional.ofNullable(propertyValue)).orElse(null);
126+
127+
if (transformedValue == null) {
128+
criteria = tryToAppendMustNotSentence(criteria, path, exampleSpecAccessor);
129+
} else {
130+
if (property.isEntity()) {
131+
return applyPropertySpecs(criteria, path, transformedValue,
132+
mappingContext.getRequiredPersistentEntity(property), exampleSpecAccessor, matchMode);
133+
} else {
134+
return applyStringMatcher(applyMatchMode(criteria, path, matchMode), transformedValue, stringMatcher);
135+
}
136+
}
137+
return criteria;
138+
}
139+
140+
private Criteria tryToAppendMustNotSentence(Criteria criteria, String path,
141+
ExampleMatcherAccessor exampleSpecAccessor) {
142+
if (ExampleMatcher.NullHandler.INCLUDE.equals(exampleSpecAccessor.getNullHandler())
143+
|| exampleSpecAccessor.hasPropertySpecifier(path)) {
144+
return criteria.and(path).not().exists();
145+
}
146+
return criteria;
147+
}
148+
149+
private Criteria applyMatchMode(Criteria criteria, String path, ExampleMatcher.MatchMode matchMode) {
150+
if (matchMode == ExampleMatcher.MatchMode.ALL) {
151+
return criteria.and(path);
152+
} else {
153+
return criteria.or(path);
154+
}
155+
}
156+
157+
private Criteria applyStringMatcher(Criteria criteria, Object value, ExampleMatcher.StringMatcher stringMatcher) {
158+
return switch (stringMatcher) {
159+
case DEFAULT, EXACT -> criteria.is(value);
160+
case STARTING -> criteria.startsWith(validateString(value));
161+
case ENDING -> criteria.endsWith(validateString(value));
162+
case CONTAINING -> criteria.contains(validateString(value));
163+
case REGEX -> throw new UnsupportedOperationException("REGEX matcher is unsupported");
164+
};
165+
}
166+
167+
private String validateString(Object value) {
168+
if (value instanceof String) {
169+
return value.toString();
170+
}
171+
throw new IllegalArgumentException("This operation requires a String but got " + value.getClass());
172+
}
173+
174+
}

0 commit comments

Comments
 (0)