PortablePortfolioGitService.java

package com.taxonomy.portfolio.service;

import com.taxonomy.dsl.ast.BlockAst;
import com.taxonomy.dsl.ast.DocumentAst;
import com.taxonomy.dsl.ast.PropertyAst;
import com.taxonomy.dsl.parser.TaxDslParser;
import com.taxonomy.dsl.serializer.TaxDslSerializer;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.portfolio.model.ArchitectureProject;
import com.taxonomy.portfolio.model.ProjectRequirement;
import com.taxonomy.portfolio.model.ProjectRequirementVersion;
import com.taxonomy.portfolio.repository.ArchitectureProjectRepository;
import com.taxonomy.portfolio.repository.ProjectRequirementRepository;
import com.taxonomy.portfolio.repository.ProjectRequirementVersionRepository;
import com.taxonomy.portfolio.repository.RequirementElementMappingRepository;
import com.taxonomy.workspace.service.WorkspaceContext;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Portable Git projection variant.
 *
 * <p>Database primary keys are not part of the collaboration contract. The
 * current requirement version is represented and restored by its project-local,
 * monotonically increasing version number so the same DSL can be materialized
 * in another database or workspace. Durable solution and product decisions are
 * contributed through stable business keys as part of the same Git document.</p>
 */
@Service
@Primary
public class PortablePortfolioGitService extends PortfolioGitService {

    private static final String CURRENT_VERSION_NUMBER = "currentVersionNumber";
    private static final String DATABASE_CURRENT_VERSION_ID = "currentVersionId";

    private final ArchitectureProjectRepository projectRepository;
    private final ProjectRequirementRepository requirementRepository;
    private final ProjectRequirementVersionRepository versionRepository;
    private final PortfolioDecisionGitContributor decisionContributor;
    private final TaxDslParser parser = new TaxDslParser();
    private final TaxDslSerializer serializer = new TaxDslSerializer();

    public PortablePortfolioGitService(
            ProjectPortfolioService projectService,
            ArchitectureProjectRepository projectRepository,
            ProjectRequirementRepository requirementRepository,
            ProjectRequirementVersionRepository versionRepository,
            RequirementElementMappingRepository elementMappingRepository,
            DslGitRepositoryFactory repositoryFactory,
            PortfolioDecisionGitContributor decisionContributor) {
        super(projectService, projectRepository, requirementRepository,
                versionRepository, elementMappingRepository, repositoryFactory);
        this.projectRepository = projectRepository;
        this.requirementRepository = requirementRepository;
        this.versionRepository = versionRepository;
        this.decisionContributor = decisionContributor;
    }

    @Override
    @Transactional(readOnly = true)
    public String exportPortfolio(String username, WorkspaceContext context) {
        String requirements = addPortableCurrentVersion(
                super.exportPortfolio(username, context), username, context);
        return decisionContributor.contributeTo(requirements, username, context);
    }

    @Override
    @Transactional(readOnly = true)
    public String contributeTo(String existingDsl,
                               String username,
                               WorkspaceContext context) {
        String requirements = addPortableCurrentVersion(
                super.contributeTo(existingDsl, username, context), username, context);
        return decisionContributor.contributeTo(requirements, username, context);
    }

    @Override
    @Transactional
    public MaterializeResult materialize(String dsl,
                                         String username,
                                         WorkspaceContext context) {
        MaterializeResult result = super.materialize(dsl, username, context);
        restorePortableCurrentVersions(dsl, username, context);
        PortfolioDecisionGitContributor.DecisionMaterializeResult decisions =
                decisionContributor.materialize(dsl, username, context);
        List<String> warnings = new ArrayList<>(result.warnings());
        warnings.addAll(decisions.warnings());
        return new MaterializeResult(
                result.projectsCreated(),
                result.requirementsCreated(),
                result.versionsCreated(),
                List.copyOf(warnings));
    }

