diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3f0faf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# db files +/database/db.mv.db +/database/db.trace.db +/database/db.lock.db + +# logs +/log diff --git a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/TournamentEndpoint.java b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/TournamentEndpoint.java index 8522ee5..d7d2d91 100644 --- a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/TournamentEndpoint.java +++ b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/TournamentEndpoint.java @@ -51,8 +51,8 @@ public class TournamentEndpoint { @Autowired public TournamentEndpoint(TournamentService tournamentService, - TournamentMapper tournamentMapper, - TeamMapper teamMapper) { + TournamentMapper tournamentMapper, + TeamMapper teamMapper) { this.tournamentService = tournamentService; this.tournamentMapper = tournamentMapper; this.teamMapper = teamMapper; @@ -61,7 +61,8 @@ public class TournamentEndpoint { @Secured("ROLE_USER") @ResponseStatus(HttpStatus.OK) @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = "Get a list of all tournaments from a specific Organizer", security = @SecurityRequirement(name = "apiKey")) + @Operation(summary = "Get a list of all tournaments from a specific Organizer", + security = @SecurityRequirement(name = "apiKey")) public ResponseEntity> tournaments(Authentication authentication) { LOG.info("GET {}", BASE_ENDPOINT); var tournaments = tournamentService.findAllByOrganizer(authentication.getName()); @@ -73,11 +74,11 @@ public class TournamentEndpoint { @PostMapping @Operation(summary = "Create a new tournament", security = @SecurityRequirement(name = "apiKey")) public TournamentDto createTournament(@Valid @RequestBody CreateTournamentDto messageDto, - Authentication authentication) { + Authentication authentication) { LOG.info("POST /api/v1/tournaments body: {}", messageDto); return tournamentMapper.entityToDto(tournamentService.create(tournamentMapper.createDtoToEntity(messageDto), - authentication.getName())); + authentication.getName())); } @Secured("ROLE_USER") @@ -85,29 +86,13 @@ public class TournamentEndpoint { @PostMapping(value = "{id}/qualification-matches") @Operation(summary = "Create a new tournament", security = @SecurityRequirement(name = "apiKey")) public List generateQualificationMatches( - @PathVariable(name = "id") Long tournamentId, Authentication authentication) { + @PathVariable(name = "id") Long tournamentId, Authentication authentication) { LOG.info("POST /api/v1/tournaments/{}/generate-qualification-matches", tournamentId); return tournamentService.generateQualificationMatchesForTournament(tournamentId, authentication.getName()) - .stream() - .map(tournamentMapper::qualificationMatchEntityToDto) - .toList(); - } - - @Secured("ROLE_USER") - @ResponseStatus(HttpStatus.OK) - @GetMapping(value = "{id}/teams") - @Operation(summary = "Get teams for a tournament", security = @SecurityRequirement(name = "apiKey")) - public List getTournamentTeams(@PathVariable(name = "id") Long tournamentId, - Authentication authentication) { - LOG.info("GET /api/v1/tournaments/{}/teams", tournamentId); - - // check if user is organizer of tournament - if (!tournamentService.isOrganizer(authentication.getName(), tournamentId)) { - throw new AccessDeniedException("Current user isn't organizer of tournament."); - } - - return tournamentService.getTournamentTeams(tournamentId).stream().map(teamMapper::entityToDto).toList(); + .stream() + .map(tournamentMapper::qualificationMatchEntityToDto) + .toList(); } @Secured("ROLE_USER") @@ -115,7 +100,7 @@ public class TournamentEndpoint { @DeleteMapping("/{tournamentId}") @Operation(summary = "Delete a tournament", security = @SecurityRequirement(name = "apiKey")) public ResponseEntity deleteTournament(@PathVariable("tournamentId") long tournamentId, - Authentication authentication) { + Authentication authentication) { LOG.info("DELETE {}/{}", BASE_ENDPOINT, tournamentId); tournamentService.deleteTournament(tournamentId, authentication.getName()); return ResponseEntity.noContent().build(); @@ -126,13 +111,13 @@ public class TournamentEndpoint { @PostMapping(value = "{id}/generate-ko-matches") @Operation(summary = "Create a new tournament", security = @SecurityRequirement(name = "apiKey")) public void generateKoMatches( - @PathVariable(name = "id") Long tournamentId, - Authentication authentication) { + @PathVariable(name = "id") Long tournamentId, + Authentication authentication) { LOG.info("POST /api/v1/tournaments/{}/generate-ko-matches", tournamentId); tournamentService.generateKoMatchesForTournament( - tournamentId, - authentication.getName()); + tournamentId, + authentication.getName()); } @@ -157,13 +142,47 @@ public class TournamentEndpoint { public record SignupTeamResponseDto(SignupTeamResult signupTeamResult) { } + @Secured("ROLE_USER") + @ResponseStatus(HttpStatus.OK) + @GetMapping(value = "{id}/teams") + @Operation(summary = "Get teams for a tournament", security = @SecurityRequirement(name = "apiKey")) + public List getTournamentTeams(@PathVariable(name = "id") Long tournamentId, + Authentication authentication) { + LOG.info("GET /api/v1/tournaments/{}/teams", tournamentId); + + // check if user is organizer of tournament + if (!tournamentService.isOrganizer(authentication.getName(), tournamentId)) { + throw new AccessDeniedException("Current user isn't organizer of tournament."); + } + + return tournamentService.getTournamentTeams(tournamentId).stream().map(teamMapper::entityToDto).toList(); + } + + @Secured("ROLE_USER") + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping(value = "{tournamentId}/teams/{teamId}") + @Operation(summary = "Delete team from a tournament", security = @SecurityRequirement(name = "apiKey")) + public ResponseEntity deleteTournamentTeam(@PathVariable("tournamentId") Long tournamentId, + @PathVariable("teamId") Long teamId, + Authentication authentication + ) { + LOG.info("Delete /api/v1/tournaments/{}/teams/{}", tournamentId, teamId); + // check if user is organizer of tournament + if (!tournamentService.isOrganizer(authentication.getName(), tournamentId)) { + throw new AccessDeniedException("Current user isn't organizer of tournament."); + } + tournamentService.deleteTeam(tournamentId, teamId); + return ResponseEntity.noContent().build(); + } + + @PermitAll @ResponseStatus(HttpStatus.OK) // No location header, thus 200 @PostMapping("{tournamentId}/teams") @Operation(summary = "Create a new team") public ResponseEntity signupTeamForTournament( - @PathVariable("tournamentId") long tournamentId, - @Valid @RequestBody CreateTeamDto messageDto) { + @PathVariable("tournamentId") long tournamentId, + @Valid @RequestBody CreateTeamDto messageDto) { LOG.info("POST {} body: {}", BASE_ENDPOINT, messageDto); final var signupResult = tournamentService.signupTeamForTournament(tournamentId, messageDto.name); if (signupResult != SignupTeamResult.SUCCESS) { diff --git a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/exceptionhandler/GlobalExceptionHandler.java b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/exceptionhandler/GlobalExceptionHandler.java index 4a66464..99a62da 100644 --- a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/exceptionhandler/GlobalExceptionHandler.java +++ b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/endpoint/exceptionhandler/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ package at.ac.tuwien.sepr.groupphase.backend.endpoint.exceptionhandler; import at.ac.tuwien.sepr.groupphase.backend.exception.NotFoundException; import at.ac.tuwien.sepr.groupphase.backend.exception.PreconditionFailedException; +import at.ac.tuwien.sepr.groupphase.backend.exception.TournamentAlreadyStartedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -37,7 +38,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { /** * Use the @ExceptionHandler annotation to write handler for custom exceptions. */ - @ExceptionHandler(value = { NotFoundException.class }) + @ExceptionHandler(value = {NotFoundException.class}) protected ResponseEntity handleNotFound(RuntimeException ex, WebRequest request) { LOGGER.warn(ex.getMessage()); return handleExceptionInternal(ex, ex.getMessage(), new HttpHeaders(), HttpStatus.NOT_FOUND, request); @@ -49,15 +50,15 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { */ @Override protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, - HttpHeaders headers, - HttpStatusCode status, WebRequest request) { + HttpHeaders headers, + HttpStatusCode status, WebRequest request) { Map body = new LinkedHashMap<>(); // Get all errors List errors = ex.getBindingResult() - .getFieldErrors() - .stream() - .map(err -> err.getField() + " " + err.getDefaultMessage()) - .collect(Collectors.toList()); + .getFieldErrors() + .stream() + .map(err -> err.getField() + " " + err.getDefaultMessage()) + .collect(Collectors.toList()); body.put("Validation errors", errors); return new ResponseEntity<>(body.toString(), headers, status); @@ -67,18 +68,18 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { * Overrides the handling of PreconditionFailedException that sends a HTTP 400 * response. */ - @ExceptionHandler(value = { PreconditionFailedException.class }) + @ExceptionHandler(value = {PreconditionFailedException.class}) protected ResponseEntity handleAccessPreconditionFailedException(RuntimeException ex, WebRequest request) { var preconditionException = (PreconditionFailedException) ex; return handleExceptionInternal( - ex, - String.format("A precondition wasn't met: %s", preconditionException.getMessage()), - new HttpHeaders(), - HttpStatus.BAD_REQUEST, - request); + ex, + String.format("A precondition wasn't met: %s", preconditionException.getMessage()), + new HttpHeaders(), + HttpStatus.BAD_REQUEST, + request); } - @ExceptionHandler({ BadCredentialsException.class, UsernameNotFoundException.class }) + @ExceptionHandler({BadCredentialsException.class, UsernameNotFoundException.class}) protected ResponseEntity handleBadCredentialsException(RuntimeException ex, WebRequest request) { LOGGER.debug(ex.getMessage()); @@ -90,4 +91,16 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { HttpStatus.FORBIDDEN, request); } + + @ExceptionHandler({TournamentAlreadyStartedException.class}) + protected ResponseEntity handleTournamentAlreadyStartedException(RuntimeException ex, WebRequest request) { + LOGGER.debug(ex.getMessage()); + + return handleExceptionInternal( + ex, + "Tournament already started", + new HttpHeaders(), + HttpStatus.CONFLICT, + request); + } } diff --git a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/exception/TournamentAlreadyStartedException.java b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/exception/TournamentAlreadyStartedException.java new file mode 100644 index 0000000..94cfe52 --- /dev/null +++ b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/exception/TournamentAlreadyStartedException.java @@ -0,0 +1,19 @@ +package at.ac.tuwien.sepr.groupphase.backend.exception; + +public class TournamentAlreadyStartedException extends RuntimeException { + public TournamentAlreadyStartedException() { + } + + + public TournamentAlreadyStartedException(String message) { + super(message); + } + + public TournamentAlreadyStartedException(String message, Throwable cause) { + super(message, cause); + } + + public TournamentAlreadyStartedException(Exception e) { + super(e); + } +} diff --git a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/TournamentService.java b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/TournamentService.java index a0940ad..2b1acd1 100644 --- a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/TournamentService.java +++ b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/TournamentService.java @@ -94,4 +94,12 @@ public interface TournamentService { * (domain-level authorization) */ void generateKoMatchesForTournament(Long tournamentId, String subject); + + /** + * Delete a team from a tournament. + * + * @param tournamentId the id of the tournament entity + * @param teamId the id of the team entity + */ + void deleteTeam(Long tournamentId, Long teamId); } diff --git a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/impl/TournamentServiceImpl.java b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/impl/TournamentServiceImpl.java index 05389f9..d722345 100644 --- a/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/impl/TournamentServiceImpl.java +++ b/backend/src/main/java/at/ac/tuwien/sepr/groupphase/backend/service/impl/TournamentServiceImpl.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.stream.Collectors; import at.ac.tuwien.sepr.groupphase.backend.entity.Team; +import at.ac.tuwien.sepr.groupphase.backend.exception.TournamentAlreadyStartedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; @@ -42,12 +43,12 @@ public class TournamentServiceImpl implements TournamentService { private KoStandingsRepository koStandingsRepository; public TournamentServiceImpl( - TournamentRepository tournamentRepository, - TeamRepository teamRepository, - QualificationMatchRepository qualificatonRepository, - UserRepository userRepository, - QualificationParticipationRepository qualificationParticipationRepository, - KoStandingsRepository koStandingsRepository) { + TournamentRepository tournamentRepository, + TeamRepository teamRepository, + QualificationMatchRepository qualificatonRepository, + UserRepository userRepository, + QualificationParticipationRepository qualificationParticipationRepository, + KoStandingsRepository koStandingsRepository) { super(); this.teamRepository = teamRepository; @@ -84,33 +85,33 @@ public class TournamentServiceImpl implements TournamentService { @Override public List generateQualificationMatchesForTournament(Long tournamentId, String currentUserName) - throws PreconditionFailedException, AccessDeniedException, NotFoundException { + throws PreconditionFailedException, AccessDeniedException, NotFoundException { LOGGER.debug("Create qualifying matches for tournament with id {}", tournamentId); var tournamentOrganizer = tournamentRepository - .findById(tournamentId) - .orElseThrow(() -> new NotFoundException()) - .getOrganizer(); + .findById(tournamentId) + .orElseThrow(() -> new NotFoundException()) + .getOrganizer(); if (tournamentOrganizer == null || !tournamentOrganizer.getUsername().equals(currentUserName)) { LOGGER.debug( - "Couldn't create qualifying matches for tournament with id {}, because the user who started the process isn't the same as the creator of the tournament.", - tournamentId); + "Couldn't create qualifying matches for tournament with id {}, because the user who started the process isn't the same as the creator of the tournament.", + tournamentId); throw new AccessDeniedException("Current user isn't organizer of tournament."); } var teams = teamRepository.findAllByTournamentId(tournamentId); if (teams.size() < 16) { LOGGER.debug( - "Couldn't create qualifying matches for tournament with id {}, because there were less than 16 teams assigned to the tournament.", - tournamentId); + "Couldn't create qualifying matches for tournament with id {}, because there were less than 16 teams assigned to the tournament.", + tournamentId); throw new PreconditionFailedException("Not enough teams in specified tournament."); } if (teams.stream().anyMatch(t -> qualificationParticipationRepository.existsByTeamId(t.getId()))) { LOGGER.debug( - "Couldn't create qualifying matches for tournament with id {}, because there were already qualification matches assigned.", - tournamentId); + "Couldn't create qualifying matches for tournament with id {}, because there were already qualification matches assigned.", + tournamentId); throw new PreconditionFailedException("Qualification matches already created for tournament."); } @@ -146,7 +147,7 @@ public class TournamentServiceImpl implements TournamentService { LOGGER.debug("Create new team {} for tournament {}", name, tournamentId); final var tournament = tournamentRepository.findById(tournamentId) - .orElseThrow(() -> new IllegalArgumentException("Tournament not found")); + .orElseThrow(() -> new IllegalArgumentException("Tournament not found")); final var newTeam = new Team(name, tournament); var result = tournament.signupTeam(newTeam); @@ -163,7 +164,7 @@ public class TournamentServiceImpl implements TournamentService { LOGGER.debug("Get basic information about tournament {}", tournamentId); var tournament = tournamentRepository.findById(tournamentId) - .orElseThrow(() -> new IllegalArgumentException("Tournament not found")); + .orElseThrow(() -> new IllegalArgumentException("Tournament not found")); return tournament; } @@ -183,11 +184,11 @@ public class TournamentServiceImpl implements TournamentService { @Transactional public void deleteTournament(long tournamentId, String currentUserName) - throws NotFoundException, AccessDeniedException { + throws NotFoundException, AccessDeniedException { LOGGER.debug("Deleting tournament with id {}", tournamentId); Tournament tournament = tournamentRepository.findById(tournamentId) - .orElseThrow(() -> new NotFoundException("Tournament not found")); + .orElseThrow(() -> new NotFoundException("Tournament not found")); if (!tournament.getOrganizer().getUsername().equals(currentUserName)) { throw new AccessDeniedException("You do not have permission to delete this tournament"); @@ -199,15 +200,15 @@ public class TournamentServiceImpl implements TournamentService { @Transactional public void generateKoMatchesForTournament(Long tournamentId, String subjectName) - throws NotFoundException, AccessDeniedException { + throws NotFoundException, AccessDeniedException { LOGGER.debug("Create knockout matches for tournament with id {}", tournamentId); // authorization final Tournament tournament = tournamentRepository.getReferenceById(tournamentId); if (!tournament.getOrganizer().getUsername().equals(subjectName)) { LOGGER.debug( - "Subject {} illegally tried to generate KO phase of non-owned tournament {}", - subjectName, tournamentId); + "Subject {} illegally tried to generate KO phase of non-owned tournament {}", + subjectName, tournamentId); throw new AccessDeniedException("Current user isn't organizer of tournament."); } @@ -216,8 +217,8 @@ public class TournamentServiceImpl implements TournamentService { qualiIncomplete = matches.stream().anyMatch(m -> m.getWinner() == null); if (qualiIncomplete) { LOGGER.debug( - "Couldn't create knockout matches for tournament with id {}, because there were still qualification matches running.", - tournamentId); + "Couldn't create knockout matches for tournament with id {}, because there were still qualification matches running.", + tournamentId); throw new PreconditionFailedException("Qualification matches still running."); } @@ -230,14 +231,14 @@ public class TournamentServiceImpl implements TournamentService { final var participations = qualificationParticipationRepository.findByQualificationMatchIn(matches); final var pointsByTeam = participations.stream() - .collect(Collectors.groupingBy(QualificationParticipation::getTeam, - Collectors.summingLong(p -> p.getQualificiatonMatch().getWinnerPoints()))); + .collect(Collectors.groupingBy(QualificationParticipation::getTeam, + Collectors.summingLong(p -> p.getQualificiatonMatch().getWinnerPoints()))); final var rankedTeamIds = pointsByTeam.entrySet().stream() - .sorted((a, b) -> Long.compare(b.getValue(), a.getValue())).map(e -> e.getKey()) - .limit(limit) - .map(team -> new KoStanding(tournament, null, team)) - .toArray(KoStanding[]::new); + .sorted((a, b) -> Long.compare(b.getValue(), a.getValue())).map(e -> e.getKey()) + .limit(limit) + .map(team -> new KoStanding(tournament, null, team)) + .toArray(KoStanding[]::new); // map the teams to the leaf layer of the KO tree // undefined ranking logic, so i just cross-match the teams like in wendy's @@ -258,7 +259,7 @@ public class TournamentServiceImpl implements TournamentService { for (int i = 0; i < layer.length; i++) { final var preceeding = List.of(prevLayer[i * 2], prevLayer[i * 2 + 1]); layer[i] = new KoStanding(tournament, - preceeding, null); + preceeding, null); // passing the preceeding standings to the parent does not help us, as the child // owns the relationship for (var p : preceeding) { @@ -277,4 +278,21 @@ public class TournamentServiceImpl implements TournamentService { koStandingsRepository.saveAndFlush(layer[0]); } + @Override + public void deleteTeam(Long tournamentId, Long teamId) + throws NotFoundException, IllegalArgumentException, IllegalStateException { + LOGGER.debug("Delete team with id {} from tournament with id {}", teamId, tournamentId); + var team = teamRepository.findById(teamId).orElseThrow(() -> new NotFoundException("Team not found in tournament")); + var tournament = team.getTournament(); + + if (!tournament.getId().equals(tournamentId)) { + throw new NotFoundException("Team not found in tournament"); + } + if ((long) tournament.getQualificationMatches().size() > 0) { + throw new TournamentAlreadyStartedException(); + } + + teamRepository.delete(team); + } + }