Skip to content

Commit eaadba9

Browse files
Merge branch 'dev' into feature/add-version-optimistic-locking
2 parents a73be0a + 3534bab commit eaadba9

47 files changed

Lines changed: 1705 additions & 143 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/com/itasocialacademy/oitassist/competition/service/CompetitionServiceImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ public CompetitionResponse changeStatus(Long competitionId, ChangeCompetitionSta
107107
if (request.status() == CompetitionStatus.ENROLLMENT
108108
|| request.status() == CompetitionStatus.PUBLISHED) {
109109
validatePublishingRequirements(competitionId);
110+
} else if (status == CompetitionStatus.FINISHED) {
111+
validator.validateAllStagesCompletedForCompetition(competitionId);
110112
}
111113

112114
competition.setCompetitionStatus(request.status());

src/main/java/com/itasocialacademy/oitassist/competition/validation/HierarchyValidator.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,27 @@ public void validateStageEligibilityToStart(Stage currentStage) {
299299
}
300300
}
301301

302+
@Transactional(readOnly = true)
303+
public void validateAllStagesCompletedForCompetition(Long competitionId) {
304+
List<Stage> stages = stageRepository.findAllByCompetitionIdOrderBySortPositionAsc(competitionId);
305+
306+
if (stages.isEmpty()) {
307+
throw new CompetitionHierarchyValidationException(
308+
"Cannot finish competition: Competition must have at least one stage.");
309+
}
310+
List<String> incompleteStages = stages.stream()
311+
.filter(stage -> stage.getStatus() != StageStatus.FINISHED
312+
&& stage.getStatus() != StageStatus.CANCELLED)
313+
.map(stage -> "'%s' (Status: %s)".formatted(stage.getTitle(), stage.getStatus()))
314+
.toList();
315+
316+
if (!incompleteStages.isEmpty()) {
317+
throw new CompetitionHierarchyValidationException(
318+
"Cannot finish competition: Not all stages are completed. Incomplete stages: "
319+
+ String.join(", ", incompleteStages));
320+
}
321+
}
322+
302323
@Transactional(readOnly = true)
303324
public void validateAllToursCompletedForStage(Long stageId) {
304325
List<Tour> tours = tourRepository.findAllByStageIdOrderBySortPositionAsc(stageId);

src/main/java/com/itasocialacademy/oitassist/core/controller/SpaController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
@Controller
77
public class SpaController {
8-
@GetMapping({"/"})
8+
@GetMapping({"/ui", "/ui/**"})
99
public String forward() {
1010
return "forward:/index.html";
1111
}

src/main/java/com/itasocialacademy/oitassist/core/web/GlobalExceptionHandler.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.springframework.security.authorization.AuthorizationDeniedException;
1818
import org.springframework.validation.FieldError;
1919
import org.springframework.web.bind.MethodArgumentNotValidException;
20+
import org.springframework.web.bind.MissingServletRequestParameterException;
2021
import org.springframework.web.bind.annotation.ControllerAdvice;
2122
import org.springframework.web.bind.annotation.ExceptionHandler;
2223
import java.time.Instant;
@@ -259,4 +260,35 @@ public ResponseEntity<ErrorResponse> handleMissingServletRequestPart(
259260
"Required request part '" + ex.getRequestPartName() + "' is not present",
260261
HttpStatus.BAD_REQUEST.value(), null));
261262
}
263+
264+
/**
265+
* Handles {@link MissingServletRequestParameterException} by generating an
266+
* appropriate error response. This exception is thrown when a required request
267+
* parameter is missing from the HTTP request.
268+
*
269+
* @param ex the exception object containing details of the missing request
270+
* parameter
271+
* @param request the HTTP request that triggered the exception
272+
* @return a {@link ResponseEntity} containing an {@link ErrorResponse} with
273+
* error details, including the name of the missing request parameter
274+
*/
275+
@ExceptionHandler(MissingServletRequestParameterException.class)
276+
public ResponseEntity<ErrorResponse> handleMissingRequestParameter(
277+
MissingServletRequestParameterException ex,
278+
HttpServletRequest request) {
279+
log.warn(
280+
"Missing request parameter: traceId={}, parameter={}",
281+
MDC.get(TRACE_ID_MDC),
282+
ex.getParameterName());
283+
284+
HttpStatus status = HttpStatus.BAD_REQUEST;
285+
286+
return ResponseEntity.status(status)
287+
.body(buildResponse(
288+
request,
289+
ErrorCode.COMMON_VALIDATION_FAILED,
290+
"Required request parameter '" + ex.getParameterName() + "' is not present",
291+
status.value(),
292+
Map.of("parameter", ex.getParameterName())));
293+
}
262294
}

