DslGitRepositoryFactory.java
package com.taxonomy.dsl.storage;
import com.taxonomy.workspace.service.WorkspaceContext;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Factory for creating and caching {@link DslGitRepository} instances.
*
* <p>Provides per-workspace Git repository isolation by creating logically
* separate repositories (each with its own {@code repositoryName}) in the
* same database. The system repository (shared "draft" branch, legacy data)
* uses the well-known name {@code "taxonomy-dsl"}.
*
* <p>Instances are cached by repository name so that repeated lookups for
* the same workspace return the same {@link DslGitRepository} object.
*
* <p>This class is instantiated as a Spring {@code @Bean} in
* {@link DslStorageConfig} — do not add {@code @Service} or
* {@code @Component} here to avoid duplicate bean registration.
*/
public class DslGitRepositoryFactory {
private static final Logger log = LoggerFactory.getLogger(DslGitRepositoryFactory.class);
/** The well-known repository name for the system (shared) repository. */
static final String SYSTEM_REPO_NAME = "taxonomy-dsl";
/** Prefix for workspace repository names. */
static final String WORKSPACE_REPO_PREFIX = "ws-";
private final SessionFactory sessionFactory;
private final ConcurrentMap<String, DslGitRepository> cache = new ConcurrentHashMap<>();
public DslGitRepositoryFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
/**
* Get the system repository (shared "draft" branch, legacy data).
*
* @return the system-wide shared DslGitRepository
*/
public DslGitRepository getSystemRepository() {
return cache.computeIfAbsent(SYSTEM_REPO_NAME, this::createRepository);
}
/**
* Get a per-workspace repository with a logically separate Git namespace.
*
* <p>When a workspace repository is created for the first time it is
* <em>seeded</em> from the system repository's {@code draft} branch.
* This mirrors the {@code git clone} workflow: the initial taxonomy
* import commit that the bootstrap created on the system repo is copied
* into the workspace so that the user starts with a non-empty history.
*
* @param workspaceId the workspace identifier
* @return the workspace-specific DslGitRepository
*/
public DslGitRepository getWorkspaceRepository(String workspaceId) {
String repoName = WORKSPACE_REPO_PREFIX + workspaceId;
return cache.computeIfAbsent(repoName, name -> {
DslGitRepository repo = createRepository(name);
seedFromSystemRepo(repo);
return repo;
});
}
/**
* Resolve the appropriate repository based on a {@link WorkspaceContext}.
*
* <p>If the context is {@code null} or has no workspace ID (shared context),
* the system repository is returned. Otherwise, the per-workspace repository
* for the given workspace ID is returned.
*
* @param ctx the workspace context (may be null)
* @return the resolved DslGitRepository
*/
public DslGitRepository resolveRepository(WorkspaceContext ctx) {
if (ctx == null || ctx.workspaceId() == null) {
return getSystemRepository();
}
return getWorkspaceRepository(ctx.workspaceId());
}
/**
* Evict a workspace repository from the cache.
*
* <p>Call this when a workspace is deleted to free the cached instance.
*
* @param workspaceId the workspace identifier to evict
*/
public void evict(String workspaceId) {
cache.remove(WORKSPACE_REPO_PREFIX + workspaceId);
}
/**
* Create a new DslGitRepository for the given name.
*
* <p>If a {@link SessionFactory} is available, creates a database-backed
* repository. Otherwise (e.g. in tests), creates an in-memory repository.
*
* @param name the logical repository name
* @return a new DslGitRepository instance
*/
protected DslGitRepository createRepository(String name) {
if (sessionFactory != null) {
log.info("Creating database-backed DslGitRepository '{}'", name);
return new DslGitRepository(sessionFactory, name);
}
log.info("Creating in-memory DslGitRepository '{}' (test mode)", name);
return new DslGitRepository(
new InMemoryRepository(new DfsRepositoryDescription(name)));
}
/**
* Seed a newly created workspace repository from the system repository.
*
* <p>Reads the DSL content at the HEAD of the system repo's {@code draft}
* branch and commits it as the initial commit on the workspace repo's
* {@code draft} branch. This is the equivalent of {@code git clone}:
* every workspace starts with the taxonomy import that the bootstrap
* created on the central repository.
*
* <p>If the system repository has no {@code draft} branch yet (e.g. during
* early bootstrap), the seeding is silently skipped — the workspace will
* be populated once the user makes their first commit.
*
* @param workspace the newly created workspace repository to seed
*/
private void seedFromSystemRepo(DslGitRepository workspace) {
try {
DslGitRepository system = getSystemRepository();
String dsl = system.getDslAtHead("draft");
if (dsl != null && !dsl.isBlank()) {
workspace.commitDsl("draft", dsl, "system",
"Initial clone from system repository");
log.info("Seeded workspace repo from system repo draft branch");
} else {
log.debug("System repo draft branch is empty — skipping workspace seed");
}
} catch (IOException e) {
log.warn("Failed to seed workspace repo from system repo: {}", e.getMessage());
}
}
}