Skip to content

Commit c044869

Browse files
Implemented basic QueryByExample functionality
1 parent a780a13 commit c044869

File tree

7 files changed

+435
-69
lines changed

7 files changed

+435
-69
lines changed

spring-data-eclipse-store/src/main/java/software/xdev/spring/data/eclipse/store/repository/query/EclipseStoreQueryCreator.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import java.util.Iterator;
2020
import java.util.Objects;
2121

22+
import jakarta.annotation.Nonnull;
23+
import jakarta.annotation.Nullable;
24+
2225
import org.springframework.data.domain.Sort;
2326
import org.springframework.data.repository.query.ParameterAccessor;
2427
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -28,10 +31,6 @@
2831
import org.springframework.data.util.TypeInformation;
2932
import org.springframework.util.ObjectUtils;
3033

31-
import jakarta.annotation.Nonnull;
32-
import jakarta.annotation.Nullable;
33-
import software.xdev.spring.data.eclipse.store.exceptions.FieldAccessReflectionException;
34-
import software.xdev.spring.data.eclipse.store.repository.access.AccessHelper;
3534
import software.xdev.spring.data.eclipse.store.repository.query.criteria.AbstractCriteriaNode;
3635
import software.xdev.spring.data.eclipse.store.repository.query.criteria.Criteria;
3736
import software.xdev.spring.data.eclipse.store.repository.query.criteria.CriteriaSingleNode;
@@ -249,16 +248,6 @@ private boolean isSimpleComparisonPossible(final Part part)
249248
private ReflectedField<T, ?> getDeclaredField(final Part part)
250249
{
251250
final String fieldName = part.getProperty().getSegment();
252-
try
253-
{
254-
return new ReflectedField<>(AccessHelper.getInheritedPrivateField(this.domainClass, fieldName));
255-
}
256-
catch(final NoSuchFieldException e)
257-
{
258-
throw new FieldAccessReflectionException(String.format(
259-
"Field %s in class %s was not found!",
260-
fieldName,
261-
this.domainClass.getSimpleName()), e);
262-
}
251+
return ReflectedField.createReflectedField(this.domainClass, fieldName);
263252
}
264253
}

spring-data-eclipse-store/src/main/java/software/xdev/spring/data/eclipse/store/repository/query/ReflectedField.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.Objects;
2020

2121
import jakarta.annotation.Nonnull;
22+
23+
import software.xdev.spring.data.eclipse.store.exceptions.FieldAccessReflectionException;
2224
import software.xdev.spring.data.eclipse.store.repository.access.AccessHelper;
2325

2426

@@ -38,6 +40,21 @@ public ReflectedField(final Field field)
3840
this.field = Objects.requireNonNull(field);
3941
}
4042

