SystemRepositoryService.java

package com.taxonomy.workspace.service;

import com.taxonomy.workspace.model.RepositoryLifecycleState;
import com.taxonomy.workspace.model.RepositoryOwnerType;
import com.taxonomy.workspace.model.RepositoryTopologyMode;
import com.taxonomy.workspace.model.RepositoryVisibility;
import com.taxonomy.workspace.model.SystemRepository;
import com.taxonomy.workspace.repository.SystemRepositoryRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.regex.Pattern;

/** Manages the catalog of central architecture repositories. */
@Service
public class SystemRepositoryService {

    /** Historic storage name that must remain stable for existing installations. */
    public static final String PRIMARY_STORAGE_NAME = "taxonomy-dsl";

    private static final Logger log = LoggerFactory.getLogger(SystemRepositoryService.class);
    private static final Pattern SLUG_PATTERN = Pattern.compile("[a-z0-9]+(?:-[a-z0-9]+)*");
    private static final int MAX_PROVISIONING_ERROR_LENGTH = 2000;

    private final SystemRepositoryRepository repository;

    public SystemRepositoryService(SystemRepositoryRepository repository) {
        this.repository = repository;
    }

    /**
     * Ensure a primary catalog entry exists, backfill multi-repository metadata,
     * and erase credentials persisted by releases that stored external Git tokens
     * in plaintext.
     *
     * <p>This method is invoked by {@link SystemRepositoryCatalogInitializer}
     * through the Spring-managed service proxy. Initialization therefore runs in
     * one real transaction and fails closed before later application runners use
     * repository-owned state.</p>
     */
    @Transactional
    public void ensureSystemRepository() {
        try {
            var primaryRepository = repository.findByPrimaryRepoTrue();
            if (primaryRepository.isEmpty()) {
                SystemRepository systemRepository = new SystemRepository();
                systemRepository.setRepositoryId(UUID.randomUUID().toString());
                systemRepository.setStorageRepositoryName(PRIMARY_STORAGE_NAME);
                systemRepository.setSlug("shared-architecture");
                systemRepository.setDisplayName("Shared Architecture Repository");
                systemRepository.setDescription("Default central architecture repository");
                systemRepository.setVisibility(RepositoryVisibility.ORGANIZATION);
                systemRepository.setLifecycleState(RepositoryLifecycleState.ACTIVE);
                systemRepository.setProvisioningError(null);
                systemRepository.setOwnerType(RepositoryOwnerType.SYSTEM);
                systemRepository.setOwnerId("system");
                systemRepository.setTopologyMode(RepositoryTopologyMode.INTERNAL_SHARED);
                systemRepository.setDefaultBranch("draft");
                systemRepository.setPrimaryRepo(true);
                systemRepository.setCreatedBy("system");
                systemRepository.setCreatedAt(Instant.now());
                systemRepository.setUpdatedAt(Instant.now());
                repository.save(systemRepository);
                log.info("Created primary architecture repository catalog entry "
                        + "(storage={}, topology=INTERNAL_SHARED, branch=draft)",
                        PRIMARY_STORAGE_NAME);
                return;
            }

            SystemRepository existing = primaryRepository.get();
            boolean changed = backfillPrimaryCatalogMetadata(existing);
            if (existing.hasLegacyPlaintextCredential()) {
                existing.clearLegacyPlaintextCredential();
                changed = true;
                log.warn("Removed a legacy plaintext external Git credential from the database. "
                        + "Configure TAXONOMY_EXTERNAL_GIT_TOKEN through deployment secret storage.");
            }
            if (changed) {
                existing.setUpdatedAt(Instant.now());
                repository.save(existing);
            }
        } catch (RuntimeException exception) {
            log.error("Could not initialize the architecture repository catalog safely", exception);
            throw new IllegalStateException(
                    "Architecture repository initialization or credential cleanup failed",
                    exception);
        }
    }

