Refactor: dynamic loading for ChatGptClient

The ChatGptClient class has been moved to
`mode.stateless...ChatGptClientStateless` and is now loaded
dynamically based on the `gptMode` setting, paving the way for the
upcoming ChatGptClientStateful class.

Change-Id: I19c2a1a62238208e2ba0cc5a759f35eb65c2e011
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
index 952011f..01e3b69 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
@@ -4,7 +4,7 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
 import com.googlesource.gerrit.plugins.chatgpt.data.ChangeSetDataHandler;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.chatgpt.ChatGptClient;
+import com.googlesource.gerrit.plugins.chatgpt.mode.ModeClassLoader;
 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.GerritClientReview;
@@ -16,10 +16,15 @@
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.gerrit.GerritComment;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.review.ReviewBatch;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.chatgpt.ChatGptClientStateless;
+import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.chatgpt.IChatGptClient;
+import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.*;
 
+import static com.googlesource.gerrit.plugins.chatgpt.utils.ClassUtils.registerDynamicClasses;
+
 @Slf4j
 public class PatchSetReviewer {
     private static final String SPLIT_REVIEW_MSG = "Too many changes. Please consider splitting into patches smaller " +
@@ -27,18 +32,18 @@
 
     private final Gson gson = new Gson();
     private final GerritClient gerritClient;
-    private final ChatGptClient chatGptClient;
 
     private Configuration config;
+    @Getter
+    private IChatGptClient chatGptClient;
     private GerritCommentRange gerritCommentRange;
     private List<ReviewBatch> reviewBatches;
     private List<GerritComment> commentProperties;
     private List<Integer> reviewScores;
 
     @Inject
-    PatchSetReviewer(GerritClient gerritClient, ChatGptClient chatGptClient) {
+    PatchSetReviewer(GerritClient gerritClient) {
         this.gerritClient = gerritClient;
-        this.chatGptClient = chatGptClient;
     }
 
     public void review(Configuration config, GerritChange change) throws Exception {
@@ -126,6 +131,10 @@
             log.warn("Patch set too large. Skipping review. changeId: {}", change.getFullChangeId());
             return String.format(SPLIT_REVIEW_MSG, config.getMaxReviewLines());
         }
+        chatGptClient = (IChatGptClient) ModeClassLoader.getInstance(
+                "client.api.chatgpt.ChatGptClient", config);
+        registerDynamicClasses(ChatGptClientStateless.class);
+
         return chatGptClient.ask(config, change, patchSet);
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptTools.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptTools.java
index cb86332..30fff94 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptTools.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptTools.java
@@ -7,7 +7,7 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 
-class ChatGptTools {
+public class ChatGptTools {
     private final static Gson gson = new Gson();
 
     public static ChatGptRequest retrieveTools() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/interfaces/client/api/chatgpt/IChatGptClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/interfaces/client/api/chatgpt/IChatGptClient.java
new file mode 100644
index 0000000..4c307ed
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/interfaces/client/api/chatgpt/IChatGptClient.java
@@ -0,0 +1,10 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.chatgpt;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+
+public interface IChatGptClient {
+    String ask(Configuration config, String changeId, String patchSet) throws Exception;
+    String ask(Configuration config, GerritChange change, String patchSet) throws Exception;
+    String getRequestBody();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/api/chatgpt/ChatGptClientStateless.java
similarity index 94%
rename from src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClient.java
rename to src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/api/chatgpt/ChatGptClientStateless.java
index 88188d0..51d3967 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/api/chatgpt/ChatGptClientStateless.java
@@ -1,4 +1,4 @@
-package com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.chatgpt;
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.chatgpt;
 
 import com.google.common.net.HttpHeaders;
 import com.google.gson.Gson;
@@ -7,9 +7,11 @@
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.HttpClientWithRetry;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.UriResourceLocator;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.chatgpt.ChatGptTools;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.prompt.ChatGptPrompt;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.*;
+import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.chatgpt.IChatGptClient;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.entity.ContentType;
@@ -25,7 +27,7 @@
 
 @Slf4j
 @Singleton
-public class ChatGptClient {
+public class ChatGptClientStateless implements IChatGptClient {
     private static final int REVIEW_ATTEMPT_LIMIT = 3;
     @Getter
     private String requestBody;
@@ -35,6 +37,7 @@
     private final HttpClientWithRetry httpClientWithRetry = new HttpClientWithRetry();
     private boolean isCommentEvent = false;
 
+    @Override
     public String ask(Configuration config, String changeId, String patchSet) throws Exception {
         for (int attemptInd = 0; attemptInd < REVIEW_ATTEMPT_LIMIT; attemptInd++) {
             HttpRequest request = createRequest(config, changeId, patchSet);
@@ -56,6 +59,7 @@
         throw new RuntimeException("Failed to receive valid ChatGPT response");
     }
 
+    @Override
     public String ask(Configuration config, GerritChange change, String patchSet) throws Exception {
         isCommentEvent = change.getIsCommentEvent();
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptParameters.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/api/chatgpt/ChatGptParameters.java
similarity index 93%
rename from src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptParameters.java
rename to src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/api/chatgpt/ChatGptParameters.java
index 4670a67..a4bc242 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptParameters.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/api/chatgpt/ChatGptParameters.java
@@ -1,4 +1,4 @@
-package com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.chatgpt;
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.chatgpt;
 
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
index a588594..9565234 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
@@ -25,7 +25,6 @@
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.UriResourceLocator;
 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.chatgpt.ChatGptClient;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.prompt.ChatGptPrompt;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.entity.ContentType;
@@ -259,8 +258,7 @@
     @Test
     public void patchSetCreatedOrUpdatedStreamed() throws InterruptedException, NoSuchProjectException, ExecutionException {
         GerritClient gerritClient = new GerritClient();
-        ChatGptClient chatGptClient = new ChatGptClient();
-        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, chatGptClient);
+        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient);
         ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
         when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
         String reviewUserPrompt = joinWithNewLine(Arrays.asList(
@@ -291,7 +289,7 @@
                 WireMock.urlEqualTo(gerritSetReviewUri(getGerritChange().getFullChangeId())));
         List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
         Assert.assertEquals(1, loggedRequests.size());
-        JsonObject gptRequestBody = gson.fromJson(chatGptClient.getRequestBody(), JsonObject.class);
+        JsonObject gptRequestBody = gson.fromJson(patchSetReviewer.getChatGptClient().getRequestBody(), JsonObject.class);
         JsonArray prompts = gptRequestBody.get("messages").getAsJsonArray();
         String systemPrompt = prompts.get(0).getAsJsonObject().get("content").getAsString();
         Assert.assertEquals(expectedSystemPromptReview, systemPrompt);
@@ -309,8 +307,7 @@
         when(globalConfig.getBoolean(Mockito.eq("enabledVoting"), Mockito.anyBoolean()))
                 .thenReturn(true);
         GerritClient gerritClient = new GerritClient();
-        ChatGptClient chatGptClient = new ChatGptClient();
-        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, chatGptClient);
+        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient);
         ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
         when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
         String reviewVoteUserPrompt = joinWithNewLine(Arrays.asList(
@@ -348,7 +345,7 @@
                 WireMock.urlEqualTo(gerritSetReviewUri(getGerritChange().getFullChangeId())));
         List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
         Assert.assertEquals(1, loggedRequests.size());
-        JsonObject gptRequestBody = gson.fromJson(chatGptClient.getRequestBody(), JsonObject.class);
+        JsonObject gptRequestBody = gson.fromJson(patchSetReviewer.getChatGptClient().getRequestBody(), JsonObject.class);
         JsonArray prompts = gptRequestBody.get("messages").getAsJsonArray();
         String userPrompt = prompts.get(1).getAsJsonObject().get("content").getAsString();
         Assert.assertEquals(reviewVoteUserPrompt, userPrompt);
@@ -362,8 +359,7 @@
         when(globalConfig.getString(Mockito.eq("disabledGroups"), Mockito.anyString()))
                 .thenReturn(GERRIT_USER_GROUP);
         GerritClient gerritClient = new GerritClient();
-        ChatGptClient chatGptClient = new ChatGptClient();
-        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, chatGptClient);
+        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient);
         ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
         when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
 
@@ -386,8 +382,7 @@
     public void gptMentionedInComment() throws InterruptedException, NoSuchProjectException, ExecutionException {
         GerritChange gerritChange = getGerritChange();
         GerritClient gerritClient = new GerritClient();
-        ChatGptClient chatGptClient = new ChatGptClient();
-        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, chatGptClient);
+        PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient);
         ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
         when(config.getGerritUserName()).thenReturn(GERRIT_GPT_USERNAME);
         when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
@@ -426,7 +421,7 @@
                 WireMock.urlEqualTo(gerritSetReviewUri(gerritChange.getFullChangeId())));
         List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
         Assert.assertEquals(1, loggedRequests.size());
-        JsonObject gptRequestBody = gson.fromJson(chatGptClient.getRequestBody(), JsonObject.class);
+        JsonObject gptRequestBody = gson.fromJson(patchSetReviewer.getChatGptClient().getRequestBody(), JsonObject.class);
         JsonArray prompts = gptRequestBody.get("messages").getAsJsonArray();
         String userPrompt = prompts.get(1).getAsJsonObject().get("content").getAsString();
         Assert.assertEquals(commentUserPrompt, userPrompt);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java
index c6bfcf9..f44da74 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java
@@ -1,11 +1,11 @@
 package com.googlesource.gerrit.plugins.chatgpt.integration;
 
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.chatgpt.ChatGptClient;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClient;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClientReview;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.prompt.ChatGptPrompt;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.review.ReviewBatch;
+import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.chatgpt.IChatGptClient;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -32,7 +32,7 @@
     private GerritClient gerritClient;
 
     @InjectMocks
-    private ChatGptClient chatGptClient;
+    private IChatGptClient chatGptClient;
 
     @Test
     public void sayHelloToGPT() throws Exception {