This is an automated email from the ASF dual-hosted git repository.
yufei pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 35d66a3fe8 Core: Support Custom Table/View Operations in RESTCatalog
(#14465)
35d66a3fe8 is described below
commit 35d66a3fe8ad76e1c76b39a898d9477f1da023ae
Author: Rulin Xing <[email protected]>
AuthorDate: Tue Dec 2 13:06:29 2025 -0800
Core: Support Custom Table/View Operations in RESTCatalog (#14465)
---
.../java/org/apache/iceberg/rest/RESTCatalog.java | 16 +-
.../apache/iceberg/rest/RESTSessionCatalog.java | 93 ++++++++++-
.../org/apache/iceberg/rest/TestRESTCatalog.java | 173 +++++++++++++++++++++
.../apache/iceberg/rest/TestRESTViewCatalog.java | 110 +++++++++++++
4 files changed, 383 insertions(+), 9 deletions(-)
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
b/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
index 0176128ed5..aff8832c6b 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
@@ -69,13 +69,27 @@ public class RESTCatalog
public RESTCatalog(
SessionCatalog.SessionContext context,
Function<Map<String, String>, RESTClient> clientBuilder) {
- this.sessionCatalog = new RESTSessionCatalog(clientBuilder, null);
+ this.sessionCatalog = newSessionCatalog(clientBuilder);
this.delegate = sessionCatalog.asCatalog(context);
this.nsDelegate = (SupportsNamespaces) delegate;
this.context = context;
this.viewSessionCatalog = sessionCatalog.asViewCatalog(context);
}
+ /**
+ * Create a new {@link RESTSessionCatalog} instance.
+ *
+ * <p>This method can be overridden in subclasses to provide custom {@link
RESTSessionCatalog}
+ * implementations.
+ *
+ * @param clientBuilder a function to build REST clients
+ * @return a new RESTSessionCatalog instance
+ */
+ protected RESTSessionCatalog newSessionCatalog(
+ Function<Map<String, String>, RESTClient> clientBuilder) {
+ return new RESTSessionCatalog(clientBuilder, null);
+ }
+
@Override
public void initialize(String name, Map<String, String> props) {
Preconditions.checkArgument(props != null, "Invalid configuration: null");
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
index b903f13adc..85b04f3868 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
@@ -27,6 +27,7 @@ import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.iceberg.BaseTable;
import org.apache.iceberg.CatalogProperties;
@@ -450,7 +451,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
RESTClient tableClient = client.withAuthSession(tableSession);
RESTTableOperations ops =
- new RESTTableOperations(
+ newTableOps(
tableClient,
paths.table(finalIdentifier),
Map::of,
@@ -529,7 +530,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
AuthSession tableSession = authManager.tableSession(ident, tableConf,
contextualSession);
RESTClient tableClient = client.withAuthSession(tableSession);
RESTTableOperations ops =
- new RESTTableOperations(
+ newTableOps(
tableClient,
paths.table(ident),
Map::of,
@@ -788,7 +789,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
AuthSession tableSession = authManager.tableSession(ident, tableConf,
contextualSession);
RESTClient tableClient = client.withAuthSession(tableSession);
RESTTableOperations ops =
- new RESTTableOperations(
+ newTableOps(
tableClient,
paths.table(ident),
Map::of,
@@ -815,7 +816,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
RESTClient tableClient = client.withAuthSession(tableSession);
RESTTableOperations ops =
- new RESTTableOperations(
+ newTableOps(
tableClient,
paths.table(ident),
Map::of,
@@ -878,7 +879,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
RESTClient tableClient = client.withAuthSession(tableSession);
RESTTableOperations ops =
- new RESTTableOperations(
+ newTableOps(
tableClient,
paths.table(ident),
Map::of,
@@ -1010,6 +1011,82 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
return newFileIO(context, fullConf, storageCredentials);
}
+ /**
+ * Create a new {@link RESTTableOperations} instance for simple table
operations.
+ *
+ * <p>This method can be overridden in subclasses to provide custom table
operations
+ * implementations.
+ *
+ * @param restClient the REST client to use for communicating with the
catalog server
+ * @param path the REST path for the table
+ * @param headers a supplier for additional HTTP headers to include in
requests
+ * @param fileIO the FileIO implementation for reading and writing table
metadata and data files
+ * @param current the current table metadata
+ * @param supportedEndpoints the set of supported REST endpoints
+ * @return a new RESTTableOperations instance
+ */
+ protected RESTTableOperations newTableOps(
+ RESTClient restClient,
+ String path,
+ Supplier<Map<String, String>> headers,
+ FileIO fileIO,
+ TableMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ return new RESTTableOperations(restClient, path, headers, fileIO, current,
supportedEndpoints);
+ }
+
+ /**
+ * Create a new {@link RESTTableOperations} instance for transaction-based
operations (create or
+ * replace).
+ *
+ * <p>This method can be overridden in subclasses to provide custom table
operations
+ * implementations for transaction-based operations.
+ *
+ * @param restClient the REST client to use for communicating with the
catalog server
+ * @param path the REST path for the table
+ * @param headers a supplier for additional HTTP headers to include in
requests
+ * @param fileIO the FileIO implementation for reading and writing table
metadata and data files
+ * @param updateType the {@link RESTTableOperations.UpdateType} being
performed
+ * @param createChanges the list of metadata updates to apply during table
creation or replacement
+ * @param current the current table metadata (may be null for CREATE
operations)
+ * @param supportedEndpoints the set of supported REST endpoints
+ * @return a new RESTTableOperations instance
+ */
+ protected RESTTableOperations newTableOps(
+ RESTClient restClient,
+ String path,
+ Supplier<Map<String, String>> headers,
+ FileIO fileIO,
+ RESTTableOperations.UpdateType updateType,
+ List<MetadataUpdate> createChanges,
+ TableMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ return new RESTTableOperations(
+ restClient, path, headers, fileIO, updateType, createChanges, current,
supportedEndpoints);
+ }
+
+ /**
+ * Create a new {@link RESTViewOperations} instance.
+ *
+ * <p>This method can be overridden in subclasses to provide custom view
operations
+ * implementations.
+ *
+ * @param restClient the REST client to use for communicating with the
catalog server
+ * @param path the REST path for the view
+ * @param headers a supplier for additional HTTP headers to include in
requests
+ * @param current the current view metadata
+ * @param supportedEndpoints the set of supported REST endpoints
+ * @return a new RESTViewOperations instance
+ */
+ protected RESTViewOperations newViewOps(
+ RESTClient restClient,
+ String path,
+ Supplier<Map<String, String>> headers,
+ ViewMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ return new RESTViewOperations(restClient, path, headers, current,
supportedEndpoints);
+ }
+
private static ConfigResponse fetchConfig(
RESTClient client, AuthSession initialAuth, Map<String, String>
properties) {
// send the client's warehouse location to the service to keep in sync
@@ -1154,7 +1231,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
ViewMetadata metadata = response.metadata();
RESTViewOperations ops =
- new RESTViewOperations(
+ newViewOps(
client.withAuthSession(tableSession),
paths.view(identifier),
Map::of,
@@ -1333,7 +1410,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
Map<String, String> tableConf = response.config();
AuthSession tableSession = authManager.tableSession(identifier,
tableConf, contextualSession);
RESTViewOperations ops =
- new RESTViewOperations(
+ newViewOps(
client.withAuthSession(tableSession),
paths.view(identifier),
Map::of,
@@ -1424,7 +1501,7 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
AuthSession contextualSession = authManager.contextualSession(context,
catalogAuth);
AuthSession tableSession = authManager.tableSession(identifier,
tableConf, contextualSession);
RESTViewOperations ops =
- new RESTViewOperations(
+ newViewOps(
client.withAuthSession(tableSession),
paths.view(identifier),
Map::of,
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
b/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
index 59e91150ee..efe76e2bf0 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
@@ -41,9 +41,15 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
import org.apache.hadoop.conf.Configuration;
import org.apache.http.HttpHeaders;
import org.apache.iceberg.BaseTable;
@@ -72,6 +78,7 @@ import org.apache.iceberg.exceptions.NotFoundException;
import org.apache.iceberg.exceptions.ServiceFailureException;
import org.apache.iceberg.expressions.Expressions;
import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.io.FileIO;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
@@ -3107,6 +3114,153 @@ public class TestRESTCatalog extends
CatalogTests<RESTCatalog> {
.satisfies(ex -> assertThat(((CommitStateUnknownException)
ex).getSuppressed()).isEmpty());
}
+ @Test
+ public void testCustomTableOperationsInjection() throws IOException {
+ AtomicBoolean customTableOpsCalled = new AtomicBoolean();
+ AtomicBoolean customTransactionTableOpsCalled = new AtomicBoolean();
+ AtomicReference<RESTTableOperations> capturedOps = new AtomicReference<>();
+ RESTCatalogAdapter adapter = Mockito.spy(new
RESTCatalogAdapter(backendCatalog));
+ Map<String, String> customHeaders =
+ ImmutableMap.of("X-Custom-Table-Header", "custom-value-12345");
+
+ // Custom RESTTableOperations that adds a custom header
+ class CustomRESTTableOperations extends RESTTableOperations {
+ CustomRESTTableOperations(
+ RESTClient client,
+ String path,
+ Supplier<Map<String, String>> headers,
+ FileIO fileIO,
+ TableMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ super(client, path, () -> customHeaders, fileIO, current,
supportedEndpoints);
+ customTableOpsCalled.set(true);
+ }
+
+ CustomRESTTableOperations(
+ RESTClient client,
+ String path,
+ Supplier<Map<String, String>> headers,
+ FileIO fileIO,
+ RESTTableOperations.UpdateType updateType,
+ List<MetadataUpdate> createChanges,
+ TableMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ super(
+ client,
+ path,
+ () -> customHeaders,
+ fileIO,
+ updateType,
+ createChanges,
+ current,
+ supportedEndpoints);
+ customTransactionTableOpsCalled.set(true);
+ }
+ }
+
+ // Custom RESTSessionCatalog that overrides table operations creation
+ class CustomRESTSessionCatalog extends RESTSessionCatalog {
+ CustomRESTSessionCatalog(
+ Function<Map<String, String>, RESTClient> clientBuilder,
+ BiFunction<SessionCatalog.SessionContext, Map<String, String>,
FileIO> ioBuilder) {
+ super(clientBuilder, ioBuilder);
+ }
+
+ @Override
+ protected RESTTableOperations newTableOps(
+ RESTClient restClient,
+ String path,
+ Supplier<Map<String, String>> headers,
+ FileIO fileIO,
+ TableMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ RESTTableOperations ops =
+ new CustomRESTTableOperations(
+ restClient, path, headers, fileIO, current,
supportedEndpoints);
+ RESTTableOperations spy = Mockito.spy(ops);
+ capturedOps.set(spy);
+ return spy;
+ }
+
+ @Override
+ protected RESTTableOperations newTableOps(
+ RESTClient restClient,
+ String path,
+ Supplier<Map<String, String>> headers,
+ FileIO fileIO,
+ RESTTableOperations.UpdateType updateType,
+ List<MetadataUpdate> createChanges,
+ TableMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ RESTTableOperations ops =
+ new CustomRESTTableOperations(
+ restClient,
+ path,
+ headers,
+ fileIO,
+ updateType,
+ createChanges,
+ current,
+ supportedEndpoints);
+ RESTTableOperations spy = Mockito.spy(ops);
+ capturedOps.set(spy);
+ return spy;
+ }
+ }
+
+ try (RESTCatalog catalog =
+ catalog(adapter, clientBuilder -> new
CustomRESTSessionCatalog(clientBuilder, null))) {
+ catalog.createNamespace(NS);
+
+ // Test table operations without UpdateType
+ assertThat(customTableOpsCalled).isFalse();
+ assertThat(customTransactionTableOpsCalled).isFalse();
+ Table table = catalog.createTable(TABLE, SCHEMA);
+ assertThat(customTableOpsCalled).isTrue();
+ assertThat(customTransactionTableOpsCalled).isFalse();
+
+ // Trigger a commit through the custom operations
+ table.updateProperties().set("test-key", "test-value").commit();
+
+ // Verify the custom operations object was created and used
+ assertThat(capturedOps.get()).isNotNull();
+ Mockito.verify(capturedOps.get(), Mockito.atLeastOnce()).current();
+ Mockito.verify(capturedOps.get(), Mockito.atLeastOnce()).commit(any(),
any());
+
+ // Verify the custom operations were used with custom headers
+ Mockito.verify(adapter, Mockito.atLeastOnce())
+ .execute(
+ reqMatcher(HTTPMethod.POST, RESOURCE_PATHS.table(TABLE),
customHeaders),
+ eq(LoadTableResponse.class),
+ any(),
+ any());
+
+ // Test table operations with UpdateType and createChanges
+ capturedOps.set(null);
+ customTableOpsCalled.set(false);
+ TableIdentifier table2 = TableIdentifier.of(NS, "table2");
+ catalog.buildTable(table2,
SCHEMA).createTransaction().commitTransaction();
+ assertThat(customTableOpsCalled).isFalse();
+ assertThat(customTransactionTableOpsCalled).isTrue();
+
+ // Trigger another commit to verify transaction operations also work
+ catalog.loadTable(table2).updateProperties().set("test-key-2",
"test-value-2").commit();
+
+ // Verify the custom operations object was created and used
+ assertThat(capturedOps.get()).isNotNull();
+ Mockito.verify(capturedOps.get(), Mockito.atLeastOnce()).current();
+ Mockito.verify(capturedOps.get(), Mockito.atLeastOnce()).commit(any(),
any());
+
+ // Verify the custom operations were used with custom headers
+ Mockito.verify(adapter, Mockito.atLeastOnce())
+ .execute(
+ reqMatcher(HTTPMethod.POST, RESOURCE_PATHS.table(table2),
customHeaders),
+ eq(LoadTableResponse.class),
+ any(),
+ any());
+ }
+ }
+
private RESTCatalog catalog(RESTCatalogAdapter adapter) {
RESTCatalog catalog =
new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config)
-> adapter);
@@ -3117,6 +3271,25 @@ public class TestRESTCatalog extends
CatalogTests<RESTCatalog> {
return catalog;
}
+ private RESTCatalog catalog(
+ RESTCatalogAdapter adapter,
+ Function<Function<Map<String, String>, RESTClient>, RESTSessionCatalog>
+ sessionCatalogFactory) {
+ RESTCatalog catalog =
+ new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config)
-> adapter) {
+ @Override
+ protected RESTSessionCatalog newSessionCatalog(
+ Function<Map<String, String>, RESTClient> clientBuilder) {
+ return sessionCatalogFactory.apply(clientBuilder);
+ }
+ };
+ catalog.initialize(
+ "test",
+ ImmutableMap.of(
+ CatalogProperties.FILE_IO_IMPL,
"org.apache.iceberg.inmemory.InMemoryFileIO"));
+ return catalog;
+ }
+
static HTTPRequest reqMatcher(HTTPMethod method) {
return argThat(req -> req.method() == method);
}
diff --git
a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java
b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java
index f3ad68c002..6b39907098 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java
@@ -32,22 +32,31 @@ import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
import org.apache.iceberg.CatalogProperties;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.SessionCatalog;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.inmemory.InMemoryCatalog;
+import org.apache.iceberg.io.FileIO;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
import org.apache.iceberg.rest.HTTPRequest.HTTPMethod;
import org.apache.iceberg.rest.responses.ConfigResponse;
import org.apache.iceberg.rest.responses.ErrorResponse;
import org.apache.iceberg.rest.responses.ListTablesResponse;
import org.apache.iceberg.rest.responses.LoadViewResponse;
import org.apache.iceberg.view.ViewCatalogTests;
+import org.apache.iceberg.view.ViewMetadata;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
@@ -308,6 +317,107 @@ public class TestRESTViewCatalog extends
ViewCatalogTests<RESTCatalog> {
ImmutableMap.of(RESTCatalogProperties.VIEW_ENDPOINTS_SUPPORTED,
"true"));
}
+ @Test
+ public void testCustomViewOperationsInjection() throws Exception {
+ AtomicBoolean customViewOpsCalled = new AtomicBoolean();
+ AtomicReference<RESTViewOperations> capturedOps = new AtomicReference<>();
+ RESTCatalogAdapter adapter = Mockito.spy(new
RESTCatalogAdapter(backendCatalog));
+ Map<String, String> customHeaders =
+ ImmutableMap.of("X-Custom-View-Header", "custom-value-12345");
+
+ // Custom RESTViewOperations that adds a custom header
+ class CustomRESTViewOperations extends RESTViewOperations {
+ CustomRESTViewOperations(
+ RESTClient client,
+ String path,
+ Supplier<Map<String, String>> headers,
+ ViewMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ super(client, path, () -> customHeaders, current, supportedEndpoints);
+ customViewOpsCalled.set(true);
+ }
+ }
+
+ // Custom RESTSessionCatalog that overrides view operations creation
+ class CustomRESTSessionCatalog extends RESTSessionCatalog {
+ CustomRESTSessionCatalog(
+ Function<Map<String, String>, RESTClient> clientBuilder,
+ BiFunction<SessionCatalog.SessionContext, Map<String, String>,
FileIO> ioBuilder) {
+ super(clientBuilder, ioBuilder);
+ }
+
+ @Override
+ protected RESTViewOperations newViewOps(
+ RESTClient restClient,
+ String path,
+ Supplier<Map<String, String>> headers,
+ ViewMetadata current,
+ Set<Endpoint> supportedEndpoints) {
+ RESTViewOperations ops =
+ new CustomRESTViewOperations(restClient, path, headers, current,
supportedEndpoints);
+ RESTViewOperations spy = Mockito.spy(ops);
+ capturedOps.set(spy);
+ return spy;
+ }
+ }
+
+ try (RESTCatalog catalog =
+ catalog(adapter, clientBuilder -> new
CustomRESTSessionCatalog(clientBuilder, null))) {
+ Namespace namespace = Namespace.of("ns");
+ catalog.createNamespace(namespace);
+
+ // Test view operations
+ assertThat(customViewOpsCalled).isFalse();
+ TableIdentifier viewIdentifier = TableIdentifier.of(namespace, "view1");
+ org.apache.iceberg.view.View view =
+ catalog
+ .buildView(viewIdentifier)
+ .withSchema(SCHEMA)
+ .withDefaultNamespace(namespace)
+ .withQuery("spark", "select * from ns.table")
+ .create();
+
+ // Verify custom operations was created
+ assertThat(customViewOpsCalled).isTrue();
+
+ // Update view properties to trigger a commit through the custom
operations
+ view.updateProperties().set("test-key", "test-value").commit();
+
+ // Verify the custom operations object was created and used
+ assertThat(capturedOps.get()).isNotNull();
+ Mockito.verify(capturedOps.get(), Mockito.atLeastOnce()).current();
+ Mockito.verify(capturedOps.get(), Mockito.atLeastOnce()).commit(any(),
any());
+
+ // Verify the custom operations were used with custom headers
+ ResourcePaths resourcePaths =
ResourcePaths.forCatalogProperties(Maps.newHashMap());
+ Mockito.verify(adapter, Mockito.atLeastOnce())
+ .execute(
+ reqMatcher(HTTPMethod.POST, resourcePaths.view(viewIdentifier),
customHeaders),
+ eq(LoadViewResponse.class),
+ any(),
+ any());
+ }
+ }
+
+ private RESTCatalog catalog(
+ RESTCatalogAdapter adapter,
+ Function<Function<Map<String, String>, RESTClient>, RESTSessionCatalog>
+ sessionCatalogFactory) {
+ RESTCatalog catalog =
+ new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config)
-> adapter) {
+ @Override
+ protected RESTSessionCatalog newSessionCatalog(
+ Function<Map<String, String>, RESTClient> clientBuilder) {
+ return sessionCatalogFactory.apply(clientBuilder);
+ }
+ };
+ catalog.initialize(
+ "test",
+ ImmutableMap.of(
+ CatalogProperties.FILE_IO_IMPL,
"org.apache.iceberg.inmemory.InMemoryFileIO"));
+ return catalog;
+ }
+
@Override
protected RESTCatalog catalog() {
return restCatalog;