43+
public static <T, E> ReflectedField<T, E> createReflectedField(final Class<T> domainClass, final String fieldName)
44+
{
45+
try
46+
{
47+
return new ReflectedField<>(AccessHelper.getInheritedPrivateField(domainClass, fieldName));
48+
}
49+
catch(final NoSuchFieldException e)
50+
{
51+
throw new FieldAccessReflectionException(String.format(
52+
"Field %s in class %s was not found!",
53+
fieldName,
54+
domainClass.getSimpleName()), e);
55+
}
56+
}
57+
4158
/**
4259
* Reads the field of the given object. If the fields is not accessible, it is made accessible with the
4360
* {@link AccessHelper#readFieldVariable(Field, Object)}.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright © 2024 XDEV Software (https://xdev.software)
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+
* http://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 software.xdev.spring.data.eclipse.store.repository.query.criteria;
17+
18+
import java.lang.reflect.Field;
19+
import java.util.Collection;
20+
import java.util.Locale;
21+
import java.util.Map;
22+
import java.util.Optional;
23+
import java.util.function.Predicate;
24+
import java.util.regex.Pattern;
25+
26+
import org.springframework.data.domain.Example;
27+
import org.springframework.data.domain.ExampleMatcher;
28+
29+
import software.xdev.spring.data.eclipse.store.repository.access.AccessHelper;
30+
import software.xdev.spring.data.eclipse.store.repository.query.ReflectedField;
31+
32+
33+
public class CriteriaByExample<T, S extends T> implements Criteria<T>
34+
{
35+
private final Predicate<T> predicate;
36+
37+
public CriteriaByExample(final Example<S> example)
38+
{
39+
if(example.getMatcher().isAllMatching())
40+
{
41+
this.predicate = entity -> this.getDefinedOrDefaultSpecifiers(example)
42+
.stream()
43+
.allMatch(this.createPredicateForSpecifier(example, entity));
44+
}
45+
else
46+
{
47+
this.predicate = entity -> this.getDefinedOrDefaultSpecifiers(example)
48+
.stream()
49+
.anyMatch(this.createPredicateForSpecifier(example, entity));
50+
}
51+
}
52+
53+
private Collection<ExampleMatcher.PropertySpecifier> getDefinedOrDefaultSpecifiers(final Example<S> example)
54+
{
55+
final Collection<ExampleMatcher.PropertySpecifier> specifiers =
56+
example.getMatcher().getPropertySpecifiers().getSpecifiers();
57+
if(!specifiers.isEmpty())
58+
{
59+
return specifiers;
60+
}
61+
62+
ExampleMatcher matcher = ExampleMatcher.matching();
63+
64+
final Map<String, Field> allFields = AccessHelper.getInheritedPrivateFieldsByName(example.getProbeType());
65+
for(final String fieldName : allFields.keySet())
66+
{
67+
matcher = matcher.withMatcher(fieldName, ExampleMatcher.GenericPropertyMatchers.exact());
68+
}
69+
return matcher.getPropertySpecifiers().getSpecifiers();
70+
}
71+
72+
@Override
73+
public boolean evaluate(final T object)
74+
{
75+
return this.predicate.test(object);
76+
}
77+
78+
private <T> Predicate<ExampleMatcher.PropertySpecifier> createPredicateForSpecifier(
79+
final Example<S> example,
80+
final T entity)
81+
{
82+
return specifier ->
83+
{
84+
final ReflectedField<T, Object> reflectedField =
85+
(ReflectedField<T, Object>)ReflectedField.createReflectedField(
86+
example.getProbeType(),
87+
specifier.getPath());
88+
89+
final Object exampleValue = reflectedField.readValue((T)example.getProbe());
90+
final Optional<Object> transformedExampledValue =
91+
specifier.getPropertyValueTransformer().apply(Optional.ofNullable(exampleValue));
92+
93+
if(transformedExampledValue.isEmpty())
94+
{
95+
return true;
96+
}
97+
98+
final Object value = reflectedField.readValue(entity);
99+
final Optional<Object> transformedValue =
100+
specifier.getPropertyValueTransformer().apply(Optional.ofNullable(value));
101+
102+
final ExampleMatcher.StringMatcher setOrDefaultMatcher = specifier.getStringMatcher() == null ?
103+
example.getMatcher().getDefaultStringMatcher() :
104+
specifier.getStringMatcher();
105+
106+
switch(setOrDefaultMatcher)
107+
{
108+
case DEFAULT, EXACT ->
109+
{
110+
if(transformedExampledValue.get() instanceof String)
111+
{
112+
return this.valueToString(transformedValue, specifier).equals(this.valueToString(
113+
transformedExampledValue,
114+
specifier));
115+
}
116+
return transformedExampledValue.equals(transformedValue);
117+
}
118+
case STARTING ->
119+
{
120+
final Optional<String> valueAsString = this.valueToString(transformedValue, specifier);
121+
if(valueAsString.isEmpty())
122+
{
123+
return false;
124+
}
125+
return valueAsString.get()
126+
.startsWith(this.valueToString(transformedExampledValue, specifier).get());
127+
}
128+
case ENDING ->
129+
{
130+
final Optional<String> valueAsString = this.valueToString(transformedValue, specifier);
131+
if(valueAsString.isEmpty())
132+
{
133+
return false;
134+
}
135+
return valueAsString.get().endsWith(this.valueToString(transformedExampledValue, specifier).get());
136+
}
137+
case CONTAINING ->
138+
{
139+
final Optional<String> valueAsString = this.valueToString(transformedValue, specifier);
140+
if(valueAsString.isEmpty())
141+
{
142+
return false;
143+
}
144+
return valueAsString.get().contains(this.valueToString(transformedExampledValue, specifier).get());
145+
}
146+
case REGEX ->
147+
{
148+
final Optional<String> valueAsString = this.valueToString(transformedValue, specifier);
149+
if(valueAsString.isEmpty())
150+
{
151+
return false;
152+
}
153+
return Pattern.compile(
154+
this.valueToString(transformedExampledValue, specifier).get()
155+
)
156+
.matcher(valueAsString.get()).find();
157+
}
158+
}
159+
return true;
160+
};
161+
}
162+
163+
private Optional<String> valueToString(
164+
final Optional<Object> value,
165+
final ExampleMatcher.PropertySpecifier specifier)
166+
{
167+
if(value.isEmpty())
168+
{
169+
return Optional.empty();
170+
}
171+
if(specifier != null && Boolean.TRUE.equals(specifier.getIgnoreCase()))
172+
{
173+
return Optional.of(value.get().toString().toLowerCase(Locale.ROOT));
174+
}
175+
return Optional.of(value.get().toString());
176+
}
177+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright © 2024 XDEV Software (https://xdev.software)
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+
* http://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 software.xdev.spring.data.eclipse.store.repository.query.executors;
17+
18+
import java.util.Collection;
19+
import java.util.Objects;
20+
import java.util.stream.Stream;
21+
22+
import jakarta.annotation.Nullable;
23+
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
import software.xdev.spring.data.eclipse.store.repository.query.criteria.Criteria;
28+
29+
30+
/**
31+
* Executes queries that are optionally sorted and paged in collections.
32+
**/
33+
public class CountQueryExecutor<T> implements QueryExecutor<T>
34+
{
35+
private static final Logger LOG = LoggerFactory.getLogger(CountQueryExecutor.class);
36+
private final Criteria<T> criteria;
37+
38+
public CountQueryExecutor(final Criteria<T> criteria)
39+
{
40+
this.criteria = criteria;
41+
}
42+
43+
/**
44+
* {@inheritDoc}
45+
*
46+
* @return a list of the found/sorted/paged entities
47+
*/
48+
@Override
49+
public Long execute(final Class<T> clazz, @Nullable final Collection<T> entities, final Object[] values)
50+
{
51+
Objects.requireNonNull(entities);
52+
53+
final Stream<T> entityStream = entities
54+
.stream()
55+
.filter(this.criteria::evaluate);
56+
57+
final long result = entityStream.count();
58+
59+
if(LOG.isTraceEnabled())
60+
{
61+
LOG.trace("Found {} entries.", result);
62+
}
63+
return result;
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright © 2024 XDEV Software (https://xdev.software)
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+
* http://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 software.xdev.spring.data.eclipse.store.repository.query.executors;
17+
18+
import java.util.Collection;
19+
import java.util.Objects;
20+
import java.util.Optional;
21+
import java.util.stream.Stream;
22+
23+
import jakarta.annotation.Nullable;
24+
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import software.xdev.spring.data.eclipse.store.repository.query.criteria.Criteria;
29+
30+
31+
/**
32+
* Queries entities and returns the result wrapped in an optional.
33+
*
34+
* @param <T> Entity-Type to query
35+
*/
36+
public class ExistsQueryExecutor<T> implements QueryExecutor<T>
37+
{
38+
private static final Logger LOG = LoggerFactory.getLogger(ExistsQueryExecutor.class);
39+
private final Criteria<T> criteria;
40+
41+
public ExistsQueryExecutor(final Criteria<T> criteria)
42+
{
43+
this.criteria = Objects.requireNonNull(criteria);
44+
}
45+
46+
/**
47+
* {@inheritDoc}
48+
*
49+
* @return whether the entity exists
50+
*/
51+
@Override
52+
public Boolean execute(
53+
final Class<T> clazz,
54+
@Nullable final Collection<T> entities,
55+
@Nullable final Object[] values)
56+
{
57+
Objects.requireNonNull(clazz);
58+
if(entities == null || entities.isEmpty())
59+
{
60+
return false;
61+
}
62+
final Stream<T> entityStream = entities
63+
.stream()
64+
.filter(this.criteria::evaluate);
65+
66+
final Optional<T> result = entityStream.findAny();
67+
if(LOG.isDebugEnabled())
68+
{
69+
LOG.debug(
70+
"Query for class {} found an entity: {}",
71+
clazz.getSimpleName(),
72+
result.isPresent()
73+
);
74+
}
75+
return result.isPresent();
76+
}
77+
}

0 commit comments

Comments
 (0)