AnalysisWorkingDraftController.java

package com.taxonomy.analysis.session;

import com.taxonomy.analysis.session.AnalysisDraftDtos.AnalysisDraftView;
import com.taxonomy.analysis.session.AnalysisDraftDtos.ResetAnalysisDraftRequest;
import com.taxonomy.analysis.session.AnalysisDraftDtos.SaveAnalysisDraftRequest;
import com.taxonomy.workspace.service.WorkspaceContextResolver;
import com.taxonomy.workspace.service.WorkspaceResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/** REST boundary for resumable, user-owned ad-hoc analysis drafts. */
@RestController
@RequestMapping("/api/analysis-drafts")
@Tag(name = "Analysis Working Drafts")
public class AnalysisWorkingDraftController {

    private final AnalysisWorkingDraftService service;
    private final WorkspaceResolver workspaceResolver;

    public AnalysisWorkingDraftController(
            AnalysisWorkingDraftService service,
            WorkspaceResolver workspaceResolver) {
        this.service = service;
        this.workspaceResolver = workspaceResolver;
    }

    @GetMapping("/{workspaceId}")
    @Operation(summary = "Restore the current user's ad-hoc analysis draft")
    public ResponseEntity<AnalysisDraftView> read(
            @PathVariable String workspaceId,
            HttpServletRequest request) {
        requireMatchingExplicitPin(request, workspaceId);
        return service.read(workspaceResolver.resolveCurrentUsername(), workspaceId)
                .map(this::versionedResponse)
                .orElseGet(AnalysisWorkingDraftController::noContentResponse);
    }

    @PutMapping("/{workspaceId}")
    @Operation(summary = "Create or optimistically update an ad-hoc analysis draft")
    public ResponseEntity<AnalysisDraftView> save(
            @PathVariable String workspaceId,
            @RequestBody SaveAnalysisDraftRequest requestBody,
            HttpServletRequest request) {
        requireMatchingExplicitPin(request, workspaceId);
        AnalysisDraftView view = service.save(
                workspaceResolver.resolveCurrentUsername(), workspaceId, requestBody);
        return versionedResponse(view);
    }

    @PostMapping("/{workspaceId}/reset")
    @Operation(summary = "Start a new empty ad-hoc analysis lifecycle")
    public ResponseEntity<AnalysisDraftView> reset(
            @PathVariable String workspaceId,
            @RequestBody(required = false) ResetAnalysisDraftRequest requestBody,
            HttpServletRequest request) {
        requireMatchingExplicitPin(request, workspaceId);
        AnalysisDraftView view = service.reset(
                workspaceResolver.resolveCurrentUsername(), workspaceId, requestBody);
        return versionedResponse(view);
    }

    @DeleteMapping("/{workspaceId}")
    @Operation(summary = "Discard the current user's ad-hoc analysis draft")
    public ResponseEntity<Void> delete(
            @PathVariable String workspaceId,
            @RequestParam(required = false) Long expectedVersion,
            HttpServletRequest request) {
        requireMatchingExplicitPin(request, workspaceId);
        service.delete(
                workspaceResolver.resolveCurrentUsername(), workspaceId, expectedVersion);
        return noContentResponse();
    }

    private static void requireMatchingExplicitPin(
            HttpServletRequest request, String workspaceId) {
        String explicitWorkspaceId = request.getHeader(
                WorkspaceContextResolver.WORKSPACE_HEADER);
        if (!hasText(explicitWorkspaceId)) {
            explicitWorkspaceId = request.getParameter(
                    WorkspaceContextResolver.WORKSPACE_QUERY_PARAMETER);
        }
        if (hasText(explicitWorkspaceId)
                && !explicitWorkspaceId.strip().equals(workspaceId.strip())) {
            throw new AccessDeniedException(
                    "Analysis draft workspace does not match the pinned request context");
        }
    }

    private static boolean hasText(String value) {
        return value != null && !value.isBlank();
    }

    private ResponseEntity<AnalysisDraftView> versionedResponse(AnalysisDraftView view) {
        return ResponseEntity.ok()
                .cacheControl(CacheControl.noStore())
                .eTag("\"" + view.version() + "\"")
                .body(view);
    }

    private static <T> ResponseEntity<T> noContentResponse() {
        return ResponseEntity.noContent()
                .cacheControl(CacheControl.noStore())
                .build();
    }
}