Feature/ implement basic search function #40
@@ -2,7 +2,7 @@ package com.vaessl.app.connection;
|
|||||||
|
|
||||||
public enum Endpoint {
|
public enum Endpoint {
|
||||||
HOMEBOX_LOGIN("/api/v1/users/login"), LOGIN("/login"), CONNECTION_STATUS(
|
HOMEBOX_LOGIN("/api/v1/users/login"), LOGIN("/login"), CONNECTION_STATUS(
|
||||||
"/connections/status"), HOMEBOX_QUERY_ALL_ITEMS("/api/v1/items"), SEARCH("/search");
|
"/connections/status"), HOMEBOX_QUERY_ALL_ITEMS("/api/v1/entities"), SEARCH("/search");
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class HomeboxConnectionProvider implements ConnectionProvider {
|
|||||||
.body(HomeboxLoginResponse.class);
|
.body(HomeboxLoginResponse.class);
|
||||||
|
|
||||||
if (hbResponse == null) {
|
if (hbResponse == null) {
|
||||||
throw new RemoteApiException(request.appUrl());
|
throw new RemoteApiException(request.appUrl(), HOMEBOX_LOGIN.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> attachmentToken = new HashMap<>();
|
Map<String, Object> attachmentToken = new HashMap<>();
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ public enum ErrorMessage {
|
|||||||
HttpStatus.NOT_FOUND,
|
HttpStatus.NOT_FOUND,
|
||||||
"No active connection found for this service."), REMOTE_API_EMPTY_RESPONSE(
|
"No active connection found for this service."), REMOTE_API_EMPTY_RESPONSE(
|
||||||
HttpStatus.BAD_GATEWAY,
|
HttpStatus.BAD_GATEWAY,
|
||||||
"Remote API returned empty response for ");
|
"Remote API returned empty response for "), REMOTE_API_CLIENT_ERROR(
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
"Remote API returned an unexpected error: ");
|
||||||
|
|
||||||
private final HttpStatus status;
|
private final HttpStatus status;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.vaessl.app.exception;
|
package com.vaessl.app.exception;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ProblemDetail;
|
import org.springframework.http.ProblemDetail;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
@@ -16,6 +18,8 @@ import java.util.stream.Collectors;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ProblemDetail handleEmptyCredentialInput(MethodArgumentNotValidException e) {
|
public ProblemDetail handleEmptyCredentialInput(MethodArgumentNotValidException e) {
|
||||||
|
|
||||||
@@ -33,6 +37,15 @@ public class GlobalExceptionHandler {
|
|||||||
UNAUTHORIZED_WRONG_LOGIN.getMessage());
|
UNAUTHORIZED_WRONG_LOGIN.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catches any other 4xx from a remote API (e.g. 404 caused by a changed endpoint).
|
||||||
|
// Must come after the Unauthorized handler so Spring matches 401 there first.
|
||||||
|
@ExceptionHandler(HttpClientErrorException.class)
|
||||||
|
public ProblemDetail handleRemoteClientError(HttpClientErrorException e) {
|
||||||
|
log.error("Remote API returned {}: {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||||
|
return ProblemDetail.forStatusAndDetail(REMOTE_API_CLIENT_ERROR.getStatus(),
|
||||||
|
REMOTE_API_CLIENT_ERROR.getMessage() + e.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(ResourceAccessException.class)
|
@ExceptionHandler(ResourceAccessException.class)
|
||||||
public ProblemDetail handleNoConnection(ResourceAccessException e) {
|
public ProblemDetail handleNoConnection(ResourceAccessException e) {
|
||||||
|
|
||||||
@@ -42,7 +55,7 @@ public class GlobalExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(HttpServerErrorException.class)
|
@ExceptionHandler(HttpServerErrorException.class)
|
||||||
public ProblemDetail handleTimeoutOrNotFound(HttpServerErrorException e) {
|
public ProblemDetail handleTimeoutOrNotFound(HttpServerErrorException e) {
|
||||||
|
log.error("Remote API server error {}: {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||||
return ProblemDetail.forStatusAndDetail(e.getStatusCode(),
|
return ProblemDetail.forStatusAndDetail(e.getStatusCode(),
|
||||||
SERVER_ERROR_GENERAL.getMessage() + e.getStatusText());
|
SERVER_ERROR_GENERAL.getMessage() + e.getStatusText());
|
||||||
}
|
}
|
||||||
@@ -61,12 +74,14 @@ public class GlobalExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(ConnectionNotFoundException.class)
|
@ExceptionHandler(ConnectionNotFoundException.class)
|
||||||
public ProblemDetail handleConnectionNotFound(ConnectionNotFoundException e) {
|
public ProblemDetail handleConnectionNotFound(ConnectionNotFoundException e) {
|
||||||
return ProblemDetail.forStatusAndDetail(CONNECTION_NOT_FOUND.getStatus(), CONNECTION_NOT_FOUND.getMessage());
|
return ProblemDetail.forStatusAndDetail(CONNECTION_NOT_FOUND.getStatus(),
|
||||||
|
CONNECTION_NOT_FOUND.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(RemoteApiException.class)
|
@ExceptionHandler(RemoteApiException.class)
|
||||||
public ProblemDetail handleRemoteApiException(RemoteApiException e) {
|
public ProblemDetail handleRemoteApiException(RemoteApiException e) {
|
||||||
|
log.error("Remote API empty body: {}{}", e.getAppUrl(), e.getPath());
|
||||||
return ProblemDetail.forStatusAndDetail(REMOTE_API_EMPTY_RESPONSE.getStatus(),
|
return ProblemDetail.forStatusAndDetail(REMOTE_API_EMPTY_RESPONSE.getStatus(),
|
||||||
REMOTE_API_EMPTY_RESPONSE.getMessage() + e.getAppUrl());
|
REMOTE_API_EMPTY_RESPONSE.getMessage() + e.getAppUrl() + e.getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package com.vaessl.app.exception;
|
package com.vaessl.app.exception;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class RemoteApiException extends RuntimeException {
|
public class RemoteApiException extends RuntimeException {
|
||||||
|
|
||||||
private final String appUrl;
|
private final String appUrl;
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
public RemoteApiException(String appUrl, String path) {
|
||||||
|
super("Remote API returned empty body: " + appUrl + path);
|
||||||
|
this.appUrl = appUrl;
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,17 +50,17 @@ public class HomeboxSearchProvider implements SearchProvider {
|
|||||||
.queryParam("q", request.query())
|
.queryParam("q", request.query())
|
||||||
.queryParam("page", pageable.getPageNumber() + 1)
|
.queryParam("page", pageable.getPageNumber() + 1)
|
||||||
.queryParam("pageSize", pageable.getPageSize()).build())
|
.queryParam("pageSize", pageable.getPageSize()).build())
|
||||||
.headers(h -> h.setBearerAuth(
|
.headers(h -> h.setBearerAuth(hbEntity.getToken())).retrieve()
|
||||||
hbEntity.getToken())).retrieve().body(HomeboxSearchResponse.class);
|
.body(HomeboxSearchResponse.class);
|
||||||
|
|
||||||
if (hbResponse == null) {
|
if (hbResponse == null) {
|
||||||
throw new RemoteApiException(request.appUrl());
|
throw new RemoteApiException(request.appUrl(), HOMEBOX_QUERY_ALL_ITEMS.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SearchResponse> items = hbResponse.items().stream().map(i -> {
|
List<SearchResponse> items = hbResponse.items().stream().map(i -> {
|
||||||
String title = i.name();
|
String title = i.name();
|
||||||
String description = i.description();
|
String description = i.description();
|
||||||
Map<String, Object> extraSearchResponseData = Map.of("location", i.location());
|
Map<String, Object> extraSearchResponseData = Map.of("location", i.parent());
|
||||||
return new SearchResponse(title, description, extraSearchResponseData);
|
return new SearchResponse(title, description, extraSearchResponseData);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ public class HomeboxSearchProvider implements SearchProvider {
|
|||||||
List<HomeboxItem> items) {
|
List<HomeboxItem> items) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record HomeboxItem(String name, String description, HomeboxLocation location) {
|
private record HomeboxItem(String name, String description, HomeboxLocation parent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record HomeboxLocation(String name, String description) {
|
private record HomeboxLocation(String name, String description) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class SearchControllerTest {
|
|||||||
"createdAt": "2026-05-13T19:52:20.016176Z",
|
"createdAt": "2026-05-13T19:52:20.016176Z",
|
||||||
"updatedAt": "2026-05-14T12:39:11.836403Z",
|
"updatedAt": "2026-05-14T12:39:11.836403Z",
|
||||||
"purchasePrice": 0,
|
"purchasePrice": 0,
|
||||||
"location": {
|
"parent": {
|
||||||
"id": "b6f60ab8-3a2a-4a8d-a4bf-897d0555f636",
|
"id": "b6f60ab8-3a2a-4a8d-a4bf-897d0555f636",
|
||||||
"name": "Server Schrank Ikea weiß",
|
"name": "Server Schrank Ikea weiß",
|
||||||
"description": "Weißer Ikea Schrank, wo sich der Server befindet.",
|
"description": "Weißer Ikea Schrank, wo sich der Server befindet.",
|
||||||
|
|||||||
Reference in New Issue
Block a user