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>

Reply via email to