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 c0f61e2668 [core] Add database API implementation in RESTCatalog
(#4676)
c0f61e2668 is described below
commit c0f61e2668b051f226255d4e3a11c965aa258870
Author: lining <[email protected]>
AuthorDate: Wed Dec 11 22:18:23 2024 +0800
[core] Add database API implementation in RESTCatalog (#4676)
---
.../apache/paimon/rest/DefaultErrorHandler.java | 27 ++--
.../java/org/apache/paimon/rest/HttpClient.java | 24 ++-
.../java/org/apache/paimon/rest/RESTCatalog.java | 45 +++++-
.../paimon/rest/RESTCatalogInternalOptions.java | 5 +
.../java/org/apache/paimon/rest/RESTClient.java | 2 +
.../java/org/apache/paimon/rest/RESTMessage.java | 3 +
.../java/org/apache/paimon/rest/ResourcePaths.java | 11 ++
.../org/apache/paimon/rest/auth/AuthSession.java | 54 ++++---
.../AlreadyExistsException.java} | 11 +-
.../NoSuchResourceException.java} | 11 +-
.../rest/requests/CreateDatabaseRequest.java | 69 ++++++++
.../paimon/rest/requests/DropDatabaseRequest.java | 56 +++++++
.../paimon/rest/responses/ConfigResponse.java | 15 +-
.../rest/responses/CreateDatabaseResponse.java | 58 +++++++
.../DatabaseName.java} | 29 ++--
.../paimon/rest/responses/ErrorResponse.java | 20 ++-
.../paimon/rest/responses/GetDatabaseResponse.java | 78 +++++++++
.../rest/responses/ListDatabasesResponse.java | 45 ++++++
.../paimon/rest/DefaultErrorHandlerTest.java | 8 +
.../org/apache/paimon/rest/HttpClientTest.java | 14 ++
.../org/apache/paimon/rest/MockRESTMessage.java | 74 +++++++++
.../org/apache/paimon/rest/RESTCatalogTest.java | 58 ++++++-
.../apache/paimon/rest/RESTObjectMapperTest.java | 64 +++++++-
.../apache/paimon/rest/auth/AuthSessionTest.java | 18 +++
paimon-open-api/generate.sh | 1 +
paimon-open-api/rest-catalog-open-api.yaml | 180 ++++++++++++++++++++-
.../paimon/open/api/RESTCatalogController.java | 120 ++++++++++++--
.../paimon/open/api/config/OpenAPIConfig.java | 1 -
28 files changed, 1007 insertions(+), 94 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/DefaultErrorHandler.java
b/paimon-core/src/main/java/org/apache/paimon/rest/DefaultErrorHandler.java
index 1a8618c1c6..ce2cbb56ae 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/DefaultErrorHandler.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/DefaultErrorHandler.java
@@ -18,8 +18,10 @@
package org.apache.paimon.rest;
+import org.apache.paimon.rest.exceptions.AlreadyExistsException;
import org.apache.paimon.rest.exceptions.BadRequestException;
import org.apache.paimon.rest.exceptions.ForbiddenException;
+import org.apache.paimon.rest.exceptions.NoSuchResourceException;
import org.apache.paimon.rest.exceptions.NotAuthorizedException;
import org.apache.paimon.rest.exceptions.RESTException;
import org.apache.paimon.rest.exceptions.ServiceFailureException;
@@ -28,6 +30,7 @@ import org.apache.paimon.rest.responses.ErrorResponse;
/** Default error handler. */
public class DefaultErrorHandler extends ErrorHandler {
+
private static final ErrorHandler INSTANCE = new DefaultErrorHandler();
public static ErrorHandler getInstance() {
@@ -36,26 +39,32 @@ public class DefaultErrorHandler extends ErrorHandler {
@Override
public void accept(ErrorResponse error) {
- int code = error.code();
+ int code = error.getCode();
+ String message = error.getMessage();
switch (code) {
case 400:
- throw new BadRequestException(
- String.format("Malformed request: %s",
error.message()));
+ throw new BadRequestException(String.format("Malformed
request: %s", message));
case 401:
- throw new NotAuthorizedException("Not authorized: %s",
error.message());
+ throw new NotAuthorizedException("Not authorized: %s",
message);
case 403:
- throw new ForbiddenException("Forbidden: %s", error.message());
+ throw new ForbiddenException("Forbidden: %s", message);
+ case 404:
+ throw new NoSuchResourceException("%s", message);
case 405:
case 406:
break;
+ case 409:
+ throw new AlreadyExistsException("%s", message);
case 500:
- throw new ServiceFailureException("Server error: %s",
error.message());
+ throw new ServiceFailureException("Server error: %s", message);
case 501:
- throw new UnsupportedOperationException(error.message());
+ throw new UnsupportedOperationException(message);
case 503:
- throw new ServiceUnavailableException("Service unavailable:
%s", error.message());
+ throw new ServiceUnavailableException("Service unavailable:
%s", message);
+ default:
+ break;
}
- throw new RESTException("Unable to process: %s", error.message());
+ throw new RESTException("Unable to process: %s", message);
}
}
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 e092711e5f..97696aef09 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
@@ -95,6 +95,23 @@ public class HttpClient implements RESTClient {
}
}
+ @Override
+ public <T extends RESTResponse> T delete(
+ String path, RESTRequest body, Map<String, String> headers) {
+ try {
+ RequestBody requestBody = buildRequestBody(body);
+ Request request =
+ new Request.Builder()
+ .url(uri + path)
+ .delete(requestBody)
+ .headers(Headers.of(headers))
+ .build();
+ return exec(request, null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
@Override
public void close() throws IOException {
okHttpClient.dispatcher().cancelAll();
@@ -111,10 +128,13 @@ public class HttpClient implements RESTClient {
response.code());
errorHandler.accept(error);
}
- if (responseBodyStr == null) {
+ if (responseType != null && responseBodyStr != null) {
+ return mapper.readValue(responseBodyStr, responseType);
+ } else if (responseType == null) {
+ return null;
+ } else {
throw new RESTException("response body is null.");
}
- return mapper.readValue(responseBodyStr, responseType);
} catch (Exception e) {
throw new RESTException(e, "rest exception");
}
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 f3007bf4bf..3c2538df0c 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
@@ -29,12 +29,21 @@ import org.apache.paimon.options.Options;
import org.apache.paimon.rest.auth.AuthSession;
import org.apache.paimon.rest.auth.CredentialsProvider;
import org.apache.paimon.rest.auth.CredentialsProviderFactory;
+import org.apache.paimon.rest.exceptions.AlreadyExistsException;
+import org.apache.paimon.rest.exceptions.NoSuchResourceException;
+import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.DropDatabaseRequest;
import org.apache.paimon.rest.responses.ConfigResponse;
+import org.apache.paimon.rest.responses.CreateDatabaseResponse;
+import org.apache.paimon.rest.responses.DatabaseName;
+import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.ListDatabasesResponse;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaChange;
import org.apache.paimon.table.Table;
import
org.apache.paimon.shade.guava30.com.google.common.annotations.VisibleForTesting;
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
@@ -42,6 +51,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
import static
org.apache.paimon.utils.ThreadPoolUtils.createScheduledThreadPool;
@@ -113,24 +123,49 @@ public class RESTCatalog implements Catalog {
@Override
public List<String> listDatabases() {
- throw new UnsupportedOperationException();
+ ListDatabasesResponse response =
+ client.get(resourcePaths.databases(),
ListDatabasesResponse.class, headers());
+ if (response.getDatabases() != null) {
+ return response.getDatabases().stream()
+ .map(DatabaseName::getName)
+ .collect(Collectors.toList());
+ }
+ return ImmutableList.of();
}
@Override
public void createDatabase(String name, boolean ignoreIfExists,
Map<String, String> properties)
throws DatabaseAlreadyExistException {
- throw new UnsupportedOperationException();
+ CreateDatabaseRequest request = new CreateDatabaseRequest(name,
ignoreIfExists, properties);
+ try {
+ client.post(
+ resourcePaths.databases(), request,
CreateDatabaseResponse.class, headers());
+ } catch (AlreadyExistsException e) {
+ throw new DatabaseAlreadyExistException(name);
+ }
}
@Override
public Database getDatabase(String name) throws DatabaseNotExistException {
- throw new UnsupportedOperationException();
+ try {
+ GetDatabaseResponse response =
+ client.get(resourcePaths.database(name),
GetDatabaseResponse.class, headers());
+ return new Database.DatabaseImpl(
+ name, response.options(), response.comment().orElseGet(()
-> null));
+ } catch (NoSuchResourceException e) {
+ throw new DatabaseNotExistException(name);
+ }
}
@Override
public void dropDatabase(String name, boolean ignoreIfNotExists, boolean
cascade)
throws DatabaseNotExistException, DatabaseNotEmptyException {
- throw new UnsupportedOperationException();
+ DropDatabaseRequest request = new
DropDatabaseRequest(ignoreIfNotExists, cascade);
+ try {
+ client.delete(resourcePaths.database(name), request, headers());
+ } catch (NoSuchResourceException e) {
+ throw new DatabaseNotExistException(name);
+ }
}
@Override
@@ -208,7 +243,7 @@ public class RESTCatalog implements Catalog {
Map<String, String> fetchOptionsFromServer(
Map<String, String> headers, Map<String, String> clientProperties)
{
ConfigResponse response =
- client.get(ResourcePaths.V1_CONFIG, ConfigResponse.class,
headers());
+ client.get(ResourcePaths.V1_CONFIG, ConfigResponse.class,
headers);
return response.merge(clientProperties);
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogInternalOptions.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogInternalOptions.java
index 62a8bf134a..722010923c 100644
---
a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogInternalOptions.java
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogInternalOptions.java
@@ -33,4 +33,9 @@ public class RESTCatalogInternalOptions {
.stringType()
.noDefaultValue()
.withDescription("REST Catalog auth credentials
provider.");
+ public static final ConfigOption<String> DATABASE_COMMENT =
+ ConfigOptions.key("comment")
+ .stringType()
+ .defaultValue(null)
+ .withDescription("REST Catalog database comment.");
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTClient.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTClient.java
index feeed06a41..d0244f309e 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTClient.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTClient.java
@@ -28,4 +28,6 @@ public interface RESTClient extends Closeable {
<T extends RESTResponse> T post(
String path, RESTRequest body, Class<T> responseType, Map<String,
String> headers);
+
+ <T extends RESTResponse> T delete(String path, RESTRequest body,
Map<String, String> headers);
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
index 6cb0b6fa65..31d46df7ef 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
@@ -18,5 +18,8 @@
package org.apache.paimon.rest;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
/** Interface to mark both REST requests and responses. */
+@JsonIgnoreProperties(ignoreUnknown = true)
public interface RESTMessage {}
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 aaca619380..a6d0000a22 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
@@ -18,10 +18,13 @@
package org.apache.paimon.rest;
+import java.util.StringJoiner;
+
/** Resource paths for REST catalog. */
public class ResourcePaths {
public static final String V1_CONFIG = "/api/v1/config";
+ private static final StringJoiner SLASH = new StringJoiner("/");
public static ResourcePaths forCatalogProperties(String prefix) {
return new ResourcePaths(prefix);
@@ -32,4 +35,12 @@ public class ResourcePaths {
public ResourcePaths(String prefix) {
this.prefix = prefix;
}
+
+ public String databases() {
+ return
SLASH.add("api").add("v1").add(prefix).add("databases").toString();
+ }
+
+ public String database(String databaseName) {
+ return
SLASH.add("api").add("v1").add(prefix).add("databases").add(databaseName).toString();
+ }
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/auth/AuthSession.java
b/paimon-core/src/main/java/org/apache/paimon/rest/auth/AuthSession.java
index 74efb8508a..3ca7590e5f 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/auth/AuthSession.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/auth/AuthSession.java
@@ -33,9 +33,10 @@ import java.util.concurrent.TimeUnit;
public class AuthSession {
static final int TOKEN_REFRESH_NUM_RETRIES = 5;
+ static final long MIN_REFRESH_WAIT_MILLIS = 10;
+ static final long MAX_REFRESH_WINDOW_MILLIS = 300_000; // 5 minutes
+
private static final Logger log =
LoggerFactory.getLogger(AuthSession.class);
- private static final long MAX_REFRESH_WINDOW_MILLIS = 300_000; // 5 minutes
- private static final long MIN_REFRESH_WAIT_MILLIS = 10;
private final CredentialsProvider credentialsProvider;
private volatile Map<String, String> headers;
@@ -76,12 +77,38 @@ public class AuthSession {
return headers;
}
+ public Boolean refresh() {
+ if (this.credentialsProvider.supportRefresh()
+ && this.credentialsProvider.keepRefreshed()
+ && this.credentialsProvider.expiresInMills().isPresent()) {
+ boolean isSuccessful = this.credentialsProvider.refresh();
+ if (isSuccessful) {
+ Map<String, String> currentHeaders = this.headers;
+ this.headers =
+ RESTUtil.merge(currentHeaders,
this.credentialsProvider.authHeader());
+ }
+ return isSuccessful;
+ }
+
+ return false;
+ }
+
@VisibleForTesting
static void scheduleTokenRefresh(
ScheduledExecutorService executor, AuthSession session, long
expiresAtMillis) {
scheduleTokenRefresh(executor, session, expiresAtMillis, 0);
}
+ @VisibleForTesting
+ static long getTimeToWaitByExpiresInMills(long expiresInMillis) {
+ // how much ahead of time to start the refresh to allow it to complete
+ long refreshWindowMillis = Math.min(expiresInMillis,
MAX_REFRESH_WINDOW_MILLIS);
+ // how much time to wait before expiration
+ long waitIntervalMillis = expiresInMillis - refreshWindowMillis;
+ // how much time to actually wait
+ return Math.max(waitIntervalMillis, MIN_REFRESH_WAIT_MILLIS);
+ }
+
private static void scheduleTokenRefresh(
ScheduledExecutorService executor,
AuthSession session,
@@ -89,12 +116,7 @@ public class AuthSession {
int retryTimes) {
if (retryTimes < TOKEN_REFRESH_NUM_RETRIES) {
long expiresInMillis = expiresAtMillis -
System.currentTimeMillis();
- // how much ahead of time to start the refresh to allow it to
complete
- long refreshWindowMillis = Math.min(expiresInMillis,
MAX_REFRESH_WINDOW_MILLIS);
- // how much time to wait before expiration
- long waitIntervalMillis = expiresInMillis - refreshWindowMillis;
- // how much time to actually wait
- long timeToWait = Math.max(waitIntervalMillis,
MIN_REFRESH_WAIT_MILLIS);
+ long timeToWait = getTimeToWaitByExpiresInMills(expiresInMillis);
executor.schedule(
() -> {
@@ -118,20 +140,4 @@ public class AuthSession {
log.warn("Failed to refresh token after {} retries.",
TOKEN_REFRESH_NUM_RETRIES);
}
}
-
- public Boolean refresh() {
- if (this.credentialsProvider.supportRefresh()
- && this.credentialsProvider.keepRefreshed()
- && this.credentialsProvider.expiresInMills().isPresent()) {
- boolean isSuccessful = this.credentialsProvider.refresh();
- if (isSuccessful) {
- Map<String, String> currentHeaders = this.headers;
- this.headers =
- RESTUtil.merge(currentHeaders,
this.credentialsProvider.authHeader());
- }
- return isSuccessful;
- }
-
- return false;
- }
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/AlreadyExistsException.java
similarity index 74%
copy from paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
copy to
paimon-core/src/main/java/org/apache/paimon/rest/exceptions/AlreadyExistsException.java
index 6cb0b6fa65..8e30c8375b 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/AlreadyExistsException.java
@@ -16,7 +16,12 @@
* limitations under the License.
*/
-package org.apache.paimon.rest;
+package org.apache.paimon.rest.exceptions;
-/** Interface to mark both REST requests and responses. */
-public interface RESTMessage {}
+/** Exception thrown on HTTP 409 means a resource already exists. */
+public class AlreadyExistsException extends RESTException {
+
+ public AlreadyExistsException(String message, Object... args) {
+ super(message, args);
+ }
+}
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/NoSuchResourceException.java
similarity index 74%
copy from paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
copy to
paimon-core/src/main/java/org/apache/paimon/rest/exceptions/NoSuchResourceException.java
index 6cb0b6fa65..cc4c7881f4 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/NoSuchResourceException.java
@@ -16,7 +16,12 @@
* limitations under the License.
*/
-package org.apache.paimon.rest;
+package org.apache.paimon.rest.exceptions;
-/** Interface to mark both REST requests and responses. */
-public interface RESTMessage {}
+/** Exception thrown on HTTP 404 means a resource not exists. */
+public class NoSuchResourceException extends RESTException {
+
+ public NoSuchResourceException(String message, Object... args) {
+ super(message, args);
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java
new file mode 100644
index 0000000000..6067bf544b
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.requests;
+
+import org.apache.paimon.rest.RESTRequest;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Map;
+
+/** Request for creating database. */
+public class CreateDatabaseRequest implements RESTRequest {
+
+ private static final String FIELD_NAME = "name";
+ private static final String FIELD_IGNORE_IF_EXISTS = "ignoreIfExists";
+ private static final String FIELD_OPTIONS = "options";
+
+ @JsonProperty(FIELD_NAME)
+ private String name;
+
+ @JsonProperty(FIELD_IGNORE_IF_EXISTS)
+ private boolean ignoreIfExists;
+
+ @JsonProperty(FIELD_OPTIONS)
+ private Map<String, String> options;
+
+ @JsonCreator
+ public CreateDatabaseRequest(
+ @JsonProperty(FIELD_NAME) String name,
+ @JsonProperty(FIELD_IGNORE_IF_EXISTS) boolean ignoreIfExists,
+ @JsonProperty(FIELD_OPTIONS) Map<String, String> options) {
+ this.name = name;
+ this.ignoreIfExists = ignoreIfExists;
+ this.options = options;
+ }
+
+ @JsonGetter(FIELD_NAME)
+ public String getName() {
+ return name;
+ }
+
+ @JsonGetter(FIELD_IGNORE_IF_EXISTS)
+ public boolean getIgnoreIfExists() {
+ return ignoreIfExists;
+ }
+
+ @JsonGetter(FIELD_OPTIONS)
+ public Map<String, String> getOptions() {
+ return options;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/requests/DropDatabaseRequest.java
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/DropDatabaseRequest.java
new file mode 100644
index 0000000000..d97f211c1c
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/DropDatabaseRequest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.requests;
+
+import org.apache.paimon.rest.RESTRequest;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+/** Request for DropDatabase. */
+public class DropDatabaseRequest implements RESTRequest {
+
+ private static final String FIELD_IGNORE_IF_EXISTS = "ignoreIfExists";
+ private static final String FIELD_CASCADE = "cascade";
+
+ @JsonProperty(FIELD_IGNORE_IF_EXISTS)
+ private final boolean ignoreIfNotExists;
+
+ @JsonProperty(FIELD_CASCADE)
+ private final boolean cascade;
+
+ @JsonCreator
+ public DropDatabaseRequest(
+ @JsonProperty(FIELD_IGNORE_IF_EXISTS) boolean ignoreIfNotExists,
+ @JsonProperty(FIELD_CASCADE) boolean cascade) {
+ this.ignoreIfNotExists = ignoreIfNotExists;
+ this.cascade = cascade;
+ }
+
+ @JsonGetter(FIELD_IGNORE_IF_EXISTS)
+ public boolean getIgnoreIfNotExists() {
+ return ignoreIfNotExists;
+ }
+
+ @JsonGetter(FIELD_CASCADE)
+ public boolean getCascade() {
+ return cascade;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ConfigResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ConfigResponse.java
index 903cfc84b4..e8fff88b09 100644
---
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ConfigResponse.java
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ConfigResponse.java
@@ -23,17 +23,16 @@ 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
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
-import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
-import java.beans.ConstructorProperties;
import java.util.Map;
import java.util.Objects;
/** Response for getting config. */
-@JsonIgnoreProperties(ignoreUnknown = true)
public class ConfigResponse implements RESTResponse {
+
private static final String FIELD_DEFAULTS = "defaults";
private static final String FIELD_OVERRIDES = "overrides";
@@ -43,8 +42,10 @@ public class ConfigResponse implements RESTResponse {
@JsonProperty(FIELD_OVERRIDES)
private Map<String, String> overrides;
- @ConstructorProperties({FIELD_DEFAULTS, FIELD_OVERRIDES})
- public ConfigResponse(Map<String, String> defaults, Map<String, String>
overrides) {
+ @JsonCreator
+ public ConfigResponse(
+ @JsonProperty(FIELD_DEFAULTS) Map<String, String> defaults,
+ @JsonProperty(FIELD_OVERRIDES) Map<String, String> overrides) {
this.defaults = defaults;
this.overrides = overrides;
}
@@ -65,12 +66,12 @@ public class ConfigResponse implements RESTResponse {
}
@JsonGetter(FIELD_DEFAULTS)
- public Map<String, String> defaults() {
+ public Map<String, String> getDefaults() {
return defaults;
}
@JsonGetter(FIELD_OVERRIDES)
- public Map<String, String> overrides() {
+ public Map<String, String> getOverrides() {
return overrides;
}
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/CreateDatabaseResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/CreateDatabaseResponse.java
new file mode 100644
index 0000000000..43c99254f3
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/CreateDatabaseResponse.java
@@ -0,0 +1,58 @@
+/*
+ * 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.responses;
+
+import org.apache.paimon.rest.RESTResponse;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Map;
+
+/** Response for creating database. */
+public class CreateDatabaseResponse implements RESTResponse {
+
+ private static final String FIELD_NAME = "name";
+ private static final String FIELD_OPTIONS = "options";
+
+ @JsonProperty(FIELD_NAME)
+ private String name;
+
+ @JsonProperty(FIELD_OPTIONS)
+ private Map<String, String> options;
+
+ @JsonCreator
+ public CreateDatabaseResponse(
+ @JsonProperty(FIELD_NAME) String name,
+ @JsonProperty(FIELD_OPTIONS) Map<String, String> options) {
+ this.name = name;
+ this.options = options;
+ }
+
+ @JsonGetter(FIELD_NAME)
+ public String getName() {
+ return name;
+ }
+
+ @JsonGetter(FIELD_OPTIONS)
+ public Map<String, String> getOptions() {
+ return options;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/DatabaseName.java
similarity index 53%
copy from paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
copy to
paimon-core/src/main/java/org/apache/paimon/rest/responses/DatabaseName.java
index aaca619380..9a93b2fd1e 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/DatabaseName.java
@@ -16,20 +16,29 @@
* limitations under the License.
*/
-package org.apache.paimon.rest;
+package org.apache.paimon.rest.responses;
-/** Resource paths for REST catalog. */
-public class ResourcePaths {
+import org.apache.paimon.rest.RESTMessage;
- public static final String V1_CONFIG = "/api/v1/config";
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
- public static ResourcePaths forCatalogProperties(String prefix) {
- return new ResourcePaths(prefix);
- }
+/** Class for Database entity. */
+public class DatabaseName implements RESTMessage {
+
+ private static final String FIELD_NAME = "name";
- private final String prefix;
+ @JsonProperty(FIELD_NAME)
+ private String name;
+
+ @JsonCreator
+ public DatabaseName(@JsonProperty(FIELD_NAME) String name) {
+ this.name = name;
+ }
- public ResourcePaths(String prefix) {
- this.prefix = prefix;
+ @JsonGetter(FIELD_NAME)
+ public String getName() {
+ return this.name;
}
}
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 685fe53071..d24c8f0f99 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
@@ -18,10 +18,12 @@
package org.apache.paimon.rest.responses;
+import org.apache.paimon.rest.RESTResponse;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
-import java.beans.ConstructorProperties;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
@@ -29,7 +31,8 @@ import java.util.Arrays;
import java.util.List;
/** Response for error. */
-public class ErrorResponse {
+public class ErrorResponse implements RESTResponse {
+
private static final String FIELD_MESSAGE = "message";
private static final String FIELD_CODE = "code";
private static final String FIELD_STACK = "stack";
@@ -49,8 +52,11 @@ public class ErrorResponse {
this.stack = new ArrayList<String>();
}
- @ConstructorProperties({FIELD_MESSAGE, FIELD_CODE, FIELD_STACK})
- public ErrorResponse(String message, int code, List<String> stack) {
+ @JsonCreator
+ public ErrorResponse(
+ @JsonProperty(FIELD_MESSAGE) String message,
+ @JsonProperty(FIELD_CODE) int code,
+ @JsonProperty(FIELD_STACK) List<String> stack) {
this.message = message;
this.code = code;
this.stack = stack;
@@ -63,17 +69,17 @@ public class ErrorResponse {
}
@JsonGetter(FIELD_MESSAGE)
- public String message() {
+ public String getMessage() {
return message;
}
@JsonGetter(FIELD_CODE)
- public Integer code() {
+ public Integer getCode() {
return code;
}
@JsonGetter(FIELD_STACK)
- public List<String> stack() {
+ public List<String> getStack() {
return stack;
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetDatabaseResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetDatabaseResponse.java
new file mode 100644
index 0000000000..f8f7c8794b
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetDatabaseResponse.java
@@ -0,0 +1,78 @@
+/*
+ * 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.responses;
+
+import org.apache.paimon.catalog.Database;
+import org.apache.paimon.rest.RESTResponse;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static
org.apache.paimon.rest.RESTCatalogInternalOptions.DATABASE_COMMENT;
+
+/** Response for getting database. */
+public class GetDatabaseResponse implements RESTResponse, Database {
+
+ private static final String FIELD_NAME = "name";
+ private static final String FIELD_OPTIONS = "options";
+
+ @JsonProperty(FIELD_NAME)
+ private final String name;
+
+ @JsonProperty(FIELD_OPTIONS)
+ private final Map<String, String> options;
+
+ @JsonCreator
+ public GetDatabaseResponse(
+ @JsonProperty(FIELD_NAME) String name,
+ @JsonProperty(FIELD_OPTIONS) Map<String, String> options) {
+ this.name = name;
+ this.options = options;
+ }
+
+ @JsonGetter(FIELD_NAME)
+ public String getName() {
+ return name;
+ }
+
+ @JsonGetter(FIELD_OPTIONS)
+ public Map<String, String> getOptions() {
+ return options;
+ }
+
+ @Override
+ public String name() {
+ return this.getName();
+ }
+
+ @Override
+ public Map<String, String> options() {
+ return this.getOptions();
+ }
+
+ @Override
+ public Optional<String> comment() {
+ return Optional.ofNullable(
+ this.options.getOrDefault(DATABASE_COMMENT.key(),
DATABASE_COMMENT.defaultValue()));
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListDatabasesResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListDatabasesResponse.java
new file mode 100644
index 0000000000..38773f354b
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListDatabasesResponse.java
@@ -0,0 +1,45 @@
+/*
+ * 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.responses;
+
+import org.apache.paimon.rest.RESTResponse;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/** Response for listing databases. */
+public class ListDatabasesResponse implements RESTResponse {
+ private static final String FIELD_DATABASES = "databases";
+
+ @JsonProperty(FIELD_DATABASES)
+ private List<DatabaseName> databases;
+
+ @JsonCreator
+ public ListDatabasesResponse(@JsonProperty(FIELD_DATABASES)
List<DatabaseName> databases) {
+ this.databases = databases;
+ }
+
+ @JsonGetter(FIELD_DATABASES)
+ public List<DatabaseName> getDatabases() {
+ return this.databases;
+ }
+}
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 1f1b9c01aa..340e38f6a7 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
@@ -18,8 +18,10 @@
package org.apache.paimon.rest;
+import org.apache.paimon.rest.exceptions.AlreadyExistsException;
import org.apache.paimon.rest.exceptions.BadRequestException;
import org.apache.paimon.rest.exceptions.ForbiddenException;
+import org.apache.paimon.rest.exceptions.NoSuchResourceException;
import org.apache.paimon.rest.exceptions.NotAuthorizedException;
import org.apache.paimon.rest.exceptions.RESTException;
import org.apache.paimon.rest.exceptions.ServiceFailureException;
@@ -54,10 +56,16 @@ public class DefaultErrorHandlerTest {
assertThrows(
ForbiddenException.class,
() -> defaultErrorHandler.accept(generateErrorResponse(403)));
+ assertThrows(
+ NoSuchResourceException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(404)));
assertThrows(
RESTException.class, () ->
defaultErrorHandler.accept(generateErrorResponse(405)));
assertThrows(
RESTException.class, () ->
defaultErrorHandler.accept(generateErrorResponse(406)));
+ assertThrows(
+ AlreadyExistsException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(409)));
assertThrows(
ServiceFailureException.class,
() -> defaultErrorHandler.accept(generateErrorResponse(500)));
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 17c13b932f..f12af12a9d 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
@@ -110,6 +110,20 @@ public class HttpClientTest {
verify(errorHandler, times(1)).accept(any());
}
+ @Test
+ public void testDeleteSuccess() {
+ mockHttpCallWithCode(mockResponseDataStr, 200);
+ MockRESTData response = httpClient.delete(MOCK_PATH, mockResponseData,
headers);
+ verify(errorHandler, times(0)).accept(any());
+ }
+
+ @Test
+ public void testDeleteFail() {
+ mockHttpCallWithCode(mockResponseDataStr, 400);
+ httpClient.delete(MOCK_PATH, mockResponseData, headers);
+ verify(errorHandler, times(1)).accept(any());
+ }
+
private Map<String, String> headers(String token) {
Map<String, String> header = new HashMap<>();
header.put("Authorization", "Bearer " + token);
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
new file mode 100644
index 0000000000..f111c41f6a
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
@@ -0,0 +1,74 @@
+/*
+ * 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.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.DropDatabaseRequest;
+import org.apache.paimon.rest.responses.CreateDatabaseResponse;
+import org.apache.paimon.rest.responses.DatabaseName;
+import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.ListDatabasesResponse;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static
org.apache.paimon.rest.RESTCatalogInternalOptions.DATABASE_COMMENT;
+
+/** Mock REST message. */
+public class MockRESTMessage {
+
+ public static String databaseName() {
+ return "database";
+ }
+
+ public static CreateDatabaseRequest createDatabaseRequest(String name) {
+ boolean ignoreIfExists = true;
+ Map<String, String> options = new HashMap<>();
+ options.put("a", "b");
+ return new CreateDatabaseRequest(name, ignoreIfExists, options);
+ }
+
+ public static DropDatabaseRequest dropDatabaseRequest() {
+ boolean ignoreIfNotExists = true;
+ boolean cascade = true;
+ return new DropDatabaseRequest(ignoreIfNotExists, cascade);
+ }
+
+ public static CreateDatabaseResponse createDatabaseResponse(String name) {
+ Map<String, String> options = new HashMap<>();
+ options.put("a", "b");
+ return new CreateDatabaseResponse(name, options);
+ }
+
+ public static GetDatabaseResponse getDatabaseResponse(String name) {
+ Map<String, String> options = new HashMap<>();
+ options.put("a", "b");
+ options.put(DATABASE_COMMENT.key(), "comment");
+ return new GetDatabaseResponse(name, options);
+ }
+
+ public static ListDatabasesResponse listDatabasesResponse(String name) {
+ DatabaseName databaseName = new DatabaseName(name);
+ List<DatabaseName> databaseNameList = new ArrayList<>();
+ databaseNameList.add(databaseName);
+ return new ListDatabasesResponse(databaseNameList);
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
index f3f56e9721..cffac60466 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
@@ -18,8 +18,15 @@
package org.apache.paimon.rest;
+import org.apache.paimon.catalog.Database;
import org.apache.paimon.options.CatalogOptions;
import org.apache.paimon.options.Options;
+import org.apache.paimon.rest.responses.CreateDatabaseResponse;
+import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.ListDatabasesResponse;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
@@ -29,14 +36,17 @@ import org.junit.Test;
import java.io.IOException;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/** Test for REST Catalog. */
public class RESTCatalogTest {
+ private ObjectMapper mapper = RESTObjectMapper.create();
private MockWebServer mockWebServer;
private RESTCatalog restCatalog;
@@ -50,7 +60,11 @@ public class RESTCatalogTest {
String initToken = "init_token";
options.set(RESTCatalogOptions.TOKEN, initToken);
options.set(RESTCatalogOptions.THREAD_POOL_SIZE, 1);
- mockOptions(RESTCatalogInternalOptions.PREFIX.key(), "prefix");
+ String mockResponse =
+ String.format(
+ "{\"defaults\": {\"%s\": \"%s\"}}",
+ RESTCatalogInternalOptions.PREFIX.key(), "prefix");
+ mockResponse(mockResponse);
restCatalog = new RESTCatalog(options);
}
@@ -70,14 +84,50 @@ public class RESTCatalogTest {
public void testGetConfig() {
String key = "a";
String value = "b";
- mockOptions(key, value);
+ String mockResponse = String.format("{\"defaults\": {\"%s\":
\"%s\"}}", key, value);
+ mockResponse(mockResponse);
Map<String, String> header = new HashMap<>();
Map<String, String> response =
restCatalog.fetchOptionsFromServer(header, new HashMap<>());
assertEquals(value, response.get(key));
}
- private void mockOptions(String key, String value) {
- String mockResponse = String.format("{\"defaults\": {\"%s\":
\"%s\"}}", key, value);
+ @Test
+ public void testListDatabases() throws JsonProcessingException {
+ String name = MockRESTMessage.databaseName();
+ ListDatabasesResponse response =
MockRESTMessage.listDatabasesResponse(name);
+ mockResponse(mapper.writeValueAsString(response));
+ List<String> result = restCatalog.listDatabases();
+ assertEquals(response.getDatabases().size(), result.size());
+ assertEquals(name, result.get(0));
+ }
+
+ @Test
+ public void testCreateDatabase() throws Exception {
+ String name = MockRESTMessage.databaseName();
+ CreateDatabaseResponse response =
MockRESTMessage.createDatabaseResponse(name);
+ mockResponse(mapper.writeValueAsString(response));
+ assertDoesNotThrow(() -> restCatalog.createDatabase(name, false,
response.getOptions()));
+ }
+
+ @Test
+ public void testGetDatabase() throws Exception {
+ String name = MockRESTMessage.databaseName();
+ GetDatabaseResponse response =
MockRESTMessage.getDatabaseResponse(name);
+ mockResponse(mapper.writeValueAsString(response));
+ Database result = restCatalog.getDatabase(name);
+ assertEquals(name, result.name());
+ assertEquals(response.getOptions().size(), result.options().size());
+ assertEquals(response.comment().get(), result.comment().get());
+ }
+
+ @Test
+ public void testDropDatabase() {
+ String name = "name";
+ mockResponse("");
+ assertDoesNotThrow(() -> restCatalog.dropDatabase(name, false, false));
+ }
+
+ private void mockResponse(String mockResponse) {
MockResponse mockResponseObj =
new MockResponse()
.setBody(mockResponse)
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 83a8805d29..622a989936 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
@@ -18,8 +18,13 @@
package org.apache.paimon.rest;
+import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.DropDatabaseRequest;
import org.apache.paimon.rest.responses.ConfigResponse;
+import org.apache.paimon.rest.responses.CreateDatabaseResponse;
import org.apache.paimon.rest.responses.ErrorResponse;
+import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.ListDatabasesResponse;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
@@ -43,7 +48,7 @@ public class RESTObjectMapperTest {
ConfigResponse response = new ConfigResponse(conf, conf);
String responseStr = mapper.writeValueAsString(response);
ConfigResponse parseData = mapper.readValue(responseStr,
ConfigResponse.class);
- assertEquals(conf.get(confKey), parseData.defaults().get(confKey));
+ assertEquals(conf.get(confKey), parseData.getDefaults().get(confKey));
}
@Test
@@ -53,7 +58,60 @@ public class RESTObjectMapperTest {
ErrorResponse response = new ErrorResponse(message, code, new
ArrayList<String>());
String responseStr = mapper.writeValueAsString(response);
ErrorResponse parseData = mapper.readValue(responseStr,
ErrorResponse.class);
- assertEquals(message, parseData.message());
- assertEquals(code, parseData.code());
+ assertEquals(message, parseData.getMessage());
+ assertEquals(code, parseData.getCode());
+ }
+
+ @Test
+ public void createDatabaseRequestParseTest() throws Exception {
+ String name = MockRESTMessage.databaseName();
+ CreateDatabaseRequest request =
MockRESTMessage.createDatabaseRequest(name);
+ String requestStr = mapper.writeValueAsString(request);
+ CreateDatabaseRequest parseData = mapper.readValue(requestStr,
CreateDatabaseRequest.class);
+ assertEquals(request.getName(), parseData.getName());
+ assertEquals(request.getIgnoreIfExists(),
parseData.getIgnoreIfExists());
+ assertEquals(request.getOptions().size(),
parseData.getOptions().size());
+ }
+
+ @Test
+ public void dropDatabaseRequestParseTest() throws Exception {
+ DropDatabaseRequest request = MockRESTMessage.dropDatabaseRequest();
+ String requestStr = mapper.writeValueAsString(request);
+ DropDatabaseRequest parseData = mapper.readValue(requestStr,
DropDatabaseRequest.class);
+ assertEquals(request.getIgnoreIfNotExists(),
parseData.getIgnoreIfNotExists());
+ assertEquals(request.getCascade(), parseData.getCascade());
+ }
+
+ @Test
+ public void createDatabaseResponseParseTest() throws Exception {
+ String name = MockRESTMessage.databaseName();
+ CreateDatabaseResponse response =
MockRESTMessage.createDatabaseResponse(name);
+ String responseStr = mapper.writeValueAsString(response);
+ CreateDatabaseResponse parseData =
+ mapper.readValue(responseStr, CreateDatabaseResponse.class);
+ assertEquals(name, parseData.getName());
+ assertEquals(response.getOptions().size(),
parseData.getOptions().size());
+ }
+
+ @Test
+ public void getDatabaseResponseParseTest() throws Exception {
+ String name = MockRESTMessage.databaseName();
+ GetDatabaseResponse response =
MockRESTMessage.getDatabaseResponse(name);
+ String responseStr = mapper.writeValueAsString(response);
+ GetDatabaseResponse parseData = mapper.readValue(responseStr,
GetDatabaseResponse.class);
+ assertEquals(name, parseData.getName());
+ assertEquals(response.getOptions().size(),
parseData.getOptions().size());
+ assertEquals(response.comment().get(), parseData.comment().get());
+ }
+
+ @Test
+ public void listDatabaseResponseParseTest() throws Exception {
+ String name = MockRESTMessage.databaseName();
+ ListDatabasesResponse response =
MockRESTMessage.listDatabasesResponse(name);
+ String responseStr = mapper.writeValueAsString(response);
+ ListDatabasesResponse parseData =
+ mapper.readValue(responseStr, ListDatabasesResponse.class);
+ assertEquals(response.getDatabases().size(),
parseData.getDatabases().size());
+ assertEquals(name, parseData.getDatabases().get(0).getName());
}
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/auth/AuthSessionTest.java
b/paimon-core/src/test/java/org/apache/paimon/rest/auth/AuthSessionTest.java
index 81b3ea57b7..1f4a48fd5e 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/auth/AuthSessionTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/auth/AuthSessionTest.java
@@ -35,6 +35,8 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
+import static
org.apache.paimon.rest.auth.AuthSession.MAX_REFRESH_WINDOW_MILLIS;
+import static org.apache.paimon.rest.auth.AuthSession.MIN_REFRESH_WAIT_MILLIS;
import static
org.apache.paimon.rest.auth.AuthSession.TOKEN_REFRESH_NUM_RETRIES;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
@@ -121,6 +123,22 @@ public class AuthSessionTest {
verify(credentialsProvider, Mockito.times(TOKEN_REFRESH_NUM_RETRIES +
1)).refresh();
}
+ @Test
+ public void testGetTimeToWaitByExpiresInMills() {
+ long expiresInMillis = -100L;
+ long timeToWait =
AuthSession.getTimeToWaitByExpiresInMills(expiresInMillis);
+ assertEquals(MIN_REFRESH_WAIT_MILLIS, timeToWait);
+ expiresInMillis = (long) (MAX_REFRESH_WINDOW_MILLIS * 0.5);
+ timeToWait =
AuthSession.getTimeToWaitByExpiresInMills(expiresInMillis);
+ assertEquals(MIN_REFRESH_WAIT_MILLIS, timeToWait);
+ expiresInMillis = MAX_REFRESH_WINDOW_MILLIS;
+ timeToWait =
AuthSession.getTimeToWaitByExpiresInMills(expiresInMillis);
+ assertEquals(timeToWait, MIN_REFRESH_WAIT_MILLIS);
+ expiresInMillis = MAX_REFRESH_WINDOW_MILLIS * 2L;
+ timeToWait =
AuthSession.getTimeToWaitByExpiresInMills(expiresInMillis);
+ assertEquals(timeToWait, MAX_REFRESH_WINDOW_MILLIS);
+ }
+
private Pair<File, String> generateTokenAndWriteToFile(String fileName)
throws IOException {
File tokenFile = folder.newFile(fileName);
String token = UUID.randomUUID().toString();
diff --git a/paimon-open-api/generate.sh b/paimon-open-api/generate.sh
index b63aa538ab..619b642ab7 100755
--- a/paimon-open-api/generate.sh
+++ b/paimon-open-api/generate.sh
@@ -17,6 +17,7 @@
# Start the application
cd ..
+mvn spotless:apply
mvn clean install -DskipTests
cd ./paimon-open-api
mvn spring-boot:run &
diff --git a/paimon-open-api/rest-catalog-open-api.yaml
b/paimon-open-api/rest-catalog-open-api.yaml
index 432ee123b8..2a5d1dc584 100644
--- a/paimon-open-api/rest-catalog-open-api.yaml
+++ b/paimon-open-api/rest-catalog-open-api.yaml
@@ -28,6 +28,120 @@ servers:
- url: http://localhost:8080
description: Server URL in Development environment
paths:
+ /api/v1/{prefix}/databases:
+ get:
+ tags:
+ - database
+ summary: List Databases
+ operationId: listDatabases
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ListDatabasesResponse'
+ "500":
+ description: Internal Server Error
+ post:
+ tags:
+ - database
+ summary: Create Databases
+ operationId: createDatabases
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateDatabaseRequest'
+ responses:
+ "500":
+ description: Internal Server Error
+ "409":
+ description: Resource has exist
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateDatabaseResponse'
+ /api/v1/{prefix}/databases/{database}:
+ get:
+ tags:
+ - database
+ summary: Get Database
+ operationId: getDatabases
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: database
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GetDatabaseResponse'
+ "404":
+ description: Resource not found
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "500":
+ description: Internal Server Error
+ delete:
+ tags:
+ - database
+ summary: Drop Database
+ operationId: dropDatabases
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: database
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DropDatabaseRequest'
+ responses:
+ "404":
+ description: Resource not found
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "500":
+ description: Internal Server Error
/api/v1/config:
get:
tags:
@@ -37,14 +151,67 @@ paths:
responses:
"500":
description: Internal Server Error
- "201":
- description: Created
+ "200":
+ description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ConfigResponse'
components:
schemas:
+ CreateDatabaseRequest:
+ type: object
+ properties:
+ name:
+ type: string
+ ignoreIfExists:
+ type: boolean
+ options:
+ type: object
+ additionalProperties:
+ type: string
+ ErrorResponse:
+ type: object
+ properties:
+ message:
+ type: string
+ code:
+ type: integer
+ format: int32
+ stack:
+ type: array
+ items:
+ type: string
+ CreateDatabaseResponse:
+ type: object
+ properties:
+ name:
+ type: string
+ options:
+ type: object
+ additionalProperties:
+ type: string
+ DatabaseName:
+ type: object
+ properties:
+ name:
+ type: string
+ ListDatabasesResponse:
+ type: object
+ properties:
+ databases:
+ type: array
+ items:
+ $ref: '#/components/schemas/DatabaseName'
+ GetDatabaseResponse:
+ type: object
+ properties:
+ name:
+ type: string
+ options:
+ type: object
+ additionalProperties:
+ type: string
ConfigResponse:
type: object
properties:
@@ -52,9 +219,14 @@ components:
type: object
additionalProperties:
type: string
- writeOnly: true
overrides:
type: object
additionalProperties:
type: string
- writeOnly: true
+ DropDatabaseRequest:
+ type: object
+ properties:
+ ignoreIfNotExists:
+ type: boolean
+ cascade:
+ type: boolean
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 b475540571..364cc5adbb 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
@@ -19,17 +19,28 @@
package org.apache.paimon.open.api;
import org.apache.paimon.rest.ResourcePaths;
+import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.DropDatabaseRequest;
import org.apache.paimon.rest.responses.ConfigResponse;
+import org.apache.paimon.rest.responses.CreateDatabaseResponse;
+import org.apache.paimon.rest.responses.DatabaseName;
+import org.apache.paimon.rest.responses.ErrorResponse;
+import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.ListDatabasesResponse;
+
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@@ -45,7 +56,7 @@ public class RESTCatalogController {
tags = {"config"})
@ApiResponses({
@ApiResponse(
- responseCode = "201",
+ responseCode = "200",
content = {
@Content(
schema = @Schema(implementation =
ConfigResponse.class),
@@ -56,14 +67,99 @@ public class RESTCatalogController {
content = {@Content(schema = @Schema())})
})
@GetMapping(ResourcePaths.V1_CONFIG)
- public ResponseEntity<ConfigResponse> getConfig() {
- try {
- Map<String, String> defaults = new HashMap<>();
- Map<String, String> overrides = new HashMap<>();
- ConfigResponse response = new ConfigResponse(defaults, overrides);
- return new ResponseEntity<>(response, HttpStatus.CREATED);
- } catch (Exception e) {
- return new ResponseEntity<>(null,
HttpStatus.INTERNAL_SERVER_ERROR);
- }
+ public ConfigResponse getConfig() {
+ Map<String, String> defaults = new HashMap<>();
+ Map<String, String> overrides = new HashMap<>();
+ return new ConfigResponse(defaults, overrides);
+ }
+
+ @Operation(
+ summary = "List Databases",
+ tags = {"database"})
+ @ApiResponses({
+ @ApiResponse(
+ responseCode = "200",
+ content = {
+ @Content(
+ schema = @Schema(implementation =
ListDatabasesResponse.class),
+ mediaType = "application/json")
+ }),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema())})
+ })
+ @GetMapping("/api/v1/{prefix}/databases")
+ public ListDatabasesResponse listDatabases(@PathVariable String prefix) {
+ return new ListDatabasesResponse(ImmutableList.of(new
DatabaseName("account")));
+ }
+
+ @Operation(
+ summary = "Create Databases",
+ tags = {"database"})
+ @ApiResponses({
+ @ApiResponse(
+ responseCode = "200",
+ content = {
+ @Content(
+ schema = @Schema(implementation =
CreateDatabaseResponse.class),
+ mediaType = "application/json")
+ }),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Resource has exist",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema())})
+ })
+ @PostMapping("/api/v1/{prefix}/databases")
+ public CreateDatabaseResponse createDatabases(
+ @PathVariable String prefix, @RequestBody CreateDatabaseRequest
request) {
+ Map<String, String> properties = new HashMap<>();
+ return new CreateDatabaseResponse("name", properties);
}
+
+ @Operation(
+ summary = "Get Database",
+ tags = {"database"})
+ @ApiResponses({
+ @ApiResponse(
+ responseCode = "200",
+ content = {
+ @Content(
+ schema = @Schema(implementation =
GetDatabaseResponse.class),
+ mediaType = "application/json")
+ }),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Resource not found",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema())})
+ })
+ @GetMapping("/api/v1/{prefix}/databases/{database}")
+ public GetDatabaseResponse getDatabases(
+ @PathVariable String prefix, @PathVariable String database) {
+ Map<String, String> options = new HashMap<>();
+ return new GetDatabaseResponse("name", options);
+ }
+
+ @Operation(
+ summary = "Drop Database",
+ tags = {"database"})
+ @ApiResponses({
+ @ApiResponse(
+ responseCode = "404",
+ description = "Resource not found",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema())})
+ })
+ @DeleteMapping("/api/v1/{prefix}/databases/{database}")
+ public void dropDatabases(
+ @PathVariable String prefix,
+ @PathVariable String database,
+ @RequestBody DropDatabaseRequest request) {}
}
diff --git
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/config/OpenAPIConfig.java
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/config/OpenAPIConfig.java
index 01234c41bb..0e28cd95f9 100644
---
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/config/OpenAPIConfig.java
+++
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/config/OpenAPIConfig.java
@@ -32,7 +32,6 @@ import java.util.List;
/** Config for OpenAPI. */
@Configuration
public class OpenAPIConfig {
-
@Value("${openapi.url}")
private String devUrl;