src/main/java/com/itasocialacademy/oitassist/logfile/controller/LogFileController.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.itasocialacademy.oitassist.logfile.api.PageResponse;
55
import com.itasocialacademy.oitassist.logfile.service.LogFileService;
66
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
78
import io.swagger.v3.oas.annotations.responses.ApiResponse;
89
import io.swagger.v3.oas.annotations.responses.ApiResponses;
910
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -14,6 +15,7 @@
1415
import org.springframework.security.access.prepost.PreAuthorize;
1516
import org.springframework.web.bind.annotation.GetMapping;
1617
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RequestParam;
1719
import org.springframework.web.bind.annotation.RestController;
1820

1921
@RestController
@@ -47,4 +49,26 @@ public PageResponse<LogFileResponse> getAll(
4749
direction = Sort.Direction.DESC) Pageable pageable) {
4850
return logFileService.getAll(pageable);
4951
}
52+
53+
@GetMapping("/search")
54+
@Operation(
55+
summary = "Search log files by name",
56+
description = """
57+
Searches application log files by a partial file name match.
58+
The search is case-insensitive and supports pagination and sorting.
59+
Access is restricted to administrators.
60+
""")
61+
@ApiResponses({
62+
@ApiResponse(responseCode = "200",
63+
description = "Log files matching the specified name were successfully retrieved"),
64+
@ApiResponse(responseCode = "400", description = "Invalid search or sorting parameters"),
65+
@ApiResponse(responseCode = "401", description = "Unauthorized - token is missing or invalid"),
66+
@ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions")})
67+
public PageResponse<LogFileResponse> searchByName(
68+
@Parameter(
69+
description = "Full or partial log file name to search for",
70+
example = "app") @RequestParam String name,
71+
@ParameterObject @PageableDefault(size = 10) Pageable pageable) {
72+
return logFileService.searchByName(name, pageable);
73+
}
5074
}

src/main/java/com/itasocialacademy/oitassist/logfile/dao/FileSystemLogFileDao.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
import com.itasocialacademy.oitassist.logfile.exceptions.LogFileListingException;
44
import java.io.IOException;
55
import java.io.UncheckedIOException;
6-
import java.nio.file.*;
6+
import java.nio.file.Files;
7+
import java.nio.file.InvalidPathException;
8+
import java.nio.file.LinkOption;
9+
import java.nio.file.NoSuchFileException;
10+
import java.nio.file.Path;
711
import java.nio.file.attribute.BasicFileAttributes;
812
import java.util.List;
13+
import java.util.Locale;
914
import java.util.Optional;
15+
import java.util.function.Predicate;
1016
import java.util.stream.Stream;
1117
import lombok.extern.slf4j.Slf4j;
1218
import org.springframework.beans.factory.annotation.Value;
@@ -25,10 +31,24 @@ public FileSystemLogFileDao(
2531

2632
@Override
2733
public List<LogFileMetadata> findAll() {
34+
return findFiles(path -> true);
35+
}
36+
37+
@Override
38+
public List<LogFileMetadata> findByNameContainingIgnoreCase(String name) {
39+
String normalizedName = name.toLowerCase(Locale.ROOT);
40+
return findFiles(path -> path.getFileName()
41+
.toString()
42+
.toLowerCase(Locale.ROOT)
43+
.contains(normalizedName));
44+
}
45+
46+
private List<LogFileMetadata> findFiles(Predicate<Path> filter) {
2847
validateLogDirectory();
2948

3049
try (Stream<Path> paths = Files.list(logDirectory)) {
3150
return paths
51+
.filter(filter)
3252
.map(this::readMetadata)
3353
.flatMap(Optional::stream)
3454
.toList();
@@ -37,6 +57,7 @@ public List<LogFileMetadata> findAll() {
3757
"Failed to read metadata from log directory: {}",
3858
logDirectory,
3959
exception.getCause());
60+
4061
throw new LogFileListingException();
4162
} catch (IOException | SecurityException exception) {
4263
log.error("Failed to access log directory: {}", logDirectory, exception);

src/main/java/com/itasocialacademy/oitassist/logfile/dao/LogFileDao.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44

55
public interface LogFileDao {
66
List<LogFileMetadata> findAll();
7+
8+
List<LogFileMetadata> findByNameContainingIgnoreCase(String name);
79
}

src/main/java/com/itasocialacademy/oitassist/logfile/service/LogFileService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66

77
public interface LogFileService {
88
PageResponse<LogFileResponse> getAll(Pageable pageable);
9+
10+
PageResponse<LogFileResponse> searchByName(String name, Pageable pageable);
911
}

src/main/java/com/itasocialacademy/oitassist/logfile/service/LogFileServiceImpl.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,33 @@ public PageResponse<LogFileResponse> getAll(Pageable pageable) {
4646
Comparator<LogFileMetadata> sortOrder =
4747
resolveSortOrder(pageable.getSort());
4848

49-
List<LogFileMetadata> sortedFiles = logFileDao.findAll().stream()
49+
return createPageResponse(logFileDao.findAll(), pageable, sortOrder);
50+
}
51+
52+
@Override
53+
public PageResponse<LogFileResponse> searchByName(String name, Pageable pageable) {
54+
if (name == null || name.isBlank()) {
55+
throw new ValidationException(
56+
"Log file name must not be blank",
57+
ErrorCode.COMMON_VALIDATION_FAILED);
58+
}
59+
String searchName = name.trim();
60+
61+
log.debug(
62+
"Searching log files by name: '{}', page={}, size={}, sort={}",
63+
searchName,
64+
pageable.getPageNumber(),
65+
pageable.getPageSize(),
66+
pageable.getSort());
67+
Comparator<LogFileMetadata> sortOrder =
68+
resolveSortOrder(pageable.getSort());
69+
70+
return createPageResponse(logFileDao.findByNameContainingIgnoreCase(searchName), pageable, sortOrder);
71+
}
72+
73+
private PageResponse<LogFileResponse> createPageResponse(List<LogFileMetadata> files,
74+
Pageable pageable, Comparator<LogFileMetadata> sortOrder) {
75+
List<LogFileMetadata> sortedFiles = files.stream()
5076
.sorted(sortOrder)
5177
.toList();
5278

@@ -56,7 +82,6 @@ public PageResponse<LogFileResponse> getAll(Pageable pageable) {
5682
.toList();
5783

5884
Page<LogFileMetadata> metadataPage = new PageImpl<>(pageContent, pageable, sortedFiles.size());
59-
6085
Page<LogFileResponse> responsePage = metadataPage.map(logFileMapper::toResponse);
6186

6287
log.debug(

src/main/java/com/itasocialacademy/oitassist/news/controller/NewsController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public ResponseEntity<List<ArchivedNewsByYearDto>> getArchivedNews() {
124124
@GetMapping("/admin")
125125
@PreAuthorize("hasAnyRole('ADMIN', 'ORG')")
126126
public ResponseEntity<PageResponse<ResponseNewsAdminListItemDto>> getAllNewsForAdmin(
127-
@ParameterObject @PageableDefault(size = 15, sort = "createdAt", direction = DESC) Pageable pageable,
127+
@ParameterObject @PageableDefault(size = 15, sort = "updatedAt", direction = DESC) Pageable pageable,
128128
@RequestParam(required = false) String search,
129129
@RequestParam(required = false) List<NewsStatus> statuses,
130130
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateFrom,

0 commit comments

Comments
 (0)