-
Notifications
You must be signed in to change notification settings - Fork 82
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
Feature #8103 - Implemented functionality for retrieving database metadata and data with export options. #8174
base: dev
Are you sure you want to change the base?
Changes from all commits
589c7e8
91db373
3cdb143
556d87f
90e0bc6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package greencity.controller; | ||
|
||
import greencity.constant.HttpStatuses; | ||
import greencity.dto.exportsettings.TableRowsDto; | ||
import greencity.dto.exportsettings.TablesMetadataDto; | ||
import greencity.service.ExportSettingsService; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.ExampleObject; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
import jakarta.validation.constraints.Pattern; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.core.io.InputStreamResource; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import org.springframework.web.bind.annotation.RequestHeader; | ||
|
||
@RequiredArgsConstructor | ||
@RestController | ||
@RequestMapping("/export/settings") | ||
public class ExportSettingsController { | ||
private final ExportSettingsService exportSettingsService; | ||
|
||
/** | ||
* Method for receiving all DB tables names. | ||
* | ||
* @return dto {@link TablesMetadataDto} | ||
*/ | ||
@Operation(summary = "Get all tables names and columns.") | ||
@ApiResponses(value = { | ||
@ApiResponse(responseCode = "200", description = HttpStatuses.OK, | ||
content = @Content(schema = @Schema(implementation = TablesMetadataDto.class))), | ||
@ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN, | ||
content = @Content(examples = @ExampleObject(HttpStatuses.FORBIDDEN))), | ||
}) | ||
@GetMapping(value = "/tables", produces = MediaType.APPLICATION_JSON_VALUE) | ||
public ResponseEntity<TablesMetadataDto> getTablesInfo(@RequestHeader String secretKey) { | ||
return ResponseEntity.ok(exportSettingsService.getTablesMetadata(secretKey)); | ||
} | ||
|
||
/** | ||
* Method for receiving rows from DB by table name, limit and offset. | ||
* | ||
* @return dto {@link TableRowsDto} | ||
*/ | ||
@Operation(summary = "Get table rows by params.") | ||
@ApiResponses(value = { | ||
@ApiResponse(responseCode = "200", description = HttpStatuses.OK, | ||
content = @Content(schema = @Schema(implementation = TableRowsDto.class))), | ||
@ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN, | ||
content = @Content(examples = @ExampleObject(HttpStatuses.FORBIDDEN))), | ||
@ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST, | ||
content = @Content(examples = @ExampleObject(HttpStatuses.BAD_REQUEST))) | ||
}) | ||
@GetMapping("/select") | ||
public ResponseEntity<TableRowsDto> getSelected(@RequestParam @Pattern(regexp = "^[A-Za-z_]+$") String tableName, | ||
@RequestParam int limit, | ||
@RequestParam int offset, | ||
@RequestHeader String secretKey) { | ||
return ResponseEntity.ok(exportSettingsService.selectFromTable(tableName, limit, offset, secretKey)); | ||
} | ||
|
||
/** | ||
* Method for receiving an .xlsx file with rows from DB by table name, limit and | ||
* offset. | ||
* | ||
* @return dto {@link TableRowsDto} | ||
*/ | ||
@Operation(summary = "Get excel file with table rows by params.") | ||
@ApiResponses(value = { | ||
@ApiResponse(responseCode = "200", description = HttpStatuses.OK, | ||
content = @Content(schema = @Schema(implementation = TableRowsDto.class))), | ||
@ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN, | ||
content = @Content(examples = @ExampleObject(HttpStatuses.FORBIDDEN))), | ||
}) | ||
@GetMapping("/download-table-data") | ||
public ResponseEntity<InputStreamResource> downloadExcel( | ||
@RequestParam @Pattern(regexp = "^[A-Za-z_]+$") String tableName, | ||
@RequestParam int limit, | ||
@RequestParam int offset, | ||
@RequestHeader String secretKey) { | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.add(HttpHeaders.CONTENT_DISPOSITION, | ||
String.format("attachment; filename= %s(%d - %d).xlsx", tableName, offset, limit)); | ||
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); | ||
|
||
return ResponseEntity.ok() | ||
.headers(headers) | ||
.body(new InputStreamResource( | ||
exportSettingsService.getExcelFileAsResource(tableName, limit, offset, secretKey))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,9 +9,12 @@ | |||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.BadRequestException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.BadSecretKeyException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.BadSocialNetworkLinksException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.DatabaseMetadataException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.EventDtoValidationException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.FileGenerationException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.FileReadException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.FunctionalityNotAvailableException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.InvalidLimitException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.InvalidStatusException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.InvalidURLException; | ||||||||||||||||||||||||||||||||||
import greencity.exception.exceptions.LowRoleLevelException; | ||||||||||||||||||||||||||||||||||
|
@@ -623,4 +626,56 @@ public final ResponseEntity<Object> handleFunctionalityNotAvailableException(Fun | |||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(exceptionResponse); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* Method intercepts exception {@link DatabaseMetadataException}. | ||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||
* @param ex Exception that should be intercepted. | ||||||||||||||||||||||||||||||||||
* @param request Contains details about the occurred exception. | ||||||||||||||||||||||||||||||||||
* @return {@code ResponseEntity} which contains the HTTP status and body with | ||||||||||||||||||||||||||||||||||
* the exception message. | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
@ExceptionHandler(DatabaseMetadataException.class) | ||||||||||||||||||||||||||||||||||
public final ResponseEntity<Object> handleDatabaseMetadataException(DatabaseMetadataException ex, | ||||||||||||||||||||||||||||||||||
WebRequest request) { | ||||||||||||||||||||||||||||||||||
log.error(ex.getMessage(), ex); | ||||||||||||||||||||||||||||||||||
ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request)); | ||||||||||||||||||||||||||||||||||
exceptionResponse.setMessage(ex.getMessage()); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(exceptionResponse); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* Method intercepts exception {@link InvalidLimitException}. | ||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||
* @param ex Exception that should be intercepted. | ||||||||||||||||||||||||||||||||||
* @param request Contains details about the occurred exception. | ||||||||||||||||||||||||||||||||||
* @return {@code ResponseEntity} which contains the HTTP status and body with | ||||||||||||||||||||||||||||||||||
* the exception message. | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
@ExceptionHandler(InvalidLimitException.class) | ||||||||||||||||||||||||||||||||||
public final ResponseEntity<Object> handleInvalidOffsetException(InvalidLimitException ex, WebRequest request) { | ||||||||||||||||||||||||||||||||||
log.error(ex.getMessage(), ex); | ||||||||||||||||||||||||||||||||||
ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request)); | ||||||||||||||||||||||||||||||||||
exceptionResponse.setMessage(ex.getMessage()); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionResponse); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+656
to
+663
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Method name doesn't match the exception being handled. While the implementation is correct, the method name Apply this change: -public final ResponseEntity<Object> handleInvalidOffsetException(InvalidLimitException ex, WebRequest request) {
+public final ResponseEntity<Object> handleInvalidLimitException(InvalidLimitException ex, WebRequest request) { 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* Method intercepts exception {@link FileGenerationException}. | ||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||
* @param ex Exception that should be intercepted. | ||||||||||||||||||||||||||||||||||
* @param request Contains details about the occurred exception. | ||||||||||||||||||||||||||||||||||
* @return {@code ResponseEntity} which contains the HTTP status and body with | ||||||||||||||||||||||||||||||||||
* the exception message. | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
@ExceptionHandler(FileGenerationException.class) | ||||||||||||||||||||||||||||||||||
public final ResponseEntity<Object> handleFileGenerationException(FileGenerationException ex, WebRequest request) { | ||||||||||||||||||||||||||||||||||
log.error(ex.getMessage(), ex); | ||||||||||||||||||||||||||||||||||
ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request)); | ||||||||||||||||||||||||||||||||||
exceptionResponse.setMessage(ex.getMessage()); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(exceptionResponse); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,6 +43,8 @@ | |
import greencity.dto.location.MapBoundsDto; | ||
import greencity.dto.logs.filter.LogFileFilterDto; | ||
import greencity.dto.place.PlaceByBoundsDto; | ||
import greencity.dto.exportsettings.TableRowsDto; | ||
import greencity.dto.exportsettings.TablesMetadataDto; | ||
import greencity.dto.todolistitem.CustomToDoListItemResponseDto; | ||
import greencity.dto.todolistitem.ToDoListItemPostDto; | ||
import greencity.dto.todolistitem.ToDoListItemRequestDto; | ||
|
@@ -81,6 +83,8 @@ | |
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.LinkedList; | ||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.stream.Stream; | ||
|
@@ -660,4 +664,29 @@ public static AddressDto getAddressDtoCorrect() { | |
.countryEn("Country") | ||
.build(); | ||
} | ||
|
||
public static TablesMetadataDto getTablesMetadataDto() { | ||
Map<String, List<String>> tables = new HashMap<>(); | ||
List<String> columns = List.of("id", "name", "email"); | ||
tables.put("users", columns); | ||
|
||
return TablesMetadataDto.builder() | ||
.tables(tables) | ||
.build(); | ||
} | ||
|
||
public static TableRowsDto getTableRowsDto() { | ||
List<Map<String, String>> tableData = new LinkedList<>(); | ||
Map<String, String> row = new LinkedHashMap<>(); | ||
row.put("id", "1"); | ||
row.put("date_of_registration", "1970-01-01 00:00:00"); | ||
row.put("email", "[email protected]"); | ||
row.put("name", "Name"); | ||
row.put("role", "ROLE_ADMIN"); | ||
tableData.add(row); | ||
|
||
return TableRowsDto.builder() | ||
.tableData(tableData) | ||
.build(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Input validation for table selection needs enhancement.
While the regex validation on tableName is a good security practice, the
limit
andoffset
parameters should also be validated to prevent potential issues:Consider adding validation annotations such as:
📝 Committable suggestion