DslOperationsFacade.java
package com.taxonomy.versioning.service;
import com.taxonomy.architecture.model.ArchitectureCommitIndex;
import com.taxonomy.architecture.model.ArchitectureDslDocument;
import com.taxonomy.architecture.repository.ArchitectureDslDocumentRepository;
import com.taxonomy.architecture.service.CommitIndexService;
import com.taxonomy.dsl.diff.ModelDiff;
import com.taxonomy.dsl.export.DslMaterializeService;
import com.taxonomy.dsl.export.TaxDslExportService;
import com.taxonomy.dsl.model.CanonicalArchitectureModel;
import com.taxonomy.dsl.storage.DslBranch;
import com.taxonomy.dsl.storage.DslCommit;
import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.dto.ElementHistoryAggregation;
import com.taxonomy.dto.ViewContext;
import com.taxonomy.workspace.service.RepositoryContext;
import com.taxonomy.workspace.service.WorkspaceArchitectureVersionPort;
import com.taxonomy.workspace.service.RepositoryStateGuard;
import com.taxonomy.workspace.service.WorkspaceContext;
import com.taxonomy.workspace.service.WorkspaceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
/**
* High-level facade that aggregates the DSL operation services.
*
* <p>Provides coarse-grained operations for Git versioning, DSL export,
* materialization, commit indexing, and conflict detection, so that
* the {@code DslApiController} does not need direct access to
* repositories or low-level services.</p>
*/
@Service
public class DslOperationsFacade {
private static final Logger log = LoggerFactory.getLogger(DslOperationsFacade.class);
private final TaxDslExportService exportService;
private final DslMaterializeService materializeService;
private final ArchitectureDslDocumentRepository documentRepository;
private final DslGitRepositoryFactory repositoryFactory;
private final CommitIndexService commitIndexService;
private final ConflictDetectionService conflictDetectionService;
private final RepositoryStateGuard stateGuard;
private final RepositoryStateService repositoryStateService;
private final WorkspaceResolver workspaceResolver;
private final WorkspaceArchitectureVersionPort editorVersions;
public DslOperationsFacade(TaxDslExportService exportService,
DslMaterializeService materializeService,
ArchitectureDslDocumentRepository documentRepository,
DslGitRepositoryFactory repositoryFactory,
CommitIndexService commitIndexService,
ConflictDetectionService conflictDetectionService,
RepositoryStateGuard stateGuard,
RepositoryStateService repositoryStateService,
WorkspaceResolver workspaceResolver, WorkspaceArchitectureVersionPort editorVersions) {
this.exportService = exportService;
this.materializeService = materializeService;
this.documentRepository = documentRepository;
this.repositoryFactory = repositoryFactory;
this.commitIndexService = commitIndexService;
this.conflictDetectionService = conflictDetectionService;
this.stateGuard = stateGuard;
this.repositoryStateService = repositoryStateService;
this.workspaceResolver = workspaceResolver;
this.editorVersions = editorVersions;
}
private DslGitRepository resolveRepository() {
return repositoryFactory.resolveRepository(resolveRepositoryContext());
}
private DslGitRepository resolveRepository(WorkspaceContext workspaceContext) {
return repositoryFactory.resolveRepository(workspaceContext);
}
/**
* Resolves the request-bound workspace after eager provisioning.
*
* <p>This deliberately fails closed. Falling back to the shared repository
* after a resolver, persistence or provisioning failure could make a read,
* commit, merge or index operation cross the authenticated user's workspace
* boundary.</p>
*/
private WorkspaceContext resolveContext() {
String username = workspaceResolver.resolveCurrentUsername();
repositoryStateService.ensureWorkspaceState(username);
WorkspaceContext context = workspaceResolver.resolveCurrentContext();
if (context == null) {
throw new IllegalStateException("Workspace context resolver returned null");
}
return context;
}
/** Resolve the mandatory logical repository identity for this request. */
private RepositoryContext resolveRepositoryContext() {
String username = workspaceResolver.resolveCurrentUsername();
repositoryStateService.ensureWorkspaceState(username);
RepositoryContext context = workspaceResolver.resolveCurrentRepositoryContext();
if (context == null) {
throw new IllegalStateException("Repository context resolver returned null");
}
return context;
}
// ── Export ───────────────────────────────────────────────────────
public String exportAll(String namespace) {
return exportService.exportAll(namespace);
}
public CanonicalArchitectureModel buildCanonicalModel() {
return exportService.buildCanonicalModel();
}
// ── Materialization ─────────────────────────────────────────────
public DslMaterializeService.MaterializeResult materialize(
String dslText, String path, String branch, String commitId) {
return materializeService.materialize(dslText, path, branch, commitId);
}
public DslMaterializeService.MaterializeResult materializeIncremental(
Long beforeDocId, Long afterDocId) {
return materializeService.materializeIncremental(beforeDocId, afterDocId);
}
public Optional<ArchitectureDslDocument> findDocumentById(Long id) {
return documentRepository.findById(id);
}
// ── Git operations ──────────────────────────────────────────────
public String commitDsl(String branch, String dslText, String author, String message)
throws IOException {
return version(branch, message, () -> resolveRepository().commitDsl(branch, dslText, author, message));
}
protected <T> T version(String branch, String rationale, WorkspaceArchitectureVersionPort.GitAction<T> action) throws IOException {
RepositoryContext context = resolveRepositoryContext();
RepositoryContext selected = new RepositoryContext(context.repositoryId(), context.workspaceId(),
branch, context.username(), context.scope());
return editorVersions.version(selected, versionRationale(rationale), action);
}
/** The bounded journal rationale is a summary; the Git action retains its complete original message. */
private static String versionRationale(String message) {
if (message == null) return "Create architecture version";
String normalized = message.codePoints()
.map(codePoint -> Character.isISOControl(codePoint) || Character.isWhitespace(codePoint) ? ' ' : codePoint)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString().strip().replaceAll(" +", " ");
if (normalized.isEmpty()) return "Create architecture version";
if (normalized.length() <= 1000) return normalized;
int end = Character.isHighSurrogate(normalized.charAt(999)) ? 999 : 1000;
return normalized.substring(0, end).stripTrailing();
}
public boolean isDatabaseBacked() {
return resolveRepository().isDatabaseBacked();
}
public List<DslCommit> getDslHistory(String branch) throws IOException {
return resolveRepository().getDslHistory(branch);
}
public List<DslCommit> getDslHistory(
String branch, WorkspaceContext workspaceContext) throws IOException {
return resolveRepository(workspaceContext).getDslHistory(branch);
}
public Optional<Long> findDocumentIdByCommitId(String commitId) {
return documentRepository.findByCommitId(commitId)
.map(ArchitectureDslDocument::getId);
}
public ModelDiff diffBetween(String beforeId, String afterId) throws Exception {
if (looksLikeGitSha(beforeId) && looksLikeGitSha(afterId)) {
return resolveRepository().diffBetween(beforeId, afterId);
}
return materializeService.diffDocuments(
Long.valueOf(beforeId), Long.valueOf(afterId));
}
public String textDiff(String beforeId, String afterId) throws Exception {
return resolveRepository().textDiff(beforeId, afterId);
}
public List<DslBranch> listBranches() throws IOException {
return resolveRepository().listBranches();
}
public String createBranch(String name, String fromBranch) throws IOException {
return version(fromBranch, "Create architecture branch " + name, () -> resolveRepository().createBranch(name, fromBranch));
}
public String cherryPick(String commitId, String targetBranch) throws IOException {
return version(targetBranch, "Cherry-pick architecture version", () -> resolveRepository().cherryPick(commitId, targetBranch));
}
public String merge(String fromBranch, String intoBranch) throws IOException {
return mergeVersion(fromBranch, intoBranch, () -> resolveRepository().merge(fromBranch, intoBranch));
}
protected <T> T mergeVersion(String fromBranch, String intoBranch, WorkspaceArchitectureVersionPort.GitAction<T> action) throws IOException {
if (fromBranch.equals(intoBranch)) return version(intoBranch, "Merge architecture versions", action);
String first = fromBranch.compareTo(intoBranch) <= 0 ? fromBranch : intoBranch;
String second = first.equals(fromBranch) ? intoBranch : fromBranch;
return version(first, "Merge architecture versions", () -> version(second, "Merge architecture versions", action));
}
public String revert(String commitId, String branch) throws IOException {
return version(branch, "Revert architecture version", () -> resolveRepository().revert(commitId, branch));
}
public String undoLast(String branch) throws IOException {
return version(branch, "Revert latest architecture version", () -> resolveRepository().undoLast(branch));
}
public String restore(String commitId, String branch) throws Exception {
return version(branch, "Restore architecture as a new version", () -> {
try { return resolveRepository().restore(commitId, branch); }
catch (IOException | RuntimeException failure) { throw failure; }
catch (Exception failure) { throw new IOException("Architecture restore failed", failure); }
});
}
public boolean deleteBranch(String name) throws IOException {
return resolveRepository().deleteBranch(name);
}
public String getDslAtHead(String branch) throws IOException {
return resolveRepository().getDslAtHead(branch);
}
public String getDslAtHead(
String branch, WorkspaceContext workspaceContext) throws IOException {
return resolveRepository(workspaceContext).getDslAtHead(branch);
}
public String getDslAtCommit(String commitId) throws Exception {
return resolveRepository().getDslAtCommit(commitId);
}
public String getHeadCommit(String branch) throws IOException {
return resolveRepository().getHeadCommit(branch);
}
// ── Conflict detection ──────────────────────────────────────────
public ConflictDetectionService.MergePreview previewMerge(String from, String into) {
return conflictDetectionService.previewMerge(from, into, resolveContext());
}
public ConflictDetectionService.CherryPickPreview previewCherryPick(
String commitId, String targetBranch) {
return conflictDetectionService.previewCherryPick(
commitId, targetBranch, resolveContext());
}
public ConflictDetectionService.ConflictDetails getMergeConflictDetails(
String from, String into) {
return conflictDetectionService.getMergeConflictDetails(
from, into, resolveContext());
}
public ConflictDetectionService.ConflictDetails getCherryPickConflictDetails(
String commitId, String targetBranch) {
return conflictDetectionService.getCherryPickConflictDetails(
commitId, targetBranch, resolveContext());
}
// ── View context & state guard ──────────────────────────────────
public ViewContext getViewContext(String branch) {
return repositoryStateService.getViewContext(
workspaceResolver.resolveCurrentUsername(), branch, resolveContext());
}
public ViewContext getViewContext(
String username,
String branch,
WorkspaceContext workspaceContext) {
return repositoryStateService.getViewContext(
username, branch, workspaceContext);
}
public RepositoryStateGuard.OperationCheck checkWriteOperation(
String branch, String operationType) {
return stateGuard.checkWriteOperation(
workspaceResolver.resolveCurrentUsername(), branch, operationType);
}
public String resolveCurrentUsername() {
return workspaceResolver.resolveCurrentUsername();
}
public String resolveWorkspaceBranch(String username) {
return repositoryStateService.resolveWorkspaceBranch(username);
}
// ── Document listing ────────────────────────────────────────────
public List<ArchitectureDslDocument> listDocuments() {
return documentRepository.findAll();
}
// ── History search ──────────────────────────────────────────────
public int indexBranch(String branch) {
return commitIndexService.indexBranch(branch, resolveRepositoryContext());
}
public int rebuildHistoryBranch(String branch) {
return commitIndexService.rebuildBranch(branch, resolveRepositoryContext());
}
public int purgeHistoryIndex() {
return commitIndexService.purge(resolveRepositoryContext());
}
public List<ArchitectureCommitIndex> searchHistory(
String query, int maxResults) {
return commitIndexService.search(
query, maxResults, resolveRepositoryContext());
}
public List<ArchitectureCommitIndex> findByElement(String elementId) {
return commitIndexService.findByElement(
elementId, resolveRepositoryContext());
}
public List<ArchitectureCommitIndex> findByRelation(String key) {
return commitIndexService.findByRelation(
key, resolveRepositoryContext());
}
public ElementHistoryAggregation aggregateElementHistory(String elementId) {
return commitIndexService.aggregateElementHistory(
elementId, resolveRepositoryContext());
}
private static boolean looksLikeGitSha(String value) {
return value != null
&& value.length() == 40
&& value.matches("[0-9a-f]+");
}
}