QualityApiController.java

package com.taxonomy.relations.controller;

import com.taxonomy.dto.ProvenanceMetrics;
import com.taxonomy.dto.RelationQualityMetrics;
import com.taxonomy.dto.RelationTypeMetrics;
import com.taxonomy.dto.TopRejectedProposal;
import com.taxonomy.relations.service.RelationQualityService;
import com.taxonomy.workspace.service.RepositoryContext;
import com.taxonomy.workspace.service.WorkspaceResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * REST API for the Relation Quality Dashboard.
 *
 * <p>Every request resolves one stable selected repository context. Responses
 * are not cacheable because the URL is intentionally independent of the
 * repository/workspace selection stored in the user session.</p>
 */
@RestController
@RequestMapping("/api/relations/metrics")
@Tag(name = "Quality Metrics")
public class QualityApiController {

    private final RelationQualityService qualityService;
    private final WorkspaceResolver workspaceResolver;

    public QualityApiController(
            RelationQualityService qualityService,
            WorkspaceResolver workspaceResolver) {
        this.qualityService = qualityService;
        this.workspaceResolver = workspaceResolver;
    }

    /** Returns the full quality dashboard metrics. */
    @Operation(
            summary = "Quality dashboard",
            description = "Returns quality metrics for the selected repository context")
    @GetMapping
    public ResponseEntity<RelationQualityMetrics> getMetrics() {
        RepositoryContext context = currentContext();
        return noStore(qualityService.calculateMetrics(context));
    }

    /** Returns metrics broken down by relation type. */
    @Operation(
            summary = "Metrics by relation type",
            description = "Returns relation-type metrics for the selected repository context")
    @GetMapping("/by-type")
    public ResponseEntity<List<RelationTypeMetrics>> getMetricsByType() {
        RepositoryContext context = currentContext();
        return noStore(qualityService.metricsByRelationType(context));
    }

    /** Returns metrics broken down by provenance. */
    @Operation(
            summary = "Metrics by provenance",
            description = "Returns provenance metrics for the selected repository context")
    @GetMapping("/by-provenance")
    public ResponseEntity<List<ProvenanceMetrics>> getMetricsByProvenance() {
        RepositoryContext context = currentContext();
        return noStore(qualityService.metricsByProvenance(context));
    }

    /** Returns the highest-confidence rejected proposals in the visible scope. */
    @Operation(
            summary = "Top rejected proposals",
            description = "Returns rejected proposals from the selected repository context")
    @GetMapping("/top-rejected")
    public ResponseEntity<List<TopRejectedProposal>> getTopRejected(
            @Parameter(description = "Maximum number of results")
            @RequestParam(defaultValue = "10") int limit) {
        RepositoryContext context = currentContext();
        return noStore(qualityService.topRejected(limit, context));
    }

    private RepositoryContext currentContext() {
        return workspaceResolver.resolveCurrentRepositoryContext();
    }

    private static <T> ResponseEntity<T> noStore(T body) {
        return ResponseEntity.ok()
                .cacheControl(CacheControl.noStore())
                .body(body);
    }
}