    private String addPortableCurrentVersion(String dsl,
                                             String username,
                                             WorkspaceContext context) {
        Map<String, Integer> currentVersions = currentVersionNumbers(username, context);
        DocumentAst document = parser.parse(dsl, "portfolio-portable-version.taxdsl");
        List<BlockAst> blocks = new ArrayList<>(document.getBlocks().size());
        for (BlockAst block : document.getBlocks()) {
            if (!REQUIREMENT_BLOCK.equals(block.getKind())
                    || block.getHeaderTokens().size() < 2) {
                blocks.add(block);
                continue;
            }
            String identity = composite(
                    block.getHeaderTokens().get(0), block.getHeaderTokens().get(1));
            Integer versionNumber = currentVersions.get(identity);
            if (versionNumber == null) {
                blocks.add(block);
                continue;
            }
            List<PropertyAst> properties = new ArrayList<>();
            for (PropertyAst property : block.getProperties()) {
                if (!CURRENT_VERSION_NUMBER.equals(property.key())
                        && !DATABASE_CURRENT_VERSION_ID.equals(property.key())) {
                    properties.add(property);
                }
            }
            properties.add(new PropertyAst(
                    CURRENT_VERSION_NUMBER,
                    Integer.toString(versionNumber),
                    block.getSourceLocation()));
            blocks.add(new BlockAst(
                    block.getKind(),
                    block.getHeaderTokens(),
                    properties,
                    block.getChildren(),
                    block.getExtensions(),
                    block.getSourceLocation()));
        }
        return serializer.serialize(new DocumentAst(document.getMeta(), blocks));
    }

    private Map<String, Integer> currentVersionNumbers(String username,
                                                       WorkspaceContext context) {
        Map<String, Integer> result = new HashMap<>();
        String scopeKey = PortfolioScope.key(username, context);
        for (ArchitectureProject project :
                projectRepository.findByScopeKeyOrderByUpdatedAtDesc(scopeKey)) {
            for (ProjectRequirement requirement : requirementRepository
                    .findByProjectIdAndScopeKeyOrderByRequirementKeyAsc(
                            project.getId(), scopeKey)) {
                if (requirement.getCurrentVersionId() == null) continue;
                versionRepository.findByIdAndRequirementIdAndScopeKey(
                                requirement.getCurrentVersionId(),
                                requirement.getId(),
                                scopeKey)
                        .ifPresent(version -> result.put(
                                composite(project.getProjectKey(), requirement.getRequirementKey()),
                                version.getVersionNumber()));
            }
        }
        return result;
    }

    private void restorePortableCurrentVersions(String dsl,
                                                String username,
                                                WorkspaceContext context) {
        DocumentAst document = parser.parse(dsl, "portfolio-current-version.taxdsl");
        String scopeKey = PortfolioScope.key(username, context);
        Instant now = Instant.now();
        for (BlockAst block : document.blocksOfKind(REQUIREMENT_BLOCK)) {
            if (block.getHeaderTokens().size() < 2) continue;
            Integer versionNumber = parseVersionNumber(block.property(CURRENT_VERSION_NUMBER));
            if (versionNumber == null) continue;
            String projectKey = block.getHeaderTokens().get(0);
            String requirementKey = block.getHeaderTokens().get(1);
            ArchitectureProject project = projectRepository
                    .findByScopeKeyAndProjectKeyIgnoreCase(scopeKey, projectKey)
                    .orElse(null);
            if (project == null) continue;
            ProjectRequirement requirement = requirementRepository
                    .findByProjectIdAndScopeKeyAndRequirementKeyIgnoreCase(
                            project.getId(), scopeKey, requirementKey)
                    .orElse(null);
            if (requirement == null) continue;
            ProjectRequirementVersion version = versionRepository
                    .findByRequirementIdAndScopeKeyAndVersionNumber(
                            requirement.getId(), scopeKey, versionNumber)
                    .orElse(null);
            if (version != null
                    && !Objects.equals(requirement.getCurrentVersionId(), version.getId())) {
                requirement.pointToVersion(version.getId(), now);
            }
        }
    }

    private static Integer parseVersionNumber(String value) {
        if (value == null || value.isBlank()) return null;
        try {
            int parsed = Integer.parseInt(value.strip());
            return parsed > 0 ? parsed : null;
        } catch (NumberFormatException exception) {
            return null;
        }
    }

    private static String composite(String projectKey, String requirementKey) {
        return projectKey + '\u0000' + requirementKey;
    }
}