Refactor: introduction of GerritChange Class

Introduced the GerritChange class to manage parameters associated with
Gerrit Change Events, aiming to
- Minimize parameter passing;
- Enforce division of class responsibilities;
- Set up the basis for scalable approaches in calculating
`fullChangeId`, when used as key to access the GerritClientFacade
Singleton.

Jira-Id: IT-103
Change-Id: Ibb61d0c75fab2f44ce25c41313f0925104b7210b
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 63027b0..b972d5a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
@@ -4,6 +4,7 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.chatgpt.client.*;
 import com.googlesource.gerrit.plugins.chatgpt.client.chatgpt.ChatGptClient;
+import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritClient;
 import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritClientDetail;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.chatGpt.ChatGptReplyItem;
@@ -30,8 +31,6 @@
     private List<ReviewBatch> reviewBatches;
     private List<GerritComment> commentProperties;
     private HashMap<String, FileDiffProcessed> fileDiffsProcessed;
-    @Setter
-    private boolean isCommentEvent;
 
     @Inject
     PatchSetReviewer(GerritClient gerritClient, ChatGptClient chatGptClient) {
@@ -39,33 +38,33 @@
         this.chatGptClient = chatGptClient;
     }
 
-    public void review(Configuration config, String fullChangeId) throws Exception {
+    public void review(Configuration config, GerritChange change) throws Exception {
         reviewBatches = new ArrayList<>();
-        commentProperties = gerritClient.getCommentProperties(fullChangeId);
-        String patchSet = gerritClient.getPatchSet(fullChangeId, isCommentEvent);
+        commentProperties = gerritClient.getCommentProperties(change);
+        String patchSet = gerritClient.getPatchSet(change);
         if (patchSet.isEmpty()) {
             log.info("No file to review has been found in the PatchSet");
             return;
         }
-        updateDynamicConfiguration(config, fullChangeId);
+        updateDynamicConfiguration(config, change);
 
-        String reviewReply = getReviewReply(config, fullChangeId, patchSet);
+        String reviewReply = getReviewReply(config, change, patchSet);
         log.debug("ChatGPT response: {}", reviewReply);
         ChatGptResponseContent reviewJson = gson.fromJson(reviewReply, ChatGptResponseContent.class);
-        retrieveReviewFromJson(reviewJson, fullChangeId);
+        retrieveReviewFromJson(reviewJson, change);
 
-        gerritClient.setReview(fullChangeId, reviewBatches, reviewJson.getScore());
+        gerritClient.setReview(change, reviewBatches, reviewJson.getScore());
     }
 
-    private void updateDynamicConfiguration(Configuration config, String fullChangeId) {
+    private void updateDynamicConfiguration(Configuration config, GerritChange change) {
         config.configureDynamically(Configuration.KEY_GPT_REQUEST_USER_PROMPT,
-                gerritClient.getUserRequests(fullChangeId));
+                gerritClient.getUserRequests(change));
         config.configureDynamically(Configuration.KEY_COMMENT_PROPERTIES_SIZE, commentProperties.size());
-        if (config.isVotingEnabled() && !isCommentEvent) {
+        if (config.isVotingEnabled() && !change.getIsCommentEvent()) {
             GerritClientDetail gerritClientDetail = new GerritClientDetail(config,
-                    gerritClient.getGptAccountId(fullChangeId));
+                    gerritClient.getGptAccountId(change));
             GerritPatchSetDetail.PermittedVotingRange permittedVotingRange = gerritClientDetail.getPermittedVotingRange(
-                    fullChangeId);
+                    change.getFullChangeId());
             if (permittedVotingRange != null) {
                 if (permittedVotingRange.getMin() > config.getVotingMinScore()) {
                     log.debug("Minimum ChatGPT voting score set to {}", permittedVotingRange.getMin());
@@ -123,11 +122,11 @@
         return gerritCommentRange;
     }
 
-    private void retrieveReviewFromJson(ChatGptResponseContent reviewJson, String fullChangeId) {
-        fileDiffsProcessed = gerritClient.getFileDiffsProcessed(fullChangeId);
+    private void retrieveReviewFromJson(ChatGptResponseContent reviewJson, GerritChange change) {
+        fileDiffsProcessed = gerritClient.getFileDiffsProcessed(change);
         for (ChatGptReplyItem replyItem : reviewJson.getReplies()) {
             ReviewBatch batchMap = new ReviewBatch();
-            if (isCommentEvent && replyItem.getId() != null) {
+            if (change.getIsCommentEvent() && replyItem.getId() != null) {
                 addReviewBatch(replyItem.getId(), replyItem.getReply());
             }
             else {
@@ -145,13 +144,13 @@
         log.debug("fileDiffsProcessed Keys: {}", fileDiffsProcessed.keySet());
     }
 
-    private String getReviewReply(Configuration config, String changeId, String patchSet) throws Exception {
+    private String getReviewReply(Configuration config, GerritChange change, String patchSet) throws Exception {
         List<String> patchLines = Arrays.asList(patchSet.split("\n"));
         if (patchLines.size() > config.getMaxReviewLines()) {
-            log.warn("Patch set too large. Skipping review. changeId: {}", changeId);
+            log.warn("Patch set too large. Skipping review. changeId: {}", change.getFullChangeId());
             return String.format(SPLIT_REVIEW_MSG, config.getMaxReviewLines());
         }
-        return chatGptClient.ask(config, changeId, patchSet, isCommentEvent);
+        return chatGptClient.ask(config, change, patchSet);
     }
 }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptClient.java
index e8b942f..34673aa 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptClient.java
@@ -6,6 +6,7 @@
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.chatgpt.client.HttpClientWithRetry;
 import com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator;
+import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.chatGpt.*;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
 import lombok.Getter;
@@ -56,11 +57,11 @@
         throw new RuntimeException("Failed to receive valid ChatGPT response");
     }
 
-    public String ask(Configuration config, String changeId, String patchSet, boolean isCommentEvent) throws Exception {
-        this.isCommentEvent = isCommentEvent;
-        chatGptTools = new ChatGptTools(isCommentEvent);
+    public String ask(Configuration config, GerritChange change, String patchSet) throws Exception {
+        isCommentEvent = change.getIsCommentEvent();
+        chatGptTools = new ChatGptTools(config, isCommentEvent);
 
-        return this.ask(config, changeId, patchSet);
+        return this.ask(config, change.getFullChangeId(), patchSet);
     }
 
     private String extractContent(Configuration config, String body) throws Exception {
@@ -123,7 +124,7 @@
                 .build();
 
         List<ChatGptRequest.Message> messages = List.of(systemMessage, userMessage);
-        ChatGptRequest tools = chatGptTools.retrieveTools(config);
+        ChatGptRequest tools = chatGptTools.retrieveTools();
         ChatGptRequest chatGptRequest = ChatGptRequest.builder()
                 .model(config.getGptModel())
                 .messages(messages)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptTools.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptTools.java
index 395b2d7..50147b3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptTools.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/chatgpt/ChatGptTools.java
@@ -11,13 +11,15 @@
 class ChatGptTools {
 
     private final Gson gson = new Gson();
+    private final Configuration config;
     private final boolean isCommentEvent;
 
-    public ChatGptTools(boolean isCommentEvent) {
+    public ChatGptTools(Configuration config, Boolean isCommentEvent) {
+        this.config = config;
         this.isCommentEvent = isCommentEvent;
     }
 
-    public ChatGptRequest retrieveTools(Configuration config) {
+    public ChatGptRequest retrieveTools() {
         ChatGptRequest tools;
         try (InputStreamReader reader = FileUtils.getInputStreamReader("Config/tools.json")) {
             tools = gson.fromJson(reader, ChatGptRequest.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritChange.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritChange.java
new file mode 100644
index 0000000..d7d0d5a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritChange.java
@@ -0,0 +1,69 @@
+package com.googlesource.gerrit.plugins.chatgpt.client.gerrit;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.PatchSetEvent;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+@Slf4j
+@Getter
+public class GerritChange {
+    private Event event;
+    private String eventType;
+    private long eventTimeStamp;
+    private PatchSetEvent patchSetEvent;
+    private Project.NameKey projectNameKey;
+    private BranchNameKey branchNameKey;
+    private Change.Key changeKey;
+    private String fullChangeId;
+    @Setter
+    private Boolean isCommentEvent;
+
+    public GerritChange(Project.NameKey projectNameKey, BranchNameKey branchNameKey, Change.Key changeKey) {
+        this.projectNameKey = projectNameKey;
+        this.branchNameKey = branchNameKey;
+        this.changeKey = changeKey;
+        buildFullChangeId();
+    }
+
+    public GerritChange(Event event) {
+        this(
+                ((PatchSetEvent) event).getProjectNameKey(),
+                ((PatchSetEvent) event).getBranchNameKey(),
+                ((PatchSetEvent) event).getChangeKey()
+        );
+        this.event = event;
+        eventType = event.getType();
+        eventTimeStamp = event.eventCreatedOn;
+        patchSetEvent = (PatchSetEvent) event;
+    }
+
+    // Incomplete initialization used by CodeReviewPluginIT
+    public GerritChange(String fullChangeId) {
+        this.fullChangeId = fullChangeId;
+    }
+
+    public Optional<PatchSetAttribute> getPatchSetAttribute() {
+        try {
+            return Optional.ofNullable(patchSetEvent.patchSet.get());
+        }
+        catch (NullPointerException e) {
+            return Optional.empty();
+        }
+    }
+
+    private void buildFullChangeId() {
+        fullChangeId = String.join("~", URLEncoder.encode(projectNameKey.get(), StandardCharsets.UTF_8),
+                branchNameKey.shortName(), changeKey.get());
+    }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java
index b908eb3..b11edd9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClient.java
@@ -1,6 +1,5 @@
 package com.googlesource.gerrit.plugins.chatgpt.client.gerrit;
 
-import com.google.gerrit.server.events.Event;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.chatgpt.client.FileDiffProcessed;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.gerrit.GerritComment;
@@ -19,26 +18,26 @@
     private static GerritClientFacade gerritClientFacade;
 
     public void initialize(Configuration config) {
-        initialize(config, DEFAULT_CHANGE_ID);
+        initialize(config, new GerritChange(DEFAULT_CHANGE_ID));
     }
 
-    public void initialize(Configuration config, String fullChangeId) {
-        log.debug("Initializing client instances for change: {}", fullChangeId);
+    public void initialize(Configuration config, GerritChange change) {
+        log.debug("Initializing client instances for change: {}", change.getFullChangeId());
         config.resetDynamicConfiguration();
-        gerritClientFacade = SingletonManager.getInstance(GerritClientFacade.class, fullChangeId, config);
+        gerritClientFacade = SingletonManager.getInstance(GerritClientFacade.class, change.getFullChangeId(), config);
     }
 
     public String getPatchSet(String fullChangeId) throws Exception {
-        return getPatchSet(fullChangeId, false);
+        return getPatchSet(new GerritChange(fullChangeId));
     }
 
-    public String getPatchSet(String fullChangeId, boolean isCommentEvent) throws Exception {
-        updateGerritClientFacade(fullChangeId);
-        return gerritClientFacade.getPatchSet(fullChangeId, isCommentEvent);
+    public String getPatchSet(GerritChange change) throws Exception {
+        updateGerritClientFacade(change);
+        return gerritClientFacade.getPatchSet(change);
     }
 
-    public boolean getForcedReview(String fullChangeId) {
-        updateGerritClientFacade(fullChangeId);
+    public boolean getForcedReview(GerritChange change) {
+        updateGerritClientFacade(change);
         return gerritClientFacade.getForcedReview();
     }
 
@@ -50,47 +49,47 @@
         return gerritClientFacade.isDisabledTopic(topic);
     }
 
-    public HashMap<String, FileDiffProcessed> getFileDiffsProcessed(String fullChangeId) {
-        updateGerritClientFacade(fullChangeId);
+    public HashMap<String, FileDiffProcessed> getFileDiffsProcessed(GerritChange change) {
+        updateGerritClientFacade(change);
         return gerritClientFacade.getFileDiffsProcessed();
     }
 
-    public Integer getGptAccountId(String fullChangeId) {
-        updateGerritClientFacade(fullChangeId);
+    public Integer getGptAccountId(GerritChange change) {
+        updateGerritClientFacade(change);
         return gerritClientFacade.getGptAccountId();
     }
 
-    public List<GerritComment> getCommentProperties(String fullChangeId) {
-        updateGerritClientFacade(fullChangeId);
+    public List<GerritComment> getCommentProperties(GerritChange change) {
+        updateGerritClientFacade(change);
         return gerritClientFacade.getCommentProperties();
     }
 
-    public void setReview(String fullChangeId, List<ReviewBatch> reviewBatches, Integer reviewScore) throws Exception {
-        updateGerritClientFacade(fullChangeId);
-        gerritClientFacade.setReview(fullChangeId, reviewBatches, reviewScore);
-    }
-
     public void setReview(String fullChangeId, List<ReviewBatch> reviewBatches) throws Exception {
-        setReview(fullChangeId, reviewBatches, null);
+        setReview(new GerritChange(fullChangeId), reviewBatches, null);
     }
 
-    public boolean retrieveLastComments(String fullChangeId, Event event) {
-        updateGerritClientFacade(fullChangeId);
-        return gerritClientFacade.retrieveLastComments(fullChangeId, event);
+    public void setReview(GerritChange change, List<ReviewBatch> reviewBatches, Integer reviewScore) throws Exception {
+        updateGerritClientFacade(change);
+        gerritClientFacade.setReview(change.getFullChangeId(), reviewBatches, reviewScore);
     }
 
-    public String getUserRequests(String fullChangeId) {
-        updateGerritClientFacade(fullChangeId);
+    public boolean retrieveLastComments(GerritChange change) {
+        updateGerritClientFacade(change);
+        return gerritClientFacade.retrieveLastComments(change);
+    }
+
+    public String getUserRequests(GerritChange change) {
+        updateGerritClientFacade(change);
         return gerritClientFacade.getUserRequests();
     }
 
-    public void destroy(String fullChangeId) {
-        log.debug("Destroying GerritClientFacade instance for change: {}", fullChangeId);
-        SingletonManager.removeInstance(GerritClientFacade.class, fullChangeId);
+    public void destroy(GerritChange change) {
+        log.debug("Destroying GerritClientFacade instance for change: {}", change.getFullChangeId());
+        SingletonManager.removeInstance(GerritClientFacade.class, change.getFullChangeId());
     }
 
-    private void updateGerritClientFacade(String fullChangeId) {
-        gerritClientFacade = SingletonManager.getInstance(GerritClientFacade.class, fullChangeId);
+    private void updateGerritClientFacade(GerritChange change) {
+        gerritClientFacade = SingletonManager.getInstance(GerritClientFacade.class, change.getFullChangeId());
     }
 
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java
index 2adadbe..98f6327 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientComments.java
@@ -1,7 +1,6 @@
 package com.googlesource.gerrit.plugins.chatgpt.client.gerrit;
 
 import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.Event;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import com.googlesource.gerrit.plugins.chatgpt.client.FileDiffProcessed;
@@ -33,7 +32,6 @@
     @Getter
     private final Integer gptAccountId;
     private final HashMap<String, GerritComment> commentMap;
-    private long commentsStartTimestamp;
     private String authorUsername;
     @Getter
     private List<GerritComment> commentProperties;
@@ -48,12 +46,11 @@
                 "Error retrieving ChatGPT account ID in Gerrit"));
     }
 
-    public boolean retrieveLastComments(Event event, String fullChangeId) {
-        commentsStartTimestamp = event.eventCreatedOn;
+    public boolean retrieveLastComments(GerritChange change) {
         forcedReview = false;
-        CommentAddedEvent commentAddedEvent = (CommentAddedEvent) event;
+        CommentAddedEvent commentAddedEvent = (CommentAddedEvent) change.getEvent();
         authorUsername = commentAddedEvent.author.get().username;
-        log.debug("Found comments by '{}' on {}", authorUsername, commentsStartTimestamp);
+        log.debug("Found comments by '{}' on {}", authorUsername, change.getEventTimeStamp());
         if (authorUsername.equals(config.getGerritUserName())) {
             log.debug("These are the Bot's own comments, do not process them.");
             return false;
@@ -62,7 +59,7 @@
             log.info("Review of comments from user '{}' is disabled.", authorUsername);
             return false;
         }
-        addAllComments(fullChangeId);
+        addAllComments(change);
 
         return !commentProperties.isEmpty();
     }
@@ -77,9 +74,9 @@
         return requestItems.isEmpty() ? "" : gson.toJson(requestItems);
     }
 
-    private List<GerritComment> getLastComments(String fullChangeId) throws Exception {
+    private List<GerritComment> getLastComments(GerritChange change) throws Exception {
         URI uri = URI.create(config.getGerritAuthBaseUrl()
-                + UriResourceLocator.gerritGetAllPatchSetCommentsUri(fullChangeId));
+                + UriResourceLocator.gerritGetAllPatchSetCommentsUri(change.getFullChangeId()));
         String responseBody = forwardGetRequest(uri);
         Type mapEntryType = new TypeToken<Map<String, List<GerritComment>>>(){}.getType();
         Map<String, List<GerritComment>> lastCommentMap = gson.fromJson(responseBody, mapEntryType);
@@ -100,7 +97,7 @@
                 log.debug("Change Message Id: {} - Author: {}", latestChangeMessageId, commentAuthorUsername);
                 long updatedTimeStamp = getTimeStamp(commentObject.getUpdated());
                 if (commentAuthorUsername.equals(authorUsername) &&
-                        updatedTimeStamp >= commentsStartTimestamp - MAX_SECS_GAP_BETWEEN_EVENT_AND_COMMENT) {
+                        updatedTimeStamp >= change.getEventTimeStamp() - MAX_SECS_GAP_BETWEEN_EVENT_AND_COMMENT) {
                     log.debug("Found comment with updatedTimeStamp : {}", updatedTimeStamp);
                     latestChangeMessageId = changeMessageId;
                 }
@@ -136,9 +133,9 @@
         return reviewCommandMatcher.find();
     }
 
-    private void addAllComments(String fullChangeId) {
+    private void addAllComments(GerritChange change) {
         try {
-            List<GerritComment> latestComments = getLastComments(fullChangeId);
+            List<GerritComment> latestComments = getLastComments(change);
             if (latestComments == null) {
                 return;
             }
@@ -155,7 +152,7 @@
                 }
             }
         } catch (Exception e) {
-            log.error("Error while retrieving comments for change: {}", fullChangeId, e);
+            log.error("Error while retrieving comments for change: {}", change.getFullChangeId(), e);
         }
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientFacade.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientFacade.java
index f1c866e..e9d4733 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientFacade.java
@@ -1,6 +1,5 @@
 package com.googlesource.gerrit.plugins.chatgpt.client.gerrit;
 
-import com.google.gerrit.server.events.Event;
 import com.googlesource.gerrit.plugins.chatgpt.client.FileDiffProcessed;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.ReviewBatch;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.gerrit.GerritComment;
@@ -23,8 +22,8 @@
         gerritClientReview = new GerritClientReview(config);
     }
 
-    public String getPatchSet(String fullChangeId, boolean isCommentEvent) throws Exception {
-        return gerritClientPatchSet.getPatchSet(fullChangeId, isCommentEvent);
+    public String getPatchSet(GerritChange change) throws Exception {
+        return gerritClientPatchSet.getPatchSet(change);
     }
 
     public boolean getForcedReview() {
@@ -55,8 +54,8 @@
         gerritClientReview.setReview(fullChangeId, reviewBatches, reviewScore);
     }
 
-    public boolean retrieveLastComments(String fullChangeId, Event event) {
-        return gerritClientComments.retrieveLastComments(event, fullChangeId);
+    public boolean retrieveLastComments(GerritChange change) {
+        return gerritClientComments.retrieveLastComments(change);
     }
 
     public String getUserRequests() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientPatchSet.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientPatchSet.java
index c81e616..fa160a8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientPatchSet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/gerrit/GerritClientPatchSet.java
@@ -27,14 +27,14 @@
         diffs = new ArrayList<>();
     }
 
-    public String getPatchSet(String fullChangeId, boolean isCommentEvent) throws Exception {
-        int revisionBase = isCommentEvent ? 0 : retrieveRevisionBase(fullChangeId);
+    public String getPatchSet(GerritChange change) throws Exception {
+        int revisionBase = change.getIsCommentEvent() ? 0 : retrieveRevisionBase(change.getFullChangeId());
         log.debug("Revision base: {}", revisionBase);
 
-        List<String> files = getAffectedFiles(fullChangeId, revisionBase);
+        List<String> files = getAffectedFiles(change.getFullChangeId(), revisionBase);
         log.debug("Patch files: {}", files);
 
-        String fileDiffsJson = getFileDiffsJson(fullChangeId, files, revisionBase);
+        String fileDiffsJson = getFileDiffsJson(change.getFullChangeId(), files, revisionBase);
         log.debug("File diffs: {}", fileDiffsJson);
 
         return fileDiffsJson;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
index 2887d38..836d9fc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
@@ -2,23 +2,18 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.events.PatchSetEvent;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.chatgpt.PatchSetReviewer;
+import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritClient;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -54,39 +49,29 @@
         addShutdownHoot();
     }
 
-    public static String buildFullChangeId(Project.NameKey projectName, BranchNameKey branchName, Change.Key changeKey) {
-        return String.join("~", URLEncoder.encode(projectName.get(), StandardCharsets.UTF_8),
-                branchName.shortName(), changeKey.get());
-    }
-
     public void handleEvent(Configuration config, Event event) {
         this.config = config;
-        PatchSetEvent patchSetEvent = (PatchSetEvent) event;
-        Project.NameKey projectNameKey = patchSetEvent.getProjectNameKey();
-        BranchNameKey branchNameKey = patchSetEvent.getBranchNameKey();
-        Change.Key changeKey = patchSetEvent.getChangeKey();
-        String fullChangeId = buildFullChangeId(projectNameKey, branchNameKey, changeKey);
+        GerritChange change = new GerritChange(event);
+        gerritClient.initialize(config, change);
 
-        gerritClient.initialize(config, fullChangeId);
-
-        if (!preprocessEvent(event, fullChangeId, projectNameKey)) {
-            gerritClient.destroy(fullChangeId);
+        if (!preprocessEvent(change)) {
+            gerritClient.destroy(change);
             return;
         }
 
         // Execute the potentially time-consuming operation asynchronously
         latestFuture = CompletableFuture.runAsync(() -> {
             try {
-                log.info("Processing change: {}", fullChangeId);
-                reviewer.review(config, fullChangeId);
-                log.info("Finished processing change: {}", fullChangeId);
+                log.info("Processing change: {}", change.getFullChangeId());
+                reviewer.review(config, change);
+                log.info("Finished processing change: {}", change.getFullChangeId());
             } catch (Exception e) {
-                log.error("Error while processing change: {}", fullChangeId, e);
+                log.error("Error while processing change: {}", change.getFullChangeId(), e);
                 if (e instanceof InterruptedException) {
                     Thread.currentThread().interrupt();
                 }
             } finally {
-                gerritClient.destroy(fullChangeId);
+                gerritClient.destroy(change);
             }
         }, executorService);
     }
@@ -105,18 +90,9 @@
         }));
     }
 
-    private Optional<PatchSetAttribute> getPatchSetAttribute(PatchSetEvent patchSetEvent) {
+    private Optional<String> getTopic(GerritChange change) {
         try {
-            return Optional.ofNullable(patchSetEvent.patchSet.get());
-        }
-        catch (NullPointerException e) {
-            return Optional.empty();
-        }
-    }
-
-    private Optional<String> getTopic(PatchSetEvent patchSetEvent) {
-        try {
-            ChangeAttribute changeAttribute = patchSetEvent.change.get();
+            ChangeAttribute changeAttribute = change.getPatchSetEvent().change.get();
             return Optional.ofNullable(changeAttribute.topic);
         }
         catch (NullPointerException e) {
@@ -124,17 +100,17 @@
         }
     }
 
-    private boolean isReviewEnabled(PatchSetEvent patchSetEvent, Project.NameKey projectNameKey) {
+    private boolean isReviewEnabled(GerritChange change) {
         List<String> enabledProjects = Splitter.on(",").omitEmptyStrings()
                 .splitToList(config.getEnabledProjects());
         if (!config.isGlobalEnable() &&
-                !enabledProjects.contains(projectNameKey.get()) &&
+                !enabledProjects.contains(change.getProjectNameKey().get()) &&
                 !config.isProjectEnable()) {
-            log.debug("The project {} is not enabled for review", projectNameKey);
+            log.debug("The project {} is not enabled for review", change.getProjectNameKey());
             return false;
         }
 
-        String topic = getTopic(patchSetEvent).orElse("");
+        String topic = getTopic(change).orElse("");
         log.debug("PatchSet Topic retrieved: '{}'", topic);
         if (gerritClient.isDisabledTopic(topic)) {
             log.info("Disabled review for PatchSets with Topic '{}'", topic);
@@ -144,12 +120,12 @@
         return true;
     }
 
-    private boolean isPatchSetReviewEnabled(PatchSetEvent patchSetEvent) {
+    private boolean isPatchSetReviewEnabled(GerritChange change) {
         if (!config.getGptReviewPatchSet()) {
             log.debug("Disabled review function for created or updated PatchSets.");
             return false;
         }
-        Optional<PatchSetAttribute> patchSetAttributeOptional = getPatchSetAttribute(patchSetEvent);
+        Optional<PatchSetAttribute> patchSetAttributeOptional = change.getPatchSetAttribute();
         if (patchSetAttributeOptional.isEmpty()) {
             log.info("PatchSetAttribute event properties not retrieved");
             return false;
@@ -168,21 +144,20 @@
         return true;
     }
 
-    private boolean preprocessEvent(Event event, String fullChangeId, Project.NameKey projectNameKey) {
-        String eventType = Optional.ofNullable(event.getType()).orElse("");
+    private boolean preprocessEvent(GerritChange change) {
+        String eventType = Optional.ofNullable(change.getEventType()).orElse("");
         log.info("Event type {}", eventType);
         if (!EVENT_COMMENT_MAP.containsKey(eventType) ) {
             return false;
         }
-        PatchSetEvent patchSetEvent = (PatchSetEvent) event;
 
-        if (!isReviewEnabled(patchSetEvent, projectNameKey)) {
+        if (!isReviewEnabled(change)) {
             return false;
         }
         boolean isCommentEvent = EVENT_COMMENT_MAP.get(eventType);
         if (isCommentEvent) {
-            if (!gerritClient.retrieveLastComments(fullChangeId, event)) {
-                if (gerritClient.getForcedReview(fullChangeId)) {
+            if (!gerritClient.retrieveLastComments(change)) {
+                if (gerritClient.getForcedReview(change)) {
                     isCommentEvent = false;
                 }
                 else {
@@ -192,13 +167,13 @@
             }
         }
         else {
-            if (!isPatchSetReviewEnabled(patchSetEvent)) {
+            if (!isPatchSetReviewEnabled(change)) {
                 log.debug("Patch Set review disabled");
                 return false;
             }
         }
         log.debug("Flag `isCommentEvent` set to {}", isCommentEvent);
-        reviewer.setCommentEvent(isCommentEvent);
+        change.setIsCommentEvent(isCommentEvent);
 
         return true;
     }
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 0f40ec4..bb939a9 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTest.java
@@ -17,6 +17,7 @@
 import com.google.gson.JsonArray;
 import com.google.gson.Gson;
 import com.google.gson.JsonObject;
+import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.client.gerrit.GerritClient;
 import com.googlesource.gerrit.plugins.chatgpt.client.chatgpt.ChatGptClient;
 import com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator;
@@ -48,7 +49,6 @@
 
 import static com.google.gerrit.extensions.client.ChangeKind.REWORK;
 import static com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator.*;
-import static com.googlesource.gerrit.plugins.chatgpt.listener.EventListenerHandler.buildFullChangeId;
 import static java.net.HttpURLConnection.HTTP_OK;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -103,6 +103,10 @@
         initComparisonContent();
     }
 
+    private String getFullChangeId() {
+        return new GerritChange(PROJECT_NAME, BRANCH_NAME, CHANGE_ID).getFullChangeId();
+    }
+
     private void initConfig() {
         globalConfig = mock(PluginConfig.class);
         Answer<Object> returnDefaultArgument = invocation -> {
@@ -137,7 +141,7 @@
 
     private void setupMockRequests() {
         Configuration config = new Configuration(globalConfig, projectConfig);
-        String fullChangeId = buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID);
+        String fullChangeId = getFullChangeId();
 
         // Mock the behavior of the gerritAccountIdUri request
         WireMock.stubFor(WireMock.get(gerritAccountIdUri(GERRIT_GPT_USERNAME))
@@ -278,7 +282,7 @@
         future.get();
 
         RequestPatternBuilder requestPatternBuilder = WireMock.postRequestedFor(
-                WireMock.urlEqualTo(gerritSetReviewUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID))));
+                WireMock.urlEqualTo(gerritSetReviewUri(getFullChangeId())));
         List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
         Assert.assertEquals(1, loggedRequests.size());
         JsonObject gptRequestBody = gson.fromJson(chatGptClient.getRequestBody(), JsonObject.class);
@@ -325,7 +329,7 @@
         future.get();
 
         RequestPatternBuilder requestPatternBuilder = WireMock.postRequestedFor(
-                WireMock.urlEqualTo(gerritSetReviewUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID))));
+                WireMock.urlEqualTo(gerritSetReviewUri(getFullChangeId())));
         List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
         Assert.assertEquals(1, loggedRequests.size());
         JsonObject gptRequestBody = gson.fromJson(chatGptClient.getRequestBody(), JsonObject.class);
@@ -402,7 +406,7 @@
                 config.getCommentRequestUserPrompt()
         ));
         RequestPatternBuilder requestPatternBuilder = WireMock.postRequestedFor(
-                WireMock.urlEqualTo(gerritSetReviewUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID))));
+                WireMock.urlEqualTo(gerritSetReviewUri(getFullChangeId())));
         List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
         Assert.assertEquals(1, loggedRequests.size());
         JsonObject gptRequestBody = gson.fromJson(chatGptClient.getRequestBody(), JsonObject.class);