WebMvcConfig.java

package com.taxonomy.shared.config;

import com.taxonomy.versioning.controller.DslWorkspacePreResolutionInterceptor;
import com.taxonomy.workspace.service.ExplicitWorkspacePinValidationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

import java.util.Locale;

/**
 * Web MVC configuration for internationalization and request-bound workspace
 * isolation.
 *
 * <p>Locale resolution priority:
 * <ol>
 *   <li>{@code ?lang=de} query parameter (persisted to cookie)</li>
 *   <li>{@code lang} cookie</li>
 *   <li>{@code Accept-Language} header</li>
 *   <li>Fallback: English</li>
 * </ol>
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final ExplicitWorkspacePinValidationInterceptor pinValidationInterceptor;
    private final DslWorkspacePreResolutionInterceptor dslWorkspaceInterceptor;

    public WebMvcConfig(
            ExplicitWorkspacePinValidationInterceptor pinValidationInterceptor,
            DslWorkspacePreResolutionInterceptor dslWorkspaceInterceptor) {
        this.pinValidationInterceptor = pinValidationInterceptor;
        this.dslWorkspaceInterceptor = dslWorkspaceInterceptor;
    }

    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver resolver = new CookieLocaleResolver("lang");
        resolver.setDefaultLocale(Locale.ENGLISH);
        return resolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());

        // Any explicit tab pin is an authorization-sensitive request identity.
        // Validate it for every API before endpoint-specific exception handling
        // can convert a denied pin into a shared or otherwise ambiguous context.
        // Workspace switching is the recovery path from an obsolete tab pin and
        // validates the requested target independently in WorkspaceManager.
        registry.addInterceptor(pinValidationInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/workspace/*/switch");

        registry.addInterceptor(dslWorkspaceInterceptor)
                .addPathPatterns(
                        "/api/dsl/current",
                        "/api/dsl/history",
                        "/api/dsl/git/head",
                        "/api/dsl/hypotheses/**",
                        "/api/analyze",
                        "/api/search/graph",
                        "/api/projects/**",
                        "/api/solutions/**",
                        "/api/products/**");
    }
}