    /** Compatibility adapter for callers not yet migrated to explicit repository IDs. */
    public SystemRepository getPrimaryRepository() {
        return repository.findByPrimaryRepoTrue()
                .orElseThrow(() -> new IllegalStateException(
                        "No primary architecture repository configured"));
    }

    public SystemRepository getRepository(String repositoryId) {
        if (repositoryId == null || repositoryId.isBlank()) {
            throw new IllegalArgumentException("repositoryId must not be blank");
        }
        return repository.findByRepositoryId(repositoryId)
                .orElseThrow(() -> new IllegalArgumentException(
                        "Architecture repository not found: " + repositoryId));
    }

    public List<SystemRepository> listActiveRepositories() {
        return repository.findByLifecycleStateOrderByDisplayNameAsc(
                RepositoryLifecycleState.ACTIVE);
    }

    /**
     * Reserve a new central repository identity in the catalog.
     *
     * <p>The entry remains {@link RepositoryLifecycleState#PROVISIONING} until the
     * caller has created the logical JGit repository and its initial branch. A
     * failed allocation therefore remains diagnosable and can be retried or cleaned
     * explicitly instead of appearing as an active but unusable repository.</p>
     */
    @Transactional
    public SystemRepository createCentralRepository(
            String displayName,
            String requestedSlug,
            String description,
            RepositoryVisibility visibility,
            String ownerId,
            String defaultBranch) {
        String name = requireText(displayName, "displayName");
        String slug = normalizeSlug(requestedSlug != null && !requestedSlug.isBlank()
                ? requestedSlug : name);
        if (repository.findBySlug(slug).isPresent()) {
            throw new IllegalArgumentException("Repository slug already exists: " + slug);
        }

        String repositoryId = UUID.randomUUID().toString();
        String storageName = "central-" + repositoryId;
        if (repository.findByStorageRepositoryName(storageName).isPresent()) {
            throw new IllegalStateException("Generated repository storage name already exists");
        }

        String owner = requireText(ownerId, "ownerId");
        Instant now = Instant.now();
        SystemRepository architectureRepository = new SystemRepository();
        architectureRepository.setRepositoryId(repositoryId);
        architectureRepository.setStorageRepositoryName(storageName);
        architectureRepository.setSlug(slug);
        architectureRepository.setDisplayName(name);
        architectureRepository.setDescription(description);
        architectureRepository.setVisibility(
                visibility != null ? visibility : RepositoryVisibility.PRIVATE);
        architectureRepository.setLifecycleState(RepositoryLifecycleState.PROVISIONING);
        architectureRepository.setProvisioningError(null);
        architectureRepository.setOwnerType(RepositoryOwnerType.USER);
        architectureRepository.setOwnerId(owner);
        architectureRepository.setTopologyMode(RepositoryTopologyMode.INTERNAL_SHARED);
        architectureRepository.setDefaultBranch(
                defaultBranch == null || defaultBranch.isBlank() ? "draft" : defaultBranch.strip());
        architectureRepository.setPrimaryRepo(false);
        architectureRepository.setCreatedBy(owner);
        architectureRepository.setCreatedAt(now);
        architectureRepository.setUpdatedAt(now);
        return repository.save(architectureRepository);
    }

    @Transactional
    public SystemRepository createForkMetadata(
            String sourceRepositoryId,
            String sourceBranch,
            String forkPointCommit,
            String displayName,
            String slug,
            String description,
            RepositoryVisibility visibility,
            String ownerId) {
        SystemRepository source = getRepository(sourceRepositoryId);
        String branch = sourceBranch == null || sourceBranch.isBlank()
                ? source.getDefaultBranch()
                : sourceBranch.strip();
        SystemRepository fork = createCentralRepository(
                displayName,
                slug,
                description,
                visibility,
                ownerId,
                branch);
        fork.setUpstreamRepositoryId(source.getRepositoryId());
        fork.setUpstreamBranch(branch);
        fork.setForkPointCommit(forkPointCommit);
        fork.setUpdatedAt(Instant.now());
        return repository.save(fork);
    }

