TeamController.java

package no.nav.data.team.team;

import io.swagger.v3.oas.annotations.Operation;
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 io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import no.nav.data.common.exceptions.NotFoundException;
import no.nav.data.common.exceptions.TechnicalException;
import no.nav.data.common.exceptions.ValidationException;
import no.nav.data.common.rest.RestResponsePage;
import no.nav.data.common.rest.StandardResponse;
import no.nav.data.team.location.LocationRepository;
import no.nav.data.team.location.domain.Location;
import no.nav.data.team.shared.domain.DomainObjectStatus;
import no.nav.data.team.team.TeamExportService.SpreadsheetType;
import no.nav.data.team.team.domain.Team;
import no.nav.data.team.team.dto.TeamRequest;
import no.nav.data.team.team.dto.TeamResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.*;

import java.util.*;

import static no.nav.data.common.export.ExcelBuilder.SPREADSHEETML_SHEET_MIME;
import static no.nav.data.common.utils.StreamUtils.convert;


@Slf4j
@RestController
@RequestMapping("/team")
@Tag(name = "Team", description = "Team endpoint")
public class TeamController {

    private final TeamService service;
    private final TeamExportService teamExportService;
    private final LocationRepository locationRepository;

    public TeamController(TeamService service, TeamExportService teamExportService, LocationRepository locationRepository) {
        this.service = service;
        this.teamExportService = teamExportService;
        this.locationRepository = locationRepository;
    }

    @Operation(summary = "Get All Teams")
    @ApiResponse(description = "ok")
    @GetMapping()
    public ResponseEntity<RestResponsePage<TeamResponse>> getAll(
            @RequestParam(name = "productAreaId", required = false) UUID productAreaId,
            @RequestParam(name = "clusterId", required = false) UUID clusterId,
            @RequestParam(name = "locationCode", required = false) String locationCode,
            @RequestParam(name = "status", required = false, defaultValue = "ACTIVE,PLANNED,INACTIVE") String stringStatus
    ) {
        log.info("Get all Teams");

        var queryStatusList = DomainObjectStatus.fromQueryParameter(stringStatus);

        List<Team> teams;
        if (productAreaId != null) {
            teams = service.findByProductArea(productAreaId);
        } else if (clusterId != null) {
            teams = service.findByCluster(clusterId);
        } else {
            teams = service.getAll();
        }

        if(locationCode != null){
            val location = locationRepository.getLocationByCode(locationCode);
            val locations = location.isPresent() ? location.get().flatMap() : new HashMap<String, Location>();
            teams = teams.stream().filter(t -> locations.containsKey(t.getOfficeHours() != null ? t.getOfficeHours().getLocationCode() : null)).toList();
        }

        teams = teams.stream().filter(t -> queryStatusList.contains(t.getStatus())).toList();

        return ResponseEntity.ok(new RestResponsePage<>(convert(teams, Team::convertToResponse)));
    }

    @Operation(summary = "Get Team")
    @ApiResponse(description = "ok")
    @GetMapping("/{id}")
    public ResponseEntity<TeamResponse> getById(@PathVariable UUID id) {
        log.info("Get Team id={}", id);
        return ResponseEntity.ok(service.get(id).convertToResponse());
    }

    @Operation(summary = "Search teams")
    @ApiResponse(description = "Teams fetched")
    @GetMapping("/search/{name}")
    public ResponseEntity<RestResponsePage<TeamResponse>> searchTeamByName(@PathVariable String name) {
        log.info("Received request for Team with the name like {}", name);
        if (name.length() < 3) {
            throw new ValidationException("Search teams must be at least 3 characters");
        }
        var teams = service.search(name);
        log.info("Returned {} teams", teams.size());
        return new ResponseEntity<>(new RestResponsePage<>(convert(teams, Team::convertToResponse)), HttpStatus.OK);
    }

    @Operation(summary = "Get teams by naisTeam/namespace")
    @ApiResponse(description = "ok")
    @GetMapping("/naisteam/{naisTeam}")
    public ResponseEntity<RestResponsePage<TeamResponse>> getByNaisTeam(
            @PathVariable String naisTeam,
            @RequestParam(name = "status", required = false, defaultValue = "ACTIVE,PLANNED,INACTIVE") String stringStatus
    ) {
        log.info("Get teams by naisTeam");

        var queryStatusList = DomainObjectStatus.fromQueryParameter(stringStatus);

        List<Team> teams = service.findByNaisTeam(naisTeam);

        teams = teams.stream().filter(t -> queryStatusList.contains(t.getStatus())).toList();

        return ResponseEntity.ok(new RestResponsePage<>(convert(teams, Team::convertToResponse)));
    }

