diff --git a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java index f11de8802..4683fd7b6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java @@ -41,6 +41,7 @@ import org.springframework.util.Assert; * @author Artur Konczak * @author Rasmus Faber-Espensen * @author James Bodkin + * @author Peter-Josef Meisch */ class CriteriaQueryProcessor { @@ -134,17 +135,23 @@ class CriteriaQueryProcessor { return query; } - private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, - /* OperationKey key, Object value,*/ String fieldName) { - Object value = entry.getValue(); - if (value == null) { - return null; - } + private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, String fieldName) { OperationKey key = entry.getKey(); - QueryBuilder query = null; + Object value = entry.getValue(); + + if (value == null) { + + if (key == OperationKey.EXISTS) { + return existsQuery(fieldName); + } else { + return null; + } + } String searchText = QueryParserUtil.escape(value.toString()); + QueryBuilder query = null; + switch (key) { case EQUALS: query = queryStringQuery(searchText).field(fieldName).defaultOperator(AND); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index ffb2ecb63..83ae04429 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -15,10 +15,14 @@ */ package org.springframework.data.elasticsearch.core.query; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.elasticsearch.core.geo.GeoBox; import org.springframework.data.elasticsearch.core.geo.GeoPoint; @@ -26,6 +30,8 @@ import org.springframework.data.geo.Box; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Criteria is the central class when constructing queries. It follows more or less a fluent API style, which allows to @@ -34,18 +40,15 @@ import org.springframework.util.Assert; * @author Rizwan Idrees * @author Mohsin Husen * @author Franck Marchand + * @author Peter-Josef Meisch */ public class Criteria { @Override public String toString() { - return "Criteria{" + - "field=" + field.getName() + - ", boost=" + boost + - ", negating=" + negating + - ", queryCriteria=" + ObjectUtils.nullSafeToString(queryCriteria) + - ", filterCriteria=" + ObjectUtils.nullSafeToString(filterCriteria) + - '}'; + return "Criteria{" + "field=" + field.getName() + ", boost=" + boost + ", negating=" + negating + ", queryCriteria=" + + ObjectUtils.nullSafeToString(queryCriteria) + ", filterCriteria=" + + ObjectUtils.nullSafeToString(filterCriteria) + '}'; } public static final String WILDCARD = "*"; @@ -64,8 +67,7 @@ public class Criteria { private Set filterCriteria = new LinkedHashSet<>(); - public Criteria() { - } + public Criteria() {} /** * Creates a new Criteria with provided field name @@ -209,6 +211,17 @@ public class Criteria { return this; } + /** + * Creates a new CriteriaEntry for existence check. + * + * @return this object + * @since 4.0 + */ + public Criteria exists() { + queryCriteria.add(new CriteriaEntry(OperationKey.EXISTS, null)); + return this; + } + /** * Crates new CriteriaEntry with leading and trailing wildcards
* NOTE: mind your schema as leading wildcards may not be supported and/or execution might be slow. @@ -305,7 +318,7 @@ public class Criteria { throw new InvalidDataAccessApiUsageException("Range [* TO *] is not allowed"); } - queryCriteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[]{lowerBound, upperBound})); + queryCriteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); return this; } @@ -377,9 +390,9 @@ public class Criteria { private List toCollection(Object... values) { if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) { - throw new InvalidDataAccessApiUsageException("At least one element " - + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "") - + " has to be present."); + throw new InvalidDataAccessApiUsageException( + "At least one element " + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "") + + " has to be present."); } return Arrays.asList(values); } @@ -398,15 +411,14 @@ public class Criteria { * Creates new CriteriaEntry for {@code location WITHIN distance} * * @param location {@link org.springframework.data.elasticsearch.core.geo.GeoPoint} center coordinates - * @param distance {@link String} radius as a string (e.g. : '100km'). - * Distance unit : - * either mi/miles or km can be set + * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be + * set * @return Criteria the chaind criteria with the new 'within' criteria included. */ public Criteria within(GeoPoint location, String distance) { Assert.notNull(location, "Location value for near criteria must not be null"); Assert.notNull(location, "Distance value for near criteria must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[]{location, distance})); + filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance })); return this; } @@ -414,56 +426,54 @@ public class Criteria { * Creates new CriteriaEntry for {@code location WITHIN distance} * * @param location {@link org.springframework.data.geo.Point} center coordinates - * @param distance {@link org.springframework.data.geo.Distance} radius - * . + * @param distance {@link org.springframework.data.geo.Distance} radius . * @return Criteria the chaind criteria with the new 'within' criteria included. */ public Criteria within(Point location, Distance distance) { Assert.notNull(location, "Location value for near criteria must not be null"); Assert.notNull(location, "Distance value for near criteria must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[]{location, distance})); + filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance })); return this; } /** * Creates new CriteriaEntry for {@code geoLocation WITHIN distance} * - * @param geoLocation {@link String} center point - * supported formats: - * lat on = > "41.2,45.1", - * geohash = > "asd9as0d" - * @param distance {@link String} radius as a string (e.g. : '100km'). - * Distance unit : - * either mi/miles or km can be set + * @param geoLocation {@link String} center point supported formats: lat on = > "41.2,45.1", geohash = > "asd9as0d" + * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be + * set * @return */ public Criteria within(String geoLocation, String distance) { Assert.isTrue(!StringUtils.isEmpty(geoLocation), "geoLocation value must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[]{geoLocation, distance})); + filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { geoLocation, distance })); return this; } /** * Creates new CriteriaEntry for {@code location GeoBox bounding box} * - * @param boundingBox {@link org.springframework.data.elasticsearch.core.geo.GeoBox} bounding box(left top corner + right bottom corner) + * @param boundingBox {@link org.springframework.data.elasticsearch.core.geo.GeoBox} bounding box(left top corner + + * right bottom corner) * @return Criteria the chaind criteria with the new 'boundingBox' criteria included. */ public Criteria boundedBy(GeoBox boundingBox) { Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[]{boundingBox})); + filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox })); return this; } /** * Creates new CriteriaEntry for {@code location Box bounding box} * - * @param boundingBox {@link org.springframework.data.elasticsearch.core.geo.GeoBox} bounding box(left top corner + right bottom corner) + * @param boundingBox {@link org.springframework.data.elasticsearch.core.geo.GeoBox} bounding box(left top corner + + * right bottom corner) * @return Criteria the chaind criteria with the new 'boundingBox' criteria included. */ public Criteria boundedBy(Box boundingBox) { Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[]{boundingBox.getFirst(), boundingBox.getSecond()})); + filterCriteria + .add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox.getFirst(), boundingBox.getSecond() })); return this; } @@ -477,7 +487,7 @@ public class Criteria { public Criteria boundedBy(String topLeftGeohash, String bottomRightGeohash) { Assert.isTrue(!StringUtils.isEmpty(topLeftGeohash), "topLeftGeohash must not be empty"); Assert.isTrue(!StringUtils.isEmpty(bottomRightGeohash), "bottomRightGeohash must not be empty"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[]{topLeftGeohash, bottomRightGeohash})); + filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash })); return this; } @@ -491,14 +501,15 @@ public class Criteria { public Criteria boundedBy(GeoPoint topLeftPoint, GeoPoint bottomRightPoint) { Assert.notNull(topLeftPoint, "topLeftPoint must not be null"); Assert.notNull(bottomRightPoint, "bottomRightPoint must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[]{topLeftPoint, bottomRightPoint})); + filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftPoint, bottomRightPoint })); return this; } public Criteria boundedBy(Point topLeftPoint, Point bottomRightPoint) { Assert.notNull(topLeftPoint, "topLeftPoint must not be null"); Assert.notNull(bottomRightPoint, "bottomRightPoint must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[]{GeoPoint.fromPoint(topLeftPoint), GeoPoint.fromPoint(bottomRightPoint)})); + filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, + new Object[] { GeoPoint.fromPoint(topLeftPoint), GeoPoint.fromPoint(bottomRightPoint) })); return this; } @@ -587,8 +598,27 @@ public class Criteria { } } - public enum OperationKey { - EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH, EXPRESSION, BETWEEN, FUZZY, IN, NOT_IN, WITHIN, BBOX, NEAR, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL; + public enum OperationKey { // + EQUALS, // + CONTAINS, // + STARTS_WITH, // + ENDS_WITH, // + EXPRESSION, // + BETWEEN, // + FUZZY, // + IN, // + NOT_IN, // + WITHIN, // + BBOX, // + NEAR, // + LESS, // + LESS_EQUAL, // + GREATER, // + GREATER_EQUAL, // + /** + * @since 4.0 + */ + EXISTS; } public static class CriteriaEntry { @@ -611,10 +641,7 @@ public class Criteria { @Override public String toString() { - return "CriteriaEntry{" + - "key=" + key + - ", value=" + value + - '}'; + return "CriteriaEntry{" + "key=" + key + ", value=" + value + '}'; } } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java index ab5490224..f7f327efd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java @@ -138,9 +138,14 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator sortedIds = repository.findAllByOrderByText().stream() // .map(it -> it.text).collect(Collectors.toList()); - assertThat(sortedIds).containsExactly("Beet sugar", "Cane sugar", "Cane sugar", "Rock salt", "Sea salt"); + assertThat(sortedIds).containsExactly("Beet sugar", "Cane sugar", "Cane sugar", "Rock salt", "Sea salt", "no name"); } @Test // DATAES-615 @@ -204,7 +204,7 @@ class QueryKeywordsTests { List sortedIds = repository.findAllByOrderBySortName().stream() // .map(it -> it.id).collect(Collectors.toList()); - assertThat(sortedIds).containsExactly("5", "4", "3", "2", "1"); + assertThat(sortedIds).containsExactly("6", "5", "4", "3", "2", "1"); } @Test // DATAES-178 @@ -241,6 +241,22 @@ class QueryKeywordsTests { products.forEach(product -> assertThat(product.name).isEqualTo("Sugar")); } + @Test + void shouldSearchForNullValues() { + final List products = repository.findByName(null); + + assertThat(products).hasSize(1); + assertThat(products.get(0).getId()).isEqualTo("6"); + } + + @Test + void shouldDeleteWithNullValues() { + repository.deleteByName(null); + + long count = repository.count(); + assertThat(count).isEqualTo(5); + } + /** * @author Mohsin Husen * @author Artur Konczak @@ -256,28 +272,14 @@ class QueryKeywordsTests { @Id private String id; - private List title; - private String name; - private String description; - @Field(type = FieldType.Keyword) private String text; - private List categories; - - private Float weight; - @Field(type = FieldType.Float) private Float price; - private Integer popularity; - private boolean available; - private String location; - - private Date lastModified; - @Field(name = "sort-name", type = FieldType.Keyword) private String sortName; } @@ -286,6 +288,8 @@ class QueryKeywordsTests { */ interface ProductRepository extends ElasticsearchRepository { + List findByName(@Nullable String name); + List findByNameAndText(String name, String text); List findByNameAndPrice(String name, Float price); @@ -331,6 +335,8 @@ class QueryKeywordsTests { List findFirst2ByName(String name); List findTop2ByName(String name); + + void deleteByName(@Nullable String name); } }