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.
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
@@ -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<ServiceItem> items = response.items().stream().map(i -> {
|
||||||
|
String id = i.id();
|
||||||
|
String title = i.name();
|
||||||
|
String description = i.description();
|
||||||
|
|
||||||
|
HomeboxParent parent = i.parent();
|
||||||
|
Map<String, Object> 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<ServiceItem> page, Long connectionId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HomeboxItemsResponse(int page, int pageSize, int total,
|
||||||
|
List<HomeboxItem> items) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HomeboxItem(String id, String name, String description, HomeboxParent parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HomeboxParent(String name, String description) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,36 +1,19 @@
|
|||||||
package com.vaessl.app.search;
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageImpl;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestClient;
|
|
||||||
|
|
||||||
import com.vaessl.app.connection.ConnectionEntity;
|
import com.vaessl.app.homebox.HomeboxItemClient;
|
||||||
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 com.vaessl.app.shared.ServiceItem;
|
||||||
import com.vaessl.app.shared.ServiceType;
|
import com.vaessl.app.shared.ServiceType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import static com.vaessl.app.shared.Endpoint.*;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class HomeboxSearchProvider implements SearchProvider {
|
public class HomeboxSearchProvider implements SearchProvider {
|
||||||
|
|
||||||
private final RestClient.Builder restClientBuilder;
|
private final HomeboxItemClient client;
|
||||||
|
|
||||||
private final ConnectionRepository cRepository;
|
|
||||||
|
|
||||||
public HomeboxSearchProvider(RestClient.Builder restClientBuilder,
|
|
||||||
ConnectionRepository cRepository) {
|
|
||||||
this.restClientBuilder = restClientBuilder;
|
|
||||||
this.cRepository = cRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServiceType getServiceType() {
|
public ServiceType getServiceType() {
|
||||||
@@ -39,45 +22,6 @@ public class HomeboxSearchProvider implements SearchProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<ServiceItem> getSearchResults(SearchRequest request, Pageable pageable) {
|
public Page<ServiceItem> getSearchResults(SearchRequest request, Pageable pageable) {
|
||||||
|
return client.hbResponse(request, request.query(), pageable).page();
|
||||||
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<ServiceItem> items = hbResponse.items().stream().map(i -> {
|
|
||||||
String id = i.id();
|
|
||||||
String title = i.name();
|
|
||||||
String description = i.description();
|
|
||||||
Map<String, Object> 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<HomeboxItem> items) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private record HomeboxItem(String id, String name, String description,
|
|
||||||
HomeboxExtraData parent) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private record HomeboxExtraData(String name, String description) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.vaessl.app.search;
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import com.vaessl.app.connection.ConnectionIdentifiable;
|
||||||
import com.vaessl.app.shared.ServiceType;
|
import com.vaessl.app.shared.ServiceType;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
public record SearchRequest(@NotBlank String appUrl, @NotBlank String username, String query,
|
public record SearchRequest(@NotBlank String appUrl, @NotBlank String username, String query,
|
||||||
@NotNull ServiceType serviceType, boolean aiSearch) {
|
@NotNull ServiceType serviceType, boolean aiSearch) implements ConnectionIdentifiable {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import com.vaessl.app.connection.ConnectionRepository;
|
|
||||||
import com.vaessl.app.exception.ConnectionNotFoundException;
|
import com.vaessl.app.exception.ConnectionNotFoundException;
|
||||||
|
import com.vaessl.app.homebox.HomeboxItemClient;
|
||||||
import static com.vaessl.app.shared.ServiceType.HOMEBOX;
|
import static com.vaessl.app.shared.ServiceType.HOMEBOX;
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ import static com.vaessl.app.shared.ServiceType.HOMEBOX;
|
|||||||
class HomeboxSearchProviderTest {
|
class HomeboxSearchProviderTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ConnectionRepository mockRepo;
|
private HomeboxItemClient client;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private HomeboxSearchProvider provider;
|
private HomeboxSearchProvider provider;
|
||||||
@@ -27,11 +27,11 @@ class HomeboxSearchProviderTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldReturnConnectionNotFoundException() {
|
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);
|
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,
|
assertThrows(ConnectionNotFoundException.class,
|
||||||
() -> provider.getSearchResults(request, pageable));
|
() -> provider.getSearchResults(request, pageable));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class SearchControllerTest {
|
|||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.content[0].title").value("MacBook Pro A1398"))
|
.andExpect(jsonPath("$.content[0].title").value("MacBook Pro A1398"))
|
||||||
.andExpect(jsonPath("$.totalElements").value(1))
|
.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
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user