diff --git a/docs/multi-workspace-support/mws-README.md b/docs/multi-workspace-support/mws-README.md
new file mode 100644
index 000000000..e639af0c7
--- /dev/null
+++ b/docs/multi-workspace-support/mws-README.md
@@ -0,0 +1,91 @@
+# Implementing Multi-Project Workspace Support for Current Language Server
+
+The current WSO2 Micro Integrator (MI) Language Server typically serves each MI project with a separate language server instance when multiple projects exist in a workspace. This limits scalability, increases resource overhead, and breaks true multi-folder workspace experiences.
+
+## Overview
+
+
+
+### 1. Core XML Validation & Schema Isolation **(Completed)**
+
+### 2. Eliminating Global State, Singletons & Context-Aware
+
+### 3. Language Client (VS Code Extension) Integration
+
+
+---
+
+## Stage 1 Completed: LemMinX File Associations for Workspace Schema Validation
+
+### Technical Overview
+Introduced `File Associations` to replace the legacy namespace-based `catalog` implementation. Instead of relying on
+rigid catalogs, this approach maps specific file path patterns (e.g., glob matches for a project folder) to a specific target schema path. This enables different schemas (e.g., MI 4.3.0 and 4.4.0) to be applied to different projects simultaneously without conflict in a single LemMinX instance.
+
+### Changelog & Implementation Details
+
+#### 1. `Utils.java` (`org.eclipse.lemminx/customservice/synapse/utils/Utils.java`)
+* **Implementation of `updateSynapseFileAssociationSettings` Method:**
+ * Previously, the system relied on `updateSynapseCatalogSettings`.
+ * Added this new functionality to support File Association within the language server.
+
+#### 2. `XMLLanguageServer.java` (`org.eclipse.lemminx/XMLLanguageServer.java`)
+* **Initialization Mode Selection:** The language server now dynamically determines the validation method during the `initialize` step by reading the `useAssociationSettings` flag from `initializationOptions`:
+ * **If `true` (Default):** The server invokes `Utils.updateSynapseFileAssociationSettings`, utilizing the new core standard based on file associations.
+ * **If `false`:** The server routes to `Utils.updateSynapseCatalogSettings` to process legacy catalog-based configurations.
+ * **If `null`/Undefined:** The server defaults to the file association method (`true`).
+* **Temporary Bridge:**
+ * Because `SynapseLanguageService` (Stage 2) is not yet fully isolated for multi-root awareness, a temporary bridge was established.
+ * It sets the default Path to the first project in the collection: `synapseLanguageService.setSynapseXSDPath(workspaceSchemas.values().iterator().next());`
+
+#### 3. `XMLWorkspaceService.java` (`org.eclipse.lemminx/XMLWorkspaceService.java`)
+* **Enhanced `didChangeWorkspaceFolders` Logic:**
+ * Updated to capture dynamic workspace events correctly.
+ * If a user adds a new project to the workspace after server initialization, it accurately intercepts the action and generates/applies XSD schemas for the new folder.
+
+#### 4. `CleanMultiRootValidationTest.java` (`.../extensions/contentmodel/CleanMultiRootValidationTest.java`)
+Established three comprehensive multi-root tests demonstrating core functionality:
+1. **Multi-Root Isolation:** Verifies that a single Language Server successfully provides isolated validations to two independent projects governed by distinct XSD files.
+2. **Dynamic Connector Generation Test:** Ensures that dynamically generated connector schemas are instantly picked up by the validation engine logic, operating independently of server restarts.
+3. **Dynamic Workspace Handling:** Asserts that when an entirely new project is dynamically appended to the workspace context at runtime, the language server successfully triggers its standard MI validations for the newly tracked space.
+---
+## How to Test
+
+#### Testing via VS Code Extension
+To verify the multi-root support directly within the VS Code environment:
+1. **Build the Project**: Run the following command from the root directory to generate the server JAR:
+ ```bash
+ mvn clean install -DskipTests
+ ```
+2. **Update Extension Binary**: Navigate to the `target/` directory, locate the newly built JAR, and copy it into the `ls/` folder of your VS Code extension installation (replacing the existing JAR).
+3. **Configure Settings Toggle**: When testing via the extension, you can toggle between the old and new logic by passing `useAssociationSettings` within your Language Server's `initializationOptions`.
+ * **Default Behavior**: If the `useAssociationSettings` field is **not mentioned** (omitted), the server will automatically default to `true` and work with the new **File Association** logic.
+ * If explicitly set to `true`, the language server continues to use the multi-root `fileAssociation` logic.
+ * If explicitly set to `false`, the language server natively falls back to utilizing the legacy single-project `catalog` settings extraction logic.
+
+#### Standalone Test Execution
+For automated testing without the IDE:
+1. **Navigate to Root**:
+ ```bash
+ cd mi-language-server
+ ```
+2. **Run Targeted Tests**: Execute the specialized multi-root validation suite using Maven:
+ ```bash
+ mvn -Dtest=CleanMultiRootValidationTest test
+ ```
+
+---
+
+## Next Steps
+
+### Eliminating Global State, Singletons & Context-Aware
+Transition legacy singletons (e.g., `ConnectorHolder`, `SynapseLanguageService`, Mediator Handlers ....) to be
+resolved per project context instead of a global state.
+
+* Introduce `ProjectContext` and `WorkspaceManager` classes to manage memory scoped to individual projects.
+ * *Reference Files:* `multi-workspace-support/resources/ProjectContext.java`, `multi-workspace-support/resources/WorkspaceManager.java`
+
+* Isolate Language Server features (Auto-Complete, Go-To-Definition) per workspace.
+
+
+### Language Client (VS Code Extension) Integration
+Update the frontend VS Code Extension to natively support the multi-project backend API configurations and event hooks.
diff --git a/docs/multi-workspace-support/resources/ProjectContext.java b/docs/multi-workspace-support/resources/ProjectContext.java
new file mode 100644
index 000000000..e3fe99177
--- /dev/null
+++ b/docs/multi-workspace-support/resources/ProjectContext.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.eclipse.lemminx.customservice.synapse;
+
+import org.eclipse.lemminx.customservice.SynapseLanguageClientAPI;
+import org.eclipse.lemminx.customservice.synapse.connectors.AbstractConnectorLoader;
+import org.eclipse.lemminx.customservice.synapse.connectors.ConnectionHandler;
+import org.eclipse.lemminx.customservice.synapse.connectors.ConnectorHolder;
+import org.eclipse.lemminx.customservice.synapse.connectors.NewProjectConnectorLoader;
+import org.eclipse.lemminx.customservice.synapse.connectors.OldProjectConnectorLoader;
+import org.eclipse.lemminx.customservice.synapse.expression.ExpressionHelperProvider;
+import org.eclipse.lemminx.customservice.synapse.inbound.conector.InboundConnectorHolder;
+import org.eclipse.lemminx.customservice.synapse.mediatorService.MediatorHandler;
+import org.eclipse.lemminx.customservice.synapse.resourceFinder.AbstractResourceFinder;
+import org.eclipse.lemminx.customservice.synapse.resourceFinder.ResourceFinderFactory;
+import org.eclipse.lemminx.customservice.synapse.utils.Utils;
+
+import java.nio.file.Path;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Holds all per-project state required by the MI Language Server for a single
+ * workspace folder/project. In a Multi-Root Workspace scenario, one instance
+ * of {@code ProjectContext} is created per open project root, and all language
+ * features (completions, hover, validation, connectors, etc.) are resolved
+ * through the context that corresponds to the document being processed.
+ *
+ *
Instances are stored in a map keyed by the project root URI inside the
+ * language server (e.g. {@code SynapseLanguageClientAPI} or its successor
+ * manager class) so that requests for any document are dispatched to the
+ * correct context.
+ *
+ *
Immutable identity fields (projectUri, isLegacyProject, projectServerVersion)
+ * are set in the constructor and cannot be changed afterwards. All service
+ * handler fields are eagerly initialized via the {@link #initProject} method,
+ * which must be called immediately after construction. No setters are provided
+ * — Early Initialization ensures everything is ready before the context is
+ * used, eliminating loading delays during coding.
+ *
+ *
Note: The {@code TryOutManager} is intentionally excluded from
+ * this class. It manages a heavy background MI Server process that binds to
+ * a specific network port, so only one instance can run at a time across all
+ * projects. It remains a separate global concern managed by
+ * {@code SynapseLanguageService}.
+ */
+public class ProjectContext {
+
+ private static final Logger log = Logger.getLogger(ProjectContext.class.getName());
+
+ /**
+ * Tracks whether {@link #initProject} has completed successfully.
+ * Used by service-handler getters to fail fast with a clear message
+ * instead of returning {@code null}.
+ */
+ private boolean initialized = false;
+
+ // -------------------------------------------------------------------------
+ // Identity fields — set once at construction time and never changed.
+ // -------------------------------------------------------------------------
+
+ /**
+ * The root folder URI of this project (e.g. {@code file:///Users/.../ProjectA}).
+ * Used as the primary key when looking up the context for a given document URI.
+ */
+ private final String projectUri;
+
+ /**
+ * Whether this project is a legacy (state-machine-based) MI project.
+ * Legacy projects use a different activation and completion pathway compared
+ * to modern MI projects.
+ */
+ private final boolean isLegacyProject;
+
+ /**
+ * The WSO2 MI version string associated with this project
+ * (e.g. {@code "4.3.0"}, {@code "4.4.0"}). Used to select the correct
+ * XSD schemas, mediator descriptors, and feature toggles.
+ */
+ private final String projectServerVersion;
+
+ // -------------------------------------------------------------------------
+ // Schema field — resolved during initProject().
+ // -------------------------------------------------------------------------
+
+ /**
+ * Path to the extracted root {@code synapse_config.xsd} for this specific
+ * project. Resolved during {@link #initProject} by extracting the
+ * version-specific XSD bundle for this project.
+ */
+ private Path synapseXsdPath;
+
+ // -------------------------------------------------------------------------
+ // Connector fields — eagerly initialized in the constructor.
+ // -------------------------------------------------------------------------
+
+ /**
+ * Holds metadata and descriptors for all regular (outbound) connectors
+ * discovered for this project. Initialized eagerly so that connector
+ * scanning can populate it immediately after construction.
+ */
+ private final ConnectorHolder connectorHolder;
+
+ /**
+ * Holds metadata and descriptors for all inbound connectors discovered
+ * for this project. Initialized eagerly alongside {@link #connectorHolder}.
+ */
+ private final InboundConnectorHolder inboundConnectorHolder;
+
+ // -------------------------------------------------------------------------
+ // Service handler fields — eagerly initialized via initProject().
+ // -------------------------------------------------------------------------
+
+ /**
+ * Responsible for loading and refreshing connectors from the project's
+ * connector directory. The concrete type (Old vs New) depends on
+ * {@link #isLegacyProject}.
+ */
+ private AbstractConnectorLoader connectorLoader;
+
+ /**
+ * Handles completion proposals and hover information for Synapse mediators
+ * within this project. Depends on {@link #projectServerVersion} to load
+ * the correct mediator descriptor set.
+ */
+ private MediatorHandler mediatorHandler;
+
+ /**
+ * Provides completion and documentation support for {@code ${}} expression
+ * syntax (e.g. payload-factory, data-mapper expressions) within this project.
+ */
+ private ExpressionHelperProvider expressionHelperProvider;
+
+ /**
+ * Manages named connection artifacts (e.g. connector local-entries) for
+ * this project, enabling connection-aware completions and validations.
+ */
+ private ConnectionHandler connectionHandler;
+
+ /**
+ * Locates and resolves project-internal resources (endpoints, sequences,
+ * message-stores, etc.) referenced by documents in this project.
+ */
+ private AbstractResourceFinder resourceFinder;
+
+ // -------------------------------------------------------------------------
+ // Constructor
+ // -------------------------------------------------------------------------
+
+ /**
+ * Creates a new {@code ProjectContext} for the given project root.
+ *
+ *
The {@link ConnectorHolder} and {@link InboundConnectorHolder} are
+ * created eagerly so that connector-scanning routines can start populating
+ * them immediately. All other service handlers remain {@code null} until
+ * {@link #initProject} is called.
+ *
+ * @param projectUri root folder URI of the project
+ * (e.g. {@code "file:///Users/.../ProjectA"})
+ * @param isLegacyProject {@code true} if this is a state-machine based
+ * (legacy) MI project
+ * @param projectServerVersion the MI version string for this project
+ * (e.g. {@code "4.3.0"})
+ */
+ public ProjectContext(String projectUri, boolean isLegacyProject, String projectServerVersion) {
+
+ this.projectUri = projectUri;
+ this.isLegacyProject = isLegacyProject;
+ this.projectServerVersion = projectServerVersion;
+
+ // Eagerly initialize connector holders so scanning can begin immediately.
+ this.connectorHolder = ConnectorHolder.getInstance();
+ this.inboundConnectorHolder = new InboundConnectorHolder();
+ }
+
+ // -------------------------------------------------------------------------
+ // Early Initialization
+ // -------------------------------------------------------------------------
+
+ /**
+ * Eagerly initializes all service handlers for this project in the correct
+ * dependency order, mirroring the initialization sequence from
+ * {@code SynapseLanguageService.init(...)}.
+ *
+ *
This method must be called exactly once, immediately after construction,
+ * before the context is registered for use. After this call returns
+ * successfully, every getter is guaranteed to return a non-null, fully
+ * initialized instance — eliminating any lazy-loading delays during coding.
+ *
+ *
+ *
+ *
+ * @param miServerPath absolute path to the local MI server installation
+ * @param languageClient the language-client proxy for sending notifications
+ * back to the IDE
+ * @throws Exception if any step in the initialization pipeline fails
+ */
+ public void initProject(String miServerPath, SynapseLanguageClientAPI languageClient) throws Exception {
+
+ log.log(Level.INFO, "Initializing ProjectContext for: " + projectUri);
+
+ // 1. Initialize inbound connector metadata.
+ inboundConnectorHolder.init(projectUri, projectServerVersion);
+
+ // 2. Instantiate the correct connector loader based on project type.
+ if (isLegacyProject) {
+ this.connectorLoader = new OldProjectConnectorLoader(languageClient, connectorHolder);
+ } else {
+ this.connectorLoader = new NewProjectConnectorLoader(languageClient, connectorHolder,
+ inboundConnectorHolder);
+ }
+ connectorLoader.init(projectUri);
+
+ // 3. Initialize the mediator handler with version-specific descriptors.
+ this.mediatorHandler = new MediatorHandler();
+ mediatorHandler.init(projectUri, projectServerVersion, connectorHolder);
+
+ // 4. Initialize the connection handler.
+ this.connectionHandler = new ConnectionHandler();
+ connectionHandler.init(connectorHolder);
+
+ // 5. Create the expression helper provider.
+ this.expressionHelperProvider = new ExpressionHelperProvider(projectUri);
+
+ // 6. Create and load the resource finder.
+ this.resourceFinder = ResourceFinderFactory.getResourceFinder(isLegacyProject);
+ try {
+ resourceFinder.loadDependentResources(projectUri);
+ } catch (Exception e) {
+ log.log(Level.SEVERE, "Failed to initialize ProjectContext for: " + projectUri + ". Error: " + e.getMessage());
+ }
+
+ // 7. Resolve the synapse XSD path for this project's MI version.
+ this.synapseXsdPath = Utils.copyXSDFiles(projectUri);
+
+ this.initialized = true;
+ log.log(Level.INFO, "ProjectContext initialized successfully for: " + projectUri);
+ }
+
+ // -------------------------------------------------------------------------
+ // Getters — identity fields
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the root folder URI of this project.
+ *
+ * @return the project root URI (never {@code null})
+ */
+ public String getProjectUri() {
+ return projectUri;
+ }
+
+ /**
+ * Returns whether this is a legacy (state-machine-based) MI project.
+ *
+ * @return {@code true} for legacy projects
+ */
+ public boolean isLegacyProject() {
+ return isLegacyProject;
+ }
+
+ /**
+ * Returns the WSO2 MI version string associated with this project.
+ *
+ * @return the project server version (e.g. {@code "4.3.0"})
+ */
+ public String getProjectServerVersion() {
+ return projectServerVersion;
+ }
+
+ // -------------------------------------------------------------------------
+ // Getter — synapseXsdPath (no setter; resolved in initProject())
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the path to the extracted root {@code synapse_config.xsd} for
+ * this project.
+ *
+ * @return the XSD path (non-null after {@link #initProject})
+ * @throws IllegalStateException if {@link #initProject} has not been called
+ */
+ public Path getSynapseXsdPath() {
+ checkInitialized();
+ return synapseXsdPath;
+ }
+
+ // -------------------------------------------------------------------------
+ // Getters — connector holders (no setters; initialized in constructor)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the {@link ConnectorHolder} for this project's regular (outbound)
+ * connectors.
+ *
+ * @return the connector holder (never {@code null})
+ */
+ public ConnectorHolder getConnectorHolder() {
+ return connectorHolder;
+ }
+
+ /**
+ * Returns the {@link InboundConnectorHolder} for this project's inbound
+ * connectors.
+ *
+ * @return the inbound connector holder (never {@code null})
+ */
+ public InboundConnectorHolder getInboundConnectorHolder() {
+ return inboundConnectorHolder;
+ }
+
+ // -------------------------------------------------------------------------
+ // Getters — service handlers (no setters; initialized in initProject())
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the {@link AbstractConnectorLoader} responsible for loading
+ * connectors for this project.
+ *
+ * @return the connector loader (non-null after {@link #initProject})
+ * @throws IllegalStateException if {@link #initProject} has not been called
+ */
+ public AbstractConnectorLoader getConnectorLoader() {
+ checkInitialized();
+ return connectorLoader;
+ }
+
+ /**
+ * Returns the {@link MediatorHandler} that provides completion and hover
+ * support for Synapse mediators in this project.
+ *
+ * @return the mediator handler (non-null after {@link #initProject})
+ * @throws IllegalStateException if {@link #initProject} has not been called
+ */
+ public MediatorHandler getMediatorHandler() {
+ checkInitialized();
+ return mediatorHandler;
+ }
+
+ /**
+ * Returns the {@link ExpressionHelperProvider} that handles {@code ${}}
+ * expression completions and documentation for this project.
+ *
+ * @return the expression helper provider (non-null after {@link #initProject})
+ * @throws IllegalStateException if {@link #initProject} has not been called
+ */
+ public ExpressionHelperProvider getExpressionHelperProvider() {
+ checkInitialized();
+ return expressionHelperProvider;
+ }
+
+ /**
+ * Returns the {@link ConnectionHandler} that manages named connections for
+ * this project.
+ *
+ * @return the connection handler (non-null after {@link #initProject})
+ * @throws IllegalStateException if {@link #initProject} has not been called
+ */
+ public ConnectionHandler getConnectionHandler() {
+ checkInitialized();
+ return connectionHandler;
+ }
+
+ /**
+ * Returns the {@link AbstractResourceFinder} that locates project-internal
+ * resources (endpoints, sequences, etc.) for this project.
+ *
+ * @return the resource finder (non-null after {@link #initProject})
+ * @throws IllegalStateException if {@link #initProject} has not been called
+ */
+ public AbstractResourceFinder getResourceFinder() {
+ checkInitialized();
+ return resourceFinder;
+ }
+
+ // -------------------------------------------------------------------------
+ // Internal helpers
+ // -------------------------------------------------------------------------
+
+ /**
+ * Throws {@link IllegalStateException} if {@link #initProject} has not
+ * been called yet. Guards service-handler getters so that callers get a
+ * clear error message instead of a downstream {@code NullPointerException}.
+ */
+ private void checkInitialized() {
+ if (!initialized) {
+ throw new IllegalStateException(
+ "ProjectContext not initialized. Call initProject() first. Project: " + projectUri);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Object overrides
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns a human-readable representation of this context, primarily for
+ * logging and debugging purposes.
+ *
+ * @return a string in the format {@code ProjectContext{uri=..., version=..., legacy=...}}
+ */
+ @Override
+ public String toString() {
+ return "ProjectContext{" +
+ "projectUri='" + projectUri + '\'' +
+ ", projectServerVersion='" + projectServerVersion + '\'' +
+ ", isLegacyProject=" + isLegacyProject +
+ '}';
+ }
+}
diff --git a/docs/multi-workspace-support/resources/WorkspaceManager.java b/docs/multi-workspace-support/resources/WorkspaceManager.java
new file mode 100644
index 000000000..b36216f40
--- /dev/null
+++ b/docs/multi-workspace-support/resources/WorkspaceManager.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.eclipse.lemminx.customservice.synapse;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Central registry that maps open workspace folder URIs to their isolated
+ * {@link ProjectContext} instances.
+ *
+ *
In a Multi-Root Workspace scenario, VS Code may have several MI projects
+ * open simultaneously, each with its own MI version, connectors, and handlers.
+ * {@code WorkspaceManager} is the single source of truth for resolving:
+ *
+ * document URI → correct {@link ProjectContext}
+ *
+ *
+ *
All operations are thread-safe; the underlying map is a
+ * {@link ConcurrentHashMap} so concurrent LSP request threads can look up
+ * contexts without external synchronization.
+ *
+ *
URI contract: All URIs stored in and passed to this class must be
+ * in normalized {@code file:///} format (e.g.
+ * {@code file:///Users/me/ProjectA}). Callers are responsible for normalizing
+ * URIs before invoking any method.
+ */
+public class WorkspaceManager {
+
+ private static final Logger log = Logger.getLogger(WorkspaceManager.class.getName());
+
+ /**
+ * Map from project root URI to the {@link ProjectContext} for that project.
+ * Uses {@link ConcurrentHashMap} for lock-free, thread-safe reads.
+ *
+ *
Key: normalized project root URI (e.g. {@code file:///Users/me/ProjectA})
+ * Value: the fully initialized {@link ProjectContext} for that root
+ */
+ private final Map projects = new ConcurrentHashMap<>();
+
+ // -------------------------------------------------------------------------
+ // Mutating operations
+ // -------------------------------------------------------------------------
+
+ /**
+ * Registers a new {@link ProjectContext} for the given project root URI.
+ *
+ *
If a context is already registered for {@code projectUri}, a warning
+ * is logged and the existing entry is not overwritten. Call
+ * {@link #removeProject} first if you need to replace a context.
+ *
+ * @param projectUri the normalized root URI of the project
+ * (e.g. {@code "file:///Users/me/ProjectA"})
+ * @param context the fully initialized {@link ProjectContext} to register
+ */
+ public void addProject(String projectUri, ProjectContext context) {
+
+ if (projectUri == null || context == null) {
+ log.log(Level.WARNING, "addProject called with null projectUri or context — ignoring.");
+ return;
+ }
+ ProjectContext existing = projects.putIfAbsent(projectUri, context);
+ if (existing != null) {
+ log.log(Level.WARNING,
+ "A ProjectContext is already registered for URI: " + projectUri
+ + ". The existing context was NOT replaced. Call removeProject() first.");
+ } else {
+ log.log(Level.INFO, "Registered ProjectContext for: " + projectUri);
+ }
+ }
+
+ /**
+ * Removes and returns the {@link ProjectContext} for the given project root URI.
+ *
+ *
If no context is registered for {@code projectUri}, a warning is
+ * logged and {@code null} is returned.
+ *
+ * @param projectUri the normalized root URI of the project to remove
+ * @return the removed {@link ProjectContext}, or {@code null} if not found
+ */
+ public ProjectContext removeProject(String projectUri) {
+
+ if (projectUri == null) {
+ log.log(Level.WARNING, "removeProject called with null projectUri \u2014 ignoring.");
+ return null;
+ }
+
+ ProjectContext removed = projects.remove(projectUri);
+ if (removed == null) {
+ log.log(Level.WARNING,
+ "removeProject: no ProjectContext found for URI: " + projectUri);
+ } else {
+ log.log(Level.INFO, "Removed ProjectContext for: " + projectUri);
+ }
+ return removed;
+ }
+
+ // -------------------------------------------------------------------------
+ // Query operations
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the {@link ProjectContext} for an exact project root URI match.
+ *
+ * @param projectUri the normalized root URI of the project
+ * @return the registered {@link ProjectContext}, or {@code null} if not found
+ */
+ public ProjectContext getProject(String projectUri) {
+
+ return projects.get(projectUri);
+ }
+
+ /**
+ * Resolves a document URI to the {@link ProjectContext} of the project it
+ * belongs to, using a longest-prefix match.
+ *
+ *
Given a document URI such as
+ * {@code file:///Users/me/ProjectA/src/main/synapse-config/api/MyAPI.xml},
+ * this method iterates all registered project root URIs and returns the
+ * context whose root URI is the longest prefix of the document URI. The
+ * longest-prefix rule ensures correctness when one project root is nested
+ * inside another.
+ *
+ *