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 0fe18e89f0 [core] Add basic implementation to support REST Catalog
(#4553)
0fe18e89f0 is described below
commit 0fe18e89f04f2c32a7d41560dfa2ac03134904b1
Author: lining <[email protected]>
AuthorDate: Thu Nov 28 19:58:31 2024 +0800
[core] Add basic implementation to support REST Catalog (#4553)
---
.../org/apache/paimon/utils/ThreadPoolUtils.java | 12 +-
paimon-core/pom.xml | 57 ++++++
.../apache/paimon/rest/DefaultErrorHandler.java | 61 +++++++
.../java/org/apache/paimon/rest/ErrorHandler.java | 26 +++
.../java/org/apache/paimon/rest/HttpClient.java | 142 +++++++++++++++
.../org/apache/paimon/rest/HttpClientOptions.java | 74 ++++++++
.../java/org/apache/paimon/rest/RESTCatalog.java | 197 +++++++++++++++++++++
.../org/apache/paimon/rest/RESTCatalogFactory.java | 38 ++++
.../paimon/rest/RESTCatalogInternalOptions.java | 31 ++++
.../org/apache/paimon/rest/RESTCatalogOptions.java | 53 ++++++
.../java/org/apache/paimon/rest/RESTClient.java | 31 ++++
.../java/org/apache/paimon/rest/RESTMessage.java | 22 +++
.../org/apache/paimon/rest/RESTObjectMapper.java | 35 ++++
.../java/org/apache/paimon/rest/RESTRequest.java | 22 +++
.../java/org/apache/paimon/rest/RESTResponse.java | 22 +++
.../main/java/org/apache/paimon/rest/RESTUtil.java | 55 ++++++
.../java/org/apache/paimon/rest/ResourcePaths.java | 34 ++++
.../rest/exceptions/BadRequestException.java | 27 +++
.../paimon/rest/exceptions/ForbiddenException.java | 26 +++
.../rest/exceptions/NotAuthorizedException.java | 26 +++
.../paimon/rest/exceptions/RESTException.java | 30 ++++
.../rest/exceptions/ServiceFailureException.java | 26 +++
.../exceptions/ServiceUnavailableException.java | 26 +++
.../paimon/rest/responses/ConfigResponse.java | 76 ++++++++
.../paimon/rest/responses/ErrorResponse.java | 91 ++++++++++
paimon-core/src/main/resources/META-INF/NOTICE | 8 +
.../services/org.apache.paimon.factories.Factory | 1 +
.../paimon/rest/DefaultErrorHandlerTest.java | 77 ++++++++
.../org/apache/paimon/rest/HttpClientTest.java | 129 ++++++++++++++
.../java/org/apache/paimon/rest/MockRESTData.java | 44 +++++
.../org/apache/paimon/rest/RESTCatalogTest.java | 86 +++++++++
.../apache/paimon/rest/RESTObjectMapperTest.java | 59 ++++++
paimon-open-api/Makefile | 25 +++
paimon-open-api/README.md | 10 ++
paimon-open-api/generate.sh | 48 +++++
paimon-open-api/pom.xml | 85 +++++++++
paimon-open-api/rest-catalog-open-api.yaml | 60 +++++++
.../apache/paimon/open/api/OpenApiApplication.java | 31 ++++
.../paimon/open/api/RESTCatalogController.java | 69 ++++++++
.../paimon/open/api/config/OpenAPIConfig.java | 60 +++++++
.../src/main/resources/application.properties | 22 +++
pom.xml | 1 +
42 files changed, 2054 insertions(+), 1 deletion(-)
diff --git
a/paimon-common/src/main/java/org/apache/paimon/utils/ThreadPoolUtils.java
b/paimon-common/src/main/java/org/apache/paimon/utils/ThreadPoolUtils.java
index 112b9ad1cd..f8959def67 100644
--- a/paimon-common/src/main/java/org/apache/paimon/utils/ThreadPoolUtils.java
+++ b/paimon-common/src/main/java/org/apache/paimon/utils/ThreadPoolUtils.java
@@ -30,6 +30,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -54,13 +55,22 @@ public class ThreadPoolUtils {
* is max thread number.
*/
public static ThreadPoolExecutor createCachedThreadPool(int threadNum,
String namePrefix) {
+ return createCachedThreadPool(threadNum, namePrefix, new
LinkedBlockingQueue<>());
+ }
+
+ /**
+ * Create a thread pool with max thread number and define queue. Inactive
threads will
+ * automatically exit.
+ */
+ public static ThreadPoolExecutor createCachedThreadPool(
+ int threadNum, String namePrefix, BlockingQueue<Runnable>
workQueue) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
threadNum,
threadNum,
1,
TimeUnit.MINUTES,
- new LinkedBlockingQueue<>(),
+ workQueue,
newDaemonThreadFactory(namePrefix));
executor.allowCoreThreadTimeOut(true);
return executor;
diff --git a/paimon-core/pom.xml b/paimon-core/pom.xml
index 399f0b5d6c..e137d57a6d 100644
--- a/paimon-core/pom.xml
+++ b/paimon-core/pom.xml
@@ -33,6 +33,7 @@ under the License.
<properties>
<frocksdbjni.version>6.20.3-ververica-2.0</frocksdbjni.version>
+ <okhttp.version>4.12.0</okhttp.version>
</properties>
<dependencies>
@@ -63,6 +64,14 @@ under the License.
<scope>provided</scope>
</dependency>
+ <!-- REST Catalog dependencies -->
+
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>${okhttp.version}</version>
+ </dependency>
+
<!-- test dependencies -->
<dependency>
@@ -204,6 +213,20 @@ under the License.
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>${okhttp.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>${mockito.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
<build>
@@ -219,6 +242,40 @@ under the License.
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>shade-paimon</id>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <filters>
+ <filter>
+ <artifact>*</artifact>
+ <excludes>
+
<exclude>okhttp3/internal/publicsuffix/NOTICE</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <artifactSet>
+ <includes combine.children="append">
+
<include>com.squareup.okhttp3:okhttp</include>
+ </includes>
+ </artifactSet>
+ <relocations>
+ <relocation>
+ <pattern>okhttp3</pattern>
+
<shadedPattern>org.apache.paimon.shade.okhttp3</shadedPattern>
+ </relocation>
+ </relocations>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
</plugins>
</build>
</project>
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
new file mode 100644
index 0000000000..1a8618c1c6
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/DefaultErrorHandler.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest;
+
+import org.apache.paimon.rest.exceptions.BadRequestException;
+import org.apache.paimon.rest.exceptions.ForbiddenException;
+import org.apache.paimon.rest.exceptions.NotAuthorizedException;
+import org.apache.paimon.rest.exceptions.RESTException;
+import org.apache.paimon.rest.exceptions.ServiceFailureException;
+import org.apache.paimon.rest.exceptions.ServiceUnavailableException;
+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() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void accept(ErrorResponse error) {
+ int code = error.code();
+ switch (code) {
+ case 400:
+ throw new BadRequestException(
+ String.format("Malformed request: %s",
error.message()));
+ case 401:
+ throw new NotAuthorizedException("Not authorized: %s",
error.message());
+ case 403:
+ throw new ForbiddenException("Forbidden: %s", error.message());
+ case 405:
+ case 406:
+ break;
+ case 500:
+ throw new ServiceFailureException("Server error: %s",
error.message());
+ case 501:
+ throw new UnsupportedOperationException(error.message());
+ case 503:
+ throw new ServiceUnavailableException("Service unavailable:
%s", error.message());
+ }
+
+ throw new RESTException("Unable to process: %s", error.message());
+ }
+}
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/ErrorHandler.java
b/paimon-core/src/main/java/org/apache/paimon/rest/ErrorHandler.java
new file mode 100644
index 0000000000..cdfa4bcdfa
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/ErrorHandler.java
@@ -0,0 +1,26 @@
+/*
+ * 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.responses.ErrorResponse;
+
+import java.util.function.Consumer;
+
+/** Error handler for REST client. */
+public abstract class ErrorHandler implements Consumer<ErrorResponse> {}
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
new file mode 100644
index 0000000000..e092711e5f
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/HttpClient.java
@@ -0,0 +1,142 @@
+/*
+ * 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.exceptions.RESTException;
+import org.apache.paimon.rest.responses.ErrorResponse;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
+
+import okhttp3.Dispatcher;
+import okhttp3.Headers;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+
+import static okhttp3.ConnectionSpec.CLEARTEXT;
+import static okhttp3.ConnectionSpec.COMPATIBLE_TLS;
+import static okhttp3.ConnectionSpec.MODERN_TLS;
+import static org.apache.paimon.utils.ThreadPoolUtils.createCachedThreadPool;
+
+/** HTTP client for REST catalog. */
+public class HttpClient implements RESTClient {
+
+ private final OkHttpClient okHttpClient;
+ private final String uri;
+ private final ObjectMapper mapper;
+ private final ErrorHandler errorHandler;
+
+ private static final String THREAD_NAME =
"REST-CATALOG-HTTP-CLIENT-THREAD-POOL";
+ private static final MediaType MEDIA_TYPE =
MediaType.parse("application/json");
+
+ public HttpClient(HttpClientOptions httpClientOptions) {
+ this.uri = httpClientOptions.uri();
+ this.mapper = httpClientOptions.mapper();
+ this.okHttpClient = createHttpClient(httpClientOptions);
+ this.errorHandler = httpClientOptions.errorHandler();
+ }
+
+ @Override
+ public <T extends RESTResponse> T get(
+ String path, Class<T> responseType, Map<String, String> headers) {
+ try {
+ Request request =
+ new Request.Builder()
+ .url(uri + path)
+ .get()
+ .headers(Headers.of(headers))
+ .build();
+ return exec(request, responseType);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public <T extends RESTResponse> T post(
+ String path, RESTRequest body, Class<T> responseType, Map<String,
String> headers) {
+ try {
+ RequestBody requestBody = buildRequestBody(body);
+ Request request =
+ new Request.Builder()
+ .url(uri + path)
+ .post(requestBody)
+ .headers(Headers.of(headers))
+ .build();
+ return exec(request, responseType);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ okHttpClient.dispatcher().cancelAll();
+ okHttpClient.connectionPool().evictAll();
+ }
+
+ private <T extends RESTResponse> T exec(Request request, Class<T>
responseType) {
+ try (Response response = okHttpClient.newCall(request).execute()) {
+ String responseBodyStr = response.body() != null ?
response.body().string() : null;
+ if (!response.isSuccessful()) {
+ ErrorResponse error =
+ new ErrorResponse(
+ responseBodyStr != null ? responseBodyStr :
"response body is null",
+ response.code());
+ errorHandler.accept(error);
+ }
+ if (responseBodyStr == null) {
+ throw new RESTException("response body is null.");
+ }
+ return mapper.readValue(responseBodyStr, responseType);
+ } catch (Exception e) {
+ throw new RESTException(e, "rest exception");
+ }
+ }
+
+ private RequestBody buildRequestBody(RESTRequest body) throws
JsonProcessingException {
+ return RequestBody.create(mapper.writeValueAsBytes(body), MEDIA_TYPE);
+ }
+
+ private static OkHttpClient createHttpClient(HttpClientOptions
httpClientOptions) {
+ BlockingQueue<Runnable> workQueue = new SynchronousQueue<>();
+ ExecutorService executorService =
+ createCachedThreadPool(httpClientOptions.threadPoolSize(),
THREAD_NAME, workQueue);
+
+ OkHttpClient.Builder builder =
+ new OkHttpClient.Builder()
+ .dispatcher(new Dispatcher(executorService))
+ .retryOnConnectionFailure(true)
+ .connectionSpecs(Arrays.asList(MODERN_TLS,
COMPATIBLE_TLS, CLEARTEXT));
+ httpClientOptions.connectTimeout().ifPresent(builder::connectTimeout);
+ httpClientOptions.readTimeout().ifPresent(builder::readTimeout);
+
+ return builder.build();
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/HttpClientOptions.java
b/paimon-core/src/main/java/org/apache/paimon/rest/HttpClientOptions.java
new file mode 100644
index 0000000000..694779cfdb
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/HttpClientOptions.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.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.time.Duration;
+import java.util.Optional;
+
+/** Options for Http Client. */
+public class HttpClientOptions {
+
+ private final String uri;
+ private final Optional<Duration> connectTimeout;
+ private final Optional<Duration> readTimeout;
+ private final ObjectMapper mapper;
+ private final int threadPoolSize;
+ private final ErrorHandler errorHandler;
+
+ public HttpClientOptions(
+ String uri,
+ Optional<Duration> connectTimeout,
+ Optional<Duration> readTimeout,
+ ObjectMapper mapper,
+ int threadPoolSize,
+ ErrorHandler errorHandler) {
+ this.uri = uri;
+ this.connectTimeout = connectTimeout;
+ this.readTimeout = readTimeout;
+ this.mapper = mapper;
+ this.threadPoolSize = threadPoolSize;
+ this.errorHandler = errorHandler;
+ }
+
+ public String uri() {
+ return uri;
+ }
+
+ public Optional<Duration> connectTimeout() {
+ return connectTimeout;
+ }
+
+ public Optional<Duration> readTimeout() {
+ return readTimeout;
+ }
+
+ public ObjectMapper mapper() {
+ return mapper;
+ }
+
+ public int threadPoolSize() {
+ return threadPoolSize;
+ }
+
+ public ErrorHandler errorHandler() {
+ return errorHandler;
+ }
+}
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
new file mode 100644
index 0000000000..c964008313
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
@@ -0,0 +1,197 @@
+/*
+ * 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.annotation.VisibleForTesting;
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Database;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.manifest.PartitionEntry;
+import org.apache.paimon.options.CatalogOptions;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.rest.responses.ConfigResponse;
+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.collect.ImmutableMap;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/** A catalog implementation for REST. */
+public class RESTCatalog implements Catalog {
+ private RESTClient client;
+ private String token;
+ private ResourcePaths resourcePaths;
+ private Map<String, String> options;
+ private Map<String, String> baseHeader;
+
+ private static final ObjectMapper objectMapper = RESTObjectMapper.create();
+ static final String AUTH_HEADER = "Authorization";
+ static final String AUTH_HEADER_VALUE_FORMAT = "Bearer %s";
+
+ public RESTCatalog(Options options) {
+ if (options.getOptional(CatalogOptions.WAREHOUSE).isPresent()) {
+ throw new IllegalArgumentException("Can not config warehouse in
RESTCatalog.");
+ }
+ String uri = options.get(RESTCatalogOptions.URI);
+ token = options.get(RESTCatalogOptions.TOKEN);
+ Optional<Duration> connectTimeout =
+ options.getOptional(RESTCatalogOptions.CONNECTION_TIMEOUT);
+ Optional<Duration> readTimeout =
options.getOptional(RESTCatalogOptions.READ_TIMEOUT);
+ Integer threadPoolSize =
options.get(RESTCatalogOptions.THREAD_POOL_SIZE);
+ HttpClientOptions httpClientOptions =
+ new HttpClientOptions(
+ uri,
+ connectTimeout,
+ readTimeout,
+ objectMapper,
+ threadPoolSize,
+ DefaultErrorHandler.getInstance());
+ this.client = new HttpClient(httpClientOptions);
+ Map<String, String> authHeaders =
+ ImmutableMap.of(AUTH_HEADER,
String.format(AUTH_HEADER_VALUE_FORMAT, token));
+ Map<String, String> initHeaders =
+ RESTUtil.merge(configHeaders(options.toMap()), authHeaders);
+ this.options = fetchOptionsFromServer(initHeaders, options.toMap());
+ this.baseHeader = configHeaders(this.options());
+ this.resourcePaths =
+ ResourcePaths.forCatalogProperties(
+ this.options.get(RESTCatalogInternalOptions.PREFIX));
+ }
+
+ @Override
+ public String warehouse() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Map<String, String> options() {
+ return this.options;
+ }
+
+ @Override
+ public FileIO fileIO() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<String> listDatabases() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void createDatabase(String name, boolean ignoreIfExists,
Map<String, String> properties)
+ throws DatabaseAlreadyExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Database getDatabase(String name) throws DatabaseNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void dropDatabase(String name, boolean ignoreIfNotExists, boolean
cascade)
+ throws DatabaseNotExistException, DatabaseNotEmptyException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Table getTable(Identifier identifier) throws TableNotExistException
{
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Path getTableLocation(Identifier identifier) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<String> listTables(String databaseName) throws
DatabaseNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void dropTable(Identifier identifier, boolean ignoreIfNotExists)
+ throws TableNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void createTable(Identifier identifier, Schema schema, boolean
ignoreIfExists)
+ throws TableAlreadyExistException, DatabaseNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void renameTable(Identifier fromTable, Identifier toTable, boolean
ignoreIfNotExists)
+ throws TableNotExistException, TableAlreadyExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void alterTable(
+ Identifier identifier, List<SchemaChange> changes, boolean
ignoreIfNotExists)
+ throws TableNotExistException, ColumnAlreadyExistException,
ColumnNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void createPartition(Identifier identifier, Map<String, String>
partitionSpec)
+ throws TableNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void dropPartition(Identifier identifier, Map<String, String>
partitions)
+ throws TableNotExistException, PartitionNotExistException {}
+
+ @Override
+ public List<PartitionEntry> listPartitions(Identifier identifier)
+ throws TableNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean allowUpperCase() {
+ return false;
+ }
+
+ @Override
+ public void close() throws Exception {}
+
+ @VisibleForTesting
+ Map<String, String> fetchOptionsFromServer(
+ Map<String, String> headers, Map<String, String> clientProperties)
{
+ ConfigResponse response =
+ client.get(ResourcePaths.V1_CONFIG, ConfigResponse.class,
headers);
+ return response.merge(clientProperties);
+ }
+
+ private static Map<String, String> configHeaders(Map<String, String>
properties) {
+ return RESTUtil.extractPrefixMap(properties, "header.");
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogFactory.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogFactory.java
new file mode 100644
index 0000000000..a5c773cb4b
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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.catalog.Catalog;
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.CatalogFactory;
+
+/** Factory to create {@link RESTCatalog}. */
+public class RESTCatalogFactory implements CatalogFactory {
+ public static final String IDENTIFIER = "rest";
+
+ @Override
+ public String identifier() {
+ return IDENTIFIER;
+ }
+
+ @Override
+ public Catalog create(CatalogContext context) {
+ return new RESTCatalog(context.options());
+ }
+}
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
new file mode 100644
index 0000000000..cf61caa20e
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogInternalOptions.java
@@ -0,0 +1,31 @@
+/*
+ * 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.options.ConfigOption;
+import org.apache.paimon.options.ConfigOptions;
+
+/** Internal options for REST Catalog. */
+public class RESTCatalogInternalOptions {
+ public static final ConfigOption<String> PREFIX =
+ ConfigOptions.key("prefix")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("REST Catalog uri's prefix.");
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogOptions.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogOptions.java
new file mode 100644
index 0000000000..6155b89375
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogOptions.java
@@ -0,0 +1,53 @@
+/*
+ * 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.options.ConfigOption;
+import org.apache.paimon.options.ConfigOptions;
+
+import java.time.Duration;
+
+/** Options for REST Catalog. */
+public class RESTCatalogOptions {
+ public static final ConfigOption<String> URI =
+ ConfigOptions.key("uri")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("REST Catalog server's uri.");
+ public static final ConfigOption<String> TOKEN =
+ ConfigOptions.key("token")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("REST Catalog server's auth token.");
+ public static final ConfigOption<Duration> CONNECTION_TIMEOUT =
+ ConfigOptions.key("rest.client.connection-timeout")
+ .durationType()
+ .noDefaultValue()
+ .withDescription("REST Catalog http client connect
timeout.");
+ public static final ConfigOption<Duration> READ_TIMEOUT =
+ ConfigOptions.key("rest.client.read-timeout")
+ .durationType()
+ .noDefaultValue()
+ .withDescription("REST Catalog http client read timeout.");
+ public static final ConfigOption<Integer> THREAD_POOL_SIZE =
+ ConfigOptions.key("rest.client.num-threads")
+ .intType()
+ .defaultValue(1)
+ .withDescription("REST Catalog http client thread num.");
+}
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
new file mode 100644
index 0000000000..feeed06a41
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTClient.java
@@ -0,0 +1,31 @@
+/*
+ * 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 java.io.Closeable;
+import java.util.Map;
+
+/** Interface for a basic HTTP Client for interfacing with the REST catalog. */
+public interface RESTClient extends Closeable {
+
+ <T extends RESTResponse> T get(String path, Class<T> responseType,
Map<String, String> headers);
+
+ <T extends RESTResponse> T post(
+ String path, RESTRequest body, Class<T> responseType, 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
new file mode 100644
index 0000000000..6cb0b6fa65
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTMessage.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/** Interface to mark both REST requests and responses. */
+public interface RESTMessage {}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/RESTObjectMapper.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTObjectMapper.java
new file mode 100644
index 0000000000..b1c83e9022
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTObjectMapper.java
@@ -0,0 +1,35 @@
+/*
+ * 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.shade.jackson2.com.fasterxml.jackson.databind.DeserializationFeature;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.SerializationFeature;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+/** Object mapper for REST request and response. */
+public class RESTObjectMapper {
+ public static ObjectMapper create() {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
+ mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ mapper.registerModule(new JavaTimeModule());
+ return mapper;
+ }
+}
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTRequest.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTRequest.java
new file mode 100644
index 0000000000..9c6758df14
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTRequest.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/** Interface to mark a REST request. */
+public interface RESTRequest extends RESTMessage {}
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTResponse.java
new file mode 100644
index 0000000000..a4149d3fda
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTResponse.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/** Interface to mark a REST response. */
+public interface RESTResponse extends RESTMessage {}
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
new file mode 100644
index 0000000000..3d42e99fa6
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTUtil.java
@@ -0,0 +1,55 @@
+/*
+ * 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.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.util.Map;
+
+/** Util for REST. */
+public class RESTUtil {
+ public static Map<String, String> extractPrefixMap(
+ Map<String, String> properties, String prefix) {
+ Preconditions.checkNotNull(properties, "Invalid properties map: null");
+ Map<String, String> result = Maps.newHashMap();
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ if (entry.getKey() != null && entry.getKey().startsWith(prefix)) {
+ result.put(
+ entry.getKey().substring(prefix.length()),
properties.get(entry.getKey()));
+ }
+ }
+ return result;
+ }
+
+ public static Map<String, String> merge(
+ Map<String, String> target, Map<String, String> updates) {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (Map.Entry<String, String> entry : target.entrySet()) {
+ if (!updates.containsKey(entry.getKey())) {
+ builder.put(entry.getKey(), entry.getValue());
+ }
+ }
+ updates.forEach(builder::put);
+
+ return builder.build();
+ }
+}
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
new file mode 100644
index 0000000000..1fad87588a
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/** Resource paths for REST catalog. */
+public class ResourcePaths {
+ public static final String V1_CONFIG = "/api/v1/config";
+
+ public static ResourcePaths forCatalogProperties(String prefix) {
+ return new ResourcePaths(prefix);
+ }
+
+ private final String prefix;
+
+ public ResourcePaths(String prefix) {
+ this.prefix = prefix;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/BadRequestException.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/BadRequestException.java
new file mode 100644
index 0000000000..301f3bd63f
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/BadRequestException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.exceptions;
+
+/** Exception thrown on HTTP 400 - Bad Request. */
+public class BadRequestException extends RESTException {
+
+ public BadRequestException(String message, Object... args) {
+ super(message, args);
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ForbiddenException.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ForbiddenException.java
new file mode 100644
index 0000000000..3982e5b704
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ForbiddenException.java
@@ -0,0 +1,26 @@
+/*
+ * 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.exceptions;
+
+/** Exception thrown on HTTP 403 Forbidden. */
+public class ForbiddenException extends RESTException {
+ public ForbiddenException(String message, Object... args) {
+ super(message, args);
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/NotAuthorizedException.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/NotAuthorizedException.java
new file mode 100644
index 0000000000..43c13b1a1c
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/NotAuthorizedException.java
@@ -0,0 +1,26 @@
+/*
+ * 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.exceptions;
+
+/** Exception thrown on HTTP 401 Unauthorized. */
+public class NotAuthorizedException extends RESTException {
+ public NotAuthorizedException(String message, Object... args) {
+ super(String.format(message, args));
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/RESTException.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/RESTException.java
new file mode 100644
index 0000000000..532936f430
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/RESTException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.exceptions;
+
+/** Base class for REST client exceptions. */
+public class RESTException extends RuntimeException {
+ public RESTException(String message, Object... args) {
+ super(String.format(message, args));
+ }
+
+ public RESTException(Throwable cause, String message, Object... args) {
+ super(String.format(message, args), cause);
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ServiceFailureException.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ServiceFailureException.java
new file mode 100644
index 0000000000..45c48ec0de
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ServiceFailureException.java
@@ -0,0 +1,26 @@
+/*
+ * 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.exceptions;
+
+/** Exception thrown on HTTP 500 - Bad Request. */
+public class ServiceFailureException extends RESTException {
+ public ServiceFailureException(String message, Object... args) {
+ super(String.format(message, args));
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ServiceUnavailableException.java
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ServiceUnavailableException.java
new file mode 100644
index 0000000000..fb6a05e89f
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/exceptions/ServiceUnavailableException.java
@@ -0,0 +1,26 @@
+/*
+ * 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.exceptions;
+
+/** Exception thrown on HTTP 503 - service is unavailable. */
+public class ServiceUnavailableException extends RESTException {
+ public ServiceUnavailableException(String message, Object... args) {
+ super(String.format(message, args));
+ }
+}
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
new file mode 100644
index 0000000000..e6bc934703
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ConfigResponse.java
@@ -0,0 +1,76 @@
+/*
+ * 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.utils.Preconditions;
+
+import org.apache.paimon.shade.com.fasterxml.jackson.annotation.JsonProperty;
+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.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+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";
+
+ @JsonProperty(FIELD_DEFAULTS)
+ private Map<String, String> defaults;
+
+ @JsonProperty(FIELD_OVERRIDES)
+ private Map<String, String> overrides;
+
+ @ConstructorProperties({FIELD_DEFAULTS, FIELD_OVERRIDES})
+ public ConfigResponse(Map<String, String> defaults, Map<String, String>
overrides) {
+ this.defaults = defaults;
+ this.overrides = overrides;
+ }
+
+ public Map<String, String> merge(Map<String, String> clientProperties) {
+ Preconditions.checkNotNull(
+ clientProperties,
+ "Cannot merge client properties with server-provided
properties. Invalid client configuration: null");
+ Map<String, String> merged =
+ defaults != null ? Maps.newHashMap(defaults) :
Maps.newHashMap();
+ merged.putAll(clientProperties);
+
+ if (overrides != null) {
+ merged.putAll(overrides);
+ }
+
+ return ImmutableMap.copyOf(Maps.filterValues(merged,
Objects::nonNull));
+ }
+
+ @JsonGetter(FIELD_DEFAULTS)
+ public Map<String, String> defaults() {
+ return defaults;
+ }
+
+ @JsonGetter(FIELD_OVERRIDES)
+ public Map<String, String> overrides() {
+ return overrides;
+ }
+}
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
new file mode 100644
index 0000000000..0e4b234867
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
@@ -0,0 +1,91 @@
+/*
+ * 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.shade.com.fasterxml.jackson.annotation.JsonProperty;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+
+import java.beans.ConstructorProperties;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Response for error. */
+public class ErrorResponse {
+ private static final String FIELD_MESSAGE = "message";
+ private static final String FIELD_CODE = "code";
+ private static final String FIELD_STACK = "stack";
+
+ @JsonProperty(FIELD_MESSAGE)
+ private final String message;
+
+ @JsonProperty(FIELD_CODE)
+ private final Integer code;
+
+ @JsonProperty(FIELD_STACK)
+ private final List<String> stack;
+
+ public ErrorResponse(String message, Integer code) {
+ this.code = code;
+ this.message = message;
+ this.stack = new ArrayList<String>();
+ }
+
+ @ConstructorProperties({FIELD_MESSAGE, FIELD_CODE, FIELD_STACK})
+ public ErrorResponse(String message, int code, List<String> stack) {
+ this.message = message;
+ this.code = code;
+ this.stack = stack;
+ }
+
+ public ErrorResponse(String message, int code, Throwable throwable) {
+ this.message = message;
+ this.code = code;
+ this.stack = getStackFromThrowable(throwable);
+ }
+
+ @JsonGetter(FIELD_MESSAGE)
+ public String message() {
+ return message;
+ }
+
+ @JsonGetter(FIELD_CODE)
+ public Integer code() {
+ return code;
+ }
+
+ @JsonGetter(FIELD_STACK)
+ public List<String> stack() {
+ 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/main/resources/META-INF/NOTICE
b/paimon-core/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000000..dd2479b1d6
--- /dev/null
+++ b/paimon-core/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,8 @@
+paimon-core
+Copyright 2023-2024 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+This project bundles the following dependencies under the Apache Software
License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt)
+- com.squareup.okhttp3:okhttp:4.12.0
\ No newline at end of file
diff --git
a/paimon-core/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
b/paimon-core/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
index ac6cc98fed..3b98eef52c 100644
---
a/paimon-core/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
+++
b/paimon-core/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
@@ -36,3 +36,4 @@
org.apache.paimon.mergetree.compact.aggregate.factory.FieldRoaringBitmap32AggFac
org.apache.paimon.mergetree.compact.aggregate.factory.FieldRoaringBitmap64AggFactory
org.apache.paimon.mergetree.compact.aggregate.factory.FieldSumAggFactory
org.apache.paimon.mergetree.compact.aggregate.factory.FieldThetaSketchAggFactory
+org.apache.paimon.rest.RESTCatalogFactory
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
new file mode 100644
index 0000000000..1f1b9c01aa
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/rest/DefaultErrorHandlerTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.exceptions.BadRequestException;
+import org.apache.paimon.rest.exceptions.ForbiddenException;
+import org.apache.paimon.rest.exceptions.NotAuthorizedException;
+import org.apache.paimon.rest.exceptions.RESTException;
+import org.apache.paimon.rest.exceptions.ServiceFailureException;
+import org.apache.paimon.rest.exceptions.ServiceUnavailableException;
+import org.apache.paimon.rest.responses.ErrorResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import static org.junit.Assert.assertThrows;
+
+/** Test for {@link DefaultErrorHandler}. */
+public class DefaultErrorHandlerTest {
+ private ErrorHandler defaultErrorHandler;
+
+ @Before
+ public void setUp() throws IOException {
+ defaultErrorHandler = DefaultErrorHandler.getInstance();
+ }
+
+ @Test
+ public void testHandleErrorResponse() {
+ assertThrows(
+ BadRequestException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(400)));
+ assertThrows(
+ NotAuthorizedException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(401)));
+ assertThrows(
+ ForbiddenException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(403)));
+ assertThrows(
+ RESTException.class, () ->
defaultErrorHandler.accept(generateErrorResponse(405)));
+ assertThrows(
+ RESTException.class, () ->
defaultErrorHandler.accept(generateErrorResponse(406)));
+ assertThrows(
+ ServiceFailureException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(500)));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(501)));
+ assertThrows(
+ RESTException.class, () ->
defaultErrorHandler.accept(generateErrorResponse(502)));
+ assertThrows(
+ ServiceUnavailableException.class,
+ () -> defaultErrorHandler.accept(generateErrorResponse(503)));
+ }
+
+ private ErrorResponse generateErrorResponse(int code) {
+ return new ErrorResponse("message", code, new ArrayList<String>());
+ }
+}
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
new file mode 100644
index 0000000000..1140e39982
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/HttpClientTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.shade.guava30.com.google.common.collect.ImmutableMap;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.apache.paimon.rest.RESTCatalog.AUTH_HEADER;
+import static org.apache.paimon.rest.RESTCatalog.AUTH_HEADER_VALUE_FORMAT;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/** Test for {@link HttpClient}. */
+public class HttpClientTest {
+ private MockWebServer mockWebServer;
+ private HttpClient httpClient;
+ private ObjectMapper objectMapper = RESTObjectMapper.create();
+ private ErrorHandler errorHandler;
+ private MockRESTData mockResponseData;
+ private String mockResponseDataStr;
+ private Map<String, String> headers;
+ private static final String MOCK_PATH = "/v1/api/mock";
+ private static final String TOKEN = "token";
+
+ @Before
+ public void setUp() throws IOException {
+ mockWebServer = new MockWebServer();
+ mockWebServer.start();
+ String baseUrl = mockWebServer.url("").toString();
+ errorHandler = mock(ErrorHandler.class);
+ HttpClientOptions httpClientOptions =
+ new HttpClientOptions(
+ baseUrl,
+ Optional.of(Duration.ofSeconds(3)),
+ Optional.of(Duration.ofSeconds(3)),
+ objectMapper,
+ 1,
+ errorHandler);
+ mockResponseData = new MockRESTData(MOCK_PATH);
+ mockResponseDataStr =
objectMapper.writeValueAsString(mockResponseData);
+ httpClient = new HttpClient(httpClientOptions);
+ headers = ImmutableMap.of(AUTH_HEADER,
String.format(AUTH_HEADER_VALUE_FORMAT, TOKEN));
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ public void testGetSuccess() {
+ mockHttpCallWithCode(mockResponseDataStr, 200);
+ MockRESTData response = httpClient.get(MOCK_PATH, MockRESTData.class,
headers);
+ verify(errorHandler, times(0)).accept(any());
+ assertEquals(mockResponseData.data(), response.data());
+ }
+
+ @Test
+ public void testGetFail() {
+ mockHttpCallWithCode(mockResponseDataStr, 400);
+ httpClient.get(MOCK_PATH, MockRESTData.class, headers);
+ verify(errorHandler, times(1)).accept(any());
+ }
+
+ @Test
+ public void testPostSuccess() {
+ mockHttpCallWithCode(mockResponseDataStr, 200);
+ MockRESTData response =
+ httpClient.post(MOCK_PATH, mockResponseData,
MockRESTData.class, headers);
+ verify(errorHandler, times(0)).accept(any());
+ assertEquals(mockResponseData.data(), response.data());
+ }
+
+ @Test
+ public void testPostFail() {
+ mockHttpCallWithCode(mockResponseDataStr, 400);
+ httpClient.post(MOCK_PATH, mockResponseData, MockRESTData.class,
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);
+ return header;
+ }
+
+ private void mockHttpCallWithCode(String body, Integer code) {
+ MockResponse mockResponseObj = generateMockResponse(body, code);
+ mockWebServer.enqueue(mockResponseObj);
+ }
+
+ private MockResponse generateMockResponse(String data, Integer code) {
+ return new MockResponse()
+ .setResponseCode(code)
+ .setBody(data)
+ .addHeader("Content-Type", "application/json");
+ }
+}
diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTData.java
b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTData.java
new file mode 100644
index 0000000000..55c5165ada
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTData.java
@@ -0,0 +1,44 @@
+/*
+ * 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.shade.com.fasterxml.jackson.annotation.JsonProperty;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.beans.ConstructorProperties;
+
+/** Mock REST data. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class MockRESTData implements RESTRequest, RESTResponse {
+ private static final String FIELD_DATA = "data";
+
+ @JsonProperty(FIELD_DATA)
+ private String data;
+
+ @ConstructorProperties({FIELD_DATA})
+ public MockRESTData(String data) {
+ this.data = data;
+ }
+
+ @JsonGetter(FIELD_DATA)
+ public String data() {
+ return data;
+ }
+}
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
new file mode 100644
index 0000000000..3ed8730862
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.options.CatalogOptions;
+import org.apache.paimon.options.Options;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+/** Test for REST Catalog. */
+public class RESTCatalogTest {
+ private MockWebServer mockWebServer;
+ private RESTCatalog restCatalog;
+ private final String initToken = "init_token";
+
+ @Before
+ public void setUp() throws IOException {
+ mockWebServer = new MockWebServer();
+ mockWebServer.start();
+ String baseUrl = mockWebServer.url("").toString();
+ Options options = new Options();
+ options.set(RESTCatalogOptions.URI, baseUrl);
+ options.set(RESTCatalogOptions.TOKEN, initToken);
+ options.set(RESTCatalogOptions.THREAD_POOL_SIZE, 1);
+ mockOptions(RESTCatalogInternalOptions.PREFIX.key(), "prefix");
+ restCatalog = new RESTCatalog(options);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ public void testInitFailWhenDefineWarehouse() {
+ Options options = new Options();
+ options.set(CatalogOptions.WAREHOUSE, "/a/b/c");
+ assertThrows(IllegalArgumentException.class, () -> new
RESTCatalog(options));
+ }
+
+ @Test
+ public void testGetConfig() {
+ String key = "a";
+ String value = "b";
+ mockOptions(key, value);
+ 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);
+ MockResponse mockResponseObj =
+ new MockResponse()
+ .setBody(mockResponse)
+ .addHeader("Content-Type", "application/json");
+ mockWebServer.enqueue(mockResponseObj);
+ }
+}
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
new file mode 100644
index 0000000000..83a8805d29
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.responses.ConfigResponse;
+import org.apache.paimon.rest.responses.ErrorResponse;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+/** Test for {@link RESTObjectMapper}. */
+public class RESTObjectMapperTest {
+ private ObjectMapper mapper = RESTObjectMapper.create();
+
+ @Test
+ public void configResponseParseTest() throws Exception {
+ String confKey = "a";
+ Map<String, String> conf = new HashMap<>();
+ conf.put(confKey, "b");
+ 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));
+ }
+
+ @Test
+ public void errorResponseParseTest() throws Exception {
+ String message = "message";
+ Integer code = 400;
+ 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());
+ }
+}
diff --git a/paimon-open-api/Makefile b/paimon-open-api/Makefile
new file mode 100644
index 0000000000..c3264c83db
--- /dev/null
+++ b/paimon-open-api/Makefile
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+# See:
https://cwiki.apache.org/confluence/display/INFRA/git+-+.asf.yaml+features
+
+
+install:
+ brew install yq
+
+generate:
+ @sh generate.sh
diff --git a/paimon-open-api/README.md b/paimon-open-api/README.md
new file mode 100644
index 0000000000..9d14a7cdd3
--- /dev/null
+++ b/paimon-open-api/README.md
@@ -0,0 +1,10 @@
+# Open API spec
+
+The `rest-catalog-open-api.yaml` defines the REST catalog interface.
+
+## Generate Open API Spec
+```sh
+make install
+cd paimon-open-api
+make generate
+```
\ No newline at end of file
diff --git a/paimon-open-api/generate.sh b/paimon-open-api/generate.sh
new file mode 100755
index 0000000000..b63aa538ab
--- /dev/null
+++ b/paimon-open-api/generate.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+#
+# 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.
+
+# Start the application
+cd ..
+mvn clean install -DskipTests
+cd ./paimon-open-api
+mvn spring-boot:run &
+SPRING_PID=$!
+# Wait for the application to be ready
+RETRY_COUNT=0
+MAX_RETRIES=10
+SLEEP_DURATION=5
+
+until $(curl -s -o /dev/null -w "%{http_code}"
http://localhost:8080/swagger-api-docs | grep -q "200"); do
+ ((RETRY_COUNT++))
+ if [ $RETRY_COUNT -gt $MAX_RETRIES ]; then
+ echo "Failed to start the application after $MAX_RETRIES retries."
+ exit 1
+ fi
+ echo "Application not ready yet. Retrying in $SLEEP_DURATION seconds..."
+ sleep $SLEEP_DURATION
+done
+
+echo "Application is ready".
+
+# Generate the OpenAPI specification file
+curl -s "http://localhost:8080/swagger-api-docs" | jq -M >
./rest-catalog-open-api.json
+yq --prettyPrint -o=yaml ./rest-catalog-open-api.json >
./rest-catalog-open-api.yaml
+rm -rf ./rest-catalog-open-api.json
+mvn spotless:apply
+# Stop the application
+echo "Stopping application..."
+kill $SPRING_PID
\ No newline at end of file
diff --git a/paimon-open-api/pom.xml b/paimon-open-api/pom.xml
new file mode 100644
index 0000000000..b5cee29fe4
--- /dev/null
+++ b/paimon-open-api/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.paimon</groupId>
+ <artifactId>paimon-parent</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>paimon-open-api</artifactId>
+
+ <properties>
+ <maven.compiler.source>8</maven.compiler.source>
+ <maven.compiler.target>8</maven.compiler.target>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <version>2.7.18</version>
+ <exclusions>
+ <exclusion>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!--
https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui -->
+ <dependency>
+ <groupId>org.springdoc</groupId>
+ <artifactId>springdoc-openapi-ui</artifactId>
+ <version>1.7.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.paimon</groupId>
+ <artifactId>paimon-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.swagger.core.v3</groupId>
+ <artifactId>swagger-annotations</artifactId>
+ <version>2.2.20</version>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <version>2.7.6</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>8</source>
+ <target>8</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/paimon-open-api/rest-catalog-open-api.yaml
b/paimon-open-api/rest-catalog-open-api.yaml
new file mode 100644
index 0000000000..432ee123b8
--- /dev/null
+++ b/paimon-open-api/rest-catalog-open-api.yaml
@@ -0,0 +1,60 @@
+#
+# 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.
+#
+
+---
+openapi: 3.0.1
+info:
+ title: RESTCatalog API
+ description: This API exposes endpoints to RESTCatalog.
+ license:
+ name: Apache 2.0
+ url: https://www.apache.org/licenses/LICENSE-2.0.html
+ version: "1.0"
+servers:
+ - url: http://localhost:8080
+ description: Server URL in Development environment
+paths:
+ /api/v1/config:
+ get:
+ tags:
+ - config
+ summary: Get Config
+ operationId: getConfig
+ responses:
+ "500":
+ description: Internal Server Error
+ "201":
+ description: Created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConfigResponse'
+components:
+ schemas:
+ ConfigResponse:
+ type: object
+ properties:
+ defaults:
+ type: object
+ additionalProperties:
+ type: string
+ writeOnly: true
+ overrides:
+ type: object
+ additionalProperties:
+ type: string
+ writeOnly: true
diff --git
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/OpenApiApplication.java
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/OpenApiApplication.java
new file mode 100644
index 0000000000..76ce4cbf83
--- /dev/null
+++
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/OpenApiApplication.java
@@ -0,0 +1,31 @@
+/*
+ * 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.open.api;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/** OpenAPI application. */
+@SpringBootApplication
+public class OpenApiApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OpenApiApplication.class, args);
+ }
+}
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
new file mode 100644
index 0000000000..b475540571
--- /dev/null
+++
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.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.open.api;
+
+import org.apache.paimon.rest.ResourcePaths;
+import org.apache.paimon.rest.responses.ConfigResponse;
+
+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.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.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** * RESTCatalog management APIs. */
+@CrossOrigin(origins = "http://localhost:8081")
+@RestController
+public class RESTCatalogController {
+
+ @Operation(
+ summary = "Get Config",
+ tags = {"config"})
+ @ApiResponses({
+ @ApiResponse(
+ responseCode = "201",
+ content = {
+ @Content(
+ schema = @Schema(implementation =
ConfigResponse.class),
+ mediaType = "application/json")
+ }),
+ @ApiResponse(
+ responseCode = "500",
+ 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);
+ }
+ }
+}
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
new file mode 100644
index 0000000000..01234c41bb
--- /dev/null
+++
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/config/OpenAPIConfig.java
@@ -0,0 +1,60 @@
+/*
+ * 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.open.api.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.servers.Server;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Config for OpenAPI. */
+@Configuration
+public class OpenAPIConfig {
+
+ @Value("${openapi.url}")
+ private String devUrl;
+
+ @Bean
+ public OpenAPI restCatalogOpenAPI() {
+ Server server = new Server();
+ server.setUrl(devUrl);
+ server.setDescription("Server URL in Development environment");
+
+ License mitLicense =
+ new License()
+ .name("Apache 2.0")
+
.url("https://www.apache.org/licenses/LICENSE-2.0.html");
+
+ Info info =
+ new Info()
+ .title("RESTCatalog API")
+ .version("1.0")
+ .description("This API exposes endpoints to
RESTCatalog.")
+ .license(mitLicense);
+ List<Server> servers = new ArrayList<>();
+ servers.add(server);
+ return new OpenAPI().info(info).servers(servers);
+ }
+}
diff --git a/paimon-open-api/src/main/resources/application.properties
b/paimon-open-api/src/main/resources/application.properties
new file mode 100644
index 0000000000..58a9751611
--- /dev/null
+++ b/paimon-open-api/src/main/resources/application.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+springdoc.swagger-ui.path=/swagger-api
+springdoc.api-docs.path=/swagger-api-docs
+springdoc.swagger-ui.deepLinking=true
+springdoc.swagger-ui.tryItOutEnabled=true
+springdoc.swagger-ui.filter=true
+openapi.url=http://localhost:8080
diff --git a/pom.xml b/pom.xml
index 85a880f351..904b1c73c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,7 @@ under the License.
<module>paimon-test-utils</module>
<module>paimon-arrow</module>
<module>tools/ci/paimon-ci-tools</module>
+ <module>paimon-open-api</module>
</modules>
<properties>