From 664f46a6bb82680bff832bc6147acfe23c964989 Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Wed, 10 Sep 2025 12:22:43 -0700 Subject: [PATCH] fix: resolve NoSuchElementException when using Point in projection interfaces (#661) Point fields in projection interfaces were causing NoSuchElementException due to incorrect JSON formatting during result parsing. The Point values stored as 'lon,lat' strings in Redis needed to be properly quoted for Gson's PointTypeAdapter to deserialize them correctly. --- .../repository/query/RediSearchQuery.java | 16 ++- .../document/PointProjectionTest.java | 98 +++++++++++++++++++ .../document/model/FlightWithLocation.java | 35 +++++++ .../FlightWithLocationRepository.java | 26 +++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tests/src/test/java/com/redis/om/spring/annotations/document/PointProjectionTest.java create mode 100644 tests/src/test/java/com/redis/om/spring/fixtures/document/model/FlightWithLocation.java create mode 100644 tests/src/test/java/com/redis/om/spring/fixtures/document/repository/FlightWithLocationRepository.java diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java index 815008b3..4e677eb4 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java @@ -945,8 +945,22 @@ private Object parseDocumentResult(redis.clients.jedis.search.Document doc) { jsonBuilder.append("\"").append(fieldName).append("\":"); + // Check if this field is a Point type in the domain class + boolean isPointField = false; + try { + Field domainField = ReflectionUtils.findField(domainType, fieldName); + if (domainField != null && domainField.getType() == Point.class) { + isPointField = true; + } + } catch (Exception e) { + // Ignore - field might not exist in projection + } + // Handle different types based on the raw value from Redis - if (fieldName.equals("name") || (valueStr.startsWith("\"") && valueStr.endsWith("\""))) { + if (isPointField && valueStr.contains(",") && !valueStr.startsWith("\"")) { + // Point field - stored as "lon,lat" in Redis, needs to be quoted for PointTypeAdapter + jsonBuilder.append("\"").append(valueStr).append("\""); + } else if (fieldName.equals("name") || (valueStr.startsWith("\"") && valueStr.endsWith("\""))) { // String field - quote if not already quoted if (valueStr.startsWith("\"") && valueStr.endsWith("\"")) { jsonBuilder.append(valueStr); diff --git a/tests/src/test/java/com/redis/om/spring/annotations/document/PointProjectionTest.java b/tests/src/test/java/com/redis/om/spring/annotations/document/PointProjectionTest.java new file mode 100644 index 00000000..c270edd3 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/annotations/document/PointProjectionTest.java @@ -0,0 +1,98 @@ +package com.redis.om.spring.annotations.document; + +import com.redis.om.spring.AbstractBaseDocumentTest; +import com.redis.om.spring.fixtures.document.model.FlightWithLocation; +import com.redis.om.spring.fixtures.document.repository.FlightWithLocationRepository; +import com.redis.om.spring.fixtures.document.repository.FlightWithLocationRepository.FlightProjection; +import com.redis.om.spring.fixtures.document.repository.FlightWithLocationRepository.FlightProjectionWithoutPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.geo.Point; + +import java.util.NoSuchElementException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Test for issue #661 - NoSuchElementException when using Point in projection interface + * https://github.com/redis/redis-om-spring/issues/661 + */ +class PointProjectionTest extends AbstractBaseDocumentTest { + + @Autowired + private FlightWithLocationRepository repository; + + @BeforeEach + void setup() { + repository.deleteAll(); + + // Create test data + FlightWithLocation flight1 = new FlightWithLocation("AA123", "Flight to Paris", new Point(2.3522, 48.8566)); + FlightWithLocation flight2 = new FlightWithLocation("BA456", "Flight to London", new Point(-0.1276, 51.5074)); + FlightWithLocation flight3 = new FlightWithLocation("LH789", "Flight to Berlin", new Point(13.4050, 52.5200)); + + repository.save(flight1); + repository.save(flight2); + repository.save(flight3); + } + + @Test + void testProjectionWithoutPointWorks() { + // This should work fine - projection without Point field + FlightProjectionWithoutPoint projection = repository.findByName("Flight to Paris"); + + assertThat(projection).isNotNull(); + assertThat(projection.getNumber()).isEqualTo("AA123"); + assertThat(projection.getName()).isEqualTo("Flight to Paris"); + } + + @Test + void testProjectionWithPointNowWorks() { + // This test verifies that issue #661 has been fixed + // Previously would throw NoSuchElementException, now it works + FlightProjection projection = repository.findByNumber("AA123"); + + assertThat(projection).isNotNull(); + assertThat(projection.getNumber()).isEqualTo("AA123"); + assertThat(projection.getName()).isEqualTo("Flight to Paris"); + + // After fix, accessing Point field in projection should work + Point location = projection.getLocation(); + assertThat(location).isNotNull(); + assertThat(location.getX()).isEqualTo(2.3522); + assertThat(location.getY()).isEqualTo(48.8566); + } + + @Test + void testDirectRepositoryAccessWithPointWorks() { + // Direct repository access should work fine + FlightWithLocation flight = repository.findById( + repository.findAll().iterator().next().getId() + ).orElseThrow(); + + assertThat(flight.getLocation()).isNotNull(); + assertThat(flight.getLocation().getX()).isEqualTo(2.3522); + assertThat(flight.getLocation().getY()).isEqualTo(48.8566); + } + + @Test + void testProjectionWithPointShouldWork() { + // After fix, this should work without throwing exception + FlightProjection projection = repository.findByNumber("BA456"); + + assertThat(projection).isNotNull(); + assertThat(projection.getNumber()).isEqualTo("BA456"); + assertThat(projection.getName()).isEqualTo("Flight to London"); + + // After fix, this should NOT throw exception + assertDoesNotThrow(() -> { + Point location = projection.getLocation(); + assertThat(location).isNotNull(); + assertThat(location.getX()).isEqualTo(-0.1276); + assertThat(location.getY()).isEqualTo(51.5074); + }); + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/document/model/FlightWithLocation.java b/tests/src/test/java/com/redis/om/spring/fixtures/document/model/FlightWithLocation.java new file mode 100644 index 00000000..22b73c03 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/document/model/FlightWithLocation.java @@ -0,0 +1,35 @@ +package com.redis.om.spring.fixtures.document.model; + +import com.redis.om.spring.annotations.Document; +import com.redis.om.spring.annotations.Indexed; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.geo.Point; + +/** + * Test model for issue #661 - NoSuchElementException when using Point in projection interface + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Document +public class FlightWithLocation { + @Id + private String id; + + @Indexed + private String number; + + private String name; + + @Indexed + private Point location; + + public FlightWithLocation(String number, String name, Point location) { + this.number = number; + this.name = name; + this.location = location; + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/FlightWithLocationRepository.java b/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/FlightWithLocationRepository.java new file mode 100644 index 00000000..a8ab2380 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/FlightWithLocationRepository.java @@ -0,0 +1,26 @@ +package com.redis.om.spring.fixtures.document.repository; + +import com.redis.om.spring.fixtures.document.model.FlightWithLocation; +import com.redis.om.spring.repository.RedisDocumentRepository; +import org.springframework.data.geo.Point; + +public interface FlightWithLocationRepository extends RedisDocumentRepository { + + // Projection interface for testing issue #661 + interface FlightProjection { + String getNumber(); + String getName(); + Point getLocation(); // This causes NoSuchElementException + } + + // Projection without Point for comparison + interface FlightProjectionWithoutPoint { + String getNumber(); + String getName(); + } + + // Find methods using projections + FlightProjection findByNumber(String number); + + FlightProjectionWithoutPoint findByName(String name); +} \ No newline at end of file