DataController.java

package se.jobtechdev.personaldatagateway.api.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.context.request.NativeWebRequest;
import se.jobtechdev.personaldatagateway.api.exception.ApiException;
import se.jobtechdev.personaldatagateway.api.generated.api.DataApi;
import se.jobtechdev.personaldatagateway.api.generated.entities.ClientEntity;
import se.jobtechdev.personaldatagateway.api.generated.entities.SharingEntity;
import se.jobtechdev.personaldatagateway.api.generated.model.DataAccess;
import se.jobtechdev.personaldatagateway.api.service.ClientService;
import se.jobtechdev.personaldatagateway.api.service.DataService;
import se.jobtechdev.personaldatagateway.api.service.SharingService;
import se.jobtechdev.personaldatagateway.api.util.*;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Predicate;

import static se.jobtechdev.personaldatagateway.api.util.ControllerUtil.earlyExit;
import static se.jobtechdev.personaldatagateway.api.util.ControllerUtil.throwApiExceptionOnAbsentValue;

@Controller
public class DataController implements DataApi {

  private final String baseUrl;

  private final NativeWebRequest request;

  private final ClientService clientService;

  private final SharingService sharingService;

  private final DataService dataService;

  @Autowired
  public DataController(
      @Value("${pdg-api.base-url}") String baseUrl,
      NativeWebRequest request,
      ClientService clientService,
      SharingService sharingService,
      DataService dataService) {
    this.baseUrl = baseUrl;
    this.request = request;
    this.clientService = clientService;
    this.sharingService = sharingService;
    this.dataService = dataService;
  }

  static Predicate<ClientEntity> clientInequalityCheck(ClientEntity client1) {
    return client2 -> client2 != null && !client2.equals(client1);
  }

  static Predicate<ZonedDateTime> isTimestampNullOrPassedCheck() {
    return timestamp -> timestamp != null && timestamp.isBefore(TimeProvider.now());
  }

  static SharingEntity getSharingAndAssertClientIfNotPublic(SharingService sharingService, ClientService clientService, UUID dataId, String authKey) {

    final var sharing =
        throwApiExceptionOnAbsentValue(
            sharingService.getSharingById(dataId),
            HttpStatus.NOT_FOUND,
            String.format(
                "Failed to handle GET data request - Could not find sharing by id: %s", dataId));

    final var sharingClient = sharing.getClientEntity();

    if (null != sharingClient) {
      throwApiExceptionOnAbsentValue(
          Optional.ofNullable(authKey),
          HttpStatus.UNAUTHORIZED,
          "An authorization key has to be provided in the X-Auth-Key header for non-public data access");

      byte[] decodedAuthKey;
      try {
        decodedAuthKey = AuthUtil.decodeAuthKey(authKey);
      } catch (Exception e) {
        throw new ApiException(
            ProblemDetailsFactory.createProblemDetails(
                HttpStatus.UNAUTHORIZED, "The authorization key has to be a base64 encoded value"));
      }

      final var authKeyHash = AuthUtil.hashDecodedAuthKey(decodedAuthKey);
      final var authClient =
          throwApiExceptionOnAbsentValue(
              clientService.getClientByKeyHash(authKeyHash),
              HttpStatus.UNAUTHORIZED,
              "Failed to handle GET data request - Could not find any client associated with the"
                  + " provided auth key");
      earlyExit(
          sharingClient,
          clientInequalityCheck(authClient),
          HttpStatus.FORBIDDEN,
          String.format(
              "Failed to handle GET data request - Authenticated client does not match"
                  + " the sharing's associated client"));
    }

    return sharing;
  }

  static SharingEntity assertSharingValidity(SharingEntity sharing) {
    earlyExit(sharing.getRevoked(), Objects::nonNull, HttpStatus.FORBIDDEN, null);
    earlyExit(sharing.getExpires(), isTimestampNullOrPassedCheck(), HttpStatus.FORBIDDEN, null);
    return sharing;
  }

  @Override
  @CrossOrigin(origins = "*")
  public ResponseEntity<byte[]> getData(UUID dataId, String authKey) {

    final var sharing =
        getSharingAndAssertClientIfNotPublic(sharingService, clientService, dataId, authKey);
    final var validSharing = assertSharingValidity(sharing);

    final var datasource = sharing.getDatasourceEntity();
    final var datasourceId = datasource.getId();
    final var contextPath = request.getContextPath();
    final var datasourceLink =
        Link.of(baseUrl + contextPath + "/datasources/" + datasourceId, "datasource");

    final var dataWrapper =
        dataService.retrieveData(
            sharing.getDatasourceEntity(),
            sharing.getPersonEntity().getId(),
            request.getHeader(HttpHeaders.ACCEPT));

    sharingService.createSharingAccess(validSharing);

    final var headers = LinkHeaderUtil.createHeaders(List.of(datasourceLink), new Class<?>[]{UUID.class, String.class}, dataId, authKey);

    return ResponseEntity.status(dataWrapper.status())
        .contentType(dataWrapper.contentType())
        .headers(headers)
        .body(dataWrapper.body());
  }

  @Override
  public ResponseEntity<DataAccess> getDataAccess(UUID dataId, UUID accessId) {
    final var sharingAccess =
        throwApiExceptionOnAbsentValue(
            sharingService.getSharingAccessById(accessId),
            HttpStatus.NOT_FOUND,
            String.format("Could not find data access by id: %s", accessId));

    earlyExit(
        sharingAccess.getSharingEntity().getId(),
        UuidProvider.generateInequalityCheckingLambda(dataId),
        HttpStatus.NOT_FOUND,
        String.format(
            "Could not find data access by dataId: %s and accessId: %s",
            dataId.toString(), accessId.toString()));

    final var response =
        ResponseFactory.createDataAccess(sharingAccess);

    final var headers = LinkHeaderUtil.createHeaders(new Class<?>[]{UUID.class, UUID.class}, dataId, accessId);

    return ResponseEntity.status(HttpStatus.OK)
        .contentType(MediaType.APPLICATION_JSON)
        .headers(headers)
        .body(response);
  }

  @Override
  public ResponseEntity<List<DataAccess>> getDataAccesses(
      UUID dataId, Integer offset, Integer limit) {
    final var sharing =
        throwApiExceptionOnAbsentValue(
            sharingService.getSharingById(dataId),
            HttpStatus.NOT_FOUND,
            String.format("Could not find sharing by id: %s", dataId));

    final var pageable = OffsetBasedPageRequest.of(offset, limit);
    final var sharingAccesses = sharingService.getAllSharingAccessesBySharing(sharing, pageable);

    final var response =
        sharingAccesses.stream()
            .map(
                ResponseFactory::createDataAccess)
            .toList();

    final var headers =
        LinkHeaderUtil.createHeadersWithPagination(
            sharingAccesses, new Class<?>[]{UUID.class, Integer.class, Integer.class}, dataId, (int) pageable.getOffset(), pageable.getPageSize());

    return ResponseEntity.status(HttpStatus.OK)
        .contentType(MediaType.APPLICATION_JSON)
        .headers(headers)
        .body(response);
  }
}