This is an automated email from the ASF dual-hosted git repository.

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 0bb6f936da [#9745]feat(core,IRC): Add view entity and management layer 
(#9787)
0bb6f936da is described below

commit 0bb6f936da06b8dda8d9b6b674ed89fafc2f6b5e
Author: Bharath Krishna <[email protected]>
AuthorDate: Wed Feb 4 18:43:41 2026 -0800

    [#9745]feat(core,IRC): Add view entity and management layer (#9787)
    
    ### What changes were proposed in this pull request?
    
    Adds view entity and management layer to Gravitino core and Iceberg REST
    catalog (IRC), including:
    
    - New View and ViewCatalog interfaces in the API
    - ViewManager for internal view operations
    - View support in Iceberg catalog operations (loadView, viewExists)
    - View hook dispatcher and event handling
    
    ### Why are the changes needed?
    
    To enable view support in Gravitino, providing the foundational layer
    for managing database views alongside tables. This is a prerequisite for
    exposing view operations through Gravitino APIs.
    
    Fix: #9745
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. This is internal infrastructure only. Full view operations (create,
    list, alter, drop) and view authorization may be added in future PRs
    
    ### How was this patch tested?
    
    Added unit tests for view manager, view operations, and Iceberg catalog
    view support.
---
 .../main/java/org/apache/gravitino/Catalog.java    |   9 ++
 .../gravitino/exceptions/NoSuchViewException.java  |  49 ++++++
 .../main/java/org/apache/gravitino/rel/View.java   |  54 +++++++
 .../java/org/apache/gravitino/rel/ViewCatalog.java |  57 +++++++
 .../catalog/lakehouse/iceberg/IcebergCatalog.java  |   6 +
 .../iceberg/IcebergCatalogOperations.java          |  30 +++-
 .../catalog/lakehouse/iceberg/IcebergView.java     | 108 +++++++++++++
 .../lakehouse/iceberg/TestIcebergCatalog.java      |  25 +++
 .../catalog/lakehouse/iceberg/TestIcebergView.java | 110 +++++++++++++
 .../java/org/apache/gravitino/GravitinoEnv.java    |  21 ++-
 .../apache/gravitino/catalog/CatalogManager.java   |  15 ++
 .../apache/gravitino/catalog/ViewDispatcher.java   |  29 ++++
 .../gravitino/catalog/ViewOperationDispatcher.java |  80 ++++++++++
 .../apache/gravitino/utils/MetadataObjectUtil.java |   5 +
 .../catalog/TestViewOperationDispatcher.java       | 175 ++++++++++++++++++++
 .../gravitino/connector/TestCatalogOperations.java |  17 ++
 .../org/apache/gravitino/iceberg/RESTService.java  |   5 +-
 .../service/dispatcher/IcebergOwnershipUtils.java  |  34 +++-
 .../dispatcher/IcebergViewHookDispatcher.java      | 154 ++++++++++++++++++
 .../dispatcher/TestIcebergOwnershipUtils.java      |  34 +++-
 .../TestIcebergViewOperationExecutor.java          | 176 +++++++++++++++++++++
 21 files changed, 1185 insertions(+), 8 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/Catalog.java 
b/api/src/main/java/org/apache/gravitino/Catalog.java
index ed63285e95..9880f8d8d0 100644
--- a/api/src/main/java/org/apache/gravitino/Catalog.java
+++ b/api/src/main/java/org/apache/gravitino/Catalog.java
@@ -29,6 +29,7 @@ import org.apache.gravitino.messaging.TopicCatalog;
 import org.apache.gravitino.model.ModelCatalog;
 import org.apache.gravitino.policy.SupportsPolicies;
 import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.tag.SupportsTags;
 
 /**
@@ -246,6 +247,14 @@ public interface Catalog extends Auditable {
     throw new UnsupportedOperationException("Catalog does not support function 
operations");
   }
 
+  /**
+   * @return the {@link ViewCatalog} if the catalog supports view operations.
+   * @throws UnsupportedOperationException if the catalog does not support 
view operations.
+   */
+  default ViewCatalog asViewCatalog() throws UnsupportedOperationException {
+    throw new UnsupportedOperationException("Catalog does not support view 
operations");
+  }
+
   /**
    * @return the {@link SupportsTags} if the catalog supports tag operations.
    * @throws UnsupportedOperationException if the catalog does not support tag 
operations.
diff --git 
a/api/src/main/java/org/apache/gravitino/exceptions/NoSuchViewException.java 
b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchViewException.java
new file mode 100644
index 0000000000..15cc7b0671
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchViewException.java
@@ -0,0 +1,49 @@
+/*
+ * 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.gravitino.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/** Exception thrown when a view with specified name does not exist. */
+public class NoSuchViewException extends NotFoundException {
+
+  /**
+   * Constructs a NoSuchViewException.
+   *
+   * @param message The error message.
+   * @param args Additional arguments for formatting the error message.
+   */
+  @FormatMethod
+  public NoSuchViewException(@FormatString String message, Object... args) {
+    super(message, args);
+  }
+
+  /**
+   * Constructs a NoSuchViewException with a cause.
+   *
+   * @param cause The cause of the exception.
+   * @param message The error message.
+   * @param args Additional arguments for formatting the error message.
+   */
+  @FormatMethod
+  public NoSuchViewException(Throwable cause, String message, Object... args) {
+    super(cause, message, args);
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/rel/View.java 
b/api/src/main/java/org/apache/gravitino/rel/View.java
new file mode 100644
index 0000000000..1d425e7494
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/rel/View.java
@@ -0,0 +1,54 @@
+/*
+ * 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.gravitino.rel;
+
+import java.util.Collections;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.annotation.Unstable;
+
+/**
+ * An interface representing a view in a {@link Namespace}. It defines the 
basic properties of a
+ * view. A catalog implementation with {@link ViewCatalog} should implement 
this interface.
+ */
+@Unstable
+public interface View extends Auditable {
+
+  /**
+   * @return The name of the view.
+   */
+  String name();
+
+  /**
+   * @return The comment of the view, null if no comment is set.
+   */
+  @Nullable
+  default String comment() {
+    return null;
+  }
+
+  /**
+   * @return The properties of the view, empty map if no properties are set.
+   */
+  default Map<String, String> properties() {
+    return Collections.emptyMap();
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/rel/ViewCatalog.java 
b/api/src/main/java/org/apache/gravitino/rel/ViewCatalog.java
new file mode 100644
index 0000000000..2bf423f97b
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/rel/ViewCatalog.java
@@ -0,0 +1,57 @@
+/*
+ * 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.gravitino.rel;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.Unstable;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+
+/**
+ * The ViewCatalog interface defines the public API for managing views in a 
schema. If the catalog
+ * implementation supports views, it must implement this interface.
+ *
+ * <p>Note: This is a minimal interface. Full operations (create, list, alter, 
drop) will be added
+ * when Gravitino APIs support views.
+ */
+@Unstable
+public interface ViewCatalog {
+
+  /**
+   * Load view metadata by {@link NameIdentifier} from the catalog.
+   *
+   * @param ident A view identifier.
+   * @return The view metadata.
+   * @throws NoSuchViewException If the view does not exist.
+   */
+  View loadView(NameIdentifier ident) throws NoSuchViewException;
+
+  /**
+   * Check if a view exists using its identifier.
+   *
+   * @param ident A view identifier.
+   * @return true If the view exists, false otherwise.
+   */
+  default boolean viewExists(NameIdentifier ident) {
+    try {
+      return loadView(ident) != null;
+    } catch (NoSuchViewException e) {
+      return false;
+    }
+  }
+}
diff --git 
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
index 58858ec81b..2e838e2be5 100644
--- 
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
+++ 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
@@ -23,6 +23,7 @@ import org.apache.gravitino.connector.BaseCatalog;
 import org.apache.gravitino.connector.CatalogOperations;
 import org.apache.gravitino.connector.PropertiesMetadata;
 import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.rel.ViewCatalog;
 
 /** Implementation of an Apache Iceberg catalog in Apache Gravitino. */
 public class IcebergCatalog extends BaseCatalog<IcebergCatalog> {
@@ -56,6 +57,11 @@ public class IcebergCatalog extends 
BaseCatalog<IcebergCatalog> {
     return ops;
   }
 
+  @Override
+  public ViewCatalog asViewCatalog() {
+    return (ViewCatalog) ops();
+  }
+
   @Override
   public Capability newCapability() {
     return new IcebergCatalogCapability();
diff --git 
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
index 64a951cb81..99f143e8ad 100644
--- 
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
+++ 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
@@ -46,6 +46,7 @@ import 
org.apache.gravitino.exceptions.ConnectionFailedException;
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
 import org.apache.gravitino.exceptions.NonEmptySchemaException;
 import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.apache.gravitino.exceptions.TableAlreadyExistsException;
@@ -60,6 +61,8 @@ import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.rel.expressions.distributions.Distribution;
 import org.apache.gravitino.rel.expressions.distributions.Distributions;
 import org.apache.gravitino.rel.expressions.sorts.SortOrder;
@@ -77,12 +80,14 @@ import 
org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.responses.GetNamespaceResponse;
 import org.apache.iceberg.rest.responses.ListTablesResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
 import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /** Operations for interacting with an Apache Iceberg catalog in Apache 
Gravitino. */
-public class IcebergCatalogOperations implements CatalogOperations, 
SupportsSchemas, TableCatalog {
+public class IcebergCatalogOperations
+    implements CatalogOperations, SupportsSchemas, TableCatalog, ViewCatalog {
 
   private static final String ICEBERG_TABLE_DOES_NOT_EXIST_MSG = "Iceberg 
table does not exist: %s";
 
@@ -620,6 +625,29 @@ public class IcebergCatalogOperations implements 
CatalogOperations, SupportsSche
     }
   }
 
+  /**
+   * Load view metadata from the Iceberg catalog.
+   *
+   * <p>Delegates to the underlying Iceberg REST catalog to load view metadata.
+   *
+   * @param ident The identifier of the view to load.
+   * @return The loaded view metadata.
+   * @throws NoSuchViewException If the view does not exist.
+   */
+  @Override
+  public View loadView(NameIdentifier ident) throws NoSuchViewException {
+    try {
+      LoadViewResponse response =
+          icebergCatalogWrapper.loadView(
+              IcebergCatalogWrapperHelper.buildIcebergTableIdentifier(ident));
+
+      return IcebergView.fromLoadViewResponse(response, ident.name());
+    } catch (Exception e) {
+      throw new NoSuchViewException(
+          e, "Failed to load view %s from Iceberg catalog: %s", ident, 
e.getMessage());
+    }
+  }
+
   private static Distribution getIcebergDefaultDistribution(
       boolean isSorted, boolean isPartitioned) {
     if (isSorted) {
diff --git 
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergView.java
 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergView.java
new file mode 100644
index 0000000000..73894d4745
--- /dev/null
+++ 
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergView.java
@@ -0,0 +1,108 @@
+/*
+ * 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.gravitino.catalog.lakehouse.iceberg;
+
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.Map;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.rel.View;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+
+/** Represents an Apache Iceberg View entity in the Iceberg catalog. */
+@ToString
+@Getter
+public class IcebergView implements View {
+
+  private String name;
+  private Map<String, String> properties;
+  private AuditInfo auditInfo;
+
+  private IcebergView() {}
+
+  /**
+   * Converts an Iceberg LoadViewResponse to a Gravitino IcebergView.
+   *
+   * @param response The Iceberg LoadViewResponse.
+   * @param viewName The name of the view.
+   * @return A new IcebergView instance.
+   */
+  public static IcebergView fromLoadViewResponse(LoadViewResponse response, 
String viewName) {
+    Map<String, String> properties =
+        response.metadata() != null && response.metadata().properties() != null
+            ? Maps.newHashMap(response.metadata().properties())
+            : Maps.newHashMap();
+
+    return IcebergView.builder()
+        .withName(viewName)
+        .withProperties(properties)
+        .withAuditInfo(AuditInfo.EMPTY)
+        .build();
+  }
+
+  @Override
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public Map<String, String> properties() {
+    return properties != null ? properties : Collections.emptyMap();
+  }
+
+  @Override
+  public AuditInfo auditInfo() {
+    return auditInfo;
+  }
+
+  /** Returns a new builder for constructing an IcebergView. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for IcebergView. */
+  public static class Builder {
+    private final IcebergView view;
+
+    private Builder() {
+      this.view = new IcebergView();
+    }
+
+    public Builder withName(String name) {
+      view.name = name;
+      return this;
+    }
+
+    public Builder withProperties(Map<String, String> properties) {
+      view.properties = properties != null ? Maps.newHashMap(properties) : 
Maps.newHashMap();
+      return this;
+    }
+
+    public Builder withAuditInfo(AuditInfo auditInfo) {
+      view.auditInfo = auditInfo;
+      return this;
+    }
+
+    public IcebergView build() {
+      return view;
+    }
+  }
+}
diff --git 
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
 
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
index acd3c3a1f0..e6e2f20741 100644
--- 
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
+++ 
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
@@ -33,6 +33,7 @@ import org.apache.gravitino.connector.PropertiesMetadata;
 import org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.iceberg.rest.responses.ListNamespacesResponse;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -199,4 +200,28 @@ public class TestIcebergCatalog {
           
throwable.getMessage().contains(IcebergCatalogPropertiesMetadata.CATALOG_BACKEND));
     }
   }
+
+  @Test
+  public void testAsViewCatalog() {
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+    CatalogEntity entity =
+        CatalogEntity.builder()
+            .withId(1L)
+            .withName("catalog")
+            .withNamespace(Namespace.of("metalake"))
+            .withType(IcebergCatalog.Type.RELATIONAL)
+            .withProvider("iceberg")
+            .withAuditInfo(auditInfo)
+            .build();
+
+    Map<String, String> conf = Maps.newHashMap();
+    IcebergCatalog icebergCatalog =
+        new IcebergCatalog().withCatalogConf(conf).withCatalogEntity(entity);
+
+    ViewCatalog viewCatalog = icebergCatalog.asViewCatalog();
+    Assertions.assertNotNull(viewCatalog);
+    Assertions.assertTrue(viewCatalog instanceof IcebergCatalogOperations);
+  }
 }
diff --git 
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergView.java
 
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergView.java
new file mode 100644
index 0000000000..cfc9563663
--- /dev/null
+++ 
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergView.java
@@ -0,0 +1,110 @@
+/*
+ * 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.gravitino.catalog.lakehouse.iceberg;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+import org.apache.iceberg.view.ViewMetadata;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestIcebergView {
+
+  @Test
+  public void testFromLoadViewResponse() {
+    // Test with properties
+    Map<String, String> properties = ImmutableMap.of("key1", "value1", "key2", 
"value2");
+    ViewMetadata mockMetadata = mock(ViewMetadata.class);
+    when(mockMetadata.properties()).thenReturn(properties);
+
+    LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+    when(mockResponse.metadata()).thenReturn(mockMetadata);
+
+    IcebergView view = IcebergView.fromLoadViewResponse(mockResponse, 
"test_view");
+
+    Assertions.assertEquals("test_view", view.name());
+    Assertions.assertEquals(properties, view.properties());
+    Assertions.assertEquals(AuditInfo.EMPTY, view.auditInfo());
+  }
+
+  @Test
+  public void testFromLoadViewResponseWithNullProperties() {
+    // Test with null metadata
+    LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+    when(mockResponse.metadata()).thenReturn(null);
+
+    IcebergView view = IcebergView.fromLoadViewResponse(mockResponse, 
"test_view_null");
+
+    Assertions.assertEquals("test_view_null", view.name());
+    Assertions.assertNotNull(view.properties());
+    Assertions.assertTrue(view.properties().isEmpty());
+    Assertions.assertNotNull(view.auditInfo());
+  }
+
+  @Test
+  public void testFromLoadViewResponseWithNullMetadataProperties() {
+    // Test with null properties in metadata
+    ViewMetadata mockMetadata = mock(ViewMetadata.class);
+    when(mockMetadata.properties()).thenReturn(null);
+
+    LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+    when(mockResponse.metadata()).thenReturn(mockMetadata);
+
+    IcebergView view = IcebergView.fromLoadViewResponse(mockResponse, 
"test_view_empty");
+
+    Assertions.assertEquals("test_view_empty", view.name());
+    Assertions.assertNotNull(view.properties());
+    Assertions.assertTrue(view.properties().isEmpty());
+  }
+
+  @Test
+  public void testBuilder() {
+    Map<String, String> properties = ImmutableMap.of("prop1", "val1");
+    AuditInfo auditInfo = 
AuditInfo.builder().withCreator("test_user").withCreateTime(null).build();
+
+    IcebergView view =
+        IcebergView.builder()
+            .withName("builder_view")
+            .withProperties(properties)
+            .withAuditInfo(auditInfo)
+            .build();
+
+    Assertions.assertEquals("builder_view", view.name());
+    Assertions.assertEquals(properties, view.properties());
+    Assertions.assertEquals(auditInfo, view.auditInfo());
+    Assertions.assertEquals("test_user", view.auditInfo().creator());
+  }
+
+  @Test
+  public void testBuilderWithNullProperties() {
+    AuditInfo auditInfo = 
AuditInfo.builder().withCreator("test_user").withCreateTime(null).build();
+
+    IcebergView view =
+        
IcebergView.builder().withName("view_null_props").withAuditInfo(auditInfo).build();
+
+    Assertions.assertEquals("view_null_props", view.name());
+    Assertions.assertNotNull(view.properties());
+    Assertions.assertTrue(view.properties().isEmpty());
+  }
+}
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java 
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index 6fba49b808..b0a58554d4 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -52,6 +52,8 @@ import org.apache.gravitino.catalog.TableOperationDispatcher;
 import org.apache.gravitino.catalog.TopicDispatcher;
 import org.apache.gravitino.catalog.TopicNormalizeDispatcher;
 import org.apache.gravitino.catalog.TopicOperationDispatcher;
+import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.catalog.ViewOperationDispatcher;
 import org.apache.gravitino.credential.CredentialOperationDispatcher;
 import org.apache.gravitino.hook.AccessControlHookDispatcher;
 import org.apache.gravitino.hook.CatalogHookDispatcher;
@@ -133,6 +135,8 @@ public class GravitinoEnv {
 
   private FunctionDispatcher functionDispatcher;
 
+  private ViewDispatcher viewDispatcher;
+
   private MetalakeDispatcher metalakeDispatcher;
 
   private CredentialOperationDispatcher credentialOperationDispatcher;
@@ -270,7 +274,16 @@ public class GravitinoEnv {
   }
 
   /**
-   * Get the PartitionDispatcher associated with the Gravitino environment.
+   * Get the ViewDispatcher associated with the Gravitino environment.
+   *
+   * @return The ViewDispatcher instance.
+   */
+  public ViewDispatcher viewDispatcher() {
+    return viewDispatcher;
+  }
+
+  /**
+   * * Get the PartitionDispatcher associated with the Gravitino environment.
    *
    * @return The PartitionDispatcher instance.
    */
@@ -609,6 +622,12 @@ public class GravitinoEnv {
     this.functionDispatcher =
         new FunctionNormalizeDispatcher(functionOperationDispatcher, 
catalogManager);
 
+    // TODO: Add ViewHookDispatcher and ViewEventDispatcher when needed for 
view-specific hooks
+    //  and event handling.
+    ViewOperationDispatcher viewOperationDispatcher =
+        new ViewOperationDispatcher(catalogManager, entityStore, idGenerator);
+    this.viewDispatcher = viewOperationDispatcher;
+
     this.statisticDispatcher =
         new StatisticEventDispatcher(
             eventBus, new StatisticManager(entityStore, idGenerator, config));
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java 
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index b9c21fcb1f..abc0b42517 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -98,6 +98,7 @@ import org.apache.gravitino.model.ModelCatalog;
 import org.apache.gravitino.rel.SupportsPartitions;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.storage.IdGenerator;
 import org.apache.gravitino.utils.IsolatedClassLoader;
 import org.apache.gravitino.utils.NamespaceUtil;
@@ -148,6 +149,16 @@ public class CatalogManager implements CatalogDispatcher, 
Closeable {
           });
     }
 
+    public <R> R doWithViewOps(ThrowableFunction<ViewCatalog, R> fn) throws 
Exception {
+      return classLoader.withClassLoader(
+          cl -> {
+            if (asViews() == null) {
+              throw new UnsupportedOperationException("Catalog does not 
support view operations");
+            }
+            return fn.apply(asViews());
+          });
+    }
+
     public <R> R doWithFilesetOps(ThrowableFunction<FilesetCatalog, R> fn) 
throws Exception {
       return classLoader.withClassLoader(
           cl -> {
@@ -246,6 +257,10 @@ public class CatalogManager implements CatalogDispatcher, 
Closeable {
       return catalog.ops() instanceof TableCatalog ? (TableCatalog) 
catalog.ops() : null;
     }
 
+    private ViewCatalog asViews() {
+      return catalog.ops() instanceof ViewCatalog ? (ViewCatalog) 
catalog.ops() : null;
+    }
+
     private FilesetCatalog asFilesets() {
       return catalog.ops() instanceof FilesetCatalog ? (FilesetCatalog) 
catalog.ops() : null;
     }
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ViewDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ViewDispatcher.java
new file mode 100644
index 0000000000..2961ca7890
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/catalog/ViewDispatcher.java
@@ -0,0 +1,29 @@
+/*
+ * 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.gravitino.catalog;
+
+import org.apache.gravitino.rel.ViewCatalog;
+
+/**
+ * {@code ViewDispatcher} interface acts as a specialization of the {@link 
ViewCatalog} interface.
+ * This interface is designed to potentially add custom behaviors or 
operations related to
+ * dispatching or handling view-related events or actions that are not covered 
by the standard
+ * {@code ViewCatalog} operations.
+ */
+public interface ViewDispatcher extends ViewCatalog {}
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
new file mode 100644
index 0000000000..c49a93abfb
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
@@ -0,0 +1,80 @@
+/*
+ * 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.gravitino.catalog;
+
+import static 
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier;
+
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+import org.apache.gravitino.lock.LockType;
+import org.apache.gravitino.lock.TreeLockUtils;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.storage.IdGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code ViewOperationDispatcher} is the operation dispatcher for view 
operations.
+ *
+ * <p>Currently only supports loadView(). Full CRUD operations (create, alter, 
drop) needs to be
+ * added when Gravitino APIs support view management.
+ */
+public class ViewOperationDispatcher extends OperationDispatcher implements 
ViewDispatcher {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ViewOperationDispatcher.class);
+
+  /**
+   * Creates a new ViewOperationDispatcher instance.
+   *
+   * @param catalogManager The CatalogManager instance to be used for view 
operations.
+   * @param store The EntityStore instance to be used for view operations.
+   * @param idGenerator The IdGenerator instance to be used for view 
operations.
+   */
+  public ViewOperationDispatcher(
+      CatalogManager catalogManager, EntityStore store, IdGenerator 
idGenerator) {
+    super(catalogManager, store, idGenerator);
+  }
+
+  /**
+   * Load view metadata by identifier from the catalog.
+   *
+   * <p>Delegates directly to the underlying catalog's ViewCatalog interface. 
Views are loaded from
+   * the external catalog without caching in Gravitino's EntityStore.
+   *
+   * <p>TODO(#9746): Add entity storage support to cache view metadata in 
EntityStore.
+   *
+   * @param ident The view identifier.
+   * @return The loaded view metadata.
+   * @throws NoSuchViewException If the view does not exist.
+   */
+  @Override
+  public View loadView(NameIdentifier ident) throws NoSuchViewException {
+    LOG.info("Loading view: {}", ident);
+
+    return TreeLockUtils.doWithTreeLock(
+        ident,
+        LockType.READ,
+        () ->
+            doWithCatalog(
+                getCatalogIdentifier(ident),
+                c -> c.doWithViewOps(v -> v.loadView(ident)),
+                NoSuchViewException.class));
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
index 0b0cb18760..8b075bff1f 100644
--- a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
@@ -201,6 +201,11 @@ public class MetadataObjectUtil {
         check(env.modelDispatcher().modelExists(identifier), 
exceptionToThrowSupplier);
         break;
 
+      case VIEW:
+        NameIdentifierUtil.checkView(identifier);
+        check(env.viewDispatcher().viewExists(identifier), 
exceptionToThrowSupplier);
+        break;
+
       case ROLE:
         AuthorizationUtils.checkRole(identifier);
         try {
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
new file mode 100644
index 0000000000..f602a2b53b
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
@@ -0,0 +1,175 @@
+/*
+ * 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.gravitino.catalog;
+
+import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
+import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
+import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Map;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.TestCatalog;
+import org.apache.gravitino.connector.TestCatalogOperations;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+import org.apache.gravitino.lock.LockManager;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.rel.View;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class TestViewOperationDispatcher extends TestOperationDispatcher {
+  static ViewOperationDispatcher viewOperationDispatcher;
+  static SchemaOperationDispatcher schemaOperationDispatcher;
+
+  @BeforeAll
+  public static void initialize() throws IOException, IllegalAccessException {
+    schemaOperationDispatcher =
+        new SchemaOperationDispatcher(catalogManager, entityStore, 
idGenerator);
+    viewOperationDispatcher = new ViewOperationDispatcher(catalogManager, 
entityStore, idGenerator);
+
+    Config config = mock(Config.class);
+    doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY);
+    doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY);
+    doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new 
LockManager(config), true);
+    FieldUtils.writeField(
+        GravitinoEnv.getInstance(), "schemaDispatcher", 
schemaOperationDispatcher, true);
+  }
+
+  public static ViewOperationDispatcher getViewOperationDispatcher() {
+    return viewOperationDispatcher;
+  }
+
+  public static SchemaOperationDispatcher getSchemaOperationDispatcher() {
+    return schemaOperationDispatcher;
+  }
+
+  public static CatalogManager getCatalogManager() {
+    return catalogManager;
+  }
+
+  @Test
+  public void testLoadView() throws IOException {
+    Namespace viewNs = Namespace.of(metalake, catalog, "schema61");
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    schemaOperationDispatcher.createSchema(NameIdentifier.of(viewNs.levels()), 
"comment", props);
+
+    NameIdentifier viewIdent1 = NameIdentifier.of(viewNs, "view1");
+
+    // Create a mock view through the catalog operations
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build();
+    View mockView =
+        new View() {
+          @Override
+          public String name() {
+            return "view1";
+          }
+
+          @Override
+          public Map<String, String> properties() {
+            return props;
+          }
+
+          @Override
+          public AuditInfo auditInfo() {
+            return auditInfo;
+          }
+        };
+
+    // Mock the catalog operations to return the view
+    TestCatalog testCatalog =
+        (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake, 
catalog));
+    TestCatalogOperations testCatalogOperations = (TestCatalogOperations) 
testCatalog.ops();
+    testCatalogOperations.views.put(viewIdent1, mockView);
+
+    // Test load view
+    View loadedView = viewOperationDispatcher.loadView(viewIdent1);
+    Assertions.assertEquals("view1", loadedView.name());
+    Assertions.assertEquals("test", loadedView.auditInfo().creator());
+
+    // Test load non-existent view
+    NameIdentifier viewIdent2 = NameIdentifier.of(viewNs, "non_existent_view");
+    Assertions.assertThrows(
+        NoSuchViewException.class, () -> 
viewOperationDispatcher.loadView(viewIdent2));
+  }
+
+  @Test
+  public void testLoadViewWithInvalidNamespace() {
+    Namespace invalidNs = Namespace.of(metalake, catalog, 
"non_existent_schema");
+    NameIdentifier viewIdent = NameIdentifier.of(invalidNs, "view1");
+
+    Assertions.assertThrows(
+        NoSuchViewException.class, () -> 
viewOperationDispatcher.loadView(viewIdent));
+  }
+
+  @Test
+  public void testLoadViewWithMultipleViews() throws IOException {
+    Namespace viewNs = Namespace.of(metalake, catalog, "schema62");
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    schemaOperationDispatcher.createSchema(NameIdentifier.of(viewNs.levels()), 
"comment", props);
+
+    // Create multiple views
+    TestCatalog testCatalog =
+        (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake, 
catalog));
+    TestCatalogOperations testCatalogOperations = (TestCatalogOperations) 
testCatalog.ops();
+
+    for (int i = 1; i <= 3; i++) {
+      NameIdentifier viewIdent = NameIdentifier.of(viewNs, "view" + i);
+      AuditInfo auditInfo =
+          
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build();
+      int index = i;
+      View mockView =
+          new View() {
+            @Override
+            public String name() {
+              return "view" + index;
+            }
+
+            @Override
+            public Map<String, String> properties() {
+              return props;
+            }
+
+            @Override
+            public AuditInfo auditInfo() {
+              return auditInfo;
+            }
+          };
+      testCatalogOperations.views.put(viewIdent, mockView);
+    }
+
+    // Test loading each view
+    for (int i = 1; i <= 3; i++) {
+      NameIdentifier viewIdent = NameIdentifier.of(viewNs, "view" + i);
+      View loadedView = viewOperationDispatcher.loadView(viewIdent);
+      Assertions.assertEquals("view" + i, loadedView.name());
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java 
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
index 10b6bc123c..78658035e9 100644
--- 
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
+++ 
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
@@ -70,6 +70,7 @@ import 
org.apache.gravitino.exceptions.NoSuchModelVersionURINameException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.exceptions.NoSuchTopicException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
 import org.apache.gravitino.exceptions.NonEmptySchemaException;
 import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.apache.gravitino.exceptions.TableAlreadyExistsException;
@@ -91,6 +92,8 @@ import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.rel.expressions.distributions.Distribution;
 import org.apache.gravitino.rel.expressions.sorts.SortOrder;
 import org.apache.gravitino.rel.expressions.transforms.Transform;
@@ -101,6 +104,7 @@ import org.slf4j.LoggerFactory;
 public class TestCatalogOperations
     implements CatalogOperations,
         TableCatalog,
+        ViewCatalog,
         FilesetCatalog,
         TopicCatalog,
         ModelCatalog,
@@ -121,6 +125,8 @@ public class TestCatalogOperations
 
   private final Map<Pair<NameIdentifier, String>, Integer> modelAliasToVersion;
 
+  public final Map<NameIdentifier, View> views;
+
   public static final String FAIL_CREATE = "fail-create";
 
   public static final String FAIL_TEST = "need-fail";
@@ -135,6 +141,7 @@ public class TestCatalogOperations
     models = Maps.newHashMap();
     modelVersions = Maps.newHashMap();
     modelAliasToVersion = Maps.newHashMap();
+    views = Maps.newHashMap();
   }
 
   @Override
@@ -295,6 +302,16 @@ public class TestCatalogOperations
     }
   }
 
+  // ViewCatalog methods
+  @Override
+  public View loadView(NameIdentifier ident) throws NoSuchViewException {
+    if (views.containsKey(ident)) {
+      return views.get(ident);
+    } else {
+      throw new NoSuchViewException("View %s does not exist", ident);
+    }
+  }
+
   @Override
   public NameIdentifier[] listSchemas(Namespace namespace) throws 
NoSuchCatalogException {
     return schemas.keySet().stream()
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
index 39ac49b5ff..553b803edb 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
@@ -40,6 +40,7 @@ import 
org.apache.gravitino.iceberg.service.dispatcher.IcebergTableHookDispatche
 import 
org.apache.gravitino.iceberg.service.dispatcher.IcebergTableOperationDispatcher;
 import 
org.apache.gravitino.iceberg.service.dispatcher.IcebergTableOperationExecutor;
 import 
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewEventDispatcher;
+import 
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewHookDispatcher;
 import 
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewOperationDispatcher;
 import 
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewOperationExecutor;
 import org.apache.gravitino.iceberg.service.metrics.IcebergMetricsManager;
@@ -114,8 +115,10 @@ public class RESTService implements 
GravitinoAuxiliaryService {
         new IcebergTableEventDispatcher(icebergTableOperationDispatcher, 
eventBus, metalakeName);
     IcebergViewOperationExecutor icebergViewOperationExecutor =
         new IcebergViewOperationExecutor(icebergCatalogWrapperManager);
+    IcebergViewHookDispatcher icebergViewHookDispatcher =
+        new IcebergViewHookDispatcher(icebergViewOperationExecutor, 
metalakeName);
     IcebergViewEventDispatcher icebergViewEventDispatcher =
-        new IcebergViewEventDispatcher(icebergViewOperationExecutor, eventBus, 
metalakeName);
+        new IcebergViewEventDispatcher(icebergViewHookDispatcher, eventBus, 
metalakeName);
 
     IcebergNamespaceOperationDispatcher namespaceOperationDispatcher =
         new IcebergNamespaceOperationExecutor(icebergCatalogWrapperManager);
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
index b17cdf25cd..1d30592593 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
@@ -27,8 +27,9 @@ import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
 
 /**
- * Utility class for managing ownership of Iceberg resources (schemas and 
tables) in Gravitino.
- * Provides centralized methods for setting ownership during resource creation 
operations.
+ * Utility class for managing ownership of Iceberg resources (schemas, tables, 
and views) in
+ * Gravitino. Provides centralized methods for setting ownership during 
resource creation
+ * operations.
  */
 public class IcebergOwnershipUtils {
 
@@ -87,4 +88,33 @@ public class IcebergOwnershipUtils {
           Owner.Type.USER);
     }
   }
+
+  /**
+   * Sets the owner of a view to the specified user using the provided owner 
dispatcher.
+   *
+   * @param metalake the metalake name
+   * @param catalogName the catalog name
+   * @param namespace the Iceberg namespace containing the view
+   * @param viewName the view name
+   * @param user the user to set as owner
+   * @param ownerDispatcher the owner dispatcher to use
+   */
+  public static void setViewOwner(
+      String metalake,
+      String catalogName,
+      Namespace namespace,
+      String viewName,
+      String user,
+      OwnerDispatcher ownerDispatcher) {
+    if (ownerDispatcher != null) {
+      ownerDispatcher.setOwner(
+          metalake,
+          NameIdentifierUtil.toMetadataObject(
+              IcebergIdentifierUtils.toGravitinoTableIdentifier(
+                  metalake, catalogName, TableIdentifier.of(namespace, 
viewName)),
+              Entity.EntityType.VIEW),
+          user,
+          Owner.Type.USER);
+    }
+  }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
new file mode 100644
index 0000000000..b686346ce3
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
@@ -0,0 +1,154 @@
+/*
+ *  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.gravitino.iceberg.service.dispatcher;
+
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.iceberg.common.utils.IcebergIdentifierUtils;
+import org.apache.gravitino.listener.api.event.IcebergRequestContext;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code IcebergViewHookDispatcher} is a decorator for {@link 
IcebergViewOperationDispatcher} that
+ * imports views into Gravitino's metadata catalog when they are created or 
accessed via Iceberg
+ * REST.
+ *
+ * <p>This is the key integration point between Iceberg REST views and 
Gravitino's view management
+ * system. When a view is created or loaded through Iceberg REST, this 
dispatcher ensures that
+ * Gravitino is aware of the view by calling {@link ViewDispatcher#loadView}.
+ */
+public class IcebergViewHookDispatcher implements 
IcebergViewOperationDispatcher {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(IcebergViewHookDispatcher.class);
+
+  private final IcebergViewOperationDispatcher dispatcher;
+  private final String metalake;
+
+  public IcebergViewHookDispatcher(IcebergViewOperationDispatcher dispatcher, 
String metalake) {
+    this.dispatcher = dispatcher;
+    this.metalake = metalake;
+  }
+
+  @Override
+  public LoadViewResponse createView(
+      IcebergRequestContext context, Namespace namespace, CreateViewRequest 
createViewRequest) {
+    // First, create the view in the underlying catalog
+    LoadViewResponse response = dispatcher.createView(context, namespace, 
createViewRequest);
+
+    // Then import it into Gravitino so Gravitino is aware of the view
+    importView(context.catalogName(), namespace, createViewRequest.name());
+
+    // TODO(#9746): Enable view ownership once ViewMetaService is implemented
+    // Currently disabled because VIEW entity type is not supported in
+    // RelationalEntityStoreIdResolver
+    // IcebergOwnershipUtils.setViewOwner(
+    //     metalake,
+    //     context.catalogName(),
+    //     namespace,
+    //     createViewRequest.name(),
+    //     context.userName(),
+    //     GravitinoEnv.getInstance().ownerDispatcher());
+
+    return response;
+  }
+
+  @Override
+  public LoadViewResponse loadView(IcebergRequestContext context, 
TableIdentifier viewIdentifier) {
+    return dispatcher.loadView(context, viewIdentifier);
+  }
+
+  @Override
+  public LoadViewResponse replaceView(
+      IcebergRequestContext context,
+      TableIdentifier viewIdentifier,
+      UpdateTableRequest replaceViewRequest) {
+    return dispatcher.replaceView(context, viewIdentifier, replaceViewRequest);
+  }
+
+  @Override
+  public void dropView(IcebergRequestContext context, TableIdentifier 
viewIdentifier) {
+    dispatcher.dropView(context, viewIdentifier);
+    // Note: We don't remove from Gravitino here as that will be handled by 
entity storage
+    // in future work (issue #9746)
+  }
+
+  @Override
+  public ListTablesResponse listView(IcebergRequestContext context, Namespace 
namespace) {
+    return dispatcher.listView(context, namespace);
+  }
+
+  @Override
+  public boolean viewExists(IcebergRequestContext context, TableIdentifier 
viewIdentifier) {
+    return dispatcher.viewExists(context, viewIdentifier);
+  }
+
+  @Override
+  public void renameView(IcebergRequestContext context, RenameTableRequest 
renameViewRequest) {
+    dispatcher.renameView(context, renameViewRequest);
+    // Note: Rename handling in Gravitino will be added with full view support 
(issue #9746)
+  }
+
+  /**
+   * Import a view into Gravitino's metadata catalog by loading it through the 
ViewDispatcher.
+   *
+   * <p>This method calls {@link ViewDispatcher#loadView} which delegates to 
the underlying
+   * catalog's ViewCatalog implementation. This ensures Gravitino is aware of 
the view and can apply
+   * authorization policies to it.
+   *
+   * <p>This is a best-effort operation - if it fails, we log a warning but 
don't fail the Iceberg
+   * REST operation, as the view was successfully created in the underlying 
catalog.
+   *
+   * @param catalogName The name of the Gravitino catalog.
+   * @param namespace The Iceberg namespace containing the view.
+   * @param viewName The name of the view.
+   */
+  private void importView(String catalogName, Namespace namespace, String 
viewName) {
+    ViewDispatcher viewDispatcher = 
GravitinoEnv.getInstance().viewDispatcher();
+    if (viewDispatcher != null) {
+      try {
+        viewDispatcher.loadView(
+            IcebergIdentifierUtils.toGravitinoTableIdentifier(
+                metalake, catalogName, TableIdentifier.of(namespace, 
viewName)));
+        LOG.info(
+            "Successfully imported view into Gravitino: {}.{}.{}.{}",
+            metalake,
+            catalogName,
+            namespace,
+            viewName);
+      } catch (Exception e) {
+        // Log but don't fail - view import is best-effort
+        LOG.warn(
+            "Failed to import view into Gravitino: {}.{}.{}.{} - {}",
+            metalake,
+            catalogName,
+            namespace,
+            viewName,
+            e.getMessage());
+      }
+    }
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
index 92a5e9f9bc..6415a8f066 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
@@ -25,7 +25,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import org.apache.gravitino.Entity;
+import org.apache.gravitino.Entity.EntityType;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.Owner;
@@ -46,6 +46,7 @@ public class TestIcebergOwnershipUtils {
   private static final String USER = "test_user";
   private static final String SCHEMA_NAME = "test_schema";
   private static final String TABLE_NAME = "test_table";
+  private static final String VIEW_NAME = "test_view";
 
   private OwnerDispatcher mockOwnerDispatcher;
 
@@ -60,7 +61,7 @@ public class TestIcebergOwnershipUtils {
     NameIdentifier expectedSchemaIdentifier =
         IcebergIdentifierUtils.toGravitinoSchemaIdentifier(METALAKE, CATALOG, 
namespace);
     MetadataObject expectedMetadataObject =
-        NameIdentifierUtil.toMetadataObject(expectedSchemaIdentifier, 
Entity.EntityType.SCHEMA);
+        NameIdentifierUtil.toMetadataObject(expectedSchemaIdentifier, 
EntityType.SCHEMA);
 
     IcebergOwnershipUtils.setSchemaOwner(METALAKE, CATALOG, namespace, USER, 
mockOwnerDispatcher);
 
@@ -75,7 +76,7 @@ public class TestIcebergOwnershipUtils {
     NameIdentifier expectedTableIdentifier =
         IcebergIdentifierUtils.toGravitinoTableIdentifier(METALAKE, CATALOG, 
tableIdentifier);
     MetadataObject expectedMetadataObject =
-        NameIdentifierUtil.toMetadataObject(expectedTableIdentifier, 
Entity.EntityType.TABLE);
+        NameIdentifierUtil.toMetadataObject(expectedTableIdentifier, 
EntityType.TABLE);
 
     IcebergOwnershipUtils.setTableOwner(
         METALAKE, CATALOG, namespace, TABLE_NAME, USER, mockOwnerDispatcher);
@@ -105,4 +106,31 @@ public class TestIcebergOwnershipUtils {
       fail("setTableOwner should handle null dispatcher gracefully, but threw: 
" + e.getMessage());
     }
   }
+
+  @Test
+  public void testSetViewOwner() {
+    Namespace namespace = Namespace.of(SCHEMA_NAME);
+    TableIdentifier viewIdentifier = TableIdentifier.of(namespace, VIEW_NAME);
+    NameIdentifier expectedViewIdentifier =
+        IcebergIdentifierUtils.toGravitinoTableIdentifier(METALAKE, CATALOG, 
viewIdentifier);
+    MetadataObject expectedMetadataObject =
+        NameIdentifierUtil.toMetadataObject(expectedViewIdentifier, 
EntityType.VIEW);
+
+    IcebergOwnershipUtils.setViewOwner(
+        METALAKE, CATALOG, namespace, VIEW_NAME, USER, mockOwnerDispatcher);
+
+    verify(mockOwnerDispatcher, times(1))
+        .setOwner(eq(METALAKE), eq(expectedMetadataObject), eq(USER), 
eq(Owner.Type.USER));
+  }
+
+  @Test
+  public void testSetViewOwnerWithNullOwnerDispatcher() {
+    Namespace namespace = Namespace.of(SCHEMA_NAME);
+
+    try {
+      IcebergOwnershipUtils.setViewOwner(METALAKE, CATALOG, namespace, 
VIEW_NAME, USER, null);
+    } catch (Exception e) {
+      fail("setViewOwner should handle null dispatcher gracefully, but threw: 
" + e.getMessage());
+    }
+  }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewOperationExecutor.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewOperationExecutor.java
new file mode 100644
index 0000000000..c0491677bd
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewOperationExecutor.java
@@ -0,0 +1,176 @@
+/*
+ * 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.gravitino.iceberg.service.dispatcher;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.gravitino.iceberg.service.CatalogWrapperForREST;
+import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
+import org.apache.gravitino.listener.api.event.IcebergRequestContext;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestIcebergViewOperationExecutor {
+
+  private IcebergViewOperationExecutor executor;
+  private IcebergCatalogWrapperManager mockWrapperManager;
+  private CatalogWrapperForREST mockCatalogWrapper;
+  private IcebergRequestContext mockContext;
+
+  @BeforeEach
+  public void setUp() {
+    mockWrapperManager = mock(IcebergCatalogWrapperManager.class);
+    mockCatalogWrapper = mock(CatalogWrapperForREST.class);
+    executor = new IcebergViewOperationExecutor(mockWrapperManager);
+
+    mockContext = mock(IcebergRequestContext.class);
+    when(mockContext.catalogName()).thenReturn("test_catalog");
+    
when(mockWrapperManager.getCatalogWrapper("test_catalog")).thenReturn(mockCatalogWrapper);
+  }
+
+  @Test
+  public void testCreateView() {
+    Namespace namespace = Namespace.of("test_ns");
+    CreateViewRequest mockRequest = mock(CreateViewRequest.class);
+    LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+    when(mockCatalogWrapper.createView(namespace, 
mockRequest)).thenReturn(mockResponse);
+
+    LoadViewResponse result = executor.createView(mockContext, namespace, 
mockRequest);
+
+    Assertions.assertEquals(mockResponse, result);
+    verify(mockCatalogWrapper).createView(namespace, mockRequest);
+  }
+
+  @Test
+  public void testLoadView() {
+    TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+    LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+    when(mockCatalogWrapper.loadView(viewId)).thenReturn(mockResponse);
+
+    LoadViewResponse result = executor.loadView(mockContext, viewId);
+
+    Assertions.assertEquals(mockResponse, result);
+    verify(mockCatalogWrapper).loadView(viewId);
+  }
+
+  @Test
+  public void testDropView() {
+    TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+
+    executor.dropView(mockContext, viewId);
+
+    verify(mockCatalogWrapper).dropView(viewId);
+  }
+
+  @Test
+  public void testListView() {
+    Namespace namespace = Namespace.of("test_ns");
+    ListTablesResponse mockResponse = mock(ListTablesResponse.class);
+    when(mockCatalogWrapper.listView(namespace)).thenReturn(mockResponse);
+
+    ListTablesResponse result = executor.listView(mockContext, namespace);
+
+    Assertions.assertEquals(mockResponse, result);
+    verify(mockCatalogWrapper).listView(namespace);
+  }
+
+  @Test
+  public void testViewExists() {
+    TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+    when(mockCatalogWrapper.viewExists(viewId)).thenReturn(true);
+
+    boolean result = executor.viewExists(mockContext, viewId);
+
+    Assertions.assertTrue(result);
+    verify(mockCatalogWrapper).viewExists(viewId);
+  }
+
+  @Test
+  public void testViewDoesNotExist() {
+    TableIdentifier viewId = TableIdentifier.of("test_ns", 
"non_existent_view");
+    when(mockCatalogWrapper.viewExists(viewId)).thenReturn(false);
+
+    boolean result = executor.viewExists(mockContext, viewId);
+
+    Assertions.assertFalse(result);
+    verify(mockCatalogWrapper).viewExists(viewId);
+  }
+
+  @Test
+  public void testLoadViewThrowsException() {
+    TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+    RuntimeException exception = new RuntimeException("View not found");
+    when(mockCatalogWrapper.loadView(viewId)).thenThrow(exception);
+
+    RuntimeException thrown =
+        Assertions.assertThrows(
+            RuntimeException.class, () -> executor.loadView(mockContext, 
viewId));
+
+    Assertions.assertEquals(exception, thrown);
+    verify(mockCatalogWrapper).loadView(viewId);
+  }
+
+  @Test
+  public void testCreateViewThrowsException() {
+    Namespace namespace = Namespace.of("test_ns");
+    CreateViewRequest mockRequest = mock(CreateViewRequest.class);
+    RuntimeException exception = new RuntimeException("Failed to create view");
+    when(mockCatalogWrapper.createView(namespace, 
mockRequest)).thenThrow(exception);
+
+    RuntimeException thrown =
+        Assertions.assertThrows(
+            RuntimeException.class, () -> executor.createView(mockContext, 
namespace, mockRequest));
+
+    Assertions.assertEquals(exception, thrown);
+    verify(mockCatalogWrapper).createView(namespace, mockRequest);
+  }
+
+  @Test
+  public void testReplaceView() {
+    TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+    UpdateTableRequest mockRequest = mock(UpdateTableRequest.class);
+    LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+    when(mockCatalogWrapper.updateView(viewId, 
mockRequest)).thenReturn(mockResponse);
+
+    LoadViewResponse result = executor.replaceView(mockContext, viewId, 
mockRequest);
+
+    Assertions.assertEquals(mockResponse, result);
+    verify(mockCatalogWrapper).updateView(viewId, mockRequest);
+  }
+
+  @Test
+  public void testRenameView() {
+    RenameTableRequest mockRequest = mock(RenameTableRequest.class);
+
+    executor.renameView(mockContext, mockRequest);
+
+    verify(mockCatalogWrapper).renameView(mockRequest);
+  }
+}

Reply via email to