Fix overlapping data issues in stateful reviews

Previously, when reviewing multiple projects in stateful mode, ChatGPT
assistant data retrieval for different projects would overwrite each
other. To address this, `PluginDataHandlerProvider` has been decoupled
from `PluginDataHandlerBaseProvider` and now supports multiple project
scopes. Additionally, a new 'project' scope has been introduced to
separately manage persistent data for each project.

Change-Id: I1279f86c181109640498e286e04d20a8b71fcb25
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerBaseProvider.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerBaseProvider.java
new file mode 100644
index 0000000..15344a9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerBaseProvider.java
@@ -0,0 +1,29 @@
+package com.googlesource.gerrit.plugins.chatgpt.data;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.nio.file.Path;
+
+@Singleton
+public class PluginDataHandlerBaseProvider implements Provider<PluginDataHandler> {
+    private static final String PATH_SUFFIX = ".data";
+    private static final String PATH_GLOBAL = "global";
+
+    private final Path defaultPluginDataPath;
+
+    @Inject
+    public PluginDataHandlerBaseProvider(@com.google.gerrit.extensions.annotations.PluginData Path defaultPluginDataPath) {
+        this.defaultPluginDataPath = defaultPluginDataPath;
+    }
+
+    public PluginDataHandler get(String path) {
+        return new PluginDataHandler(defaultPluginDataPath.resolve(path + PATH_SUFFIX));
+    }
+
+    @Override
+    public PluginDataHandler get() {
+        return get(PATH_GLOBAL);
+    }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java
index f7366ca..ca52d51 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java
@@ -3,24 +3,28 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
 
 import java.nio.file.Path;
 
 @Singleton
-public class PluginDataHandlerProvider implements Provider<PluginDataHandler> {
-    private final Path defaultPluginDataPath;
+public class PluginDataHandlerProvider extends PluginDataHandlerBaseProvider implements Provider<PluginDataHandler> {
+    private final String projectName;
 
     @Inject
-    public PluginDataHandlerProvider(@com.google.gerrit.extensions.annotations.PluginData Path defaultPluginDataPath) {
-        this.defaultPluginDataPath = defaultPluginDataPath;
+    public PluginDataHandlerProvider(
+            @com.google.gerrit.extensions.annotations.PluginData Path defaultPluginDataPath,
+            GerritChange change
+    ) {
+        super(defaultPluginDataPath);
+        projectName = change.getProjectName();
     }
 
-    public PluginDataHandler get(Path configPath) {
-        return new PluginDataHandler(configPath);
+    public PluginDataHandler getGlobalScope() {
+        return super.get();
     }
 
-    @Override
-    public PluginDataHandler get() {
-        return get(defaultPluginDataPath.resolve("plugin.config"));
+    public PluginDataHandler getProjectScope() {
+        return super.get(projectName);
     }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
index 7fc146c..e2548d5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
@@ -29,7 +29,7 @@
     private final ChatGptHttpClient httpClient = new ChatGptHttpClient();
     private final GerritChange change;
     private final GitRepoFiles gitRepoFiles;
-    private final PluginDataHandler pluginDataHandler;
+    private final PluginDataHandler projectDataHandler;
 
     public ChatGptAssistant(
             Configuration config,
@@ -40,16 +40,16 @@
         super(config);
         this.change = change;
         this.gitRepoFiles = gitRepoFiles;
-        this.pluginDataHandler = pluginDataHandlerProvider.get();
+        this.projectDataHandler = pluginDataHandlerProvider.getProjectScope();
     }
 
     public void setupAssistant() {
-        String assistantId = pluginDataHandler.getValue(KEY_ASSISTANT_ID);
+        String assistantId = projectDataHandler.getValue(KEY_ASSISTANT_ID);
         if (assistantId == null || config.getForceCreateAssistant()) {
             String fileId = uploadRepoFiles();
-            pluginDataHandler.setValue(KEY_FILE_ID, fileId);
+            projectDataHandler.setValue(KEY_FILE_ID, fileId);
             assistantId = createAssistant(fileId);
-            pluginDataHandler.setValue(KEY_ASSISTANT_ID, assistantId);
+            projectDataHandler.setValue(KEY_ASSISTANT_ID, assistantId);
             log.info("Project assistant created with ID: {}", assistantId);
         }
         else {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java
index 466afa1..d078e67 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java
@@ -28,7 +28,7 @@
 
     private final ChatGptHttpClient httpClient = new ChatGptHttpClient();
     private final String threadId;
-    private final PluginDataHandler pluginDataHandler;
+    private final PluginDataHandler projectDataHandler;
 
     private ChatGptResponse runResponse;
     private ChatGptListResponse stepResponse;
@@ -36,7 +36,7 @@
     public ChatGptRun(String threadId, Configuration config, PluginDataHandlerProvider pluginDataHandlerProvider) {
         super(config);
         this.threadId = threadId;
-        this.pluginDataHandler = pluginDataHandlerProvider.get();
+        this.projectDataHandler = pluginDataHandlerProvider.getProjectScope();
     }
 
     public void createRun() {
@@ -81,7 +81,7 @@
         log.debug("ChatGPT Create Run request URI: {}", uri);
 
         ChatGptCreateRunRequest requestBody = ChatGptCreateRunRequest.builder()
-                .assistantId(pluginDataHandler.getValue(KEY_ASSISTANT_ID))
+                .assistantId(projectDataHandler.getValue(KEY_ASSISTANT_ID))
                 .build();
 
         return httpClient.createRequestFromJson(uri.toString(), config.getGptToken(), requestBody);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
index 02f16c5..267f968 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
@@ -66,7 +66,7 @@
                 .thenReturn(MODES.stateful.name());
 
         // Mock the pluginDataHandlerProvider to return the mocked pluginDataHandler
-        when(pluginDataHandlerProvider.get()).thenReturn(pluginDataHandler);
+        when(pluginDataHandlerProvider.getProjectScope()).thenReturn(pluginDataHandler);
     }
 
     protected void initConfig() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
index 393a87f..b91fddc 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
@@ -2,9 +2,6 @@
 
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -36,7 +33,6 @@
 import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandlerProvider;
 import com.googlesource.gerrit.plugins.chatgpt.listener.EventHandlerTask;
 import com.googlesource.gerrit.plugins.chatgpt.localization.Localizer;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClient;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClientComments;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClientFacade;
@@ -76,7 +72,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-public class ChatGptReviewTestBase {
+public class ChatGptReviewTestBase extends ChatGptTestBase {
     protected static final Path basePath = Paths.get("src/test/resources");
     protected static final int GERRIT_GPT_ACCOUNT_ID = 1000000;
     protected static final String GERRIT_GPT_USERNAME = "gpt";
@@ -87,9 +83,6 @@
     protected static final String GERRIT_USER_GROUP = "Test";
     protected static final String GPT_TOKEN = "tk-test";
     protected static final String GPT_DOMAIN = "http://localhost:9527";
-    protected static final Project.NameKey PROJECT_NAME = Project.NameKey.parse("myProject");
-    protected static final Change.Key CHANGE_ID = Change.Key.parse("myChangeId");
-    protected static final BranchNameKey BRANCH_NAME = BranchNameKey.create(PROJECT_NAME, "myBranchName");
     protected static final boolean GPT_STREAM_OUTPUT = true;
     protected static final long TEST_TIMESTAMP = 1699270812;
     private static  final int GPT_USER_ACCOUNT_ID = 1000000;
@@ -137,10 +130,6 @@
         initTest();
     }
 
-    protected GerritChange getGerritChange() {
-        return new GerritChange(PROJECT_NAME, BRANCH_NAME, CHANGE_ID);
-    }
-
     protected void initGlobalAndProjectConfig() {
         globalConfig = mock(PluginConfig.class);
         Answer<Object> returnDefaultArgument = invocation -> {
@@ -161,7 +150,6 @@
                 .thenReturn(GPT_DOMAIN);
         when(globalConfig.getString("gerritUserName")).thenReturn(GERRIT_GPT_USERNAME);
 
-
         projectConfig = mock(PluginConfig.class);
 
         // Mock the Project Config values
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptTestBase.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptTestBase.java
new file mode 100644
index 0000000..b3488b4
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptTestBase.java
@@ -0,0 +1,16 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+
+public class ChatGptTestBase {
+    protected static final Project.NameKey PROJECT_NAME = Project.NameKey.parse("myProject");
+    protected static final Change.Key CHANGE_ID = Change.Key.parse("myChangeId");
+    protected static final BranchNameKey BRANCH_NAME = BranchNameKey.create(PROJECT_NAME, "myBranchName");
+
+    protected GerritChange getGerritChange() {
+        return new GerritChange(ChatGptTestBase.PROJECT_NAME, ChatGptTestBase.BRANCH_NAME, ChatGptTestBase.CHANGE_ID);
+    }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PluginDataTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PluginDataTest.java
index 5bdc08f..d3b687a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PluginDataTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PluginDataTest.java
@@ -17,7 +17,7 @@
 import org.mockito.junit.MockitoJUnitRunner;
 
 @RunWith(MockitoJUnitRunner.class)
-public class PluginDataTest {
+public class PluginDataTest extends ChatGptTestBase {
     @Rule
     public TemporaryFolder tempFolder = new TemporaryFolder();
 
@@ -29,31 +29,36 @@
     @Before
     public void setUp() {
         // Setup temporary folder for tests
-        realPluginDataPath = tempFolder.getRoot().toPath().resolve("plugin.config");
+        realPluginDataPath = tempFolder.getRoot().toPath().resolve("global.data");
+        Path realProjectDataPath = tempFolder.getRoot().toPath().resolve(PROJECT_NAME + ".data");
 
         // Mock the PluginData annotation behavior
-        when(mockPluginDataPath.resolve("plugin.config")).thenReturn(realPluginDataPath);
+        when(mockPluginDataPath.resolve("global.data")).thenReturn(realPluginDataPath);
+        when(mockPluginDataPath.resolve(PROJECT_NAME + ".data")).thenReturn(realProjectDataPath);
     }
 
     @Test
     public void testValueSetAndGet() {
-        PluginDataHandlerProvider provider = new PluginDataHandlerProvider(mockPluginDataPath);
-        PluginDataHandler handler = provider.get();
+        PluginDataHandlerProvider provider = new PluginDataHandlerProvider(mockPluginDataPath, getGerritChange());
+        PluginDataHandler globalHandler = provider.getGlobalScope();
+        PluginDataHandler projectHandler = provider.getProjectScope();
 
         String key = "testKey";
         String value = "testValue";
 
         // Test set value
-        handler.setValue(key, value);
+        globalHandler.setValue(key, value);
+        projectHandler.setValue(key, value);
 
         // Test get value
-        assertEquals("The value retrieved should match the value set.", value, handler.getValue(key));
+        assertEquals("The value retrieved should match the value set.", value, globalHandler.getValue(key));
+        assertEquals("The value retrieved should match the value set.", value, projectHandler.getValue(key));
     }
 
     @Test
     public void testRemoveValue() {
-        PluginDataHandlerProvider provider = new PluginDataHandlerProvider(mockPluginDataPath);
-        PluginDataHandler handler = provider.get();
+        PluginDataHandlerProvider provider = new PluginDataHandlerProvider(mockPluginDataPath, getGerritChange());
+        PluginDataHandler handler = provider.getGlobalScope();
 
         String key = "testKey";
         String value = "testValue";
@@ -72,8 +77,8 @@
         // Ensure the file doesn't exist before creating the handler
         Files.deleteIfExists(realPluginDataPath);
 
-        PluginDataHandlerProvider provider = new PluginDataHandlerProvider(mockPluginDataPath);
-        provider.get();
+        PluginDataHandlerProvider provider = new PluginDataHandlerProvider(mockPluginDataPath, getGerritChange());
+        provider.getGlobalScope();
 
         // The constructor should create the file if it doesn't exist
         assertTrue("The config file should exist after initializing the handler.", Files.exists(realPluginDataPath));