From 48d120fbffadda06e1b7f551598f22eaee02738f Mon Sep 17 00:00:00 2001 From: kasun Date: Sat, 4 Jul 2026 01:08:26 +0200 Subject: [PATCH] extract HomeboxItemClient for reuse between search and sync HomeboxSearchProvider used to own the RestClient call and item mapping directly; pulling it into a shared HomeboxItemClient lets the upcoming sync pipeline page through the same Homebox items without duplicating the fetch/mapping logic. ConnectionIdentifiable lets both SearchRequest and SyncRequest be resolved to a connection by the same client. --- .../connection/ConnectionIdentifiable.java | 17 +++ .../vaessl/app/homebox/HomeboxItemClient.java | 105 ++++++++++++++++++ .../app/search/HomeboxSearchProvider.java | 66 +---------- .../com/vaessl/app/search/SearchRequest.java | 3 +- .../app/search/HomeboxSearchProviderTest.java | 12 +- .../app/search/SearchControllerTest.java | 2 +- 6 files changed, 136 insertions(+), 69 deletions(-) create mode 100644 backend/src/main/java/com/vaessl/app/connection/ConnectionIdentifiable.java create mode 100644 backend/src/main/java/com/vaessl/app/homebox/HomeboxItemClient.java diff --git a/backend/src/main/java/com/vaessl/app/connection/ConnectionIdentifiable.java b/backend/src/main/java/com/vaessl/app/connection/ConnectionIdentifiable.java new file mode 100644 index 0000000..f056553 --- /dev/null +++ b/backend/src/main/java/com/vaessl/app/connection/ConnectionIdentifiable.java @@ -0,0 +1,17 @@ +package com.vaessl.app.connection; + +import com.vaessl.app.shared.ServiceType; + +/** + * Identifies the connection a request targets. Implemented by request records + * (e.g. {@code SearchRequest}, {@code SyncRequest}) so a single {@code HomeboxItemClient} can + * resolve the underlying connection regardless of which feature is calling it. + */ +public interface ConnectionIdentifiable { + + String appUrl(); + + String username(); + + ServiceType serviceType(); +} diff --git a/backend/src/main/java/com/vaessl/app/homebox/HomeboxItemClient.java b/backend/src/main/java/com/vaessl/app/homebox/HomeboxItemClient.java new file mode 100644 index 0000000..10c29ec --- /dev/null +++ b/backend/src/main/java/com/vaessl/app/homebox/HomeboxItemClient.java @@ -0,0 +1,105 @@ +package com.vaessl.app.homebox; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import com.vaessl.app.connection.ConnectionEntity; +import com.vaessl.app.connection.ConnectionIdentifiable; +import com.vaessl.app.connection.ConnectionRepository; +import com.vaessl.app.connection.HomeboxConnectionEntity; +import com.vaessl.app.exception.ConnectionNotFoundException; +import com.vaessl.app.exception.RemoteApiException; +import com.vaessl.app.shared.ServiceItem; +import static com.vaessl.app.shared.Endpoint.HOMEBOX_QUERY_ALL_ITEMS; + +/** + * Fetches items from the Homebox entities API. Shared by {@code HomeboxSearchProvider} and + * {@code HomeboxSyncProvider} so both keyword search and vector-store sync page through the same + * remote call and item mapping. + */ +@Component +public class HomeboxItemClient { + + private final RestClient.Builder restClientBuilder; + + private final ConnectionRepository cRepository; + + + public HomeboxItemClient(RestClient.Builder restClienBuilder, + ConnectionRepository cRepository) { + this.restClientBuilder = restClienBuilder; + this.cRepository = cRepository; + } + + /** + * Fetches one page of items from Homebox for the given connection. + * + * @param connection identifies which stored connection (app URL, username) to query + * @param query optional keyword filter; {@code null} returns all items for the page + * @param pageable page number and size to request + * @return the mapped page of items along with the resolved connection ID + * @throws com.vaessl.app.exception.ConnectionNotFoundException if no matching connection is stored + * @throws com.vaessl.app.exception.RemoteApiException if Homebox returns an empty response body + */ + public HomeboxItemPage hbResponse(ConnectionIdentifiable connection, String query, + Pageable pageable) { + ConnectionEntity entity = + cRepository.findByAppUrlAndUsername(connection.appUrl(), connection.username()); + + if (!(entity instanceof HomeboxConnectionEntity hbEntity)) { + throw new ConnectionNotFoundException(); + } + + HomeboxItemsResponse response = restClientBuilder.baseUrl(connection.appUrl()).build().get() + .uri(u -> u.path(HOMEBOX_QUERY_ALL_ITEMS.getValue()).queryParam("q", query) + .queryParam("page", pageable.getPageNumber() + 1) + .queryParam("pageSize", pageable.getPageSize()).build()) + .headers(h -> h.setBearerAuth(hbEntity.getToken())).retrieve() + .body(HomeboxItemsResponse.class); + + if (response == null) { + throw new RemoteApiException(connection.appUrl(), HOMEBOX_QUERY_ALL_ITEMS.getValue()); + } + + List items = response.items().stream().map(i -> { + String id = i.id(); + String title = i.name(); + String description = i.description(); + + HomeboxParent parent = i.parent(); + Map extraData = new HashMap<>(); + if (parent.name() != null && !parent.name().isBlank()) { + extraData.put("locationName", parent.name()); + } + if (parent.description() != null && !parent.description().isBlank()) { + extraData.put("locationDescription", parent.description()); + } + + return new ServiceItem(id, title, description, extraData); + }).toList(); + + return new HomeboxItemPage(new PageImpl<>(items, pageable, response.total()), + hbEntity.getId()); + + } + + public record HomeboxItemPage(Page page, Long connectionId) { + } + + private record HomeboxItemsResponse(int page, int pageSize, int total, + List items) { + } + + private record HomeboxItem(String id, String name, String description, HomeboxParent parent) { + } + + private record HomeboxParent(String name, String description) { + } + + +} diff --git a/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java b/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java index 6c07ec9..216097d 100644 --- a/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java +++ b/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java @@ -1,36 +1,19 @@ package com.vaessl.app.search; -import java.util.List; -import java.util.Map; - import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClient; -import com.vaessl.app.connection.ConnectionEntity; -import com.vaessl.app.connection.ConnectionRepository; -import com.vaessl.app.connection.HomeboxConnectionEntity; -import com.vaessl.app.exception.ConnectionNotFoundException; -import com.vaessl.app.exception.RemoteApiException; +import com.vaessl.app.homebox.HomeboxItemClient; import com.vaessl.app.shared.ServiceItem; import com.vaessl.app.shared.ServiceType; - -import static com.vaessl.app.shared.Endpoint.*; +import lombok.RequiredArgsConstructor; @Component +@RequiredArgsConstructor public class HomeboxSearchProvider implements SearchProvider { - private final RestClient.Builder restClientBuilder; - - private final ConnectionRepository cRepository; - - public HomeboxSearchProvider(RestClient.Builder restClientBuilder, - ConnectionRepository cRepository) { - this.restClientBuilder = restClientBuilder; - this.cRepository = cRepository; - } + private final HomeboxItemClient client; @Override public ServiceType getServiceType() { @@ -39,45 +22,6 @@ public class HomeboxSearchProvider implements SearchProvider { @Override public Page getSearchResults(SearchRequest request, Pageable pageable) { - - ConnectionEntity entity = - cRepository.findByAppUrlAndUsername(request.appUrl(), request.username()); - - if (!(entity instanceof HomeboxConnectionEntity hbEntity)) { - throw new ConnectionNotFoundException(); - } - - HomeboxSearchResponse hbResponse = restClientBuilder.baseUrl(request.appUrl()).build().get() - .uri(u -> u.path(HOMEBOX_QUERY_ALL_ITEMS.getValue()) - .queryParam("q", request.query()) - .queryParam("page", pageable.getPageNumber() + 1) - .queryParam("pageSize", pageable.getPageSize()).build()) - .headers(h -> h.setBearerAuth(hbEntity.getToken())).retrieve() - .body(HomeboxSearchResponse.class); - - if (hbResponse == null) { - throw new RemoteApiException(request.appUrl(), HOMEBOX_QUERY_ALL_ITEMS.getValue()); - } - - List items = hbResponse.items().stream().map(i -> { - String id = i.id(); - String title = i.name(); - String description = i.description(); - Map extraSearchResponseData = Map.of("parent", i.parent()); - return new ServiceItem(id, title, description, extraSearchResponseData); - }).toList(); - - return new PageImpl<>(items, pageable, hbResponse.total()); - } - - private record HomeboxSearchResponse(int page, int pageSize, int total, - List items) { - } - - private record HomeboxItem(String id, String name, String description, - HomeboxExtraData parent) { - } - - private record HomeboxExtraData(String name, String description) { + return client.hbResponse(request, request.query(), pageable).page(); } } diff --git a/backend/src/main/java/com/vaessl/app/search/SearchRequest.java b/backend/src/main/java/com/vaessl/app/search/SearchRequest.java index abd0b5f..0487494 100644 --- a/backend/src/main/java/com/vaessl/app/search/SearchRequest.java +++ b/backend/src/main/java/com/vaessl/app/search/SearchRequest.java @@ -1,9 +1,10 @@ package com.vaessl.app.search; +import com.vaessl.app.connection.ConnectionIdentifiable; import com.vaessl.app.shared.ServiceType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record SearchRequest(@NotBlank String appUrl, @NotBlank String username, String query, - @NotNull ServiceType serviceType, boolean aiSearch) { + @NotNull ServiceType serviceType, boolean aiSearch) implements ConnectionIdentifiable { } diff --git a/backend/src/test/java/com/vaessl/app/search/HomeboxSearchProviderTest.java b/backend/src/test/java/com/vaessl/app/search/HomeboxSearchProviderTest.java index c81df9a..099faba 100644 --- a/backend/src/test/java/com/vaessl/app/search/HomeboxSearchProviderTest.java +++ b/backend/src/test/java/com/vaessl/app/search/HomeboxSearchProviderTest.java @@ -10,8 +10,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import com.vaessl.app.connection.ConnectionRepository; import com.vaessl.app.exception.ConnectionNotFoundException; +import com.vaessl.app.homebox.HomeboxItemClient; import static com.vaessl.app.shared.ServiceType.HOMEBOX; @@ -19,7 +19,7 @@ import static com.vaessl.app.shared.ServiceType.HOMEBOX; class HomeboxSearchProviderTest { @Mock - private ConnectionRepository mockRepo; + private HomeboxItemClient client; @InjectMocks private HomeboxSearchProvider provider; @@ -27,11 +27,11 @@ class HomeboxSearchProviderTest { @Test void shouldReturnConnectionNotFoundException() { - when(mockRepo.findByAppUrlAndUsername(MOCK_URL, MOCK_USER)).thenReturn(null); - - SearchRequest request = - new SearchRequest(MOCK_URL, MOCK_USER, "test query", HOMEBOX, false); Pageable pageable = PageRequest.of(0, 10); + SearchRequest request = new SearchRequest(MOCK_URL, MOCK_USER, "", HOMEBOX, false); + + when(client.hbResponse(request, "", pageable)).thenThrow(new ConnectionNotFoundException()); + assertThrows(ConnectionNotFoundException.class, () -> provider.getSearchResults(request, pageable)); } diff --git a/backend/src/test/java/com/vaessl/app/search/SearchControllerTest.java b/backend/src/test/java/com/vaessl/app/search/SearchControllerTest.java index 3628988..9e2a3e1 100644 --- a/backend/src/test/java/com/vaessl/app/search/SearchControllerTest.java +++ b/backend/src/test/java/com/vaessl/app/search/SearchControllerTest.java @@ -93,7 +93,7 @@ class SearchControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.content[0].title").value("MacBook Pro A1398")) .andExpect(jsonPath("$.totalElements").value(1)) - .andExpect(jsonPath("$.content[0].extraData.parent.name").value("Server Schrank Ikea weiß")); + .andExpect(jsonPath("$.content[0].extraData.locationName").value("Server Schrank Ikea weiß")); } @Test