    @Transactional
    public SystemRepository markProvisioningReady(String repositoryId) {
        SystemRepository architectureRepository = getRepository(repositoryId);
        architectureRepository.setLifecycleState(RepositoryLifecycleState.ACTIVE);
        architectureRepository.setProvisioningError(null);
        architectureRepository.setUpdatedAt(Instant.now());
        return repository.save(architectureRepository);
    }

    @Transactional
    public SystemRepository markProvisioningFailed(String repositoryId, String reason) {
        SystemRepository architectureRepository = getRepository(repositoryId);
        architectureRepository.setLifecycleState(RepositoryLifecycleState.FAILED);
        architectureRepository.setProvisioningError(normalizeProvisioningError(reason));
        architectureRepository.setUpdatedAt(Instant.now());
        return repository.save(architectureRepository);
    }

    /** Return the configured primary branch, falling back during early startup. */
    public String getSharedBranch() {
        try {
            return getPrimaryRepository().getDefaultBranch();
        } catch (Exception exception) {
            log.debug("Primary repository not available, falling back to 'draft': {}",
                    exception.getMessage());
            return "draft";
        }
    }

    public String getDefaultBranch(String repositoryId) {
        return getRepository(repositoryId).getDefaultBranch();
    }

    @Transactional
    public SystemRepository save(SystemRepository systemRepository) {
        systemRepository.setUpdatedAt(Instant.now());
        return repository.save(systemRepository);
    }

    private boolean backfillPrimaryCatalogMetadata(SystemRepository existing) {
        boolean changed = false;
        if (existing.getStorageRepositoryName() == null
                || existing.getStorageRepositoryName().isBlank()) {
            existing.setStorageRepositoryName(PRIMARY_STORAGE_NAME);
            changed = true;
        }
        if (existing.getSlug() == null || existing.getSlug().isBlank()) {
            existing.setSlug("shared-architecture");
            changed = true;
        }
        if (existing.getVisibility() == null) {
            existing.setVisibility(RepositoryVisibility.ORGANIZATION);
            changed = true;
        }
        if (existing.getLifecycleState() == null) {
            existing.setLifecycleState(RepositoryLifecycleState.ACTIVE);
            changed = true;
        }
        if (existing.getOwnerType() == null) {
            existing.setOwnerType(RepositoryOwnerType.SYSTEM);
            changed = true;
        }
        if (existing.getOwnerId() == null || existing.getOwnerId().isBlank()) {
            existing.setOwnerId("system");
            changed = true;
        }
        if (existing.getCreatedBy() == null || existing.getCreatedBy().isBlank()) {
            existing.setCreatedBy("system");
            changed = true;
        }
        if (existing.getUpdatedAt() == null) {
            existing.setUpdatedAt(existing.getCreatedAt() != null
                    ? existing.getCreatedAt() : Instant.now());
            changed = true;
        }
        return changed;
    }

    private static String normalizeSlug(String value) {
        String normalized = requireText(value, "slug")
                .toLowerCase(Locale.ROOT)
                .replaceAll("[^a-z0-9]+", "-")
                .replaceAll("^-+|-+$", "");
        if (!SLUG_PATTERN.matcher(normalized).matches()) {
            throw new IllegalArgumentException("Repository slug is invalid: " + value);
        }
        return normalized;
    }

    private static String normalizeProvisioningError(String reason) {
        if (reason == null || reason.isBlank()) {
            return "Repository provisioning failed";
        }
        String normalized = reason.strip();
        return normalized.length() <= MAX_PROVISIONING_ERROR_LENGTH
                ? normalized
                : normalized.substring(0, MAX_PROVISIONING_ERROR_LENGTH);
    }

    private static String requireText(String value, String field) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(field + " must not be blank");
        }
        return value.strip();
    }
}