AutopilotRequirementResponseAdvice.java
package com.taxonomy.portfolio.controller;
import com.taxonomy.portfolio.dto.CopilotDtos.AiAutomationStatus;
import com.taxonomy.portfolio.dto.CopilotDtos.CopilotOperationView;
import com.taxonomy.portfolio.dto.PortfolioDtos.ImportRequirementsResult;
import com.taxonomy.portfolio.dto.PortfolioDtos.RequirementVersionView;
import com.taxonomy.portfolio.dto.PortfolioDtos.RequirementView;
import com.taxonomy.portfolio.service.CopilotAutomationService;
import com.taxonomy.workspace.service.WorkspaceContext;
import com.taxonomy.workspace.service.WorkspaceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Starts Autopilot only after a requirement write has returned successfully.
* Failure to queue optional automation never rolls back the authoritative save.
*/
@ControllerAdvice(assignableTypes = ProjectPortfolioController.class)
public class AutopilotRequirementResponseAdvice implements ResponseBodyAdvice<Object> {
private static final Logger LOGGER = LoggerFactory.getLogger(
AutopilotRequirementResponseAdvice.class);
private static final Pattern PROJECT_REQUIREMENT_PATH = Pattern.compile(
"/api/projects/(\\d+)/requirements(?:/(\\d+)(?:/versions)?)?(?:/import)?$");
private static final String OPERATION_HEADER = "X-Taxonomy-Autopilot-Operation";
private static final String OPERATION_COUNT_HEADER = "X-Taxonomy-Autopilot-Operation-Count";
private final CopilotAutomationService automationService;
private final WorkspaceResolver workspaceResolver;
public AutopilotRequirementResponseAdvice(
CopilotAutomationService automationService,
WorkspaceResolver workspaceResolver) {
this.automationService = automationService;
this.workspaceResolver = workspaceResolver;
}
@Override
public boolean supports(
MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
String method = returnType.getMethod() != null
? returnType.getMethod().getName() : "";
return "createRequirement".equals(method)
|| "addRequirementVersion".equals(method)
|| "importRequirements".equals(method);
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
AiAutomationStatus status = automationService.status();
if (!status.autopilotReady() || !status.runAfterRequirementSave()) {
return body;
}
try {
RequestIdentity identity = identity(request.getURI().getPath());
if (identity == null) return body;
String username = workspaceResolver.resolveCurrentUsername();
WorkspaceContext context = workspaceResolver.resolveCurrentContext();
DispatchSummary dispatch = new DispatchSummary();
if (body instanceof RequirementView requirement) {
dispatch.record(enqueue(
identity.projectId(), requirement.id(), username, context));
} else if (body instanceof RequirementVersionView) {
if (identity.requirementId() != null) {
dispatch.record(enqueue(
identity.projectId(), identity.requirementId(), username, context));
}
} else if (body instanceof ImportRequirementsResult imported
&& imported.analysisJob() == null
&& imported.requirements() != null) {
for (RequirementView requirement : imported.requirements()) {
dispatch.record(enqueue(
identity.projectId(), requirement.id(), username, context));
}
}
if (dispatch.count() > 0) {
response.getHeaders().set(
OPERATION_COUNT_HEADER, Integer.toString(dispatch.count()));
if (dispatch.singleOperationId() != null) {
response.getHeaders().set(
OPERATION_HEADER, dispatch.singleOperationId());
}
}
} catch (RuntimeException failure) {
LOGGER.warn("Requirement save succeeded, but optional Autopilot dispatch failed", failure);
}
return body;
}
private Optional<String> enqueue(
Long projectId,
Long requirementId,
String username,
WorkspaceContext context) {
return automationService.tryAutopilot(
projectId, requirementId, username, context)
.map(CopilotOperationView::operationId);
}
private static RequestIdentity identity(String path) {
Matcher matcher = PROJECT_REQUIREMENT_PATH.matcher(path);
if (!matcher.matches()) return null;
Long projectId = Long.valueOf(matcher.group(1));
Long requirementId = matcher.group(2) != null
? Long.valueOf(matcher.group(2)) : null;
return new RequestIdentity(projectId, requirementId);
}
private record RequestIdentity(Long projectId, Long requirementId) {
}
private static final class DispatchSummary {
private int count;
private String singleOperationId;
void record(Optional<String> operationId) {
if (operationId.isEmpty()) return;
count++;
singleOperationId = count == 1 ? operationId.orElseThrow() : null;
}
int count() {
return count;
}
String singleOperationId() {
return singleOperationId;
}
}
}