PortfolioGitController.java

package com.taxonomy.portfolio.controller;

import com.taxonomy.portfolio.dto.PortfolioGitDtos.CommitPortfolioRequest;
import com.taxonomy.portfolio.dto.PortfolioGitDtos.ExportedPortfolioDsl;
import com.taxonomy.portfolio.dto.PortfolioGitDtos.MaterializationPreview;
import com.taxonomy.portfolio.dto.PortfolioGitDtos.MaterializePortfolioRequest;
import com.taxonomy.portfolio.dto.PortfolioGitDtos.MaterializePortfolioResult;
import com.taxonomy.portfolio.dto.PortfolioGitDtos.MergePortfolioRequest;
import com.taxonomy.portfolio.dto.PortfolioGitDtos.MergePortfolioResult;
import com.taxonomy.portfolio.dto.PortfolioGitDtos.PortfolioCommitResult;
import com.taxonomy.portfolio.service.PortfolioGitApplicationService;
import com.taxonomy.versioning.service.RepositoryStateService;
import com.taxonomy.workspace.service.WorkspaceContext;
import com.taxonomy.workspace.service.WorkspaceResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

/** Git projection, reviewed materialization and semantic merge for project portfolios. */
@RestController
@RequestMapping("/api/projects/git")
@Tag(name = "Project Portfolio Git")
public class PortfolioGitController {

    private final PortfolioGitApplicationService gitService;
    private final WorkspaceResolver workspaceResolver;
    private final RepositoryStateService repositoryStateService;

    public PortfolioGitController(PortfolioGitApplicationService gitService,
                                  WorkspaceResolver workspaceResolver,
                                  RepositoryStateService repositoryStateService) {
        this.gitService = gitService;
        this.workspaceResolver = workspaceResolver;
        this.repositoryStateService = repositoryStateService;
    }

    @Operation(summary = "Export the current Git-backed portfolio projection")
    @GetMapping(value = "/export",
            produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE})
    public ResponseEntity<?> exportPortfolio(
            @RequestHeader(name = HttpHeaders.ACCEPT, required = false) String accept)
            throws IOException {
        ExportedPortfolioDsl exported = gitService.export(context().workspaceContext());
        if (explicitlyRequestsJson(accept)) {
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(exported);
        }
        return ResponseEntity.ok()
                .contentType(MediaType.TEXT_PLAIN)
                .body(exported.dsl());
    }

    @Operation(summary = "Commit the reviewed current portfolio projection")
    @PostMapping("/commit")
    public PortfolioCommitResult commit(
            @RequestParam(required = false) String branch,
            @RequestBody(required = false) CommitPortfolioRequest request) throws IOException {
        RequestContext current = context();
        String effectiveBranch = firstNonBlank(
                request != null ? request.branch() : null,
                branch,
                current.workspaceContext().currentBranch(),
                "draft");
        return gitService.commit(
                effectiveBranch,
                request != null ? request.message() : null,
                current.workspaceContext());
    }

    @Operation(summary = "Preview branch materialization without changing portfolio data")
    @GetMapping("/materialize-preview")
    public MaterializationPreview previewMaterialize(
            @RequestParam(defaultValue = "draft") String branch) throws IOException {
        return gitService.previewMaterialize(branch, context().workspaceContext());
    }

    @Operation(summary = "Materialize one reviewed branch HEAD into the portfolio")
    @PostMapping("/materialize")
    public MaterializePortfolioResult materialize(
            @RequestParam(required = false) String branch,
            @RequestBody(required = false) MaterializePortfolioRequest request) throws IOException {
        RequestContext current = context();
        String effectiveBranch = firstNonBlank(
                request != null ? request.branch() : null,
                branch,
                current.workspaceContext().currentBranch(),
                "draft");
        return gitService.materialize(
                effectiveBranch,
                request != null ? request.expectedHead() : null,
                current.workspaceContext());
    }

    @Operation(summary = "Semantically merge two branches and materialize the target portfolio")
    @PostMapping("/merge")
    public MergePortfolioResult merge(
            @RequestParam(required = false) String source,
            @RequestParam(required = false) String target,
            @RequestBody(required = false) MergePortfolioRequest request) throws IOException {
        RequestContext current = context();
        String effectiveSource = firstNonBlank(
                request != null ? request.sourceBranch() : null,
                source);
        String effectiveTarget = firstNonBlank(
                request != null ? request.targetBranch() : null,
                target,
                current.workspaceContext().currentBranch(),
                "draft");
        return gitService.merge(
                effectiveSource,
                effectiveTarget,
                request != null ? request.message() : null,
                current.workspaceContext());
    }

    /** Resolve the authenticated workspace once and fail closed on provisioning errors. */
    private RequestContext context() {
        String username = workspaceResolver.resolveCurrentUsername();
        repositoryStateService.ensureWorkspaceState(username);
        return new RequestContext(username, workspaceResolver.resolveCurrentContext());
    }

    private static boolean explicitlyRequestsJson(String accept) {
        if (accept == null || accept.isBlank()) return false;
        try {
            return MediaType.parseMediaTypes(accept).stream()
                    .filter(type -> type.getQualityValue() > 0.0d)
                    .filter(type -> !type.isWildcardType() && !type.isWildcardSubtype())
                    .anyMatch(MediaType.APPLICATION_JSON::isCompatibleWith);
        } catch (IllegalArgumentException invalidAcceptHeader) {
            return false;
        }
    }

    private static String firstNonBlank(String... values) {
        if (values != null) {
            for (String value : values) {
                if (value != null && !value.isBlank()) return value.strip();
            }
        }
        return null;
    }

    private record RequestContext(String username, WorkspaceContext workspaceContext) {
    }
}