Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.time.Instant;
Expand Down Expand Up @@ -245,4 +246,35 @@ public ResponseEntity<ErrorResponse> handleMissingServletRequestPart(
"Required request part '" + ex.getRequestPartName() + "' is not present",
HttpStatus.BAD_REQUEST.value(), null));
}

/**
* Handles {@link MissingServletRequestParameterException} by generating an
* appropriate error response. This exception is thrown when a required request
* parameter is missing from the HTTP request.
*
* @param ex the exception object containing details of the missing request
* parameter
* @param request the HTTP request that triggered the exception
* @return a {@link ResponseEntity} containing an {@link ErrorResponse} with
* error details, including the name of the missing request parameter
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ErrorResponse> handleMissingRequestParameter(
MissingServletRequestParameterException ex,
HttpServletRequest request) {
log.warn(
"Missing request parameter: traceId={}, parameter={}",
MDC.get(TRACE_ID_MDC),
ex.getParameterName());

HttpStatus status = HttpStatus.BAD_REQUEST;

return ResponseEntity.status(status)
.body(buildResponse(
request,
ErrorCode.COMMON_VALIDATION_FAILED,
"Required request parameter '" + ex.getParameterName() + "' is not present",
status.value(),
Map.of("parameter", ex.getParameterName())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.itasocialacademy.oitassist.logfile.api.PageResponse;
import com.itasocialacademy.oitassist.logfile.service.LogFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -14,6 +15,7 @@
import org.springframework.security.access.prepost.PreAuthorize;
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;

@RestController
Expand Down Expand Up @@ -47,4 +49,26 @@ public PageResponse<LogFileResponse> getAll(
direction = Sort.Direction.DESC) Pageable pageable) {
return logFileService.getAll(pageable);
}

@GetMapping("/search")
@Operation(
summary = "Search log files by name",
description = """
Searches application log files by a partial file name match.
The search is case-insensitive and supports pagination and sorting.
Access is restricted to administrators.
""")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Log files matching the specified name were successfully retrieved"),
@ApiResponse(responseCode = "400", description = "Invalid search or sorting parameters"),
@ApiResponse(responseCode = "401", description = "Unauthorized - token is missing or invalid"),
@ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions")})
public PageResponse<LogFileResponse> searchByName(
@Parameter(
description = "Full or partial log file name to search for",
example = "app") @RequestParam String name,
@ParameterObject @PageableDefault(size = 10) Pageable pageable) {
return logFileService.searchByName(name, pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
import com.itasocialacademy.oitassist.logfile.exceptions.LogFileListingException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -25,10 +31,24 @@ public FileSystemLogFileDao(

@Override
public List<LogFileMetadata> findAll() {
return findFiles(path -> true);
}

@Override
public List<LogFileMetadata> findByNameContainingIgnoreCase(String name) {
String normalizedName = name.toLowerCase(Locale.ROOT);
return findFiles(path -> path.getFileName()
.toString()
.toLowerCase(Locale.ROOT)
.contains(normalizedName));
}

private List<LogFileMetadata> findFiles(Predicate<Path> filter) {
validateLogDirectory();

try (Stream<Path> paths = Files.list(logDirectory)) {
return paths
.filter(filter)
.map(this::readMetadata)
.flatMap(Optional::stream)
.toList();
Expand All @@ -37,6 +57,7 @@ public List<LogFileMetadata> findAll() {
"Failed to read metadata from log directory: {}",
logDirectory,
exception.getCause());

throw new LogFileListingException();
} catch (IOException | SecurityException exception) {
log.error("Failed to access log directory: {}", logDirectory, exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@

public interface LogFileDao {
List<LogFileMetadata> findAll();

List<LogFileMetadata> findByNameContainingIgnoreCase(String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

public interface LogFileService {
PageResponse<LogFileResponse> getAll(Pageable pageable);

PageResponse<LogFileResponse> searchByName(String name, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,33 @@ public PageResponse<LogFileResponse> getAll(Pageable pageable) {
Comparator<LogFileMetadata> sortOrder =
resolveSortOrder(pageable.getSort());

List<LogFileMetadata> sortedFiles = logFileDao.findAll().stream()
return createPageResponse(logFileDao.findAll(), pageable, sortOrder);
}

@Override
public PageResponse<LogFileResponse> searchByName(String name, Pageable pageable) {
if (name == null || name.isBlank()) {
throw new ValidationException(
"Log file name must not be blank",
ErrorCode.COMMON_VALIDATION_FAILED);
}
String searchName = name.trim();

log.debug(
"Searching log files by name: '{}', page={}, size={}, sort={}",
searchName,
pageable.getPageNumber(),
pageable.getPageSize(),
pageable.getSort());
Comparator<LogFileMetadata> sortOrder =
resolveSortOrder(pageable.getSort());

return createPageResponse(logFileDao.findByNameContainingIgnoreCase(searchName), pageable, sortOrder);
}

private PageResponse<LogFileResponse> createPageResponse(List<LogFileMetadata> files,
Pageable pageable, Comparator<LogFileMetadata> sortOrder) {
List<LogFileMetadata> sortedFiles = files.stream()
.sorted(sortOrder)
.toList();

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

Page<LogFileMetadata> metadataPage = new PageImpl<>(pageContent, pageable, sortedFiles.size());

Page<LogFileResponse> responsePage = metadataPage.map(logFileMapper::toResponse);

log.debug(
Expand Down
Loading
Loading