This is an automated email from the ASF dual-hosted git repository.

lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 286212af7d [core] RESTCatalog:  encode resource name and query value 
in url (#5223)
286212af7d is described below

commit 286212af7d8d413e82862d93b8d1bf90481a8354
Author: jerry <[email protected]>
AuthorDate: Fri Mar 7 11:50:03 2025 +0800

    [core] RESTCatalog:  encode resource name and query value in url (#5223)
---
 .../java/org/apache/paimon/rest/HttpClient.java    |  95 ++++++--------
 .../java/org/apache/paimon/rest/RESTCatalog.java   |  21 ++-
 .../main/java/org/apache/paimon/rest/RESTUtil.java |  29 +++++
 .../java/org/apache/paimon/rest/ResourcePaths.java | 144 ++++++++++++++++-----
 .../apache/paimon/rest/auth/RESTAuthParameter.java |   8 +-
 .../paimon/rest/responses/ErrorResponse.java       |  32 +----
 .../org/apache/paimon/catalog/CatalogTestBase.java |   3 +
 .../paimon/rest/DefaultErrorHandlerTest.java       |   3 +-
 .../org/apache/paimon/rest/HttpClientTest.java     |  68 ++++++++--
 .../org/apache/paimon/rest/RESTCatalogServer.java  |  14 +-
 .../apache/paimon/rest/RESTObjectMapperTest.java   |   4 +-
 .../org/apache/paimon/rest/ResourcePathsTest.java  |  37 ++++++
 .../paimon/open/api/RESTCatalogController.java     |   2 +-
 13 files changed, 310 insertions(+), 150 deletions(-)

diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/HttpClient.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/HttpClient.java
index 4bad680f90..a693cd2f48 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/HttpClient.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/HttpClient.java
@@ -23,7 +23,6 @@ import org.apache.paimon.rest.auth.RESTAuthFunction;
 import org.apache.paimon.rest.auth.RESTAuthParameter;
 import org.apache.paimon.rest.exceptions.RESTException;
 import org.apache.paimon.rest.responses.ErrorResponse;
-import org.apache.paimon.utils.Pair;
 import org.apache.paimon.utils.StringUtils;
 
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
@@ -42,9 +41,7 @@ import java.time.Duration;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
-import java.util.Objects;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import static okhttp3.ConnectionSpec.CLEARTEXT;
 import static okhttp3.ConnectionSpec.COMPATIBLE_TLS;
@@ -86,10 +83,10 @@ public class HttpClient implements RESTClient {
     @Override
     public <T extends RESTResponse> T get(
             String path, Class<T> responseType, RESTAuthFunction 
restAuthFunction) {
-        Map<String, String> authHeaders = getHeaders(path, "GET", "", 
restAuthFunction);
+        Map<String, String> authHeaders = getHeaders(uri, path, "GET", "", 
restAuthFunction);
         Request request =
                 new Request.Builder()
-                        .url(getRequestUrl(path))
+                        .url(getRequestUrl(uri, path, null))
                         .get()
                         .headers(Headers.of(authHeaders))
                         .build();
@@ -102,10 +99,11 @@ public class HttpClient implements RESTClient {
             Map<String, String> queryParams,
             Class<T> responseType,
             RESTAuthFunction restAuthFunction) {
-        Map<String, String> authHeaders = getHeaders(path, "GET", "", 
restAuthFunction);
+        Map<String, String> authHeaders =
+                getHeaders(uri, path, queryParams, "GET", "", 
restAuthFunction);
         Request request =
                 new Request.Builder()
-                        .url(getRequestUrl(path, queryParams))
+                        .url(getRequestUrl(uri, path, queryParams))
                         .get()
                         .headers(Headers.of(authHeaders))
                         .build();
@@ -126,11 +124,12 @@ public class HttpClient implements RESTClient {
             RESTAuthFunction restAuthFunction) {
         try {
             String bodyStr = OBJECT_MAPPER.writeValueAsString(body);
-            Map<String, String> authHeaders = getHeaders(path, "POST", 
bodyStr, restAuthFunction);
+            Map<String, String> authHeaders =
+                    getHeaders(uri, path, "POST", bodyStr, restAuthFunction);
             RequestBody requestBody = buildRequestBody(bodyStr);
             Request request =
                     new Request.Builder()
-                            .url(getRequestUrl(path))
+                            .url(getRequestUrl(uri, path, null))
                             .post(requestBody)
                             .headers(Headers.of(authHeaders))
                             .build();
@@ -142,10 +141,10 @@ public class HttpClient implements RESTClient {
 
     @Override
     public <T extends RESTResponse> T delete(String path, RESTAuthFunction 
restAuthFunction) {
-        Map<String, String> authHeaders = getHeaders(path, "DELETE", "", 
restAuthFunction);
+        Map<String, String> authHeaders = getHeaders(uri, path, "DELETE", "", 
restAuthFunction);
         Request request =
                 new Request.Builder()
-                        .url(getRequestUrl(path))
+                        .url(getRequestUrl(uri, path, null))
                         .delete()
                         .headers(Headers.of(authHeaders))
                         .build();
@@ -157,11 +156,12 @@ public class HttpClient implements RESTClient {
             String path, RESTRequest body, RESTAuthFunction restAuthFunction) {
         try {
             String bodyStr = OBJECT_MAPPER.writeValueAsString(body);
-            Map<String, String> authHeaders = getHeaders(path, "DELETE", 
bodyStr, restAuthFunction);
+            Map<String, String> authHeaders =
+                    getHeaders(uri, path, "DELETE", bodyStr, restAuthFunction);
             RequestBody requestBody = buildRequestBody(bodyStr);
             Request request =
                     new Request.Builder()
-                            .url(getRequestUrl(path))
+                            .url(getRequestUrl(uri, path, null))
                             .delete(requestBody)
                             .headers(Headers.of(authHeaders))
                             .build();
@@ -171,6 +171,19 @@ public class HttpClient implements RESTClient {
         }
     }
 
+    @VisibleForTesting
+    protected static String getRequestUrl(
+            String uri, String path, Map<String, String> queryParams) {
+        String fullPath = StringUtils.isNullOrWhitespaceOnly(path) ? uri : uri 
+ path;
+        if (queryParams != null && !queryParams.isEmpty()) {
+            HttpUrl httpUrl = HttpUrl.parse(fullPath);
+            HttpUrl.Builder builder = httpUrl.newBuilder();
+            queryParams.forEach(builder::addQueryParameter);
+            fullPath = builder.build().toString();
+        }
+        return fullPath;
+    }
+
     private <T extends RESTResponse> T exec(Request request, Class<T> 
responseType) {
         try (Response response = HTTP_CLIENT.newCall(request).execute()) {
             String responseBodyStr = response.body() != null ? 
response.body().string() : null;
@@ -208,62 +221,28 @@ public class HttpClient implements RESTClient {
         return RequestBody.create(body.getBytes(StandardCharsets.UTF_8), 
MEDIA_TYPE);
     }
 
-    private String getRequestUrl(String path) {
-        return getRequestUrl(path, null);
-    }
+    private static Map<String, String> getHeaders(
+            String uri,
+            String path,
+            String method,
+            String data,
+            Function<RESTAuthParameter, Map<String, String>> headerFunction) {
 
-    private String getRequestUrl(String path, Map<String, String> queryParams) 
{
-        String fullPath = StringUtils.isNullOrWhitespaceOnly(path) ? uri : uri 
+ path;
-        if (queryParams != null && !queryParams.isEmpty()) {
-            HttpUrl httpUrl = HttpUrl.parse(fullPath);
-            if (Objects.nonNull(httpUrl)) {
-                HttpUrl.Builder builder = httpUrl.newBuilder();
-                queryParams.forEach(builder::addQueryParameter);
-                return builder.build().toString();
-            } else {
-                return fullPath;
-            }
-        } else {
-            return fullPath;
-        }
+        return getHeaders(uri, path, Collections.emptyMap(), method, data, 
headerFunction);
     }
 
-    private Map<String, String> getHeaders(
+    private static Map<String, String> getHeaders(
+            String uri,
             String path,
+            Map<String, String> queryParams,
             String method,
             String data,
             Function<RESTAuthParameter, Map<String, String>> headerFunction) {
-        Pair<String, Map<String, String>> resourcePath2Parameters = 
parsePath(path);
         RESTAuthParameter restAuthParameter =
-                new RESTAuthParameter(
-                        URI.create(uri).getHost(),
-                        resourcePath2Parameters.getLeft(),
-                        resourcePath2Parameters.getValue(),
-                        method,
-                        data);
+                new RESTAuthParameter(URI.create(uri).getHost(), path, 
queryParams, method, data);
         return headerFunction.apply(restAuthParameter);
     }
 
-    @VisibleForTesting
-    protected static Pair<String, Map<String, String>> parsePath(String path) {
-        String[] paths = path.split("\\?");
-        String resourcePath = paths[0];
-        if (paths.length == 1) {
-            return Pair.of(resourcePath, Collections.emptyMap());
-        }
-        String query = paths[1];
-        Map<String, String> parameters =
-                Arrays.stream(query.split("&"))
-                        .map(pair -> pair.split("=", 2))
-                        .collect(
-                                Collectors.toMap(
-                                        pair -> pair[0].trim(), // key
-                                        pair -> pair[1].trim(), // value
-                                        (existing, replacement) -> existing // 
handle duplicates
-                                        ));
-        return Pair.of(resourcePath, parameters);
-    }
-
     @Override
     public void close() {}
 }
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
index c0e173c38d..fb80ff37cf 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
@@ -84,6 +84,7 @@ import org.apache.paimon.view.ViewImpl;
 import org.apache.paimon.view.ViewSchema;
 
 import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap;
 import org.apache.paimon.shade.guava30.com.google.common.collect.Maps;
 
 import org.apache.commons.lang3.StringUtils;
@@ -125,6 +126,7 @@ public class RESTCatalog implements Catalog, 
SupportsSnapshots, SupportsBranches
     public static final String HEADER_PREFIX = "header.";
     public static final String MAX_RESULTS = "maxResults";
     public static final String PAGE_TOKEN = "pageToken";
+    public static final String QUERY_PARAMETER_WAREHOUSE_KEY = "warehouse";
 
     private final RESTClient client;
     private final ResourcePaths resourcePaths;
@@ -145,11 +147,16 @@ public class RESTCatalog implements Catalog, 
SupportsSnapshots, SupportsBranches
         Map<String, String> baseHeaders = Collections.emptyMap();
         if (configRequired) {
             String warehouse = options.get(WAREHOUSE);
+            Map<String, String> queryParams =
+                    StringUtils.isNotEmpty(warehouse)
+                            ? ImmutableMap.of(QUERY_PARAMETER_WAREHOUSE_KEY, 
warehouse)
+                            : ImmutableMap.of();
             baseHeaders = extractPrefixMap(context.options(), HEADER_PREFIX);
             options =
                     new Options(
                             client.get(
-                                            ResourcePaths.config(warehouse),
+                                            ResourcePaths.config(),
+                                            queryParams,
                                             ConfigResponse.class,
                                             new RESTAuthFunction(
                                                     Collections.emptyMap(), 
catalogAuth))
@@ -416,10 +423,20 @@ public class RESTCatalog implements Catalog, 
SupportsSnapshots, SupportsBranches
     private TableMetadata loadTableMetadata(Identifier identifier) throws 
TableNotExistException {
         GetTableResponse response;
         try {
+            // if the table is system table, we need to load table metadata 
from the system table's
+            // data table
+            Identifier loadTableIdentifier =
+                    identifier.isSystemTable()
+                            ? new Identifier(
+                                    identifier.getDatabaseName(),
+                                    identifier.getTableName(),
+                                    identifier.getBranchName())
+                            : identifier;
             response =
                     client.get(
                             resourcePaths.table(
-                                    identifier.getDatabaseName(), 
identifier.getObjectName()),
+                                    loadTableIdentifier.getDatabaseName(),
+                                    loadTableIdentifier.getObjectName()),
                             GetTableResponse.class,
                             restAuthFunction);
         } catch (NoSuchResourceException e) {
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTUtil.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTUtil.java
index f957ae48af..f6f3ca43f3 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTUtil.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTUtil.java
@@ -24,6 +24,11 @@ import org.apache.paimon.utils.Preconditions;
 import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap;
 import org.apache.paimon.shade.guava30.com.google.common.collect.Maps;
 
+import java.io.UncheckedIOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.Map;
 
 /** Util for REST. */
@@ -58,4 +63,28 @@ public class RESTUtil {
 
         return builder.build();
     }
+
+    public static String encodeString(String toEncode) {
+        Preconditions.checkArgument(toEncode != null, "Invalid string to 
encode: null");
+        try {
+            return URLEncoder.encode(toEncode, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            throw new UncheckedIOException(
+                    String.format(
+                            "Failed to URL encode '%s': UTF-8 encoding is not 
supported", toEncode),
+                    e);
+        }
+    }
+
+    public static String decodeString(String encoded) {
+        Preconditions.checkArgument(encoded != null, "Invalid string to 
decode: null");
+        try {
+            return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            throw new UncheckedIOException(
+                    String.format(
+                            "Failed to URL decode '%s': UTF-8 encoding is not 
supported", encoded),
+                    e);
+        }
+    }
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
index 7499ad1625..efc8269113 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
@@ -34,10 +34,9 @@ public class ResourcePaths {
     private static final String VIEWS = "views";
     private static final String TABLE_DETAILS = "table-details";
     private static final String VIEW_DETAILS = "view-details";
-    public static final String QUERY_PARAMETER_WAREHOUSE_KEY = "warehouse";
 
-    public static String config(String warehouse) {
-        return String.format("%s/config?%s=%s", V1, 
QUERY_PARAMETER_WAREHOUSE_KEY, warehouse);
+    public static String config() {
+        return String.format("%s/config", V1);
     }
 
     public static ResourcePaths forCatalogProperties(Options options) {
@@ -55,87 +54,166 @@ public class ResourcePaths {
     }
 
     public String database(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName);
+        return SLASH.join(V1, prefix, DATABASES, 
RESTUtil.encodeString(databaseName));
     }
 
     public String databaseProperties(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, "properties");
+        return SLASH.join(V1, prefix, DATABASES, 
RESTUtil.encodeString(databaseName), "properties");
     }
 
     public String tables(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES);
+        return SLASH.join(V1, prefix, DATABASES, 
RESTUtil.encodeString(databaseName), TABLES);
     }
 
     public String tableDetails(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLE_DETAILS);
+        return SLASH.join(
+                V1, prefix, DATABASES, RESTUtil.encodeString(databaseName), 
TABLE_DETAILS);
     }
 
-    public String table(String databaseName, String tableName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
tableName);
+    public String table(String databaseName, String objectName) {
+        return SLASH.join(
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName));
     }
 
     public String renameTable(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
"rename");
+        return SLASH.join(
+                V1, prefix, DATABASES, RESTUtil.encodeString(databaseName), 
TABLES, "rename");
     }
 
     public String commitTable(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
"commit");
+        return SLASH.join(
+                V1, prefix, DATABASES, RESTUtil.encodeString(databaseName), 
TABLES, "commit");
     }
 
-    public String tableToken(String databaseName, String tableName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
tableName, "token");
+    public String tableToken(String databaseName, String objectName) {
+        return SLASH.join(
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                "token");
     }
 
-    public String tableSnapshot(String databaseName, String tableName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
tableName, "snapshot");
+    public String tableSnapshot(String databaseName, String objectName) {
+        return SLASH.join(
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                "snapshot");
     }
 
-    public String partitions(String databaseName, String tableName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
tableName, PARTITIONS);
+    public String partitions(String databaseName, String objectName) {
+        return SLASH.join(
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                PARTITIONS);
     }
 
-    public String dropPartitions(String databaseName, String tableName) {
+    public String dropPartitions(String databaseName, String objectName) {
         return SLASH.join(
-                V1, prefix, DATABASES, databaseName, TABLES, tableName, 
PARTITIONS, "drop");
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                PARTITIONS,
+                "drop");
     }
 
-    public String alterPartitions(String databaseName, String tableName) {
+    public String alterPartitions(String databaseName, String objectName) {
         return SLASH.join(
-                V1, prefix, DATABASES, databaseName, TABLES, tableName, 
PARTITIONS, "alter");
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                PARTITIONS,
+                "alter");
     }
 
-    public String markDonePartitions(String databaseName, String tableName) {
+    public String markDonePartitions(String databaseName, String objectName) {
         return SLASH.join(
-                V1, prefix, DATABASES, databaseName, TABLES, tableName, 
PARTITIONS, "mark");
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                PARTITIONS,
+                "mark");
     }
 
-    public String branches(String databaseName, String tableName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
tableName, BRANCHES);
+    public String branches(String databaseName, String objectName) {
+        return SLASH.join(
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                BRANCHES);
     }
 
-    public String branch(String databaseName, String tableName, String 
branchName) {
+    public String branch(String databaseName, String objectName, String 
branchName) {
         return SLASH.join(
-                V1, prefix, DATABASES, databaseName, TABLES, tableName, 
BRANCHES, branchName);
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                BRANCHES,
+                RESTUtil.encodeString(branchName));
     }
 
-    public String forwardBranch(String databaseName, String tableName) {
+    public String forwardBranch(String databaseName, String objectName) {
         return SLASH.join(
-                V1, prefix, DATABASES, databaseName, TABLES, tableName, 
BRANCHES, "forward");
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                TABLES,
+                RESTUtil.encodeString(objectName),
+                BRANCHES,
+                "forward");
     }
 
     public String views(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, VIEWS);
+        return SLASH.join(V1, prefix, DATABASES, 
RESTUtil.encodeString(databaseName), VIEWS);
     }
 
     public String viewDetails(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, VIEW_DETAILS);
+        return SLASH.join(V1, prefix, DATABASES, 
RESTUtil.encodeString(databaseName), VIEW_DETAILS);
     }
 
     public String view(String databaseName, String viewName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, VIEWS, 
viewName);
+        return SLASH.join(
+                V1,
+                prefix,
+                DATABASES,
+                RESTUtil.encodeString(databaseName),
+                VIEWS,
+                RESTUtil.encodeString(viewName));
     }
 
     public String renameView(String databaseName) {
-        return SLASH.join(V1, prefix, DATABASES, databaseName, VIEWS, 
"rename");
+        return SLASH.join(
+                V1, prefix, DATABASES, RESTUtil.encodeString(databaseName), 
VIEWS, "rename");
     }
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/rest/auth/RESTAuthParameter.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/auth/RESTAuthParameter.java
index f0bbf50495..c8bd6a222e 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/rest/auth/RESTAuthParameter.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/rest/auth/RESTAuthParameter.java
@@ -18,6 +18,9 @@
 
 package org.apache.paimon.rest.auth;
 
+import org.apache.paimon.rest.RESTUtil;
+
+import java.util.HashMap;
 import java.util.Map;
 
 /** RestAuthParameter for building rest auth header. */
@@ -37,7 +40,10 @@ public class RESTAuthParameter {
             String data) {
         this.host = host;
         this.resourcePath = resourcePath;
-        this.parameters = parameters;
+        this.parameters = new HashMap<>();
+        for (Map.Entry<String, String> entry : parameters.entrySet()) {
+            this.parameters.put(entry.getKey(), 
RESTUtil.encodeString(entry.getValue()));
+        }
         this.method = method;
         this.data = data;
     }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
index 8e88a37b11..5911ad820c 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
@@ -25,12 +25,6 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGet
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
 /** Response for error. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class ErrorResponse implements RESTResponse {
@@ -39,7 +33,6 @@ public class ErrorResponse implements RESTResponse {
     private static final String FIELD_RESOURCE_TYPE = "resourceType";
     private static final String FIELD_RESOURCE_NAME = "resourceName";
     private static final String FIELD_CODE = "code";
-    private static final String FIELD_STACK = "stack";
 
     @JsonProperty(FIELD_RESOURCE_TYPE)
     private final ErrorResponseResourceType resourceType;
@@ -53,9 +46,6 @@ public class ErrorResponse implements RESTResponse {
     @JsonProperty(FIELD_CODE)
     private final Integer code;
 
-    @JsonProperty(FIELD_STACK)
-    private final List<String> stack;
-
     public ErrorResponse(
             ErrorResponseResourceType resourceType,
             String resourceName,
@@ -65,7 +55,6 @@ public class ErrorResponse implements RESTResponse {
         this.resourceName = resourceName;
         this.code = code;
         this.message = message;
-        this.stack = new ArrayList<String>();
     }
 
     @JsonCreator
@@ -73,13 +62,11 @@ public class ErrorResponse implements RESTResponse {
             @JsonProperty(FIELD_RESOURCE_TYPE) ErrorResponseResourceType 
resourceType,
             @JsonProperty(FIELD_RESOURCE_NAME) String resourceName,
             @JsonProperty(FIELD_MESSAGE) String message,
-            @JsonProperty(FIELD_CODE) int code,
-            @JsonProperty(FIELD_STACK) List<String> stack) {
+            @JsonProperty(FIELD_CODE) int code) {
         this.resourceType = resourceType;
         this.resourceName = resourceName;
         this.message = message;
         this.code = code;
-        this.stack = stack;
     }
 
     @JsonGetter(FIELD_MESSAGE)
@@ -101,21 +88,4 @@ public class ErrorResponse implements RESTResponse {
     public Integer getCode() {
         return code;
     }
-
-    @JsonGetter(FIELD_STACK)
-    public List<String> getStack() {
-        return stack;
-    }
-
-    private List<String> getStackFromThrowable(Throwable throwable) {
-        if (throwable == null) {
-            return new ArrayList<String>();
-        }
-        StringWriter sw = new StringWriter();
-        try (PrintWriter pw = new PrintWriter(sw)) {
-            throwable.printStackTrace(pw);
-        }
-
-        return Arrays.asList(sw.toString().split("\n"));
-    }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java 
b/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java
index de383bccc3..d400df6f04 100644
--- a/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java
+++ b/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java
@@ -489,6 +489,9 @@ public abstract class CatalogTestBase {
         catalog.createTable(identifier, DEFAULT_TABLE_SCHEMA, false);
         Table systemTable = catalog.getTable(Identifier.create("test_db", 
"test_table$snapshots"));
         assertThat(systemTable).isNotNull();
+        Table systemTableCheckWithBranch =
+                catalog.getTable(new Identifier("test_db", "test_table", 
"main", "snapshots"));
+        assertThat(systemTableCheckWithBranch).isNotNull();
         Table dataTable = catalog.getTable(identifier);
         assertThat(dataTable).isNotNull();
 
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/DefaultErrorHandlerTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/DefaultErrorHandlerTest.java
index a94eff8db4..99c5460e86 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/rest/DefaultErrorHandlerTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/rest/DefaultErrorHandlerTest.java
@@ -33,7 +33,6 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
-import java.util.ArrayList;
 
 import static org.junit.Assert.assertThrows;
 
@@ -81,6 +80,6 @@ public class DefaultErrorHandlerTest {
     }
 
     private ErrorResponse generateErrorResponse(int code) {
-        return new ErrorResponse(null, null, "message", code, new 
ArrayList<String>());
+        return new ErrorResponse(null, null, "message", code);
     }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/HttpClientTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/HttpClientTest.java
index beaf8b7078..9440b9f52d 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/HttpClientTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/HttpClientTest.java
@@ -22,10 +22,10 @@ import org.apache.paimon.rest.auth.AuthProvider;
 import org.apache.paimon.rest.auth.AuthSession;
 import org.apache.paimon.rest.auth.BearTokenAuthProvider;
 import org.apache.paimon.rest.auth.RESTAuthFunction;
+import org.apache.paimon.rest.auth.RESTAuthParameter;
 import org.apache.paimon.rest.exceptions.BadRequestException;
 import org.apache.paimon.rest.responses.ErrorResponse;
 import org.apache.paimon.rest.responses.ErrorResponseResourceType;
-import org.apache.paimon.utils.Pair;
 
 import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap;
 
@@ -34,9 +34,11 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
@@ -90,7 +92,8 @@ public class HttpClientTest {
     @Test
     public void testGetSuccessWithQueryParams() {
         server.enqueueResponse(mockResponseDataStr, 200);
-        Map<String, String> queryParams = ImmutableMap.of("maxResults", "10", 
"pageToken", "abc");
+        Map<String, String> queryParams =
+                ImmutableMap.of("maxResults", "10", "pageToken", "abc=123");
         MockRESTData response =
                 httpClient.get(MOCK_PATH, queryParams, MockRESTData.class, 
restAuthFunction);
         assertEquals(mockResponseData.data(), response.data());
@@ -153,6 +156,20 @@ public class HttpClientTest {
                 BadRequestException.class, () -> httpClient.delete(MOCK_PATH, 
restAuthFunction));
     }
 
+    @Test
+    public void testDeleteWithDataSuccess() {
+        server.enqueueResponse(mockResponseDataStr, 200);
+        assertDoesNotThrow(() -> httpClient.delete(MOCK_PATH, 
mockResponseData, restAuthFunction));
+    }
+
+    @Test
+    public void testDeleteWithDataFail() {
+        server.enqueueResponse(errorResponseStr, 400);
+        assertThrows(
+                BadRequestException.class,
+                () -> httpClient.delete(MOCK_PATH, mockResponseData, 
restAuthFunction));
+    }
+
     @Test
     public void testRetry() {
         HttpClient httpClient = new HttpClient(server.getBaseUrl());
@@ -162,18 +179,41 @@ public class HttpClientTest {
     }
 
     @Test
-    public void testParsePath() {
-        assertEquals(
-                Pair.of("/api/v1/tables", Collections.emptyMap()),
-                HttpClient.parsePath("/api/v1/tables"));
-        assertEquals(
-                Pair.of("/api/v1/tables/my_table$schemas", 
Collections.emptyMap()),
-                HttpClient.parsePath("/api/v1/tables/my_table$schemas"));
+    public void testUrl() {
+        String queryKey = "pageToken";
+        RESTAuthParameter restAuthParameter =
+                new RESTAuthParameter(
+                        "http://a.b.c:8080";,
+                        "/api/v1/tables/my_table$schemas",
+                        ImmutableMap.of(queryKey, "dt=20230101"),
+                        "GET",
+                        "");
+        String url =
+                HttpClient.getRequestUrl(
+                        "http://a.b.c:8080";,
+                        "/api/v1/tables/my_table$schemas",
+                        ImmutableMap.of("pageToken", "dt=20230101"));
         assertEquals(
-                Pair.of("/api/v1/tables", ImmutableMap.of("pageSize", "10", 
"pageNum", "1")),
-                HttpClient.parsePath("/api/v1/tables?pageSize=10&pageNum=1"));
-        assertEquals(
-                Pair.of("/api/v1/tables", ImmutableMap.of("tableName", 
"t1,t2")),
-                HttpClient.parsePath("/api/v1/tables?tableName=t1,t2"));
+                
"http://a.b.c:8080/api/v1/tables/my_table$schemas?pageToken=dt%3D20230101";, 
url);
+        Map<String, String> queryParameters = getParameters(url);
+        assertEquals(restAuthParameter.parameters().get(queryKey), 
queryParameters.get(queryKey));
+    }
+
+    private Map<String, String> getParameters(String path) {
+        String[] paths = path.split("\\?");
+        if (paths.length == 1) {
+            return Collections.emptyMap();
+        }
+        String query = paths[1];
+        Map<String, String> parameters =
+                Arrays.stream(query.split("&"))
+                        .map(pair -> pair.split("=", 2))
+                        .collect(
+                                Collectors.toMap(
+                                        pair -> pair[0].trim(), // key
+                                        pair -> pair[1].trim(), // value
+                                        (existing, replacement) -> existing // 
handle duplicates
+                                        ));
+        return parameters;
     }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
index 7a11a0a62d..052bfeea70 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
@@ -214,7 +214,10 @@ public class RESTCatalogServer {
                     if (!("Bearer " + authToken).equals(token)) {
                         return new MockResponse().setResponseCode(401);
                     }
-                    if 
(request.getPath().equals(resourcePaths.config(warehouse))) {
+                    if (request.getPath().startsWith(resourcePaths.config())
+                            && request.getRequestUrl()
+                                    
.queryParameter(RESTCatalog.QUERY_PARAMETER_WAREHOUSE_KEY)
+                                    .equals(warehouse)) {
                         return mockResponse(configResponse, 200);
                     } else if (databaseUri.equals(request.getPath())) {
                         return databasesApiHandler(request);
@@ -223,7 +226,7 @@ public class RESTCatalogServer {
                                 request.getPath()
                                         .substring((databaseUri + 
"/").length())
                                         .split("/");
-                        String databaseName = resources[0];
+                        String databaseName = 
RESTUtil.decodeString(resources[0]);
                         if (noPermissionDatabases.contains(databaseName)) {
                             throw new 
Catalog.DatabaseNoPermissionException(databaseName);
                         }
@@ -295,7 +298,8 @@ public class RESTCatalogServer {
                                 resources.length >= 3
                                                 && 
!"rename".equals(resources[2])
                                                 && 
!"commit".equals(resources[2])
-                                        ? Identifier.create(databaseName, 
resources[2])
+                                        ? Identifier.create(
+                                                databaseName, 
RESTUtil.decodeString(resources[2]))
                                         : null;
                         if (identifier != null && 
"tables".equals(resources[1])) {
                             if (!identifier.isSystemTable()
@@ -311,7 +315,7 @@ public class RESTCatalogServer {
                                 || isDropPartitions
                                 || isAlterPartitions
                                 || isMarkDonePartitions) {
-                            String tableName = resources[2];
+                            String tableName = 
RESTUtil.decodeString(resources[2]);
                             Optional<MockResponse> error =
                                     checkTablePartitioned(
                                             Identifier.create(databaseName, 
tableName));
@@ -954,7 +958,7 @@ public class RESTCatalogServer {
         try {
             switch (request.getMethod()) {
                 case "DELETE":
-                    branch = resources[4];
+                    branch = RESTUtil.decodeString(resources[4]);
                     branchIdentifier =
                             new Identifier(
                                     identifier.getDatabaseName(),
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
index 4d9015ea77..17671af8a2 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
@@ -47,7 +47,6 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessin
 
 import org.junit.Test;
 
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -72,8 +71,7 @@ public class RESTObjectMapperTest {
     public void errorResponseParseTest() throws Exception {
         String message = "message";
         Integer code = 400;
-        ErrorResponse response =
-                new ErrorResponse(null, null, message, code, new 
ArrayList<String>());
+        ErrorResponse response = new ErrorResponse(null, null, message, code);
         String responseStr = OBJECT_MAPPER.writeValueAsString(response);
         ErrorResponse parseData = OBJECT_MAPPER.readValue(responseStr, 
ErrorResponse.class);
         assertEquals(message, parseData.getMessage());
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/ResourcePathsTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/ResourcePathsTest.java
new file mode 100644
index 0000000000..18a8a3cf42
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/ResourcePathsTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.paimon.rest;
+
+import org.junit.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/** Test for {@link ResourcePaths}. */
+public class ResourcePathsTest {
+
+    @Test
+    public void testUrlEncode() {
+        String database = "test_db";
+        String objectName = "test_table$snapshot";
+        ResourcePaths resourcePaths = new ResourcePaths("paimon");
+        assertEquals(
+                "/v1/paimon/databases/test_db/tables/test_table%24snapshot",
+                resourcePaths.table(database, objectName));
+    }
+}
diff --git 
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
 
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
index a3b7e3f422..1e8c2efeed 100644
--- 
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
+++ 
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
@@ -78,7 +78,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import static 
org.apache.paimon.rest.ResourcePaths.QUERY_PARAMETER_WAREHOUSE_KEY;
+import static org.apache.paimon.rest.RESTCatalog.QUERY_PARAMETER_WAREHOUSE_KEY;
 
 /** RESTCatalog management APIs. */
 @CrossOrigin(origins = "http://localhost:8081";)


Reply via email to