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")