WorkspaceContextResolver.java

package com.taxonomy.workspace.service;

import com.taxonomy.workspace.model.SystemRepository;
import com.taxonomy.workspace.model.UserWorkspace;
import com.taxonomy.workspace.repository.UserWorkspaceRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * Resolves the current workspace and repository contexts from the authenticated
 * principal and persistent workspace metadata.
 *
 * <p>The legacy {@link WorkspaceContext} is retained while existing callers are
 * migrated. New repository-sensitive code must use {@link RepositoryContext} so
 * a {@code null} workspace can never mean an unspecified global repository.</p>
 *
 * <p>Browser tabs may pin their exact workspace with
 * {@value #WORKSPACE_HEADER}. Native {@code EventSource} cannot set a custom
 * header, so SSE endpoints may use the equivalent
 * {@value #WORKSPACE_QUERY_PARAMETER} query parameter. This prevents another
 * session of the same user from silently changing the repository context of an
 * already open page.</p>
 */
@Service
public class WorkspaceContextResolver {

    /** Request header used by one browser tab to bind API calls to its workspace. */
    public static final String WORKSPACE_HEADER = "X-Taxonomy-Workspace-Id";

    /** Query parameter used only when a browser transport cannot set headers. */
    public static final String WORKSPACE_QUERY_PARAMETER = "workspaceId";

    private static final Logger log = LoggerFactory.getLogger(WorkspaceContextResolver.class);

    private final WorkspaceManager workspaceManager;
    private final SystemRepositoryService systemRepositoryService;
    private final UserWorkspaceRepository workspaceRepository;

    @Autowired
    public WorkspaceContextResolver(WorkspaceManager workspaceManager,
                                    SystemRepositoryService systemRepositoryService,
                                    UserWorkspaceRepository workspaceRepository) {
        this.workspaceManager = workspaceManager;
        this.systemRepositoryService = systemRepositoryService;
        this.workspaceRepository = workspaceRepository;
    }

    /** Compatibility constructor for focused unit tests without persistence. */
    public WorkspaceContextResolver(WorkspaceManager workspaceManager,
                                    SystemRepositoryService systemRepositoryService) {
        this(workspaceManager, systemRepositoryService, null);
    }

    /** Resolve the legacy workspace context for the currently authenticated user. */
    public WorkspaceContext resolveCurrentContext() {
        return resolveForUser(resolveUsername());
    }

    /**
     * Resolve the legacy workspace context for a specific user.
     *
     * <p>Only users with an explicitly provisioned persistent workspace receive
     * a workspace-scoped context. Callers that need repository identity must use
     * {@link #resolveRepositoryContextForUser(String)} instead.</p>
     */
    public WorkspaceContext resolveForUser(String username) {
        if (username == null || username.isBlank()
                || WorkspaceManager.DEFAULT_USER.equals(username)) {
            return WorkspaceContext.SHARED;
        }

        UserWorkspace workspace = resolveWorkspace(username);
        if (workspace != null && hasText(workspace.getWorkspaceId())) {
            String branch = hasText(workspace.getCurrentBranch())
                    ? workspace.getCurrentBranch().strip()
                    : systemRepositoryService.getSharedBranch();
            log.debug("Resolved workspace context for user '{}': workspace={}, branch={}",
                    username, workspace.getWorkspaceId(), branch);
            return new WorkspaceContext(
                    username.strip(), workspace.getWorkspaceId().strip(), branch);
        }

        log.debug("No provisioned workspace for user '{}'; falling back to SHARED", username);
        return WorkspaceContext.SHARED;
    }

    /** Resolve an explicit repository context for the authenticated user. */
    public RepositoryContext resolveCurrentRepositoryContext() {
        return resolveRepositoryContextForUser(resolveUsername());
    }

    /**
     * Resolve the selected logical repository and optional workspace for one user.
     *
     * <p>A user without a personal workspace receives an explicit read-only
     * context for the primary central repository. A legacy workspace without
     * {@code sourceRepositoryId} is assigned to that same primary repository,
     * which is the only repository such rows could originate from before the
     * multi-repository migration. The inferred provenance is persisted so
     * remaining legacy adapters cannot later resolve the same workspace against
     * an unspecified repository.</p>
     */
    public RepositoryContext resolveRepositoryContextForUser(String username) {
        String user = normalizeUsername(username);
        UserWorkspace workspace = WorkspaceManager.DEFAULT_USER.equals(user)
                ? null
                : resolveWorkspace(user);

        if (workspace != null && hasText(workspace.getWorkspaceId())) {
            SystemRepository repository = resolveWorkspaceSource(workspace);
            String branch = hasText(workspace.getCurrentBranch())
                    ? workspace.getCurrentBranch().strip()
                    : requireBranch(repository);
            RepositoryContext context = RepositoryContext.workspace(
                    requireRepositoryId(repository),
                    workspace.getWorkspaceId().strip(),
                    branch,
                    user);
            log.debug(
                    "Resolved repository context for user '{}': repository={}, workspace={}, branch={}",
                    user, context.repositoryId(), context.workspaceId(), context.branch());
            return context;
        }

        SystemRepository primary = systemRepositoryService.getPrimaryRepository();
        RepositoryContext context = RepositoryContext.centralRead(
                requireRepositoryId(primary), requireBranch(primary), user);
        log.debug(
                "Resolved central repository context for user '{}': repository={}, branch={}",
                user, context.repositoryId(), context.branch());
        return context;
    }

    private UserWorkspace resolveWorkspace(String username) {
        String requestedWorkspaceId = requestedWorkspaceId();
        if (requestedWorkspaceId == null) {
            return findWorkspace(username);
        }

        UserWorkspace workspace = workspaceManager.getWorkspaceById(requestedWorkspaceId);
        if (workspace == null
                || !hasText(workspace.getUsername())
                || !workspace.getUsername().strip().equals(username.strip())
                || workspace.isArchived()
                || workspace.isShared()) {
            throw new AccessDeniedException(
                    "Requested workspace is not available to the authenticated user");
        }
        return workspace;
    }

    private UserWorkspace findWorkspace(String username) {
        UserWorkspace workspace = workspaceManager.findActiveWorkspace(username);
        return workspace != null ? workspace : workspaceManager.findUserWorkspace(username);
    }

    private SystemRepository resolveWorkspaceSource(UserWorkspace workspace) {
        if (hasText(workspace.getSourceRepositoryId())) {
            return systemRepositoryService.getRepository(
                    workspace.getSourceRepositoryId().strip());
        }

        SystemRepository primary = systemRepositoryService.getPrimaryRepository();
        String primaryRepositoryId = requireRepositoryId(primary);
        workspace.setSourceRepositoryId(primaryRepositoryId);
        if (workspaceRepository != null) {
            workspaceRepository.save(workspace);
        }
        log.info(
                "Persisted primary repository {} as source provenance for legacy workspace {}",
                primaryRepositoryId, workspace.getWorkspaceId());
        return primary;
    }

    private static String requestedWorkspaceId() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (!(attributes instanceof ServletRequestAttributes servletAttributes)) {
            return null;
        }
        String header = servletAttributes.getRequest().getHeader(WORKSPACE_HEADER);
        if (hasText(header)) {
            return header.strip();
        }
        String parameter = servletAttributes.getRequest()
                .getParameter(WORKSPACE_QUERY_PARAMETER);
        return hasText(parameter) ? parameter.strip() : null;
    }

    private static String requireRepositoryId(SystemRepository repository) {
        if (repository == null || !hasText(repository.getRepositoryId())) {
            throw new IllegalStateException("Resolved repository has no repositoryId");
        }
        return repository.getRepositoryId().strip();
    }

    private static String requireBranch(SystemRepository repository) {
        if (repository == null || !hasText(repository.getDefaultBranch())) {
            throw new IllegalStateException("Resolved repository has no default branch");
        }
        return repository.getDefaultBranch().strip();
    }

    private static String normalizeUsername(String username) {
        return username == null || username.isBlank()
                ? WorkspaceManager.DEFAULT_USER
                : username.strip();
    }

    private static boolean hasText(String value) {
        return value != null && !value.isBlank();
    }

    private String resolveUsername() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()
                && !"anonymousUser".equals(authentication.getPrincipal())) {
            return authentication.getName();
        }
        return WorkspaceManager.DEFAULT_USER;
    }
}