implemented basic search function with Homebox provider
This commit is contained in:
@@ -2,12 +2,10 @@ package com.vaessl.app.connection;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public interface ConnectionProvider {
|
public interface ConnectionProvider extends ServiceProvider {
|
||||||
|
|
||||||
void checkCredentials(ConnectionRequest request);
|
void checkCredentials(ConnectionRequest request);
|
||||||
|
|
||||||
String getServiceType();
|
|
||||||
|
|
||||||
ConnectionResponse authenticate(ConnectionRequest request);
|
ConnectionResponse authenticate(ConnectionRequest request);
|
||||||
|
|
||||||
ConnectionEntity findUniqueConnectionEntry(ConnectionRequest request);
|
ConnectionEntity findUniqueConnectionEntry(ConnectionRequest request);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.vaessl.app.connection;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
public enum Endpoint {
|
public enum Endpoint {
|
||||||
HOMEBOX_LOGIN("/api/v1/users/login"), LOGIN("/login"), CONNECTION_STATUS("/connections/status");
|
HOMEBOX_LOGIN("/api/v1/users/login"), LOGIN("/login"), CONNECTION_STATUS(
|
||||||
|
"/connections/status"), HOMEBOX_QUERY_ALL_ITEMS("/api/v1/items");
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ public class HomeboxConnectionProvider implements ConnectionProvider {
|
|||||||
|
|
||||||
attachmentToken.put("attachmentToken", hbResponse.attachmentToken());
|
attachmentToken.put("attachmentToken", hbResponse.attachmentToken());
|
||||||
|
|
||||||
return new ConnectionResponse(hbResponse.token(), hbResponse.expiresAt(), attachmentToken);
|
String hbRawToken = hbResponse.token();
|
||||||
|
|
||||||
|
String token = hbRawToken.startsWith("Bearer ") ? hbRawToken.substring(7) : hbRawToken;
|
||||||
|
|
||||||
|
return new ConnectionResponse(token, hbResponse.expiresAt(), attachmentToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
|
public interface ServiceProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the service type key used to look up this provider in a registry, e.g.
|
||||||
|
* {@code "HOMEBOX"}.
|
||||||
|
*/
|
||||||
|
String getServiceType();
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.vaessl.app.exception;
|
||||||
|
|
||||||
|
public class ConnectionNotFoundException extends RuntimeException {
|
||||||
|
}
|
||||||
@@ -11,7 +11,12 @@ public enum ErrorMessage {
|
|||||||
HttpStatus.SERVICE_UNAVAILABLE,
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
"The target URL is unreachable."), SERVER_ERROR_GENERAL(
|
"The target URL is unreachable."), SERVER_ERROR_GENERAL(
|
||||||
"The external app returned a server error: "), WRONG_SERVICE_TYPE(
|
"The external app returned a server error: "), WRONG_SERVICE_TYPE(
|
||||||
HttpStatus.NOT_FOUND, "No such service type.");
|
HttpStatus.NOT_FOUND,
|
||||||
|
"No such service type."), CONNECTION_NOT_FOUND(
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
"No active connection found for this service."), REMOTE_API_EMPTY_RESPONSE(
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
"Remote API returned empty response for ");
|
||||||
|
|
||||||
private final HttpStatus status;
|
private final HttpStatus status;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ public class GlobalExceptionHandler {
|
|||||||
BAD_REQUEST_EMPTY_FIELDS.getMessage() + " " + e.getMissingFields());
|
BAD_REQUEST_EMPTY_FIELDS.getMessage() + " " + e.getMissingFields());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(ConnectionNotFoundException.class)
|
||||||
|
public ProblemDetail handleConnectionNotFound(ConnectionNotFoundException e) {
|
||||||
|
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) {
|
||||||
return ProblemDetail.forStatusAndDetail(REMOTE_API_EMPTY_RESPONSE.getStatus(),
|
return ProblemDetail.forStatusAndDetail(REMOTE_API_EMPTY_RESPONSE.getStatus(),
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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.HomeboxEntity;
|
||||||
|
import com.vaessl.app.exception.ConnectionNotFoundException;
|
||||||
|
import com.vaessl.app.exception.RemoteApiException;
|
||||||
|
|
||||||
|
import static com.vaessl.app.connection.Endpoint.*;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getServiceType() {
|
||||||
|
return "HOMEBOX";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<SearchResponse> getSearchResults(SearchRequest request, Pageable pageable) {
|
||||||
|
|
||||||
|
ConnectionEntity entity =
|
||||||
|
cRepository.findByAppUrlAndUsername(request.appUrl(), request.username());
|
||||||
|
|
||||||
|
if (!(entity instanceof HomeboxEntity 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SearchResponse> items = hbResponse.items().stream().map(i -> {
|
||||||
|
String title = i.name();
|
||||||
|
String description = i.description();
|
||||||
|
Map<String, Object> extraSearchResponseData = Map.of("location", i.location());
|
||||||
|
return new SearchResponse(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 name, String description, HomeboxLocation location) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HomeboxLocation(String name, String description) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
public record PagedSearchResponse<T>(List<T> content, int page, int pageSize, long totalElements,
|
||||||
|
boolean first, boolean last, String sort) {
|
||||||
|
|
||||||
|
public static <T> PagedSearchResponse<T> from(Page<T> pageResult) {
|
||||||
|
return new PagedSearchResponse<>(pageResult.getContent(), pageResult.getNumber(),
|
||||||
|
pageResult.getSize(), pageResult.getTotalElements(), pageResult.isFirst(),
|
||||||
|
pageResult.isLast(), pageResult.getSort().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SearchController {
|
||||||
|
|
||||||
|
private final SearchService searchService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a paged search against the requested service. Returns {@code 401 Unauthorized} if
|
||||||
|
* there is no active session.
|
||||||
|
*/
|
||||||
|
@PostMapping("/search")
|
||||||
|
public ResponseEntity<PagedSearchResponse<SearchResponse>> search(
|
||||||
|
@Valid @RequestBody SearchRequest request,
|
||||||
|
@PageableDefault(size = 20) Pageable pageable, HttpServletRequest httpReq) {
|
||||||
|
HttpSession session = httpReq.getSession(false);
|
||||||
|
if (session == null
|
||||||
|
|| session.getAttribute(request.serviceType() + "_CONNECTION_ID") == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Page<SearchResponse> result = searchService.search(request, pageable);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(PagedSearchResponse.from(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
import com.vaessl.app.connection.ServiceProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by any service that supports querying its remote API for items.
|
||||||
|
*/
|
||||||
|
public interface SearchProvider extends ServiceProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a search query against the remote service and returns matching results.
|
||||||
|
*
|
||||||
|
* @param request the search request containing the query string, app URL, and user credentials
|
||||||
|
* @return a list of {@link SearchResponse} items matching the query
|
||||||
|
*/
|
||||||
|
Page<SearchResponse> getSearchResults(SearchRequest request, Pageable pageable);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record SearchRequest(@NotBlank String appUrl, @NotBlank String username, String query,
|
||||||
|
@NotBlank String serviceType) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record SearchResponse(String title, String description, Map<String, Object> extraData) {
|
||||||
|
|
||||||
|
public String getExtra(String key) {
|
||||||
|
if (extraData == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
Object value = extraData.get(key);
|
||||||
|
|
||||||
|
return value != null ? String.valueOf(value) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.vaessl.app.exception.WrongServiceTypeException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SearchService {
|
||||||
|
|
||||||
|
private final Map<String, SearchProvider> providerRegistry;
|
||||||
|
|
||||||
|
public SearchService(List<SearchProvider> providers) {
|
||||||
|
this.providerRegistry = Map.copyOf(providers.stream()
|
||||||
|
.collect(Collectors.toMap(SearchProvider::getServiceType, p -> p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches the paged search request to the provider registered for
|
||||||
|
* {@link SearchRequest#serviceType()}.
|
||||||
|
*
|
||||||
|
* @param request the search request
|
||||||
|
* @param pageable the Pageable interface
|
||||||
|
* @return results returned by the matching provider
|
||||||
|
* @throws WrongServiceTypeException if no provider is registered for the given service type
|
||||||
|
*/
|
||||||
|
public Page<SearchResponse> search(SearchRequest request, Pageable pageable) {
|
||||||
|
|
||||||
|
SearchProvider provider = providerRegistry.get(request.serviceType());
|
||||||
|
|
||||||
|
if (provider == null) {
|
||||||
|
throw new WrongServiceTypeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.getSearchResults(request, pageable);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user