Add context locator for Java function definitions

Implemented Java-specific context locator to handle function definition
requests in Java projects.

Change-Id: Ib384e68aa98813d6b152f795136d9a9abadc2b5d
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRepoUploader.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRepoUploader.java
index 45f6d9c..f2ac692 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRepoUploader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRepoUploader.java
@@ -34,7 +34,7 @@
 
     public List<String> uploadRepoFiles() throws OpenAiConnectionFailException {
         log.debug("Starting uploading repository files.");
-        List<String> repoFiles = gitRepoFiles.getGitRepoFiles(config, change);
+        List<String> repoFiles = gitRepoFiles.getGitRepoFilesAsJson(config, change);
         List<String> filesIds = new ArrayList<>();
         filenameBase = sanitizeFilename(change.getProjectName());
         filenameIndex = 0;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFiles.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFiles.java
index 43aabfc..25b8b6b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFiles.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFiles.java
@@ -10,6 +10,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 
 import java.io.File;
@@ -30,7 +31,8 @@
     private List<String> enabledFileExtensions;
     private long fileSize;
 
-    public List<String> getGitRepoFiles(Configuration config, GerritChange change) {
+    public List<String> getGitRepoFilesAsJson(Configuration config, GerritChange change) {
+        log.debug("Getting Repository files as JSON");
         gitFileChunkBuilder = new GitFileChunkBuilder(config);
         enabledFileExtensions = config.getEnabledFileExtensions();
         try (Repository repository = openRepository(change)) {
@@ -43,6 +45,18 @@
         }
     }
 
+    public List<FileEntry> getDirFiles(Configuration config, GerritChange change, String path) {
+        log.debug("Getting files from selected directory");
+        enabledFileExtensions = config.getEnabledFileExtensions();
+        try (Repository repository = openRepository(change)) {
+            Map<String, List<FileEntry>> dirFilesMap = getDirFilesMap(repository, PathFilter.create(path));
+            log.debug("Retrieved file directories: {}", dirFilesMap.keySet());
+            return dirFilesMap.get(path);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to retrieve files in path " + path, e);
+        }
+    }
+
     public String getFileContent(GerritChange change, String path) throws IOException {
         try (Repository repository = openRepository(change);
              ObjectReader reader = repository.newObjectReader()) {
@@ -59,6 +73,18 @@
     }
 
     private List<Map<String, String>> listFilesWithContent(Repository repository) throws IOException, GitAPIException {
+        Map<String, List<FileEntry>> dirFilesMap = getDirFilesMap(repository, TreeFilter.ANY_DIFF);
+        for (Map.Entry<String, List<FileEntry>> entry : dirFilesMap.entrySet()) {
+            String dirPath = entry.getKey();
+            log.debug("File from dirFilesMap processed: {}", dirPath);
+            List<FileEntry> fileEntries = entry.getValue();
+            gitFileChunkBuilder.addFiles(dirPath, fileEntries);
+        }
+
+        return gitFileChunkBuilder.getChunks();
+    }
+
+    private Map<String, List<FileEntry>> getDirFilesMap(Repository repository, TreeFilter filter) throws IOException {
         Map<String, List<FileEntry>> dirFilesMap = new LinkedHashMap<>();
 
         try (ObjectReader reader = repository.newObjectReader()) {
@@ -67,7 +93,7 @@
             try (TreeWalk treeWalk = new TreeWalk(repository)) {
                 treeWalk.addTree(tree);
                 treeWalk.setRecursive(true);
-                treeWalk.setFilter(TreeFilter.ANY_DIFF);
+                treeWalk.setFilter(filter);
 
                 while (treeWalk.next()) {
                     String path = treeWalk.getPathString();
@@ -82,14 +108,7 @@
                 }
             }
         }
-        for (Map.Entry<String, List<FileEntry>> entry : dirFilesMap.entrySet()) {
-            String dirPath = entry.getKey();
-            log.debug("File from dirFilesMap processed: {}", dirPath);
-            List<FileEntry> fileEntries = entry.getValue();
-            gitFileChunkBuilder.addFiles(dirPath, fileEntries);
-        }
-
-        return gitFileChunkBuilder.getChunks();
+        return dirFilesMap;
     }
 
     private Repository openRepository(GerritChange change) throws IOException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeFileFetcher.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeFileFetcher.java
index 9bf0b86..520ee2d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeFileFetcher.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeFileFetcher.java
@@ -4,9 +4,14 @@
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.git.FileEntry;
 import lombok.extern.slf4j.Slf4j;
 
 import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 @Slf4j
 public class CodeFileFetcher extends ClientBase {
@@ -14,6 +19,7 @@
     private final GitRepoFiles gitRepoFiles;
 
     private String basePathRegEx = "";
+    private Map<String, String> preloadedFiles = new LinkedHashMap<>();
 
     public CodeFileFetcher(Configuration config, GerritChange change, GitRepoFiles gitRepoFiles) {
         super(config);
@@ -25,9 +31,19 @@
     }
 
     public String getFileContent(String filename) throws IOException {
+        if (preloadedFiles.containsKey(filename)) {
+            return preloadedFiles.get(filename);
+        }
         if (!basePathRegEx.isEmpty()) {
             filename = filename.replaceAll(basePathRegEx, "");
         }
         return gitRepoFiles.getFileContent(change, filename);
     }
+
+    public Set<String> getFilesInDir(String dirname) {
+        preloadedFiles = gitRepoFiles.getDirFiles(config, change, dirname)
+                .stream()
+                .collect(Collectors.toMap(FileEntry::getPath, FileEntry::getContent));
+        return preloadedFiles.keySet();
+    }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/language/java/CallableLocator.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/language/java/CallableLocator.java
new file mode 100644
index 0000000..b02aec6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/language/java/CallableLocator.java
@@ -0,0 +1,68 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.locator.language.java;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.interfaces.mode.stateful.client.code.context.ondemand.IEntityLocator;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.locator.CallableLocatorBase;
+import com.googlesource.gerrit.plugins.chatgpt.utils.FileUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Slf4j
+public class CallableLocator extends CallableLocatorBase implements IEntityLocator {
+    private static final String JAVA_MODULE_EXTENSION = ".java";
+    private static final Pattern IMPORT_PATTERN = Pattern.compile(
+            String.format("^import\\s+(?:static\\s+)?(%s)", DOT_NOTATION_REGEX),
+            Pattern.MULTILINE
+    );
+
+    public CallableLocator(Configuration config, GerritChange change, GitRepoFiles gitRepoFiles) {
+        super(config, change, gitRepoFiles);
+        log.debug("Initializing FunctionLocator");
+        languageModuleExtension = JAVA_MODULE_EXTENSION;
+    }
+
+    @Override
+    protected String getFunctionRegex(String functionName) {
+        return "^\\s*(?:@\\w+(?:\\(.*?\\))?\\s*)*" +  // Optional annotations
+                "(?:(?:public|protected|private|static|final|abstract|synchronized|native|strictfp)\\s+)*" +  // Optional modifiers
+                "(?:<[^>]+>\\s*)?" +  // Optional type parameters
+                "\\S+\\s+" +  // Return type
+                Pattern.quote(functionName) +  // Method name
+                "\\s*\\(.*?\\)" +  // Parameters
+                "(?:\\s*throws\\s+[^\\{;]+)?";  // Optional throws clause
+    }
+
+    @Override
+    protected String findImportedFunctionDefinition(String functionName, String content) {
+        parseImportStatements(content);
+        retrievePackageModules();
+
+        return findInModules(functionName);
+    }
+
+    private void retrievePackageModules() {
+        log.debug("Retrieving modules from current Package");
+        List<String> packageModules = codeFileFetcher.getFilesInDir(rootFileDir)
+                .stream()
+                .map(FileUtils::removeExtension)
+                .toList();
+        log.debug("Modules retrieved from current Package: {}", packageModules);
+        importModules.addAll(packageModules);
+    }
+
+    private void parseImportStatements(String content) {
+        log.debug("Parsing import statements");
+        Matcher importMatcher = IMPORT_PATTERN.matcher(content);
+        while (importMatcher.find()) {
+            String importModulesGroup = importMatcher.group(1);
+            log.debug("Parsing IMPORT module: `{}`", importModulesGroup);
+            importModules.add(importModulesGroup);
+        }
+        log.debug("Found importModules from import statements: {}", importModules);
+    }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java
index 994f247..edabe47 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java
@@ -61,6 +61,14 @@
         return filename.substring(lastDotIndex + 1);
     }
 
+    public static String removeExtension(String filename) {
+        int lastDotIndex = filename.lastIndexOf('.');
+        if (lastDotIndex <= 0 || lastDotIndex == filename.length() - 1) {
+            return filename;
+        }
+        return filename.substring(0, lastDotIndex);
+    }
+
     public static String getDirName(String filename) {
         return Optional.ofNullable(new File(filename).getParent()).orElse("");
     }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTestBase.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTestBase.java
index cc09f67..6ab42ca 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTestBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTestBase.java
@@ -81,7 +81,7 @@
 
         // Mock the behavior of the Git Repository Manager
         String repoJson = readTestFile("__files/stateful/gitProjectFiles.json");
-        when(gitRepoFiles.getGitRepoFiles(any(), any())).thenReturn(List.of(repoJson));
+        when(gitRepoFiles.getGitRepoFilesAsJson(any(), any())).thenReturn(List.of(repoJson));
 
         // Mock the behavior of the ChatGPT create-file request
         WireMock.stubFor(WireMock.post(WireMock.urlEqualTo(UriResourceLocatorStateful.filesCreateUri()))