Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds documentation and polish #1557

Merged
merged 12 commits into from
Dec 5, 2024
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@
"projectName": "multidatasource-multitenancy",
"args": "",
"envFile": "${workspaceFolder}/.env"
},
{
"type": "java",
"name": "Spring Boot-TestMongoESApplication<boot-mongodb-elasticsearch>",
"request": "launch",
"cwd": "${workspaceFolder}",
"mainClass": "com.example.mongoes.TestMongoESApplication",
"projectName": "boot-mongodb-elasticsearch",
"args": "",
"envFile": "${workspaceFolder}/.env"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.mongoes.config;

import com.example.mongoes.response.GenericMessage;
import com.example.mongoes.web.exception.DuplicateRestaurantException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(DuplicateRestaurantException.class)
public ResponseEntity<GenericMessage> handleDuplicateRestaurantException(
DuplicateRestaurantException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(new GenericMessage(ex.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@
import java.util.Date;

public class DateUtility {

/**
* Converts a {@link Date} to {@link LocalDateTime} using the system default timezone.
*
* @param dateToConvert the date to convert, can be null
* @return the converted {@link LocalDateTime} or null if input is null
*/
public static LocalDateTime convertToLocalDateViaInstant(Date dateToConvert) {
if (dateToConvert == null) {
return null;
}
return dateToConvert.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down Expand Up @@ -77,18 +79,24 @@ public Mono<ResponseEntity<Restaurant>> addNotesToRestaurant(
}

@PostMapping
public ResponseEntity<GenericMessage> createRestaurant(
@RequestBody RestaurantRequest restaurantRequest) {
return ResponseEntity.created(
URI.create(
String.format(
"/restaurant/%s",
this.restaurantService
.createRestaurant(restaurantRequest)
.map(Restaurant::getName))))
.body(
new GenericMessage(
"restaurant with name %s created"
.formatted(restaurantRequest.name())));
public Mono<ResponseEntity<GenericMessage>> createRestaurant(
@RequestBody @Valid RestaurantRequest restaurantRequest) {
return this.restaurantService
.createRestaurant(restaurantRequest)
.map(
restaurant ->
ResponseEntity.created(
URI.create(
String.format(
"/api/restaurant/name/%s",
URLEncoder.encode(
restaurantRequest.name(),
StandardCharsets.UTF_8))))
.body(
new GenericMessage(
"restaurant with name %s created"
.formatted(
restaurantRequest
.name()))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
import com.example.mongoes.response.ResultData;
import com.example.mongoes.web.service.SearchService;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import java.util.List;
import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -145,12 +154,43 @@ public Mono<ResponseEntity<AggregationSearchResponse>> aggregateSearch(
.map(ResponseEntity::ok);
}

@Operation(
summary = "Search restaurants within range",
description = "Find restaurants within specified distance from given coordinates",
responses = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved restaurants",
content = @Content(schema = @Schema(implementation = ResultData.class))),
@ApiResponse(responseCode = "400", description = "Invalid parameters provided")
})
@GetMapping("/search/restaurant/withInRange")
public Flux<ResultData> searchRestaurantsWithInRange(
@RequestParam Double lat,
@RequestParam Double lon,
@RequestParam Double distance,
@RequestParam(defaultValue = "km", required = false) String unit) {
@Parameter(
description = "Latitude coordinate (between -90 and 90)",
example = "40.7128")
@RequestParam
@Min(-90)
@Max(90)
Double lat,
@Parameter(
description = "Longitude coordinate (between -180 and 180)",
example = "-74.0060")
@RequestParam
@Min(-180)
@Max(180)
Double lon,
@Parameter(description = "Distance from coordinates (must be positive)")
@RequestParam
@Positive
Double distance,
@Parameter(
description = "Unit of distance",
example = "km",
schema = @Schema(allowableValues = {"km", "mi"}))
@RequestParam(defaultValue = "km", required = false)
@Pattern(regexp = "^(km|mi)$", message = "Unit must be either 'km' or 'mi'")
String unit) {
return this.searchService.searchRestaurantsWithInRange(lat, lon, distance, unit);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.mongoes.web.exception;

public class DuplicateRestaurantException extends RuntimeException {

public DuplicateRestaurantException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.example.mongoes.web.service;

import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregation;
import org.springframework.stereotype.Service;

/**
* Processes Elasticsearch aggregations and transforms them into a structured map format. Supports
* 'terms' and 'dateRange' aggregation types.
*
* <p>Example output format: { "termAggregation": {"term1": 10, "term2": 20},
* "dateRangeAggregation": {"2023-01-01 - 2023-12-31": 100} }
*/
@Service
class AggregationProcessor {

private static final Logger log = LoggerFactory.getLogger(AggregationProcessor.class);

/**
* Processes Elasticsearch aggregations and returns a structured map of results.
*
* @param aggregationMap Map of aggregation key to ElasticsearchAggregation
* @return Map of aggregation key to counts, where counts is a map of bucket key to document
* count
* @throws IllegalArgumentException if aggregationMap is null
*/
public Map<String, Map<String, Long>> processAggregations(
Map<String, ElasticsearchAggregation> aggregationMap) {
if (aggregationMap == null) {
throw new IllegalArgumentException("aggregationMap must not be null");
}
Map<String, Map<String, Long>> resultMap = new HashMap<>();
aggregationMap.forEach(
(String aggregateKey, ElasticsearchAggregation aggregation) -> {
Map<String, Long> countMap = new HashMap<>();
Aggregate aggregate = aggregation.aggregation().getAggregate();
processAggregate(aggregate, countMap);
resultMap.put(aggregateKey, countMap);
});
return resultMap;
}

private void processAggregate(Aggregate aggregate, Map<String, Long> countMap) {
if (aggregate.isSterms()) {
processTermsAggregate(aggregate, countMap);
} else if (aggregate.isDateRange()) {
processDateRangeAggregate(aggregate, countMap);
} else {
log.debug(
"Unsupported aggregation type encountered: {}",
aggregate.getClass().getSimpleName());
}
}

private void processTermsAggregate(Aggregate aggregate, Map<String, Long> countMap) {
aggregate
.sterms()
.buckets()
.array()
.forEach(bucket -> countMap.put(bucket.key().stringValue(), bucket.docCount()));
}

private void processDateRangeAggregate(Aggregate aggregate, Map<String, Long> countMap) {
aggregate.dateRange().buckets().array().stream()
.filter(bucket -> bucket.docCount() != 0)
.forEach(
bucket ->
countMap.put(
bucket.fromAsString() + " - " + bucket.toAsString(),
bucket.docCount()));
}
}
Loading
Loading