    @Operation(summary = "Create Team v1")
    @ApiResponse(responseCode = "201", description = "Team created")
    @PostMapping
    public ResponseEntity<TeamResponse> createTeam(@RequestBody TeamRequest request) {
        log.info("Create Team v1");
        return createTeam_v2(request);
    }

    @Operation(summary = "Create Team v2")
    @ApiResponse(responseCode = "201", description = "Team created")
    @PostMapping("v2")
    public ResponseEntity<TeamResponse> createTeam_v2(@RequestBody TeamRequest request) {
        log.info("Create Team v2");
        var team = service.save(request);
        return new ResponseEntity<>(team.convertToResponse(), HttpStatus.CREATED);
    }

    @Operation(summary = "Create Teams")
    @ApiResponse(responseCode = "201", description = "Teams created")
    @Transactional
    @PostMapping("/batch")
    public ResponseEntity<RestResponsePage<TeamResponse>> createTeams(@RequestBody List<TeamRequest> requests) {
        log.info("Create Teams");
        var teams = convert(requests, service::save);
        return new ResponseEntity<>(new RestResponsePage<>(convert(teams, Team::convertToResponse)), HttpStatus.CREATED);
    }

    @Operation(summary = "Update Team", description = "If members is null members will not be updated")
    @ApiResponse(description = "Team updated")
    @PutMapping("/{id}")
    public ResponseEntity<TeamResponse> updateTeam(@PathVariable UUID id, @Valid @RequestBody TeamRequest request) {
        log.debug("Update Team v1 id={}", id);
        return updateTeam_v2(id, request);
    }

    @Operation(summary = "Update Team", description = "If members is null members will not be updated")
    @ApiResponse(description = "Team updated")
    @PutMapping("v2/{id}")
    public ResponseEntity<TeamResponse> updateTeam_v2(@PathVariable UUID id, @Valid @RequestBody TeamRequest request) {
        log.debug("Update Team v2 id={}", id);
        if (!Objects.equals(id, request.getIdAsUUID())) {
            throw new ValidationException(String.format("id mismatch in request %s and path %s", request.getId(), id));
        }
        var team = service.save(request);
        return ResponseEntity.ok(team.convertToResponse());
    }

    @Operation(summary = "Delete Team")
    @ApiResponse(description = "Team deleted")
    @DeleteMapping("/{id}")
    public ResponseEntity<TeamResponse> deleteTeamById(@PathVariable UUID id) {
        log.info("Delete Team id={}", id);
        var team = service.delete(id);
        return ResponseEntity.ok(team.convertToResponse());
    }

    @Operation(summary = "Get export for teams")
    @ApiResponse(description = "Doc fetched", content = @Content(schema = @Schema(implementation = byte[].class)))
    @Transactional(readOnly = true)
    @GetMapping(value = "/export/{type}", produces = SPREADSHEETML_SHEET_MIME)
    public void getExport(
            HttpServletResponse response,
            @PathVariable("type") SpreadsheetType type,
            @RequestParam(name = "id", required = false) String id
    ) {
        if (type != SpreadsheetType.ALL && id == null) {
            throw new ValidationException("missing id for spreadsheet type " + type);
        }
        byte[] doc = teamExportService.generate(type, id);
        String filename = "teams_" + type + Optional.ofNullable(id).map(s -> "-" + s).orElse("") + ".xlsx";
        response.setContentType(SPREADSHEETML_SHEET_MIME);
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
        writeDoc(response, doc);
    }

    private void writeDoc(HttpServletResponse response, byte[] doc) {
        try {
            StreamUtils.copy(doc, response.getOutputStream());
            response.flushBuffer();
        } catch (Exception e) {
            throw new TechnicalException("io error", e);
        }
    }

    public static class TeamPageResponse extends RestResponsePage<TeamResponse> {

    }

    @ExceptionHandler({
            NotFoundException.class
    })
    public ResponseEntity<StandardResponse> handleNotFoundException(NotFoundException e) {
        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(StandardResponse.builder().message(e.getMessage()).build());
    }

    @ExceptionHandler({
            Exception.class
    })
    public ResponseEntity<StandardResponse> handleException(RuntimeException e) {
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(StandardResponse.builder().message(e.getMessage()).build());
    }

}