Repository: apache/gravitino (Java)

Issue:
[Bug report] Prevent java.lang.IllegalStateException: Duplicate key in CredentialOperationDispatcher.java
### What would you like to be improved?

In core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java, getPathBasedCredentialContexts will throw an exception if a duplicate credential type is passed, even if those are for different paths.

Here's a test showing that:
```
  @Test
  void testMergePathContextsWithSameCredentialType() {
    List<PathContext> pathContexts =
        Arrays.asList(new PathContext("path1", "dummy"), new PathContext("path2", "dummy"));

    Map<String, CredentialContext> contexts =
        CredentialOperationDispatcher.getPathBasedCredentialContexts(
            CredentialPrivilege.WRITE, pathContexts);

    Assertions.assertEquals(1, contexts.size());
    PathBasedCredentialContext context = (PathBasedCredentialContext) contexts.get("dummy");
    Assertions.assertEquals(Set.of("path1", "path2"), context.getWritePaths());
    Assertions.assertTrue(context.getReadPaths().isEmpty());
  }
```

### How should we improve?

No excpetion shoudl be thrown.


Discussion:
@justinmclean 

Hi, I'd like to work on this issue. Could you assign it to me?
It's all yours. If you need any advice or help, just ask.



Patch:
diff --git a/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java b/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java
index 2ec76aeb4ad..208791569ff 100644
--- a/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/credential/CredentialOperationDispatcher.java
@@ -112,7 +112,17 @@ public static Map<String, CredentialContext> getPathBasedCredentialContexts(
                   }
                   return new PathBasedCredentialContext(
                       PrincipalUtils.getCurrentUserName(), writePaths, readPaths);
-                }));
+                },
+                CredentialOperationDispatcher::mergeContexts));
+  }
+
+  private static PathBasedCredentialContext mergeContexts(
+      CredentialContext oldValue, CredentialContext newValue) {
+    PathBasedCredentialContext oldContext = (PathBasedCredentialContext) oldValue;
+    PathBasedCredentialContext newContext = (PathBasedCredentialContext) newValue;
+    oldContext.getWritePaths().addAll(newContext.getWritePaths());
+    oldContext.getReadPaths().addAll(newContext.getReadPaths());
+    return oldContext;
   }
 
   @SuppressWarnings("UnusedVariable")


Test patch:
diff --git a/core/src/test/java/org/apache/gravitino/credential/TestCredentialOperationDispatcher.java b/core/src/test/java/org/apache/gravitino/credential/TestCredentialOperationDispatcher.java
new file mode 100644
index 00000000000..6af65237e0c
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/credential/TestCredentialOperationDispatcher.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.gravitino.credential;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.gravitino.catalog.TestOperationDispatcher;
+import org.apache.gravitino.connector.credential.PathContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestCredentialOperationDispatcher extends TestOperationDispatcher {
+
+  @Test
+  public void testMergePathContextsWithSameCredentialType() {
+    List<PathContext> pathContexts =
+        Arrays.asList(new PathContext("path1", "dummy"), new PathContext("path2", "dummy"));
+
+    Map<String, CredentialContext> contexts =
+        CredentialOperationDispatcher.getPathBasedCredentialContexts(
+            CredentialPrivilege.WRITE, pathContexts);
+
+    Assertions.assertEquals(1, contexts.size());
+    PathBasedCredentialContext context = (PathBasedCredentialContext) contexts.get("dummy");
+    Assertions.assertEquals(Set.of("path1", "path2"), context.getWritePaths());
+    Assertions.assertTrue(context.getReadPaths().isEmpty());
+  }
+
+  @Test
+  public void testMergePathContextsWithSameCredentialTypeAndSamePath() {
+    List<PathContext> pathContexts =
+        Arrays.asList(new PathContext("path1", "dummy"), new PathContext("path1", "dummy"));
+
+    Map<String, CredentialContext> contexts =
+        CredentialOperationDispatcher.getPathBasedCredentialContexts(
+            CredentialPrivilege.WRITE, pathContexts);
+
+    Assertions.assertEquals(1, contexts.size());
+    PathBasedCredentialContext context = (PathBasedCredentialContext) contexts.get("dummy");
+    Assertions.assertEquals(Set.of("path1"), context.getWritePaths());
+    Assertions.assertTrue(context.getReadPaths().isEmpty());
+  }
+}